SSD Nodes Learn 🎉 VPS mulai $5.50/bln
Panduan Matt ConnorOleh Matt Connor

Ansible Vault: Enkripsi Secret di Repositori Git

Pelajari cara mengenkripsi file vars atau satu string dengan Ansible Vault, memisahkan staging dan produksi, serta melakukan rekey tanpa menyimpan password plaintext.

Apa yang dilindungi Ansible Vault dan apa yang tidak dilindunginya

Ansible Vault mengenkripsi secret di dalam repositori playbook Anda. Dengan demikian, yang disimpan git adalah ciphertext, bukan password dalam plaintext. Perintah ansible-vault mengenkripsi seluruh file atau satu nilai di dalam file menggunakan kunci simetris yang diturunkan dari password yang Anda pilih. Ansible mendekripsi konten tersebut di memori saat play dijalankan, sehingga variabel tersebut berperilaku seperti variabel lainnya.

Model ini memiliki batasan yang jelas. Vault hanya melindungi secret saat tersimpan di repositori dan tidak lebih dari itu. Setelah sebuah task dijalankan, nilainya berada dalam plaintext di memori, template yang dirender, argumen modul, dan output eksekusi, kecuali Anda mencegahnya. Setiap orang yang dapat menjalankan playbook memiliki password vault. Jadi, vault memberikan kerahasiaan dari pihak di luar tim, bukan kontrol akses per orang di dalam tim.

Jika Anda belum menulis playbook, mulai dengan playbook Ansible pertama untuk VPS dan kembali ke sini saat playbook tersebut memerlukan password.

Mengenkripsi seluruh file atau satu string?

ansible-vault encrypt mengganti file dengan ciphertext. File tersebut menjadi satu blok teks base64 di bawah baris header yang diawali dengan $ANSIBLE_VAULT. Gunakan cara ini jika file hanya berisi secret.

ansible-vault encrypt_string mengenkripsi satu nilai dan mencetak cuplikan YAML yang dapat Anda tempel ke file vars biasa. Nama variabel tetap dapat dibaca, sedangkan hanya nilainya yang menjadi ciphertext. Gunakan cara ini jika secret berada berdampingan dengan pengaturan plaintext.

Perbedaan yang penting dalam pekerjaan sehari-hari terlihat pada diff. File vault dienkripsi ulang dengan salt acak baru setiap kali Anda menyimpannya, sehingga setiap byte ciphertext berubah. git diff kemudian menampilkan satu blok yang tidak dapat dibaca digantikan oleh blok lain yang juga tidak dapat dibaca. Akibatnya, reviewer tidak dapat mengetahui apakah Anda merotasi satu password atau menulis ulang seluruh file. Dengan encrypt_string, setiap secret menjadi blok tersendiri di dalam file plaintext. Diff akan menunjukkan variabel mana yang berubah dan membiarkan bagian file lainnya tetap sama.

Format inline memiliki konsekuensi saat rotasi: ansible-vault rekey tidak mengubah blok inline. Pilih format file jika daftar secret panjang dan jarang berubah. Pilih format inline jika file mencampur secret dengan variabel biasa dan Anda ingin code review memberikan informasi yang jelas.

Tata letak group_vars yang menunjukkan hal-hal yang dilindungi

Ansible memuat group_vars/<group>.yml, dan juga memuat setiap file di dalam direktori group_vars/<group>/. Bentuk direktori adalah pilihan yang tepat karena memungkinkan satu grup memiliki file plaintext dan file terenkripsi secara berdampingan.

inventory/
  hosts.ini
group_vars/
  all/
    vars.yml
    vault.yml
  web/
    vars.yml
    vault.yml
host_vars/
  db01/
    vars.yml
    vault.yml
playbooks/
  site.yml

Setiap vault.yml dienkripsi. Setiap vars.yml berupa plaintext. Pembaca dapat melihat nilai mana yang dilindungi tanpa membuka file apa pun karena nama file sudah menunjukkannya.

Bagian kedua dari pola ini adalah indirection. Di dalam file terenkripsi, awali setiap variabel dengan vault_.

vault_db_password: "a real password"
vault_grafana_admin_token: "a real token"

Kemudian, referensikan nama-nama tersebut dari file plaintext di sebelahnya.

db_password: "{{ vault_db_password }}"
grafana_admin_token: "{{ vault_grafana_admin_token }}"

