Introduction

Enterprise compliance isn't optional — it's audited, regulated, and fined. CIS Benchmarks, DISA STIGs, PCI-DSS, SOC 2, and HIPAA all require specific system configurations that need to be enforced consistently across hundreds or thousands of servers. Manual compliance checks don't scale; spreadsheet tracking doesn't prevent drift. Ansible turns compliance requirements into executable code: playbooks that both enforce and verify security baselines.

Compliance Frameworks

FrameworkFull NameFocusCommon Industries
CISCenter for Internet Security BenchmarksOS/application hardeningAll
STIGSecurity Technical Implementation GuideDoD security requirementsGovernment, defense
PCI-DSSPayment Card Industry Data Security StandardCardholder data protectionFinance, retail
SOC 2Service Organization Control 2Data security controlsSaaS, tech
HIPAAHealth Insurance Portability ActProtected health informationHealthcare
NIST 800-53Security and Privacy ControlsFederal information systemsGovernment

Using Community Hardening Roles

CIS Benchmark Role

# Install CIS roles from Ansible Galaxy
ansible-galaxy install ansible-lockdown.rhel8_cis
ansible-galaxy install ansible-lockdown.ubuntu2204_cis
ansible-galaxy install ansible-lockdown.windows2022_cis
---
- name: Apply CIS Level 1 benchmark to RHEL 8
  hosts: production_servers
  become: true
  vars:
    # CIS Section 1: Initial Setup
    rhel8cis_rule_1_1_1_1: true   # Disable cramfs
    rhel8cis_rule_1_1_1_2: true   # Disable squashfs
    rhel8cis_rule_1_1_1_3: true   # Disable udf
    
    # CIS Section 3: Network Configuration
    rhel8cis_rule_3_1_1: true     # Disable IPv6 (if not needed)
    rhel8cis_rule_3_2_1: true     # Disable packet redirect
    rhel8cis_rule_3_2_2: true     # Disable IP forwarding
    
    # CIS Section 5: Access, Auth, and Authorization
    rhel8cis_rule_5_2_1: true     # Configure SSH Protocol 2
    rhel8cis_rule_5_2_4: true     # Disable SSH root login
    rhel8cis_rule_5_2_11: true    # Set SSH MaxAuthTries to 4
    
    # Skip rules that break your environment
    rhel8cis_rule_1_8_1_1: false  # Skip GNOME banner (headless servers)
    rhel8cis_rule_4_2_1_1: false  # Skip rsyslog (using journald)

  roles:
    - ansible-lockdown.rhel8_cis

DISA STIG Role

ansible-galaxy install ansible-lockdown.rhel8_stig
---
- name: Apply DISA STIG to RHEL 8
  hosts: government_servers
  become: true
  vars:
    # CAT I (Critical) findings
    rhel_08_010000: true  # Operating system must be a supported release
    rhel_08_010020: true  # Must implement NIST FIPS-validated cryptography
    rhel_08_010150: true  # Must not have unnecessary accounts
    
    # CAT II (High) findings
    rhel_08_010290: true  # Must encrypt all stored passwords with SHA-512
    rhel_08_010380: true  # Must enforce minimum 15-character passwords
    rhel_08_020010: true  # Must lock account after 3 failed attempts
    
    # FIPS mode
    rhel8stig_fips_mode: true

  roles:
    - ansible-lockdown.rhel8_stig

Custom Compliance Playbooks

SSH Hardening (CIS 5.2)

