Security Anti Pattern

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_checking di production — gunakan StrictHostKeyChecking=accept-new sebagai kompromi yang lebih aman untuk environment ephemeral.
  • Prinsip least privilege untuk become — jangan set become: true di level play jika hanya beberapa task yang butuh privilege. Set become: true hanya di task yang memang memerlukannya.
  • Sudoers yang spesifik — izinkan hanya perintah yang benar-benar dibutuhkan Ansible, bukan ALL=(ALL) NOPASSWD: ALL.
  • no_log: true untuk semua task yang melibatkan credential — lebih baik kehilangan visibilitas task daripada mengekspos secret di log.
  • Validasi semua input dari -e dengan assert di pre_tasks — jangan pernah menggunakan input external langsung di hosts: atau command: tanpa validasi.

← Sebelumnya: Variable Anti Pattern   Berikutnya: Performance Anti Pattern →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact