Tóm tắt: SOPS + age: mỗi người/CI có một cặp key age, .sops.yaml khai báo file nào mã hoá cho public key nào, SOPS chỉ mã hoá giá trị nên diff vẫn đọc được. Ansible đọc trực tiếp qua collection community.sops. ansible-vault: không cần tool ngoài, mã hoá cả file hoặc từng biến, phân môi trường bằng --vault-id. So sánh tổng quan ở Quản lý secret.
Luồng hoạt động
flowchart LR A["age-keygen"] -->|"public key"| B[".sops.yaml creation_rules"] B --> C["sops -e -i secrets.sops.yaml"] C --> D["git commit (đã mã hoá)"] D --> E["CI / máy chạy ansible"] F["SOPS_AGE_KEY_FILE (private key)"] --> E E -->|"community.sops giải mã"| G["Playbook"]
Cấu hình
SOPS + age: tạo key
# Cài: tải binary sops và age từ GitHub releases, hoặc apt install age (Ubuntu 22.04+)
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
# In ra: Public key: age1... (public key, được phép commit)
chmod 600 ~/.config/sops/age/keys.txtSOPS tự tìm private key ở ~/.config/sops/age/keys.txt trên Linux; nơi khác thì đặt SOPS_AGE_KEY_FILE. Trong CI, đưa nội dung key vào CI secret và expose qua biến SOPS_AGE_KEY.
File keys.txt là private key. Backup nó vào password manager; mất key = mất khả năng giải mã. Không commit, không để trong thư mục repo.
.sops.yaml
creation_rules:
# prod: chỉ team lead + CI prod giải mã được
- path_regex: inventories/prod/.*\.sops\.ya?ml$
encrypted_regex: ^(ansible_password|ansible_become_password|.*_token|.*_key|tacacs_.*)$
age: >-
age1<PUBKEY_LEAD>,
age1<PUBKEY_CI_PROD>
# lab và mọi file .sops.* còn lại
- path_regex: .*\.sops\.(ya?ml|json|env)$
age: >-
age1<PUBKEY_LEAD>,
age1<PUBKEY_DEV1>,
age1<PUBKEY_CI_LAB>- SOPS dùng rule đầu tiên khớp
path_regex, nên đặt rule cụ thể lên trước. encrypted_regexchỉ mã hoá key khớp regex, các key khác để plaintext (dễ review). Bỏ đi thì mọi giá trị đều bị mã hoá.- Thay
age1<...>bằng public key thật (chuỗi bắt đầu bằngage1).
Lệnh SOPS
| Mục đích | Lệnh |
|---|---|
| Mã hoá file tại chỗ | sops -e -i inventories/prod/group_vars/all/secrets.sops.yaml |
Sửa (mở $EDITOR, tự mã hoá lại khi lưu) |
sops inventories/prod/group_vars/all/secrets.sops.yaml |
| Xem bản giải mã ra stdout | sops -d secrets.sops.yaml |
| Lấy một giá trị | sops -d --extract '["netbox_token"]' secrets.sops.yaml |
Thêm/bớt người sau khi sửa .sops.yaml |
sops updatekeys secrets.sops.yaml |
| Đổi data key (sau khi thu hồi người) | sops -r -i secrets.sops.yaml |
| Chạy lệnh với secret làm env, không ghi file | sops exec-env secrets.sops.env 'terraform plan' |
SOPS ≥ 3.9 có thêm dạng subcommand sops encrypt / sops decrypt / sops edit; dạng cờ -e/-d ở trên vẫn dùng được.
Dùng SOPS trong Ansible
ansible-galaxy collection install community.sops[defaults]
vars_plugins_enabled = host_group_vars,community.sops.sopsVới vars plugin trên, file group_vars/<group>/*.sops.yaml và host_vars/<host>/*.sops.yaml được giải mã tự động như biến thường. Đọc file ngoài group_vars:
- hosts: localhost
gather_facts: false
vars:
sec: "{{ lookup('community.sops.sops', 'secrets/netbox.sops.yaml') | from_yaml }}"
tasks:
- name: Lấy danh sách device từ NetBox
ansible.builtin.uri:
url: "https://netbox.example.com/api/dcim/devices/?limit=0"
headers:
Authorization: "Token {{ sec.netbox_token }}"
register: devices
no_log: trueansible-vault
# Password lưu trong file ngoài repo (hoặc script lấy từ password manager)
install -m 600 /dev/null ~/.ansible/vault-prod.pass # rồi ghi password vào file
# Mã hoá cả file
ansible-vault create --vault-id prod@~/.ansible/vault-prod.pass inventories/prod/group_vars/all/vault.yml
ansible-vault edit --vault-id prod@~/.ansible/vault-prod.pass inventories/prod/group_vars/all/vault.yml
# Mã hoá một biến, dán kết quả vào file vars thường (đọc giá trị từ stdin, không lộ trong history)
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.pass --stdin-name ansible_password
# Chạy playbook
ansible-playbook -i inventories/prod site.yml --vault-id prod@~/.ansible/vault-prod.pass
ansible-playbook -i inventories/lab site.yml --vault-id lab@prompt| Mục đích | Lệnh |
|---|---|
| Xem nội dung | ansible-vault view --vault-id prod@<file> vault.yml |
| Mã hoá / giải mã file có sẵn | ansible-vault encrypt ... / ansible-vault decrypt ... |
| Đổi password | ansible-vault rekey --vault-id prod@old.pass --new-vault-id prod@new.pass vault.yml |
| Đặt mặc định | ANSIBLE_VAULT_PASSWORD_FILE=~/.ansible/vault-prod.pass hoặc vault_identity_list trong ansible.cfg |
Quy ước dễ grep: biến bí mật đặt trong vault.yml với tiền tố vault_ (vd vault_tacacs_key), file vars.yml cạnh đó tham chiếu tacacs_key: "{{ vault_tacacs_key }}". Tìm biến không cần giải mã.
Chặn commit plaintext
#!/bin/sh
for f in $(git diff --cached --name-only | grep -E '\.sops\.(ya?ml|json|env)$'); do
grep -q '^sops:' "$f" || grep -q '"sops":' "$f" || { echo "Chưa mã hoá: $f"; exit 1; }
done
for f in $(git diff --cached --name-only | grep -E '(^|/)vault\.ya?ml$'); do
head -1 "$f" | grep -q '^\$ANSIBLE_VAULT' || { echo "Chưa mã hoá: $f"; exit 1; }
doneKết hợp gitleaks: pre-commit hooks.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
no matching creation rules found |
Đường dẫn file không khớp path_regex nào |
Kiểm tra regex; chạy sops từ thư mục gốc repo |
Failed to get the data key required to decrypt |
Máy không có private key nào khớp recipient trong file | Đặt SOPS_AGE_KEY_FILE; nếu là người mới thì cần ai đó updatekeys thêm public key |
Đã sửa .sops.yaml nhưng người mới vẫn không giải mã được |
File cũ chưa được cập nhật recipient | sops updatekeys <file> cho từng file |
MAC mismatch |
Sửa file mã hoá bằng editor thường | Revert, luôn sửa qua sops <file> |
Ansible không thấy biến trong *.sops.yaml |
Chưa bật vars plugin hoặc chưa cài collection | Thêm community.sops.sops vào vars_plugins_enabled |
Decryption failed (no vault secrets were found) |
Thiếu --vault-id / password file |
Truyền --vault-id, hoặc đặt ANSIBLE_VAULT_PASSWORD_FILE |
Secret hiện trong output -v |
Task không có no_log |
Thêm no_log: true cho task dùng secret |