---
- name: SSH hardening per CIS Benchmark
  hosts: all
  become: true
  tasks:
    - name: Configure secure SSH settings
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
        state: present
        validate: "sshd -t -f %s"
      loop:
        - { regexp: '^#?Protocol', line: 'Protocol 2' }
        - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
        - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
        - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 4' }
        - { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' }
        - { regexp: '^#?ClientAliveCountMax', line: 'ClientAliveCountMax 3' }
        - { regexp: '^#?LoginGraceTime', line: 'LoginGraceTime 60' }
        - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
        - { regexp: '^#?AllowTcpForwarding', line: 'AllowTcpForwarding no' }
        - { regexp: '^#?MaxSessions', line: 'MaxSessions 10' }
        - { regexp: '^#?PermitEmptyPasswords', line: 'PermitEmptyPasswords no' }
        - { regexp: '^#?Ciphers', line: 'Ciphers aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr' }
        - { regexp: '^#?MACs', line: 'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com' }
        - { regexp: '^#?KexAlgorithms', line: 'KexAlgorithms curve25519-sha256,diffie-hellman-group16-sha512' }
      notify: Restart sshd

    - name: Set SSH banner
      ansible.builtin.copy:
        content: |
          ═══════════════════════════════════════════════════
          WARNING: Unauthorized access to this system is prohibited.
          All activities are monitored and logged.
          ═══════════════════════════════════════════════════
        dest: /etc/issue.net
        mode: "0644"

    - name: Configure SSH banner in sshd_config
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?Banner'
        line: 'Banner /etc/issue.net'
      notify: Restart sshd

  handlers:
    - name: Restart sshd
      ansible.builtin.systemd:
        name: sshd
        state: restarted

Password Policy (CIS 5.4)

- name: Configure password policies
  hosts: all
  become: true
  tasks:
    - name: Set password aging controls
      ansible.builtin.lineinfile:
        path: /etc/login.defs
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
      loop:
        - { regexp: '^PASS_MAX_DAYS', line: 'PASS_MAX_DAYS 365' }
        - { regexp: '^PASS_MIN_DAYS', line: 'PASS_MIN_DAYS 7' }
        - { regexp: '^PASS_WARN_AGE', line: 'PASS_WARN_AGE 14' }
        - { regexp: '^PASS_MIN_LEN', line: 'PASS_MIN_LEN 15' }

    - name: Configure PAM password quality
      ansible.builtin.copy:
        content: |
          password requisite pam_pwquality.so retry=3 minlen=15 dcredit=-1 ucredit=-1 ocredit=-1 lcredit=-1 difok=8 maxrepeat=3
        dest: /etc/security/pwquality.conf.d/cis.conf
        mode: "0644"

    - name: Set account lockout policy
      ansible.builtin.copy:
        content: |
          auth required pam_faillock.so preauth deny=3 unlock_time=900 fail_interval=900
          auth required pam_faillock.so authfail deny=3 unlock_time=900 fail_interval=900
        dest: /etc/pam.d/system-auth-local
        mode: "0644"

Filesystem Hardening (CIS 1.1)

- name: Filesystem and partition hardening
  hosts: all
  become: true
  tasks:
    - name: Disable unnecessary filesystems
      ansible.builtin.copy:
        content: |
          install {{ item }} /bin/true
          blacklist {{ item }}
        dest: "/etc/modprobe.d/{{ item }}.conf"
        mode: "0644"
      loop:
        - cramfs
        - squashfs
        - udf
        - vfat
        - usb-storage

    - name: Set /tmp mount options
      ansible.posix.mount:
        path: /tmp
        src: tmpfs
        fstype: tmpfs
        opts: "defaults,nodev,nosuid,noexec,size=2G"
        state: mounted

    - name: Set sticky bit on world-writable directories
      ansible.builtin.shell: |
        df --local -P | awk '{if (NR!=1) print $6}' | xargs -I '{}' find '{}' -xdev -type d \
          \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null | head -20
      register: world_writable
      changed_when: false

    - name: Fix world-writable directories
      ansible.builtin.file:
        path: "{{ item }}"
        mode: "a+t"
      loop: "{{ world_writable.stdout_lines }}"
      when: world_writable.stdout_lines | length > 0

Compliance Audit Playbook

Check and Report (No Changes)

---
- name: CIS compliance audit — report only
  hosts: all
  become: true
  gather_facts: true
  vars:
    audit_results: []

  tasks:
    - name: Check SSH Protocol 2
      ansible.builtin.shell: sshd -T | grep -i protocol
      register: ssh_protocol
      changed_when: false
      failed_when: false

    - name: Check root login disabled
      ansible.builtin.shell: sshd -T | grep -i permitrootlogin
      register: root_login
      changed_when: false

    - name: Check password authentication
      ansible.builtin.shell: sshd -T | grep -i passwordauthentication
      register: pass_auth
      changed_when: false

    - name: Check firewall is active
      ansible.builtin.systemd:
        name: firewalld
      register: firewall_status
      failed_when: false

    - name: Check unowned files
      ansible.builtin.shell: find / -xdev -nouser -o -nogroup 2>/dev/null | head -20
      register: unowned_files
      changed_when: false

    - name: Generate compliance report
      ansible.builtin.template:
        src: compliance-report.html.j2
        dest: "/opt/reports/{{ inventory_hostname }}_cis_audit_{{ ansible_date_time.date }}.html"
      delegate_to: localhost

    - name: Send report to compliance team
      ansible.builtin.mail:
        host: smtp.internal
        to: compliance@example.com
        subject: "CIS Audit Report: {{ inventory_hostname }}"
        body: "{{ lookup('template', 'compliance-email.j2') }}"
        attach:
          - "/opt/reports/{{ inventory_hostname }}_cis_audit_{{ ansible_date_time.date }}.html"
      delegate_to: localhost

AAP Workflow for Continuous Compliance

┌─────────────┐    ┌──────────────┐    ┌───────────────┐
│ Schedule:    │───▶│ Audit        │───▶│ Findings > 0? │
│ Daily 2 AM  │    │ Playbook     │    │               │
└─────────────┘    │ (check mode) │    └──────┬────────┘
                   └──────────────┘           │
                          ▼ Yes               ▼ No
                   ┌──────────────┐    ┌───────────────┐
                   │ Remediation  │    │ Report: All   │
                   │ Playbook     │    │ Compliant ✅  │
                   │ (enforce)    │    └───────────────┘
                   └──────┬───────┘
                          │
                   ┌──────▼───────┐
                   │ Re-audit &   │
                   │ Generate     │
                   │ Report       │
                   └──────────────┘

In AAP Controller:

  1. Survey: Select environment (dev/staging/prod), CIS level (1/2), remediate (yes/no)
  2. Credentials: SSH keys from Vault, SMTP credentials for reports
  3. Schedule: Daily audit, weekly remediation
  4. Notifications: Slack/email on findings

Integrating with OSCAP

- name: Run OpenSCAP compliance scan
  hosts: all
  become: true
  tasks:
    - name: Install OpenSCAP
      ansible.builtin.package:
        name:
          - openscap-scanner
          - scap-security-guide
        state: present

    - name: Run CIS scan
      ansible.builtin.command: >
        oscap xccdf eval
        --profile xccdf_org.ssgproject.content_profile_cis
        --results /tmp/oscap-results.xml
        --report /tmp/oscap-report.html
        /usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml
      register: oscap_result
      failed_when: false
      changed_when: false

    - name: Fetch OSCAP report
      ansible.builtin.fetch:
        src: /tmp/oscap-report.html
        dest: "reports/{{ inventory_hostname }}_oscap.html"
        flat: true

Conclusion

Compliance as code with Ansible means security baselines are version-controlled, testable, and automatically enforced. Use community roles from Ansible Lockdown for CIS and STIG benchmarks, customize with variables to skip rules that conflict with your environment, run audit playbooks in check mode daily, and remediate weekly. Integrate with OpenSCAP for formal compliance scoring, and use AAP workflows for scheduled audits with approval gates. The result: continuous compliance instead of quarterly panic.