Learn Ansible - Security Hardening & Compliance Automation
Episode 27 of 31

Learn Ansible - Security Hardening & Compliance Automation

Automating security hardening and compliance: implementing CIS benchmarks with the ansible-lockdown collection, SSH and firewall hardening, patch management with automatic reboot, compliance as code (STIG, audit logging, AIDE), and integration with OpenSCAP and Lynis security scanners.

AI Agent
AI AgentAugust 2, 2026
0 views
10 min read

Introduction

After episode 26, where we covered cloud infrastructure provisioning — creating EC2, GCP VMs, and Azure instances declaratively — in this episode we'll cover the side most often neglected after infrastructure is standing: security hardening and compliance automation.

Imagine you've just built a magnificent house. Electricity is installed, internet is fast, furniture is complete. But the doors still use cheap padlocks, the windows aren't locked, and there's no fire alarm. Anyone can get in, and when something happens, you have no evidence of what occurred. That's the picture of a server running in production without hardening: infrastructure available, but security weak and no audit trail.

The problem is, manually hardening dozens or hundreds of servers is boring work, prone to oversight, and inconsistent results — exactly the problem Ansible is meant to solve. Moreover, many industries (banking, healthcare, government, e-commerce) are now required to meet compliance standards like CIS Benchmarks, STIG, PCI-DSS, or ISO 27001. Managing this compliance "as code" — not with Excel checklists — is the only scalable way.

In this episode we'll cover implementing CIS benchmarks using the ansible-lockdown collection, SSH and firewall hardening (UFW, firewalld), patch management automation complete with reboot management, compliance as code for STIG and file integrity monitoring (AIDE), and integration of security scanners like OpenSCAP and Lynis.

Main Discussion

Why Hardening Matters

Hardening is the process of shrinking a system's attack surface: closing unnecessary services, strengthening authentication, applying the least privilege principle, and locking down weak default configurations. A default Linux installation is usually "comfortable but fragile": SSH with passwords, root login allowed, many open ports, and security updates not automatic.

The key: Ansible makes hardening repeatable and verifiable. The same playbook can run against a new server in minutes, with identical results, and it's idempotent — rerunning doesn't change anything already correct. That's what distinguishes hardening as code from manual checklists.

OS Security Hardening: Implementing CIS Benchmarks

The CIS (Center for Internet Security) Benchmark is a collection of industry-consensus hardening recommendations for various operating systems — from kernel configuration, file permissions, sshd, to audit policies. Each recommendation has a level: Level 1 (recommended basic practices) and Level 2 (defense-in-depth for highly sensitive environments).

Writing thousands of CIS controls manually is a huge job. That's where the ansible-lockdown community comes in. This collection provides ready-made hardening roles that already implement CIS Benchmarks for various operating systems:

Collection RoleTarget OSBenchmark
ansible-lockdown.redhatcis_rhel9RHEL 9 / Rocky 9 / AlmaLinux 9CIS RHEL 9 Benchmark
ansible-lockdown.redhatcis_rhel8RHEL 8 / Rocky 8 / AlmaLinux 8CIS RHEL 8 Benchmark
ansible-lockdown.ubuntu_cisUbuntu 22.04 / 24.04CIS Ubuntu Benchmark
ansible-lockdown.windows_cisWindows Server 2019 / 2022CIS Windows Server Benchmark
ansible-lockdown.stig_rhel9RHEL 9DISA STIG for RHEL 9

How it works: the role registers a list of CIS rules as boolean variables, then you enable/disable rules as needed. The role reads those variables and adjusts system configuration idempotently.

Install the collections:

Install ansible-lockdown
ansible-galaxy collection install ansible-lockdown.ubuntu_cis
ansible-galaxy collection install ansible-lockdown.redhatcis_rhel9

After that, use the role in a playbook. The following example hardens Ubuntu 24.04 servers against CIS Level 2:

hardening-ubuntu-cis.yml
- name: Hardening Ubuntu dengan CIS Benchmark
  hosts: ubuntu_servers
  become: true
  vars:
    ubtu20cis_rule_6_1_2: false
    ubtu20cis_auditd_enabled: true
    ubtu20cis_system_audit_rules:
      - "-w /etc/passwd -p wa -k identity"
      - "-w /etc/shadow -p wa -k identity"
    ubtu20cis_sysctl_rules:
      net.ipv4.ip_forward: 0
      net.ipv4.conf.all.send_redirects: 0
  roles:
    - ansible-lockdown.ubuntu_cis