Role dan template menggunakan db_password dan tidak perlu mengetahui asal nilainya. Dengan demikian, pemisahan antara playbook dan role tetap jelas. File plaintext vars.yml juga berfungsi sebagai indeks yang dapat dicari: grep -r vault_ group_vars/ mencantumkan setiap secret yang diharapkan oleh repositori, tanpa mendekripsi apa pun. Konsekuensinya adalah satu nama tambahan untuk setiap secret. Typo pada nama vault_ akan muncul saat runtime sebagai variabel yang belum didefinisikan, bukan sebagai syntax error.

Enkripsi satu variabel dengan encrypt_string

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  --stdin-name 'vault_db_password'

Ketik secret, lalu tekan Ctrl-D. --stdin-name membaca nilai dari input standar sehingga nilai tersebut tidak masuk ke file riwayat shell. Bentuk lainnya menempatkan nilai pada baris perintah, sehingga shell mencatatnya:

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  'a real password' --name 'vault_db_password'

Kedua bentuk perintah tersebut mencetak blok YAML. Tempelkan blok itu ke file vars persis seperti hasil cetaknya, karena indentasi di bawah tag !vault merupakan bagian dari nilai.

vault_db_password: !vault |
          $ANSIBLE_VAULT;1.2;AES256;prod
          6638643965323633646262656665306333616466396630323136393465356136396436383331
          3131303163306665326539353837343663313762616561306534373963383531613664393332

Tag !vault memberi tahu YAML loader bahwa scalar tersebut adalah ciphertext, bukan teks biasa. Header memuat versi format, cipher, dan label vault ID yang digunakan untuk mengenkripsinya. Nilai yang dienkripsi tanpa vault ID memiliki header 1.1 tanpa label. Nilai tersebut tetap dapat digunakan dan hanya memberikan lebih sedikit informasi tentang asal password.

Di mana kata sandi vault disimpan?

Di luar repositori. Itu satu-satunya aturan tanpa pengecualian.

--ask-vault-pass meminta kata sandi satu kali setiap eksekusi dan tidak menyimpan apa pun. Cara ini sesuai untuk laptop, tetapi tidak sesuai untuk cron job atau CI runner.

Password file adalah file teks biasa yang baris pertamanya berisi kata sandi. Buat file tersebut dalam keadaan kosong dengan izin yang ketat, lalu isi menggunakan editor agar kata sandi tidak masuk ke riwayat shell:

mkdir -p ~/.ansible
install -m 600 /dev/null ~/.ansible/vault-prod.txt
$EDITOR ~/.ansible/vault-prod.txt

Arahkan perintah apa pun ke file tersebut dengan --vault-password-file:

ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
  --vault-password-file ~/.ansible/vault-prod.txt

Mengulangi flag tersebut pada setiap perintah mudah terlupakan. Karena itu, tetapkan sekali dalam ansible.cfg di root repositori.

[defaults]
inventory = inventory/hosts.ini
vault_password_file = ~/.ansible/vault-prod.txt

Pengaturan yang sama membaca variabel lingkungan ANSIBLE_VAULT_PASSWORD_FILE. Inilah cara yang biasanya digunakan CI job untuk memasoknya. Job tersebut menulis kata sandi dari credential store miliknya ke file dalam direktori sementara, mengekspor variabel tersebut, lalu menghapus file saat eksekusi selesai. Tambahkan pola nama file itu ke .gitignore, karena path dalam ansible.cfg sudah di-commit dan cepat atau lambat seseorang akan membuat file sebenarnya di dalam checkout.

Jika password file dapat dieksekusi, Ansible menjalankannya dan membaca kata sandi dari standard output, bukan membaca file tersebut sebagai teks. Dengan cara ini, Anda dapat mengambil kata sandi vault dari system keyring atau cloud secret manager tanpa menuliskannya ke disk. Script yang digunakan melalui --vault-id memiliki persyaratan tambahan: namanya harus diakhiri dengan -client atau dengan -client yang diikuti ekstensi, harus dapat dieksekusi, harus menerima opsi --vault-id, dan harus mencetak kata sandi ke standard output.

Dua ID vault: staging dan production

ID vault adalah label yang dilampirkan pada password vault dan ditulis sebagai label@source. Sumbernya adalah prompt, yaitu path ke file password atau path ke client script. Label memungkinkan satu repository menyimpan secret dengan lebih dari satu password, sehingga password staging tidak dapat membuka file production.

