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

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.