Skip to content
CloudOps
Newsletter
All guides
AI for Ansible By James Joyner IV · · 11 min read

Generating a CIS Linux-Hardening Ansible Playbook With AI and Verifying It

Use AI to draft a CIS/STIG Ansible hardening playbook for SSH, sysctl, auditd and password policy, then verify it with OpenSCAP before you lock yourself out.

  • #ansible
  • #ai
  • #cis
  • #hardening
  • #openscap

The first time I ran an AI-generated hardening playbook against a fresh OpenStack control node, it set PermitRootLogin no, switched PasswordAuthentication off, and dropped a sshd_config that referenced a key directory I hadn’t populated yet. The play finished green. Then sshd reloaded, my session died, and I had to drive to the console because that box only had key auth for a user that didn’t exist. Nobody got paged because it was a lab. But that 20-minute lesson is the whole point of this post: an AI will happily write you a CIS benchmark in thirty seconds, and it will just as happily lock you out of the machine you’re hardening. The model drafts; you verify and stay in control.

Why AI is actually good at this

CIS and STIG benchmarks are long, tedious, and extremely well-documented. That combination is exactly where a language model shines. It has seen thousands of sysctl lines, auditd rules, and sshd_config directives, and it can map them onto Ansible modules without you flipping between four PDF tabs. Asking it to “write a task to set net.ipv4.conf.all.rp_filter to 1” is a question it answers correctly and instantly.

What it is not good at is knowing your environment. It doesn’t know that your jump host uses a non-standard SSH port, that your monitoring agent needs sysctl knobs the benchmark wants closed, or that your auditd rules will fill a 2 GB partition in a week. So I treat AI output as a strong first draft from a smart colleague who has never logged into my fleet. Decode it, review it, then run it behind a wall of safety checks. If you want a head start on the wording, the prompt library has the hardening prompts I reuse.

A prompt that produces reviewable output

Vague prompts produce vague YAML. The prompt that works for me names the benchmark, the modules, and — critically — the safety constraints up front:

Write an Ansible task file for Ubuntu 22.04 that hardens SSH per CIS Benchmark section 5.2. Use the ansible.builtin.lineinfile module against /etc/ssh/sshd_config. For every task, add a comment with the CIS control number it maps to. Do NOT disable password authentication or restart sshd in the task — only validate config with sshd -t. Use a handler for the reload so it runs once at the end.

That last sentence is what keeps you logged in. The model will respect it, and you get tasks you can read against the benchmark line by line.

SSH hardening — the part that locks you out

SSH is where most lockouts happen, so it gets the most paranoia. Here’s a representative chunk of what I keep after editing the AI draft. Note the validate step and the handler — the config is checked before it’s ever live, and the reload is deferred.

- name: Harden sshd_config per CIS 5.2
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
    state: present
    validate: "/usr/sbin/sshd -t -f %s"   # refuse to write a broken config
  loop:
    - { regexp: '^#?PermitRootLogin',        line: 'PermitRootLogin no' }          # CIS 5.2.8
    - { regexp: '^#?MaxAuthTries',           line: 'MaxAuthTries 4' }              # CIS 5.2.5
    - { regexp: '^#?ClientAliveInterval',    line: 'ClientAliveInterval 300' }     # CIS 5.2.16
    - { regexp: '^#?ClientAliveCountMax',    line: 'ClientAliveCountMax 3' }       # CIS 5.2.16
    - { regexp: '^#?LoginGraceTime',         line: 'LoginGraceTime 60' }           # CIS 5.2.17
    - { regexp: '^#?Banner',                 line: 'Banner /etc/issue.net' }       # CIS 5.2.18
  notify: reload sshd

- name: Ensure an admin user with a key exists before we tighten auth
  ansible.builtin.assert:
    that:
      - admin_user is defined
      - lookup('file', admin_pubkey_path) | length > 0
    fail_msg: "Refusing to harden SSH: no admin key configured — you'll lock yourself out."

The validate argument is the unsung hero. If the AI generated a directive with a typo, sshd -t fails and lineinfile never writes the file. The assert task is my hard stop: it refuses to proceed unless a working admin user and public key exist. I wrote more about this pattern in preflight checks in Ansible with assert and fail — it’s the single best habit for not destroying your own access.

Two more rules I never let the AI talk me out of:

  • Open a second SSH session and leave it connected while you apply changes. If the new policy kills your shell, the old session is still your way back in.
  • Don’t put the sshd reload in the same task as the config change. Use a handler. If five tasks each trigger a reload mid-run, you can reload a half-written config.

The handler is boring on purpose:

handlers:
  - name: reload sshd
    ansible.builtin.service:
      name: ssh
      state: reloaded

sysctl: kernel hardening that won’t page you