ansible-vault encrypt --vault-id staging@~/.ansible/vault-staging.txt \
  group_vars/staging/vault.yml
ansible-vault encrypt --vault-id prod@~/.ansible/vault-prod.txt \
  group_vars/prod/vault.yml

Teruskan setiap ID yang mungkin diperlukan oleh suatu eksekusi:

ansible-playbook playbooks/site.yml \
  --vault-id staging@~/.ansible/vault-staging.txt \
  --vault-id prod@~/.ansible/vault-prod.txt

Atau cantumkan semuanya satu kali dalam ansible.cfg:

[defaults]
vault_identity_list = staging@~/.ansible/vault-staging.txt, prod@~/.ansible/vault-prod.txt

Satu perilaku sering mengejutkan pengguna. Secara default, label hanya berfungsi sebagai petunjuk, bukan pembatas. Ansible mencoba setiap secret yang sedang dimilikinya terhadap file tersebut sampai salah satunya berhasil mendekripsinya. Karena itu, file berlabel staging tetap dapat dibuka jika password production ternyata merupakan key yang benar. Tetapkan vault_id_match = True di bawah [defaults], atau gunakan environment variable ANSIBLE_VAULT_ID_MATCH, agar Ansible hanya menggunakan secret yang labelnya cocok dengan header file. Pemeriksaan ini memerlukan header 1.2, sehingga hanya berlaku untuk konten yang sejak awal dienkripsi dengan ID vault.

Jika lebih dari satu ID dimuat, ansible-vault encrypt tidak lagi mengetahui password mana yang harus digunakan untuk enkripsi. Tentukan ID tersebut dengan --encrypt-vault-id prod, atau tetapkan vault_encrypt_identity dalam ansible.cfg agar repository memiliki nilai default.

Manfaatnya adalah pembatasan cakupan deployment. CI job yang melakukan deployment ke staging hanya diberi password staging, sehingga runner yang dibobol tidak dapat membaca kredensial production. Setelah Anda menjalankan play pada sekumpulan server Linux dari satu mesin kontrol, pemisahan ini dapat menjadi perbedaan antara insiden kecil dan insiden yang sangat besar.

Mengganti kunci vault saat seseorang keluar

Penggantian kunci mengubah password vault dan mengenkripsi ulang konten menggunakan password baru. Tindakan ini tidak membatalkan apa pun. Siapa pun yang pernah memegang password lama tetap dapat mendekripsi salinan repository yang mereka simpan, termasuk semua commit lama dalam salinan tersebut. Karena itu, anggap password vault sudah tidak aman sejak pemegangnya keluar, lalu lakukan rotasi dengan urutan berikut.

  1. Ubah kredensial yang sebenarnya pada server dan layanan pihak ketiga. Langkah inilah yang benar-benar mencabut akses.
  2. Masukkan nilai baru ke dalam file vault dengan ansible-vault edit.
  3. Ganti kunci setiap file terenkripsi menggunakan password vault baru.
  4. Berikan password vault baru kepada orang yang masih memerlukannya melalui kanal yang bukan repository.
ansible-vault rekey --vault-id prod@~/.ansible/vault-prod-old.txt \
  --new-vault-id prod@prompt \
  group_vars/prod/vault.yml host_vars/db01/vault.yml

rekey menerima beberapa file dalam satu perintah, dan --new-vault-id prod@prompt meminta password baru satu kali, bukan membacanya dari disk. Gunakan label yang sama kecuali Anda memiliki alasan untuk mengubahnya, karena label tersebut ditulis ke dalam header setiap file yang ditulis ulang oleh perintah ini.

Di sinilah format inline memiliki konsekuensi. ansible-vault rekey bekerja pada file yang sepenuhnya terenkripsi, sehingga blok !vault yang berada di dalam file vars plaintext tidak diubah. Cari blok tersebut terlebih dahulu, lalu buat ulang setiap blok dengan encrypt_string menggunakan password baru:

grep -rl '!vault' group_vars/ host_vars/

Itulah seluruh konsekuensinya. Blok inline menghasilkan diff yang mudah dibaca, tetapi mengharuskan Anda melakukan pemeriksaan manual saat rotasi. File yang sepenuhnya terenkripsi dapat dirotasi dengan satu perintah, tetapi tidak memberikan informasi yang berguna saat ditinjau.

