Ringkasan

Catatan ini menjelaskan cara meng-hardening server Rocky Linux 9 secara otomatis menggunakan Ansible dengan rule mengikuti CIS Benchmark Level 1 / Level 2. Dilengkapi dengan Lynis sebagai tracker auditing pasca-deploy, plus integrasi CI/CD untuk enforcement di setiap provisioning. Pendekatan ini subject untuk homelab dan VPS produksi (lihat homelab-security-architecture-synthesis).

Daftar Isi

  1. Mengapa Otomasi Hardening?
  2. Prasyarat
  3. Arsitektur Playbook
  4. Implementasi Step-by-Step
  5. Role-Ansible Detail
  6. Integrasi Lynis untuk Tracking
  7. CI/CD Enforcement
  8. Verifikasi & Rollback
  9. Pitfalls
  10. Catatan Terkait

Mengapa Otomasi Hardening?

PendekatanProKontra
ManualBisa detail per-hostTidak reproducible. Drift antar server setelah 2-3 bulan.
Bash script ad-hocCepat ditulisTidak ada idempotency. State drift susah dilacak.
Ansible (rekomendasi)Idempotent, versioned di git, declarativeButuh initial learning curve
Config Management lengkap (Puppet/Salt)Powerful, real-timeOverkill untuk ≤ 50 server. Setup cost besar.

Untuk skala VPS-prod (≤ 10 instance) + homelab, Ansible sweet spot: setup 1-2 hari, maintain ringan.


Prasyarat

KomponenVersiCatatan
Rocky Linux9.x (latest)Target host. RHEL-9 clone.
Ansible Core2.15+Control node (laptop / CI-runner).
Python3.9+Default di Rocky 9.
SSH accesskey-based, sudo NOPASSWDUntuk user provisioner.
Lynis3.0+Audit tool. Install via dnf install lynis atau GitHub release.
git2.x+Version control untuk playbook.

Control node: Bisa laptop Fedora 44 (cara lihat podman-networking-ufw) atau container khusus. Wajib satu network dengan target host (atau pakai ansible_ssh_common_args melalui bastion).


Arsitektur Playbook

ansible-hardening-rocky/
├── ansible.cfg                 # Default inventory + remote_user
├── inventory/
│   ├── production.ini          # VPS prod (multimediasolusiprima, dll)
│   └── homelab.ini             # Internal lab
├── group_vars/
│   ├── all.yml                 # Variabel global
│   ├── cis_level1.yml          # Override untuk level 1 server
│   └── cis_level2.yml          # Override untuk high-security
├── roles/
│   ├── cis_baseline/           # Rule wajib (L1)
│   ├── cis_hardened/           # Aturan agresif (L2)
│   ├── auditd_stig/            # Audit daemon + STIG rules
│   ├── ssh_hardening/          # SSH config secure
│   ├── firewall_base/          # Firewalld rules
│   ├── account_lockdown/       # Pam_faillock, password policy
│   └── lynis_tracker/          # Audit berkala + report diff
├── playbooks/
│   ├── site.yml                # Entry point: apply seluruh roles
│   ├── dryrun.yml              # ansible-playbook --check
│   └── audit_only.yml          # Hanya jalankan Lynis, no change
├── files/
│   ├── sshd_config.hardened
│   ├── password-quality.conf
│   └── audit.rules.stig
└── templates/
    └── issue.j2                # MOTD + legal warning

Pattern ini dipinjam dari dev-sec/ansible-collection-hardening (open source, MIT) tapi ditulis ulang agar familiar dengan struktur vault: setiap role punya README.md + testify dir.


Implementasi Step-by-Step

Step 1 — Setup Control Node

# Install ansible-core + dependencies di laptop Fedora
sudo dnf install ansible-core python3-jinja2 python3-pyyaml openssh-clients
 
# Verify
ansible --version  # Harus 2.15+

Step 2 — Bootstrap SSH Key ke Target

# Asumsi host sudah punya user dengan sudo NOPASSWD
ssh-copy-id -i ~/.ssh/id_ed25519.pub provisioner@<target-ip>
 
# Test ansible ping
ansible all -i inventory/production.ini -m ping
# Expected: "pong" / success

Step 3 — Tulis Inventory Minimal

# inventory/production.ini
[vps_prod]
multimediasolusiprima ansible_host=206.237.97.19 ansible_user=provisioner
vps2-kuldi ansible_host=185.111.159.98 ansible_user=provisioner
 
[vps_prod:vars]
ansible_become=true
ansible_become_method=sudo
ansible_python_interpreter=/usr/bin/python3

Step 4 — First Run: Dry-Run

# Lihat apa yang akan berubah TANPA eksekusi
ansible-playbook -i inventory/production.ini playbooks/dryrun.yml --check --diff
 
# Output diff untuk sshd_config, pam.d/, firewalld, dll.
# Review, lalu...

Step 5 — Apply Level 1

ansible-playbook -i inventory/production.ini playbooks/site.yml --tags cis_level1

Step 6 — Verify dengan Lynis

ansible-playbook -i inventory/production.ini playbooks/audit_only.yml
# Output: laporan Lynis per host, disimpan di artifacts/lynis-<host>-<timestamp>.txt

Role-Ansible Detail