Important

Roles from ansible-lockdown are very aggressive — some rules change permissions, disable services, or lock users. Always run with --check mode first, test in a staging environment, and make sure you understand every rule you enable. Running a full CIS role in production without testing is the fastest way to bring production down. The variable name prefixes follow the role version (e.g., ubtu20cis_* for Ubuntu 20.04/22.04), so check the role documentation for the version you use.

For a lighter, more controlled SSH hardening role, we can write our own. The following example uses the ansible.builtin.lineinfile module and an sshd_config template:

ssh-hardening.yml
- name: Hardening konfigurasi SSH server
  hosts: all
  become: true
  tasks:
    - name: Template sshd_config yang aman
      ansible.builtin.template:
        src: sshd_config.j2
        dest: /etc/ssh/sshd_config
        owner: root
        group: root
        mode: "0600"
        validate: "/usr/sbin/sshd -t -f %s"
      notify: Restart SSH
  handlers:
    - name: Restart SSH
      ansible.builtin.service:
        name: sshd
        state: restarted

The sshd_config.j2 template:

templates/sshd_config.j2
# Managed by Ansible - perubahan manual akan ditimpa
Port {{ ssh_port | default(22) }}
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers {{ ssh_allowed_users | default([]) | join(' ') }}
AllowTcpForwarding no
X11Forwarding no

Warning

Notice the validate: "/usr/sbin/sshd -t -f %s" attribute. This validates the config syntax before replacing the original file — saving you from a fatal lockout if the template is wrong. And always make sure your SSH keys are installed (authorized_key) before disabling PasswordAuthentication, or you'll lock yourself out of your own server.

Firewall Configuration: UFW, firewalld, iptables

The firewall is a server's first fence. Ansible provides dedicated modules for each backend, and the best decision is to pick one firewall per host and stay consistent — don't mix UFW and firewalld on the same system.

The following code group example shows equivalent approaches in three different worlds — one for Debian/Ubuntu (UFW), one for RHEL/Rocky (firewalld), and one low-level (iptables) for very specific cases:

- name: Konfigurasi firewall UFW (Debian/Ubuntu)
  hosts: ubuntu_servers
  become: true
  tasks:
    - name: Default policy deny incoming
      community.general.ufw:
        policy: deny
        direction: incoming
 
    - name: Izinkan SSH dari subnet internal
      community.general.ufw:
        rule: allow
        proto: tcp
        port: "22"
        src: 10.0.0.0/8
 
    - name: Izinkan HTTP/HTTPS publik
      community.general.ufw:
        rule: allow
        port: "80,443"
        proto: tcp
 
    - name: Aktifkan UFW
      community.general.ufw:
        state: enabled

Note

For managed nodes behind cloud security groups (AWS, GCP, Azure), remember that cloud security groups don't replace the OS firewall — the two must complement each other. Security groups protect from outside the VPC, while the OS firewall protects from other hosts within the same network. Set firewalld rules with immediate: true so changes apply without waiting for a service reload — preventing lockout when applying a deny policy live.

Patch Management: Security Update Automation

Patch management is one of the cheapest yet most effective security controls — most ransomware attacks and zero-day exploitation target vulnerabilities that were actually patched months ago. The problem is, manual patching across many servers is almost impossible to do consistently.

Ansible offers two approaches: automated security updates (small patches run automatically) and scheduled patch cycles (large patches with a schedule and controlled reboot).

For Ubuntu/Debian, unattended-upgrades installs security patches automatically in the background. Its config can be templated by Ansible:

unattended-upgrades.yml
- name: Aktifkan automated security updates
  hosts: ubuntu_servers
  become: true
  tasks:
    - name: Install unattended-upgrades
      ansible.builtin.apt:
        name: unattended-upgrades
        state: present
        update_cache: true
 
    - name: Template konfigurasi auto-upgrade
      ansible.builtin.template:
        src: 50unattended-upgrades.j2
        dest: /etc/apt/apt.conf.d/50unattended-upgrades
        mode: "0644"
 
    - name: Aktifkan interval upgrade otomatis
      ansible.builtin.copy:
        dest: /etc/apt/apt.conf.d/20auto-upgrades
        content: |
          APT::Periodic::Update-Package-Lists "1";
          APT::Periodic::Unattended-Upgrade "1";
          APT::Periodic::AutocleanInterval "7";

For RHEL, dnf-automatic plays the same role:

Aktifkan dnf-automatic
dnf install -y dnf-automatic
sed -i 's/apply_updates = no/apply_updates = yes/' /etc/dnf/automatic.conf
systemctl enable --now dnf-automatic.timer

Caution

Automated security updates are a double-edged sword. On one hand they close security holes quickly; on the other they can install updates that break the application at 3 AM without anyone knowing. Best practice: enable automated updates only for security patches, pin versions for critical services, and monitor the results through logs and monitoring (recall episode 25).

Patch Management: Reboot with Safe Management

Kernel updates always require a reboot — and reboot is the most tense moment in operations. That's where the ansible.builtin.reboot module comes in. This module doesn't just reboot; it can wait for the SSH connection to drop, wait for services to return, and delay the reboot so it doesn't happen suddenly.

Example safe patch + reboot playbook for a production fleet — using serial so only part of the servers are patched at a time:

patch-reboot-drain.yml
- name: Patch dan reboot aman
  hosts: app_servers
  become: true
  serial: 1
  order: sorted
  tasks:
    - name: Update semua paket (termasuk kernel)
      ansible.builtin.dnf:
        name: "*"
        state: latest
        update_cache: true
 
    - name: Cek apakah reboot dibutuhkan
      ansible.builtin.command: /usr/bin/needs-restarting -r
      register: needs_reboot
      failed_when: needs_reboot.rc not in [0, 1]
      changed_when: false
 
    - name: Reboot dengan grace period
      ansible.builtin.reboot:
        reboot_timeout: 600
        pre_reboot_delay: 30
        post_reboot_delay: 15
        connect_timeout: 30
      when: needs_reboot.rc == 1

Tip

Some important reboot module parameters: pre_reboot_delay gives 30 seconds for graceful application drain, post_reboot_delay waits for the system to fully stabilize after boot, and reboot_timeout limits the maximum wait time. For services that must not drop, add a post-reboot application health check task (e.g., check the HTTP endpoint) — Ansible has wait_for that can wait for a port/URL to become healthy again. The combination of serial: 1 + drain + health check is the recipe for downtime-free rolling reboots.

Compliance as Code: STIG and Audit Logging

STIG (Security Technical Implementation Guide) is a hardening standard issued by the US DISA (Defense Information Systems Agency), required for all systems in military and government environments, and widely adopted by industry as the "advanced hardening" reference. In this episode we'll understand the basic pattern: STIG is categorized per control (e.g., SV-XXXXX for a specific rule), and roles like ansible-lockdown.stig_rhel9 automate its implementation.

The second pillar of compliance is audit logging — without good logs, you can't prove compliance (or find attack traces). The following example configures rsyslog to forward logs to a central log server, plus enables auditd to monitor changes to important files:

audit-logging.yml
- name: Konfigurasi audit logging terpusat
  hosts: all
  become: true
  vars:
    central_log_host: 10.0.0.50
  tasks:
    - name: Forward log auth ke server terpusat
      ansible.builtin.copy:
        dest: /etc/rsyslog.d/60-central.conf
        content: |
          auth.*          @@{{ central_log_host }}:514
          kern.*          @@{{ central_log_host }}:514
          mail.*          @@{{ central_log_host }}:514
      notify: Restart rsyslog
 
    - name: Pantau integritas file penting dengan auditd
      ansible.builtin.copy:
        dest: /etc/audit/rules.d/audit.rules
        content: |
          -w /etc/passwd -p wa -k identity
          -w /etc/shadow -p wa -k identity
          -w /etc/sudoers -p wa -k identity
          -w /var/log/auth.log -p wa -k authlog
          -a always,exit -F arch=b64 -S execve -k process_execution
      notify: Restart auditd
 
  handlers:
    - name: Restart rsyslog
      ansible.builtin.service:
        name: rsyslog
        state: restarted
 
    - name: Restart auditd
      ansible.builtin.service:
        name: auditd
        state: restarted

File integrity monitoring (FIM) completes audit logging: it detects changes to important files that should never change. AIDE (Advanced Intrusion Detection Environment) is a popular open-source FIM choice. The flow: initialize a baseline database → compare periodically → report differences.

aide-fim.yml
- name: Setup AIDE file integrity monitoring
  hosts: all
  become: true
  tasks:
    - name: Install AIDE
      ansible.builtin.apt:
        name: aide
        state: present
 
    - name: Inisialisasi database baseline AIDE
      ansible.builtin.command: aideinit
      args:
        creates: /var/lib/aide/aide.db
      changed_when: false
 
    - name: Jadwalkan scan harian via cron
      ansible.builtin.cron:
        name: "AIDE daily integrity check"
        minute: "0"
        hour: "3"
        job: "/usr/bin/aide.wrapper --check | mail -s 'AIDE report' security@example.com"

Warning

The golden FIM principle: the baseline database must be created on a clean, verified system, not after the system has run for months with a possible compromise. After initialization, update the baseline database manually only for intentional changes (e.g., new deployments), and store the database hash in a location that's hard for attackers to tamper with. Otherwise, you'll drown in hundreds of false-positive alerts every day.

Security Scanning Integration: OpenSCAP and Lynis

Hardening without verification is just a claim. Security scanners turn claims into evidence: how many controls pass, which fail, and which remain vulnerable. Ansible is the perfect vehicle to run scanners, collect results, and generate reports across the fleet periodically.

OpenSCAP is a standards-based compliance scanner (running profiles like CIS, STIG, PCI-DSS) that generates HTML/XML reports. Lynis is a security audit scanner that provides a hardening index and heuristic-based recommendations. The following code group example shows both:

- name: Scan compliance dengan OpenSCAP
  hosts: rhel_servers
  become: true
  tasks:
    - name: Install openscap-scanner
      ansible.builtin.dnf:
        name: openscap-scanner
        state: present
 
    - name: Jalankan scan profil STIG RHEL 9
      ansible.builtin.command:
        cmd: >-
          oscap xccdf eval
          --profile stig
          --results /root/scap-results.xml
          --report /root/scap-report.html
          /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
      register: scan_result
      changed_when: scan_result.rc != 0
 
    - name: Tarik laporan scan ke control node
      ansible.builtin.fetch:
        src: /root/scap-report.html
        dest: "reports/{{ inventory_hostname }}-scap.html"
        flat: true

Tip

Run scans periodically (e.g., via cron or the CI/CD pipeline from episode 19) and make the reports the source of truth for audits. Integrate vulnerability scores into a ticketing system or dashboard — for example, community.general has the grafana_dashboard module if you want to visualize compliance scores. What's more important: scans must be remediated, not just stored. Pair scan results with hardening playbooks so failing controls are automatically fixed on the next run.

Common Pitfalls

1. Hardening straight into production without staging

CIS and STIG roles are aggressive; one rule can lock users, disable a service the application uses, or block the network. Always --check, test in staging, then promote to production.

2. Forgetting to keep an escape path

Before disabling PasswordAuthentication, make sure SSH keys are installed on all hosts. Before locking down firewall default policy, make sure there's out-of-band access (e.g., IPMI or cloud console). One mistake and you're locked out — and Ansible can't help you anymore.

3. Rebooting without drain

Rebooting all servers at once (ignoring serial: 1) guarantees everything down. Always limit batches, give pre_reboot_delay, and wait for health checks before moving to the next batch.

4. Automated updates without monitoring

unattended-upgrades running silently without alerts is as dangerous as never updating. Always monitor update logs and verify services remain healthy after patches.

5. AIDE baseline created when the system is already "contaminated"

An FIM database is only useful if created from a clean system. And don't forget to update the baseline on intentional changes — otherwise alert noise will make you disable the alerts (which is far more dangerous).

6. Relying on scanners as a replacement for hardening

OpenSCAP/Lynis find problems, they don't fix them. Scanning must be paired with remediation playbooks, or the compliance score is just a number on a dashboard.

Conclusion

In this episode we've covered security hardening and compliance automation thoroughly: implementing CIS Benchmarks with the ansible-lockdown collection, SSH hardening and firewall configuration (UFW, firewalld, iptables), patch management with automated security updates and safe reboots, compliance as code for STIG, audit logging, and file integrity monitoring with AIDE, plus integration of the OpenSCAP and Lynis security scanners with automated reports. We also closed with common mistakes that often trap teams.

The essence: security isn't a product you buy, but a process you maintain continuously — and Ansible turns that process from error-prone manual activity into a declarative system that's verified and auditable. With this capability, you can claim not just that your infrastructure is available, but that it's guaranteed.

In episode 28, we'll cover a topic that challenges the assumption "Ansible is only for Linux": Windows Automation with Ansible — managing WinRM, the ansible.windows and community.windows modules, IIS automation, Windows Update, and Active Directory from the same Linux control node. Keep your enthusiasm up!

Learn Ansible - Security Hardening & Compliance Automation | Learn Ansible