Secret Management Best Practices: Vault ใน Production Playbooks

การใช้ Ansible Vault ให้ได้ประสิทธิภาพสูงสุดใน production ต้องอาศัยมากกว่าแค่คำสั่ง encrypt/decrypt — ต้องมีระบบจัดการ secrets ที่ครบวงจร ทั้งการแยก vault ตาม environment, การหมุนเวียน password อย่างปลอดภัย, การจัดเก็บ vault password ใน secret manager และการป้องกันไม่ให้ข้อมูลลับรั่วไหลเข้า git history

บทความนี้รวบรวม best practices สำหรับการใช้ Ansible Vault ใน production จริง ครอบคลุมการออกแบบโครงสร้างไฟล์ vault, vault password management, การแยก secrets ตาม environment, การป้องกัน secrets ใน git, pre-commit hooks, vault audit และ key rotation workflow ที่ปลอดภัย

โครงสร้างไฟล์ Vault ที่แนะนำ

การออกแบบโครงสร้างไฟล์ vault ที่ดีช่วยให้จัดการ secrets ได้ง่าย ลดความเสี่ยงที่ secrets จะรั่วไหล และทำให้ทีมสามารถ rotate password ได้โดยไม่กระทบ environment อื่น

# โครงสร้าง project ที่แนะนำ — แยก vault ตาม environment
myproject/
├── ansible.cfg
├── requirements.yml
├── site.yml
├── inventory/
│   ├── production/
│   │   ├── hosts
│   │   └── group_vars/
│   │       └── all/
│   │           ├── vars.yml          # ตัวแปรทั่วไป (ไม่ใช่ secret)
│   │           └── vault.yml         # secrets ของ production (encrypted)
│   └── staging/
│       ├── hosts
│       └── group_vars/
│           └── all/
│               ├── vars.yml          # ตัวแปรทั่วไป
│               └── vault.yml         # secrets ของ staging (encrypted, password ต่างกัน)
├── group_vars/
│   └── webservers/
│       ├── vars.yml
│       └── vault.yml                 # secrets เฉพาะ webservers
├── roles/
│   └── myapp/
│       └── defaults/
│           └── main.yml             # default values (ไม่ sensitive)
└── .vault_pass/                     # ห้าม commit! ใส่ใน .gitignore
    ├── production.txt               # vault password สำหรับ production
    └── staging.txt                  # vault password สำหรับ staging
# .gitignore — สิ่งที่ห้าม commit เด็ดขาด
# Vault passwords
.vault_pass/
*.vault_pass
.vault_password
vault_password.txt
.ansible_vault_pass

# Private keys
*.pem
*.key
id_rsa
id_ed25519

# Environment files
.env
.env.*
!.env.example

# Temporary files
*.tmp
*.bak

vault.yml — รูปแบบที่ถูกต้อง

ใช้ convention vault_ prefix สำหรับตัวแปรใน vault.yml และ reference ผ่านตัวแปรปกติใน vars.yml — วิธีนี้ทำให้รู้ว่าตัวแปรไหนมาจาก vault และตัวแปรไหนเป็น plain text โดยไม่ต้อง decrypt ดู

# group_vars/all/vars.yml — ตัวแปรปกติ (ไม่ encrypt)
db_host: "10.0.1.10"
db_port: 5432
db_name: "production_db"

# reference vault variables ผ่าน non-vault names
db_password: "{{ vault_db_password }}"
app_secret_key: "{{ vault_app_secret_key }}"
smtp_password: "{{ vault_smtp_password }}"
aws_access_key: "{{ vault_aws_access_key }}"
aws_secret_key: "{{ vault_aws_secret_key }}"
# group_vars/all/vault.yml — encrypted ทั้งไฟล์
# เนื้อหาก่อน encrypt:
---
vault_db_password: "Str0ng_DB_P@ssw0rd_2024!"
vault_app_secret_key: "django-insecure-xxxxxxxxxxxxxxxxxxxxxxxxxxx"
vault_smtp_password: "smtp_app_password_here"
vault_aws_access_key: "AKIAIOSFODNN7EXAMPLE"
vault_aws_secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

# หลัง encrypt:
ansible-vault encrypt group_vars/all/vault.yml
# ไฟล์จะกลายเป็น:
# $ANSIBLE_VAULT;1.1;AES256
# 62656636393865663632326666...

Vault Password Management

การจัดการ vault password อย่างถูกต้องคือหัวใจของ production security — vault password ต้องแข็งแกร่ง, เก็บใน secret manager, และไม่เคยอยู่ใน repository

# แนวทางการจัดการ vault password ใน production

# 1. ใช้ secret manager จัดเก็บ vault password
#    ตัวเลือก: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, 1Password

# 2. ดึง vault password จาก secret manager ผ่าน script
# vault_pass_script.py
#!/usr/bin/env python3
import subprocess, sys

# ดึงจาก HashiCorp Vault
result = subprocess.run(
    ['vault', 'kv', 'get', '-field=password', 'secret/ansible/production'],
    capture_output=True, text=True
)
print(result.stdout.strip())

# 3. ตั้งค่าใน ansible.cfg ให้ใช้ script อัตโนมัติ
# ansible.cfg
[defaults]
vault_password_file = ./scripts/vault_pass_script.py
# ตัวอย่าง: ดึง vault password จาก AWS Secrets Manager
#!/usr/bin/env python3
import boto3, sys, os

def get_vault_password():
    env = os.environ.get('ANSIBLE_ENV', 'staging')
    secret_name = f"ansible/vault/{env}"

    client = boto3.client('secretsmanager', region_name='ap-southeast-1')
    response = client.get_secret_value(SecretId=secret_name)
    return response['SecretString']

if __name__ == '__main__':
    print(get_vault_password())

แยก Vault Password ตาม Environment

production และ staging ต้องใช้ vault password คนละตัวเสมอ — ถ้าใช้ password เดียวกัน การ compromise password ตัวเดียวจะเข้าถึง secrets ของทุก environment ได้

# ใช้ Vault ID แยก password ตาม environment
# ansible.cfg
[defaults]
vault_identity_list = [email protected]_pass/production.txt, [email protected]_pass/staging.txt

# encrypt ระบุ vault ID
ansible-vault encrypt --vault-id [email protected]_pass/production.txt \
    inventory/production/group_vars/all/vault.yml

ansible-vault encrypt --vault-id [email protected]_pass/staging.txt \
    inventory/staging/group_vars/all/vault.yml

# รัน playbook ระบุ vault ID ที่ต้องการ
ansible-playbook -i inventory/production site.yml \
    --vault-id [email protected]_pass/production.txt

ansible-playbook -i inventory/staging site.yml \
    --vault-id [email protected]_pass/staging.txt
# ตัวอย่าง: ใช้ environment variable กำหนด vault ID อัตโนมัติ
#!/bin/bash
# deploy.sh
ENV=${1:-staging}
VAULT_PASS_FILE=".vault_pass/${ENV}.txt"

if [ ! -f "$VAULT_PASS_FILE" ]; then
    echo "Error: Vault password file not found: $VAULT_PASS_FILE"
    exit 1
fi

ansible-playbook \
    -i "inventory/${ENV}" \
    --vault-id "${ENV}@${VAULT_PASS_FILE}" \
    site.yml "$@"

# ใช้งาน:
# ./deploy.sh staging
# ./deploy.sh production

ป้องกัน Secrets ใน Git History

git history เก็บข้อมูลทุกอย่างตลอดไป — ถ้า plain-text secret เคย commit ไว้แม้จะลบแล้ว ก็ยังอยู่ใน history ต้องใช้ pre-commit hook และ git-secrets เพื่อป้องกันตั้งแต่ต้น

# ติดตั้ง pre-commit สำหรับตรวจสอบ secrets ก่อน commit
pip install pre-commit detect-secrets

# .pre-commit-config.yaml
repos:
  # ตรวจ secrets ทั่วไป (passwords, keys, tokens)
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

  # ตรวจว่า vault files ถูก encrypt แล้ว
  - repo: local
    hooks:
      - id: check-vault-encrypted
        name: Check vault files are encrypted
        entry: scripts/check_vault_encrypted.sh
        language: script
        files: 'vault\.yml$'

# ติดตั้ง hooks
pre-commit install
#!/bin/bash
# scripts/check_vault_encrypted.sh
# ตรวจสอบว่าไฟล์ vault.yml ถูก encrypt แล้ว

for file in "$@"; do
    if ! head -1 "$file" | grep -q '^\$ANSIBLE_VAULT'; then
        echo "ERROR: $file is NOT encrypted with ansible-vault!"
        echo "Run: ansible-vault encrypt $file"
        exit 1
    fi
done

echo "All vault files are encrypted ✓"
exit 0

Key Rotation Workflow

การหมุนเวียน vault password (key rotation) ควรทำอย่างน้อยทุก 90 วัน หรือทันทีเมื่อสงสัยว่า password รั่วไหล — ใช้คำสั่ง ansible-vault rekey เพื่อเปลี่ยน password โดยไม่ต้อง decrypt เนื้อหาก่อน

# Key rotation workflow — production environment
# ขั้นตอนที่ 1: สร้าง vault password ใหม่ (ใช้ password generator)
openssl rand -base64 32 > /tmp/new_vault_pass.txt

# ขั้นตอนที่ 2: rekey ทุก vault files
find inventory/production -name "vault.yml" | while read f; do
    ansible-vault rekey \
        --vault-password-file .vault_pass/production.txt \
        --new-vault-password-file /tmp/new_vault_pass.txt \
        "$f"
    echo "Rekeyed: $f"
done