RoleTujuanTags
cis_baselineCIS Level 1: partition, AIDE, disable unused services, sysctlcis_l1
cis_hardenedCIS Level 2: SELinux enforcing, MAC, AppArmor strictcis_l2
auditd_stigAudit daemon + 50+ STIG rules dari DISA STIG profilestig
ssh_hardeningsshd_config: no root login, no password auth, MaxAuthTries 3, …ssh
firewall_baseFirewalld: default deny, allow 22/80/443 onlyfw
account_lockdownpam_faillock (10 fail → lock 15 min), password minlen 14, history 5pam
lynis_trackerCron weekly Lynis scan + diff vs baselineaudit

Contoh: Role ssh_hardening

# roles/ssh_hardening/tasks/main.yml
- name: Backup sshd_config
  ansible.builtin.copy:
    src: /etc/ssh/sshd_config
    dest: /etc/ssh/sshd_config.backup-{{ ansible_date_time.date }}
    remote_src: true
 
- name: Deploy hardened sshd_config
  ansible.builtin.template:
    src: sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: "0600"
  notify: restart_sshd
 
- name: Regenerate host keys (ed25519 only)
  ansible.builtin.command: ssh-keygen -q -N "" -t ed25519 -f /etc/ssh/ssh_host_ed25519_key
  args:
    creates: /etc/ssh/ssh_host_ed25519_key
# roles/ssh_hardening/templates/sshd_config.j2
Port {{ ssh_port | default(22) }}
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries {{ ssh_max_auth_tries | default(3) }}
ClientAliveInterval {{ ssh_client_alive_interval | default(300) }}
ClientAliveCountMax {{ ssh_client_alive_count | default(2) }}
AllowGroups {{ ssh_allow_groups | default('ssh-users') }}

Integrasi Lynis untuk Tracking

# roles/lynis_tracker/tasks/main.yml
- name: Ensure lynis installed
  ansible.builtin.package:
    name: lynis
    state: present
 
- name: Run weekly Lynis audit
  ansible.builtin.cron:
    name: "lynis-weekly"
    minute: "30"
    hour: "4"
    weekday: "0"
    user: root
    job: >
      /usr/bin/lynis audit system --quiet --no-colors
      --report-file /var/log/lynis-report-$(date +\%Y\%m\%d).txt
      --logfile /var/log/lynis.log
  notify: lynis_audit_done
 
- name: Diff vs baseline
  ansible.builtin.shell: |
    lynis_baseline="/srv/lynis/baseline.txt"
    current="/var/log/lynis-report-$(date +%Y%m%d).txt"
    diff -u "$lynis_baseline" "$current" | tee /srv/lynis/diff-$(date +%Y%m%d).txt
  register: lynis_diff
  changed_when: "'Hardening index' in lynis_diff.stdout"

Baseline awal: Jalankan lynis audit system manual sekali, copy report-nya ke /srv/lynis/baseline.txt di semua host. Diff mingguan tunjukkan drift.


CI/CD Enforcement

Pendekatan principled: setiap provisioning baru (VPS spin-up) wajib lulus pipeline.

# .github/workflows/hardening-check.yml (contoh)
name: Hardening Compliance Check
on:
  schedule:
    - cron: "0 6 * * 1" # Senin 06:00 UTC
 
jobs:
  lynis-audit:
    runs-on: ubuntu-latest
    steps:
      - name: Lynis against target
        uses: ssh-action@v1
        with:
          host: ${{ secrets.VPS_HOST }}
          username: provisioner
          key: ${{ secrets.VPS_KEY }}
          script: |
            sudo lynis audit system --quiet --no-colors
            HARDENING=$(grep "Hardening index" /var/log/lynis.log | tail -1 | awk '{print $NF}')
            echo "Score=$HARDENING"
            if (( $(echo "$HARDENING < 75" | bc -l) )); then
              echo "::error::Hardening score below threshold!"
              exit 1
            fi

Skor target: Lynis Hardening Index ≥ 75 (Level 1) atau ≥ 85 (Level 2).


Verifikasi & Rollback

ItemExpectedPerintah
SSH login rootGagalssh root@<host>
SSH login dengan passwordGagalTest dengan -o PreferredAuthentications=password
Lynis Hardening Index≥ 75grep "Hardening index" /var/log/lynis.log
CIS sections “OK”≥ 85%lynis show details
Auditd statusactive (running)systemctl status auditd
Firewallddefault zone ‘public’, services 22/80/443 onlyfirewall-cmd --list-all

Rollback:

# Restore dari backup sshd_config
sudo cp /etc/ssh/sshd_config.backup-$(date +%Y-%m-%d) /etc/ssh/sshd_config
sudo systemctl restart sshd
 
# Full re-provision via playbook idempotent (re-run akan menyamakan state)
ansible-playbook -i inventory/production.ini playbooks/site.yml

Pitfalls

  1. Lock-out karena SSH hardening. Jika salah setting AllowGroups atau PasswordAuthentication no tapi user belum ada SSH key → terkunci. Solusi: selalu --check dulu + buka konsol VPS sebelum apply.
  2. SELinux enforcing di production. Bisa block service yang tidak expecting. Test di staging dulu, atau pakai permissive saat transisi, audit log, baru enforce.
  3. Lynis false positives. “Suggestion” ≠ “Vulnerability”. Beberapa suggestion Lynis seperti disable IPv6 tidak applicable untuk deployment tertentu. Always review manual.
  4. State drift setelah manual edit. Admin lokal edit /etc/ssh/sshd_config langsung → besok playbook re-run overwrite. Solusi: ansible-pull cron, atau scheduled re-enforcement.
  5. become: true tanpa become_method explicit. Default sudo bisa macet di distro yang disable sudo. Set ansible_become_method=su untuk fallback.

Catatan Terkait