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
| Framework | Full Name | Focus | Common Industries |
|---|---|---|---|
| CIS | Center for Internet Security Benchmarks | OS/application hardening | All |
| STIG | Security Technical Implementation Guide | DoD security requirements | Government, defense |
| PCI-DSS | Payment Card Industry Data Security Standard | Cardholder data protection | Finance, retail |
| SOC 2 | Service Organization Control 2 | Data security controls | SaaS, tech |
| HIPAA | Health Insurance Portability Act | Protected health information | Healthcare |
| NIST 800-53 | Security and Privacy Controls | Federal information systems | Government |
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:
- Survey: Select environment (dev/staging/prod), CIS level (1/2), remediate (yes/no)
- Credentials: SSH keys from Vault, SMTP credentials for reports
- Schedule: Daily audit, weekly remediation
- 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
Related Articles
- Ansible Best Practices Guide
- Ansible Vault Encryption
- SELinux Configuration with Ansible
- Firewall Management with UFW
- Ansible Automation Platform Guide
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.