# ขั้นตอนที่ 3: ทดสอบว่า decrypt ได้ด้วย password ใหม่
ansible-vault view \
    --vault-password-file /tmp/new_vault_pass.txt \
    inventory/production/group_vars/all/vault.yml

# ขั้นตอนที่ 4: อัพเดต secret manager ด้วย password ใหม่
# (ทำก่อนที่จะเปลี่ยนไฟล์ production.txt)

# ขั้นตอนที่ 5: แทนที่ password file เก่าด้วยใหม่
mv /tmp/new_vault_pass.txt .vault_pass/production.txt
chmod 600 .vault_pass/production.txt

# ขั้นตอนที่ 6: ทดสอบ deploy จาก staging ก่อน
ansible-playbook -i inventory/staging site.yml \
    --vault-password-file .vault_pass/production.txt \
    --check

Vault Audit — ตรวจสอบ Encrypted Files

ควรตรวจสอบ vault files สม่ำเสมอว่าไฟล์ทั้งหมดที่ควร encrypt ถูก encrypt จริง และไม่มี plain-text secrets หลุดรอดอยู่ใน repository

#!/bin/bash
# scripts/vault_audit.sh — ตรวจสอบสถานะ vault files

echo "=== Vault Audit Report ==="
echo "Date: $(date)"
echo ""

# 1. ตรวจหาไฟล์ vault ที่ยังไม่ encrypt
echo "--- Unencrypted vault files ---"
UNENCRYPTED=0
find . -name "vault.yml" -not -path "./.git/*" | while read f; do
    if ! head -1 "$f" | grep -q '^\$ANSIBLE_VAULT'; then
        echo "UNENCRYPTED: $f"
        UNENCRYPTED=$((UNENCRYPTED + 1))
    fi
done
[ $UNENCRYPTED -eq 0 ] && echo "None found ✓"

# 2. ตรวจหา pattern ที่อาจเป็น secrets ใน plain text files
echo ""
echo "--- Potential secrets in plain text ---"
grep -rn \
    -e "password\s*=\s*['\"][^'\"]+['\"]" \
    -e "secret_key\s*:\s*['\"][^'\"]+['\"]" \
    -e "api_key\s*:\s*['\"][^'\"]+['\"]" \
    --include="*.yml" --include="*.yaml" \
    --exclude-path="./.git/*" \
    . | grep -v "vault_" | grep -v "^.*#" || echo "None found ✓"

# 3. แสดงรายการ vault files ทั้งหมด
echo ""
echo "--- All vault files (encrypted) ---"
find . -name "vault.yml" -not -path "./.git/*" | while read f; do
    if head -1 "$f" | grep -q '^\$ANSIBLE_VAULT'; then
        echo "OK: $f"
    fi
done

echo ""
echo "=== Audit Complete ==="

Production Playbook Pattern

ตัวอย่าง production playbook ที่ใช้ vault secrets อย่างถูกต้อง — secrets ทุกตัวอ้างอิงผ่าน vault variables, ไม่มี hardcoded values และมีการ validate ก่อน deploy

---
# site.yml — production playbook ที่ใช้ vault secrets
- name: Deploy application stack
  hosts: webservers
  become: true

  pre_tasks:
    # ตรวจสอบว่า vault variables ถูก load มาแล้ว
    - name: Verify vault variables are defined
      assert:
        that:
          - vault_db_password is defined
          - vault_app_secret_key is defined
          - vault_smtp_password is defined
        fail_msg: "Vault variables missing — ensure vault file is decrypted"
        success_msg: "Vault variables loaded successfully"

  roles:
    - role: database
      vars:
        db_password: "{{ vault_db_password }}"
        db_host: "{{ db_host }}"
        db_name: "{{ db_name }}"

    - role: application
      vars:
        secret_key: "{{ vault_app_secret_key }}"
        debug: false

    - role: email
      vars:
        smtp_password: "{{ vault_smtp_password }}"

  post_tasks:
    - name: Verify services are running
      service:
        name: "{{ item }}"
        state: started
      loop:
        - nginx
        - postgresql
        - myapp

สรุป

การใช้ Ansible Vault ใน production ต้องการมากกว่าแค่ encrypt/decrypt — ต้องมี workflow ที่ครบถ้วน: แยก vault ตาม environment ด้วย vault ID, จัดเก็บ vault password ใน secret manager, ใช้ pre-commit hooks ป้องกัน secrets ไม่ให้เข้า git, ทำ key rotation สม่ำเสมอ และ audit vault files เป็นประจำ

Pattern สำคัญที่ควรจำ: ใช้ vault_ prefix สำหรับตัวแปรใน vault.yml และ reference ผ่าน plain variable ใน vars.yml เพื่อให้อ่าน code ได้โดยไม่ต้อง decrypt, ไม่ใช้ vault password เดียวกันระหว่าง production และ staging, และทำ key rotation ทุก 90 วันหรือทันทีที่สงสัยว่า password รั่วไหล