Ansible + InSpec — Compliance Testing for Infrastructure
Introduction
Chef InSpec is an open-source compliance testing framework that verifies infrastructure state against security policies. While Ansible configures systems, InSpec validates that the configuration is correct — checking file permissions, running services, open ports, kernel parameters, and compliance with CIS benchmarks or STIG profiles.
Together, Ansible + InSpec creates a configure → verify loop for compliance automation.
Install InSpec
---
- name: Install InSpec on control node
hosts: localhost
connection: local
tasks:
- name: Download InSpec package
ansible.builtin.get_url:
url: "https://packages.chef.io/files/stable/inspec/6.8.1/ubuntu/22.04/inspec_6.8.1-1_amd64.deb"
dest: /tmp/inspec.deb
when: ansible_os_family == "Debian"
- name: Install InSpec
ansible.builtin.apt:
deb: /tmp/inspec.deb
become: true
when: ansible_os_family == "Debian"
- name: Accept InSpec license
ansible.builtin.command:
cmd: inspec --chef-license=accept
changed_when: false
Basic InSpec Profile
# profiles/linux-baseline/controls/os.rb
control 'os-01' do
impact 1.0
title 'Ensure SSH root login is disabled'
desc 'Root should not be able to log in directly via SSH'
describe sshd_config do
its('PermitRootLogin') { should eq 'no' }
end
end
control 'os-02' do
impact 0.7
title 'Ensure password authentication is disabled'
describe sshd_config do
its('PasswordAuthentication') { should eq 'no' }
end
end
control 'os-03' do
impact 1.0
title 'Ensure firewall is active'
describe service('firewalld') do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
end
control 'os-04' do
impact 0.5
title 'Ensure NTP is configured'
describe service('chronyd') do
it { should be_enabled }
it { should be_running }
end
describe command('chronyc tracking') do
its('exit_status') { should eq 0 }
end
end
Run InSpec Against Ansible-Managed Hosts
---
- name: Run InSpec compliance checks
hosts: all
become: true
vars:
inspec_profile: "profiles/linux-baseline"
inspec_report_dir: "/tmp/inspec-reports"
tasks:
- name: Create report directory
ansible.builtin.file:
path: "{{ inspec_report_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
run_once: true
- name: Run InSpec profile against remote hosts
ansible.builtin.command:
cmd: >
inspec exec {{ inspec_profile }}
-t ssh://{{ ansible_user }}@{{ ansible_host }}
-i {{ ansible_ssh_private_key_file }}
--reporter json:{{ inspec_report_dir }}/{{ inventory_hostname }}.json
cli
delegate_to: localhost
register: inspec_result
failed_when: false
changed_when: false
- name: Report compliance status
ansible.builtin.debug:
msg: >
{{ inventory_hostname }}:
{{ 'PASS' if inspec_result.rc == 0 else 'FAIL' }}
(exit code: {{ inspec_result.rc }})
CIS Benchmark Profile
# profiles/cis-linux/controls/filesystem.rb
control 'cis-1.1.1' do
impact 1.0
title 'Ensure /tmp is a separate partition'
describe mount('/tmp') do
it { should be_mounted }
end
end
control 'cis-1.1.2' do
impact 1.0
title 'Ensure noexec option on /tmp'
describe mount('/tmp') do
its('options') { should include 'noexec' }
its('options') { should include 'nosuid' }
its('options') { should include 'nodev' }
end
end
control 'cis-1.4.1' do
impact 1.0
title 'Ensure permissions on bootloader config'
describe file('/boot/grub2/grub.cfg') do
its('mode') { should cmp '0600' }
its('owner') { should eq 'root' }
its('group') { should eq 'root' }
end
end
control 'cis-5.2.1' do
impact 1.0
title 'Ensure SSH Protocol is 2'
describe sshd_config do
its('Protocol') { should cmp 2 }
end
end
Configure → Verify Workflow
---
# Step 1: Configure with Ansible
- name: Harden servers
hosts: all
become: true
roles:
- ssh-hardening
- firewall
- ntp
- audit-rules
# Step 2: Verify with InSpec
- name: Compliance verification
hosts: all
tasks:
- name: Run CIS benchmark checks
ansible.builtin.command:
cmd: >
inspec exec profiles/cis-linux
-t ssh://{{ ansible_user }}@{{ ansible_host }}
-i {{ ansible_ssh_private_key_file }}
--reporter json:/tmp/compliance/{{ inventory_hostname }}.json
delegate_to: localhost
register: compliance
failed_when: false
changed_when: false
- name: Fail if critical controls failed
ansible.builtin.assert:
that:
- compliance.rc == 0
fail_msg: "Compliance check failed on {{ inventory_hostname }}"
Supermarket Profiles
# Use community profiles from Chef Supermarket
inspec supermarket profiles
# CIS benchmarks
inspec exec https://github.com/dev-sec/linux-baseline -t ssh://user@host
# DevSec hardening baselines
inspec exec https://github.com/dev-sec/ssh-baseline
inspec exec https://github.com/dev-sec/cis-docker-benchmark
CI/CD Integration
# .github/workflows/compliance.yml
name: Compliance Check
on:
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6am
jobs:
compliance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install InSpec
run: |
curl -L https://omnitruck.chef.io/install.sh | sudo bash -s -- -P inspec
inspec --chef-license=accept
- name: Run compliance checks
run: |
ansible-playbook playbooks/compliance-check.yml \
-i inventories/production/hosts \
--vault-password-file .vault_pass
- name: Upload reports
uses: actions/upload-artifact@v4
with:
name: compliance-reports
path: /tmp/compliance/*.json
Troubleshooting
# Verify InSpec can connect
inspec detect -t ssh://user@host
# Run single control
inspec exec profiles/cis-linux --controls cis-1.1.1 -t ssh://user@host
# Check profile syntax
inspec check profiles/cis-linux
# List available resources
inspec shell -t ssh://user@host
> help resources
Related Articles
- Ansible Compliance as Code
- Ansible + Testinfra
- Ansible Molecule Testing
- Ansible SSH Hardening
- Ansible assert Module
Conclusion
InSpec complements Ansible perfectly — Ansible enforces desired state, InSpec validates it. Use community CIS/STIG profiles for industry-standard compliance checks, custom controls for application-specific requirements, and CI/CD pipelines for continuous compliance monitoring.