sysctl tuning is lower-risk than SSH but still worth reviewing, because some knobs break things like Kubernetes networking or your load balancer. The AI draft for the network controls looks like this:

- name: Apply CIS network sysctl settings
  ansible.posix.sysctl:
    name: "{{ item.name }}"
    value: "{{ item.value }}"
    state: present
    sysctl_file: /etc/sysctl.d/60-cis-hardening.conf
    reload: true
  loop:
    - { name: net.ipv4.conf.all.rp_filter,        value: '1' }   # CIS 3.3.7
    - { name: net.ipv4.conf.all.accept_redirects,  value: '0' }   # CIS 3.3.2
    - { name: net.ipv4.conf.all.send_redirects,    value: '0' }   # CIS 3.3.4
    - { name: net.ipv4.icmp_echo_ignore_broadcasts, value: '1' }  # CIS 3.3.5
    - { name: net.ipv4.tcp_syncookies,             value: '1' }   # CIS 3.3.9

Before you ship this to a K8s node, check net.ipv4.ip_forward. The CIS benchmark wants it 0; your CNI needs it 1. This is the kind of conflict the model can’t see and you must. Drop a comment in the playbook explaining the deviation so the next person — and the next compliance audit — knows it was deliberate, not an oversight.

Password policy and auditd

Password quality and login.defs are mostly safe to apply, but get auditd rules wrong and you’ll fill a disk. I let AI generate the rule set, then I cap the log size and review what’s being watched:

- name: Set password aging policy in login.defs
  ansible.builtin.lineinfile:
    path: /etc/login.defs
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
  loop:
    - { regexp: '^PASS_MAX_DAYS', line: 'PASS_MAX_DAYS 365' }   # CIS 5.5.1.1
    - { regexp: '^PASS_MIN_DAYS', line: 'PASS_MIN_DAYS 1' }     # CIS 5.5.1.2
    - { regexp: '^PASS_WARN_AGE', line: 'PASS_WARN_AGE 7' }     # CIS 5.5.1.3

- name: Install auditd rules
  ansible.builtin.copy:
    src: cis-audit.rules
    dest: /etc/audit/rules.d/cis.rules
    owner: root
    group: root
    mode: '0640'
  notify: restart auditd

Sanity-check the auditd rule volume on a test box first. A rule that logs every execve on a busy build server is technically CIS-compliant and operationally a denial-of-service against yourself.

Idempotency is non-negotiable

A hardening playbook gets run on a schedule, not once. The second run must report zero changes, or you don’t actually have a known state — you have a coin flip. The native modules above (lineinfile, sysctl, copy) are idempotent by design. The trap is when the AI reaches for command or shell to do something a module already handles. If you see a raw shell: running sed against sshd_config, reject it and ask for the module version. Run the play twice and watch the recap:

ansible-playbook -i inventory/production hardening.yml \
  --limit 'cis_lab' \
  --check --diff          # dry run first: see every change before it happens

# then for real, against ONE host
ansible-playbook -i inventory/production hardening.yml --limit 'node01'

Always --check --diff before a live run, and always --limit to one canary host. The diff is your last chance to catch the AI doing something you didn’t intend.

Verify with OpenSCAP — don’t trust the green run

Here’s the part people skip: a playbook finishing successfully tells you Ansible ran, not that the host is compliant. To prove compliance you scan it independently with OpenSCAP against the official SCAP content. This is the verification step that separates “I ran a script” from “I can show an auditor a report.”

# Install the scanner and the CIS profile content
sudo apt-get install -y libopenscap8 ssg-debian

# Scan against the CIS Level 1 server profile and produce an HTML report
sudo oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis_level1_server \
  --results scan-results.xml \
  --report compliance-report.html \
  /usr/share/xml/scap/ssg/content/ssg-ubuntu2204-ds.xml

Open compliance-report.html and read the failures. Some are real gaps your playbook missed. Some are controls that don’t apply to your environment, which you document as exceptions. Either way you now have an independent second opinion — the scanner doesn’t care what your playbook claimed to do. To close the loop, feed an OpenSCAP failure back to the AI (“here’s the rule ID and current state, write the Ansible task to remediate it”) and verify again. AI drafts, you apply behind safety rails, the scanner verifies, repeat — that’s the whole workflow.

The takeaway

AI turns a multi-day benchmark slog into an afternoon. But it writes the same confident YAML whether it’s hardening a host or bricking your access to it. Gate SSH changes behind asserts, validate configs before they go live, run everything in check mode against one canary, and verify with OpenSCAP instead of trusting a green play. The model is the fastest junior engineer you’ve ever worked with — and like any junior, its work ships only after you’ve reviewed it. More Ansible patterns live under the Ansible category.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.