Security Anti Pattern #
Anti pattern keamanan di Ansible berbeda dari anti pattern teknikal biasa — dampaknya bisa sangat serius. Satu secret yang bocor ke Git repository bisa mengekspos seluruh infrastruktur. host_key_checking yang dimatikan membuka peluang man-in-the-middle attack. Akun dengan privilege berlebihan memperluas blast radius jika terjadi kompromi. Artikel ini membahas kesalahan keamanan paling berbahaya yang sering ditemukan di proyek Ansible.
Anti Pattern 1: Secret di Git Repository #
Ini adalah anti pattern paling serius dan paling sering terjadi:
# ANTI-PATTERN: secret langsung di file yang masuk Git
# group_vars/all.yml
db_password: "super_secret_password_123" # BAHAYA!
api_key: "sk-live-abc123def456" # BAHAYA!
aws_secret_key: "wJalrXUtnFEMI/K7MDENG" # BAHAYA!
# ansible.cfg
[defaults]
vault_password_file = ~/.vault_pass # OK jika file ini tidak di-commit
# .gitignore yang BURUK — tidak mengecualikan vault password
# (tidak ada .gitignore atau vault_pass tidak di-exclude)
# Cek apakah sudah ada secret yang bocor ke Git history
git log --all --full-history -- "**/*secret*" "**/*password*" "**/*key*"
git grep -i "password\|secret\|api_key\|aws_" $(git log --format="%H") 2>/dev/null | head -20
# Jika ditemukan, riwayat Git harus dibersihkan (dan semua credential harus dirotasi!)
# BENAR: semua secret dienkripsi dengan Ansible Vault
# group_vars/all.yml — aman di-commit
db_user: appuser
db_host: "{{ vault_db_host }}" # Referensi ke variabel vault
# group_vars/vault.yml — dienkripsi dengan ansible-vault encrypt
vault_db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
61323534386434333533333563356439...
# .gitignore
.vault_pass
*.vault_pass
Anti Pattern 2: host_key_checking Dimatikan di Production #
# ANTI-PATTERN: mematikan SSH host key checking secara global
# ansible.cfg
[defaults]
host_key_checking = False # BAHAYA DI PRODUCTION!
# ATAU dalam task:
- name: SSH ke server
command: ssh user@server "command"
environment:
ANSIBLE_HOST_KEY_CHECKING: "False"
Mematikan host_key_checking membuat koneksi SSH rentan terhadap man-in-the-middle attack — penyerang bisa menyisipkan dirinya antara control node dan managed node tanpa terdeteksi.
# BENAR: kelola known_hosts dengan benar
# Tambahkan host key saat server pertama kali di-provision
- name: Tambahkan host key ke known_hosts
known_hosts:
name: "{{ inventory_hostname }}"
key: "{{ lookup('pipe', 'ssh-keyscan ' + inventory_hostname) }}"
state: present
delegate_to: localhost
# Untuk environment CI/CD dengan ephemeral runner:
# ansible.cfg — aktifkan hanya untuk environment tertentu, bukan global
[ssh_connection]
ssh_args = -o StrictHostKeyChecking=accept-new
# "accept-new" lebih aman dari "no" — hanya menerima key yang belum pernah dilihat,
# TIDAK menerima key yang berubah (yang bisa jadi indikasi compromise)
Anti Pattern 3: Menjalankan Semua Task sebagai Root #
# ANTI-PATTERN: become: true di level play untuk semua task
- name: Configure server
hosts: all
become: true # Semua task berjalan sebagai root!
tasks:
- name: Update konfigurasi aplikasi
template:
src: app.conf.j2
dest: /etc/app/app.conf
# Tidak semua task perlu root — ini seharusnya hanya butuh write ke /etc/app
- name: Jalankan user-level task
command: app-cli validate-config
# Task ini sama sekali tidak butuh root!
# BENAR: become hanya saat benar-benar diperlukan
- name: Configure server
hosts: all
become: false # Default: tidak pakai root
tasks:
- name: Update konfigurasi sistem (butuh root)
template:
src: sysctl.conf.j2
dest: /etc/sysctl.d/99-custom.conf
become: true
- name: Update konfigurasi aplikasi (tidak butuh root)
template:
src: app.conf.j2
dest: /home/appuser/.config/app.conf
# Tidak butuh become
- name: Reload konfigurasi sistem (butuh root)
command: sysctl --system
become: true
Anti Pattern 4: Sudoers yang Terlalu Permisif #
# ANTI-PATTERN: akun Ansible punya akses sudo tanpa password ke semua perintah
# /etc/sudoers.d/ansible
ansible ALL=(ALL) NOPASSWD: ALL # BAHAYA! Akses penuh tanpa password
# BENAR: hanya izinkan perintah yang benar-benar diperlukan
# /etc/sudoers.d/ansible
ansible ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp
ansible ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx
ansible ALL=(ALL) NOPASSWD: /usr/bin/apt-get update
ansible ALL=(ALL) NOPASSWD: /usr/bin/apt-get install -y *
Atau gunakan akun yang memang tidak perlu sudo untuk sebagian besar task, dan hanya gunakan become untuk task yang spesifik membutuhkan privilege.
Anti Pattern 5: Log yang Mengekspos Credential #
# ANTI-PATTERN: task yang menampilkan credential di log
- name: Koneksi ke database
command: >
psql postgresql://{{ db_user }}:{{ db_password }}@{{ db_host }}/{{ db_name }}
-c "SELECT 1"
# db_password muncul di log output!
- name: Set environment dengan API key
lineinfile:
path: /etc/app/env
line: "API_KEY={{ api_key }}"
# api_key muncul di diff output Ansible!
# BENAR: sembunyikan credential dari log
- name: Test koneksi database
command: pg_isready -h {{ db_host }} -p {{ db_port }} -U {{ db_user }}
# Tidak perlu password untuk pg_isready, atau gunakan .pgpass
- name: Set environment dengan API key
lineinfile:
path: /etc/app/env
line: "API_KEY={{ api_key }}"
no_log: true # Sembunyikan seluruh task dari log — termasuk diff
# Untuk task dengan command yang butuh secret:
- name: Operasi yang membutuhkan credential
command: app-cli --token {{ api_token }} action
no_log: true
register: result
- name: Tampilkan status (tanpa credential)
debug:
msg: "Operasi berhasil: {{ result.rc == 0 }}"
Anti Pattern 6: Tidak Memvalidasi Input dari User #
# ANTI-PATTERN: menerima input dari -e tanpa validasi
- name: Deploy ke server tertentu
hosts: "{{ target_host }}" # Bisa diisi dengan anything!
tasks:
- name: Jalankan perintah
command: "{{ user_command }}" # SANGAT BERBAHAYA — command injection!
# BENAR: validasi semua input external
- name: Deploy ke server
hosts: all
pre_tasks:
- name: Validasi target_host ada di inventory
assert:
that:
- target_host is defined
- target_host in groups['production']
fail_msg: "target_host '{{ target_host | default('tidak diset') }}' tidak valid atau bukan server production"
delegate_to: localhost
run_once: true
- name: Validasi versi yang diminta sesuai format semver
assert:
that:
- deploy_version is defined
- deploy_version is match('^v?[0-9]+\.[0-9]+\.[0-9]+(-.*)?$')
fail_msg: "deploy_version harus format semver, contoh: 2.1.0"
delegate_to: localhost
run_once: true
Ringkasan #
- Secret tidak pernah di-commit ke Git — gunakan Ansible Vault untuk semua nilai sensitif. Periksa riwayat Git secara berkala untuk mendeteksi kebocoran yang mungkin sudah terjadi.
- Jangan matikan
host_key_checkingdi production — gunakanStrictHostKeyChecking=accept-newsebagai kompromi yang lebih aman untuk environment ephemeral.- Prinsip least privilege untuk
become— jangan setbecome: truedi level play jika hanya beberapa task yang butuh privilege. Setbecome: truehanya di task yang memang memerlukannya.- Sudoers yang spesifik — izinkan hanya perintah yang benar-benar dibutuhkan Ansible, bukan
ALL=(ALL) NOPASSWD: ALL.no_log: trueuntuk semua task yang melibatkan credential — lebih baik kehilangan visibilitas task daripada mengekspos secret di log.- Validasi semua input dari
-edenganassertdipre_tasks— jangan pernah menggunakan input external langsung dihosts:ataucommand:tanpa validasi.
← Sebelumnya: Variable Anti Pattern Berikutnya: Performance Anti Pattern →