Tools · Python and Bash for security automation · 20 / 20
Applying a security baseline with Ansible: idempotent hardening for lab Linux hosts
Write an Ansible playbook that applies SSH hardening, a firewall, automatic updates, and audit rules, run it in check mode first, and keep it in Git as the documented baseline.
Why configuration management
Hardening by hand drifts: one server gets the change and another does not. Ansible describes the desired state in YAML and applies it over SSH, without agents. Modules are idempotent, meaning running the playbook twice changes nothing the second time, which makes it safe to run regularly and to see drift in the output.
A small baseline
The playbook below sets key SSH options, installs and configures UFW, enables unattended upgrades, and adds an audit rule. A handler restarts SSH only when its configuration changes, and validate checks the sshd configuration before replacing it.
- hosts: lab_linux
become: true
tasks:
- name: SSH hardening options
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?{{ item.key }}\s'
line: '{{ item.key }} {{ item.value }}'
validate: '/usr/sbin/sshd -t -f %s'
loop:
- { key: PermitRootLogin, value: 'no' }
- { key: PasswordAuthentication, value: 'no' }
- { key: MaxAuthTries, value: '3' }
notify: restart ssh
- name: Firewall packages
ansible.builtin.apt: { name: [ufw, unattended-upgrades, auditd], state: present, update_cache: true }
- name: Allow SSH
community.general.ufw: { rule: allow, port: '22', proto: tcp }
- name: Enable firewall with default deny incoming
community.general.ufw: { state: enabled, policy: deny, direction: incoming }
- name: Audit sudoers changes
ansible.builtin.copy:
dest: /etc/audit/rules.d/sudoers.rules
content: "-w /etc/sudoers -p wa -k sudoers_change\n-w /etc/sudoers.d/ -p wa -k sudoers_change\n"
notify: reload audit
handlers:
- name: restart ssh
ansible.builtin.service: { name: ssh, state: restarted }
- name: reload audit
ansible.builtin.command: augenrules --loadLab: check, apply, verify
Create an inventory with two lab VMs reachable by SSH key. Run the playbook with --check --diff to preview, then apply it. Run it a second time and confirm every task reports ok rather than changed. Manually change PermitRootLogin on one host, rerun, and watch Ansible report and fix the drift. Make sure your SSH key login works before disabling password authentication.
cat > inventory.ini <<'EOF' [lab_linux] 192.168.56.10 192.168.56.11 EOF ansible-galaxy collection install community.general ansible-playbook -i inventory.ini baseline.yml --check --diff ansible-playbook -i inventory.ini baseline.yml ansible-playbook -i inventory.ini baseline.yml | tail -4 # changed=0 expected
Growing the baseline
Move tasks into roles, store the playbook in Git with code review, and run it on a schedule or from CI. Community roles based on CIS benchmarks exist, but read them before applying, because some settings can lock you out or break applications.
Diagram
Baseline lifecycle
- Describe
Write the desired state as tasks in Git.
- Preview
--check --diff shows what would change.
- Apply
Run the playbook; handlers restart only when needed.
- Re-run
changed=0 means no drift; changes reveal drift.
Check yourself
1. What does idempotent mean for an Ansible task?
2. Which flags preview changes without applying them?
3. Why use validate on the sshd_config task?