Mengapa rahasia masih muncul dalam output Anda

Vault selesai bekerja begitu nilainya didekripsi. Ansible melaporkan hasil task, dan module yang menampilkan argumennya akan membawa kredensial ke dalam laporan tersebut. Eksekusi verbose, penggunaan --diff pada task template, task yang gagal dan menampilkan argumennya, atau plugin callback yang menulis output ke file dapat menyimpan teks biasa tersebut. Mengenkripsi file tidak mencegah hal-hal itu.

no_log: true adalah pengaturnya. Tetapkan pengaturan ini pada setiap task yang menerima kredensial.

- name: Write the application environment file
  ansible.builtin.template:
    src: app.env.j2
    dest: /etc/myapp/app.env
    owner: myapp
    group: myapp
    mode: "0600"
  no_log: true

Ansible kemudian tidak menyertakan hasil task tersebut dalam output. Dengan demikian, log hanya mencatat bahwa task berjalan tanpa mencatat data yang diprosesnya. Tetapkan pengaturan ini terutama pada loop, karena loop melaporkan satu hasil untuk setiap item, dan loop atas daftar kredensial akan melaporkan seluruh daftar.

Ada empat tempat lain yang dapat menjadi jalur keluarnya rahasia yang telah didekripsi. Tidak satu pun dari tempat ini dicakup oleh no_log:

  • File yang dirender dari template mewarisi mode dan owner yang Anda berikan. Tetapkan mode: "0600" dan owner tertentu pada apa pun yang menyimpan kredensial. Jika tidak, rahasia dapat dibaca oleh semua pengguna pada host target.
  • Rahasia yang diteruskan ke ansible.builtin.command atau ansible.builtin.shell akan muncul dalam daftar proses pada host target selama perintah berjalan. Semua pengguna lokal dapat membacanya. Teruskan rahasia melalui file atau variabel lingkungan.
  • Caching fact menulis fact yang dikumpulkan ke disk pada mesin kontrol. Dengan demikian, variabel terdaftar yang menyimpan rahasia dapat masuk ke file cache yang tidak dianggap sensitif oleh siapa pun.
  • Rahasia yang sama biasanya juga tersimpan di tempat kedua, misalnya file lingkungan yang dibaca oleh container. Aturan untuk tempat tersebut berbeda, dan menjaga kredensial tetap di luar file env Compose membahas bagian tersebut.

no_log membuat proses debug lebih sulit. Memang itulah fungsinya. Hapus pengaturan ini sementara pada host pengujian ketika task bermasalah, lalu pasang kembali sebelum perubahan diterapkan ke produksi.

Baca dan edit file terenkripsi tanpa meninggalkan plaintext

ansible-vault view group_vars/prod/vault.yml mendekripsi ke pager dan tidak menulis apa pun ke disk. ansible-vault edit mendekripsi ke file sementara, membuka $EDITOR, lalu mengenkripsinya kembali saat Anda menutupnya. Utamakan keduanya daripada ansible-vault decrypt, yang meninggalkan file plaintext di working tree. File vault yang sudah didekripsi dan tidak sengaja di-stage adalah cara paling umum kredensial nyata masuk ke repositori publik.

Git dapat menampilkan diff yang dapat dibaca untuk file yang sepenuhnya terenkripsi dengan mendekripsinya saat proses berlangsung:

git config --local diff.ansible-vault.textconv "ansible-vault view --vault-password-file ~/.ansible/vault-prod.txt"
printf '%s\n' 'group_vars/**/vault.yml diff=ansible-vault' >> .gitattributes

Pahami dampaknya sebelum mengaktifkannya. git diff sekarang akan mencetak secret produksi ke terminal Anda. Secret tersebut akan masuk ke scrollback dan ke setiap sesi berbagi layar. Ini adalah kemudahan lokal untuk satu orang pada satu mesin. Karena itu, pertahankan git config tetap lokal, dan jangan mengharapkan checkout milik orang lain berperilaku sama kecuali mereka mengatur hal yang sama.

Kapan vault bukan lagi alat yang tepat

Vault adalah format file dengan satu kata sandi untuk setiap label. Struktur ini menentukan batas penggunaannya. Beralihlah ke secret store khusus jika salah satu kondisi berikut berlaku.

  • Anda memerlukan akses per orang. Setiap orang yang menjalankan playbook memegang kata sandi yang sama, dan vault ID hanya memisahkan akses berdasarkan lingkungan, bukan berdasarkan orang.
  • Anda memerlukan jejak audit. Vault tidak mencatat siapa yang mendekripsi apa atau kapan tindakan tersebut dilakukan.
  • Anda memerlukan rotasi terjadwal. Vault tidak memiliki masa berlaku atau versioning. Tidak ada informasi yang menunjukkan bahwa kredensial belum diubah selama dua tahun.
  • Aplikasi itu sendiri memerlukan secret saat runtime. Service yang membaca kata sandi database saat boot seharusnya tidak membacanya dari repository deployment.

Selanjutnya, polanya dibalik. Ansible berhenti menyimpan secret dan mulai mengambilnya saat runtime melalui lookup plugin, dari HashiCorp Vault (produk yang berbeda dengan nama yang membingungkan), secret manager milik penyedia cloud, atau keyring pada control machine. Repository menyimpan path, store menyimpan nilainya, dan store menyimpan access log. Untuk tim kecil, password manager yang di-host sendiri dan memiliki API, seperti server Vaultwarden, dapat menjalankan fungsi yang sama dalam skala yang lebih kecil.

Satu kredensial tetap berada di luar semua ini. SSH key yang digunakan control machine untuk terhubung ke server bukan masalah vault, karena Ansible memerlukannya sebelum play apa pun dapat dijalankan. Kelola SSH key tersebut dengan agent dan passphrase, seperti yang dijelaskan dalam dasar-dasar pengelolaan SSH key.

FAQ

Haruskah saya mengenkripsi seluruh file vars atau hanya string rahasia?

Enkripsi seluruh file jika isinya hanya rahasia, karena satu perintah dapat merotasi semuanya dan strukturnya tetap sederhana. Gunakan ansible-vault encrypt_string jika rahasia berada bersama variabel biasa, karena dalam diff hanya nilai terenkripsi yang berubah dan peninjau dapat melihat variabel mana yang diubah. Konsekuensinya adalah proses rotasi. ansible-vault rekey mencakup seluruh file dan membiarkan blok !vault inline apa adanya, sehingga blok tersebut harus dibuat ulang secara manual dengan password baru.

Di mana file password Ansible Vault harus disimpan?

Simpan di luar repository, dengan mode 0600, pada path seperti ~/.ansible/vault-prod.txt. Tunjukkan file tersebut dengan --vault-password-file, atau atur vault_password_file di bawah [defaults] dalam ansible.cfg, atau atur ANSIBLE_VAULT_PASSWORD_FILE di environment. Dalam CI, minta job menulis password dari penyimpanan kredensialnya sendiri ke file sementara, mengekspor variabel tersebut, lalu menghapus file saat job selesai. Jika file dapat dieksekusi, Ansible menjalankannya dan membaca password dari standard output. Dengan demikian, password dapat diambil dari keyring tanpa menyimpannya di disk.

Bagaimana cara menggunakan password vault yang berbeda untuk staging dan production?

Beri setiap password sebuah label dengan --vault-id staging@/path/to/file dan --vault-id prod@/path/to/file, lalu enkripsi file setiap environment menggunakan labelnya sendiri. Teruskan kedua ID saat runtime, atau cantumkan keduanya dalam vault_identity_list di bawah [defaults]. Secara default, Ansible mencoba setiap secret yang dimilikinya sampai salah satunya dapat mendekripsi file. Atur vault_id_match = True jika Anda ingin Ansible hanya mencoba secret yang labelnya cocok dengan header file. Jika beberapa ID telah dimuat, pilih ID yang digunakan untuk mengenkripsi dengan --encrypt-vault-id.

Apakah Ansible Vault mencegah password muncul dalam output eksekusi?

Tidak. Vault hanya melindungi secret saat tersimpan di repository. Setelah task dijalankan, nilainya menjadi plaintext, dan eksekusi verbose atau task yang gagal dapat memasukkannya ke dalam log. Tambahkan no_log: true ke setiap task yang menangani kredensial, atur mode dan owner yang ketat pada file apa pun yang dibuat menggunakan template, dan hindari meneruskan secret sebagai argumen perintah karena argumen tersebut terlihat dalam daftar proses di host target selama perintah berjalan.