← All cheat sheets

JINJA2 & ANSIBLE FOR HARDENING

Plain-text reference · 10 KB. Read it, search it (Ctrl-F) or print it.

Ansible automation and Jinja2 templating for security hardening,
compliance enforcement, and configuration management at scale.

INSTALLATION#

pip install ansible
# Or: pipx install ansible
# Or: sudo apt install ansible

# Verify
ansible --version

ANSIBLE BASICS#

# Inventory file (hosts.ini)
[webservers]
web1.example.com
web2.example.com

[databases]
db1.example.com ansible_user=admin ansible_port=2222

[all:vars]
ansible_user=root
ansible_ssh_private_key_file=~/.ssh/id_rsa

# Ad-hoc commands
ansible all -i hosts.ini -m ping
ansible webservers -i hosts.ini -m shell -a "whoami"
ansible all -i hosts.ini -m setup                   # Gather facts

HARDENING PLAYBOOK STRUCTURE#


            

# playbook.yml#

- name: Harden Linux servers
  hosts: all
  become: yes
  vars:
    ssh_port: 22
    allowed_users:
      - admin
      - deploy

  tasks:
    - name: Update all packages
      apt:
        upgrade: dist
        update_cache: yes
      when: ansible_os_family == "Debian"

    - name: Update all packages (RHEL)
      yum:
        name: "*"
        state: latest
      when: ansible_os_family == "RedHat"

  roles:
    - ssh-hardening
    - firewall
    - auditd

# Run playbook
ansible-playbook -i hosts.ini playbook.yml
ansible-playbook -i hosts.ini playbook.yml --check    # Dry run
ansible-playbook -i hosts.ini playbook.yml --diff      # Show changes
ansible-playbook -i hosts.ini playbook.yml --limit web1 # Single host

SSH HARDENING#


            

# roles/ssh-hardening/tasks/main.yml#

- name: Configure SSH
  template:
    src: sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: '0600'
    validate: '/usr/sbin/sshd -t -f %s'
  notify: Restart SSH

- name: Disable root SSH login
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^PermitRootLogin'
    line: 'PermitRootLogin no'
  notify: Restart SSH

- name: Set SSH protocol version
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^Protocol'
    line: 'Protocol 2'
  notify: Restart SSH

- name: Disable password authentication
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^PasswordAuthentication'
    line: 'PasswordAuthentication no'
  notify: Restart SSH

- name: Set MaxAuthTries
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^MaxAuthTries'
    line: 'MaxAuthTries 3'
  notify: Restart SSH

# roles/ssh-hardening/handlers/main.yml#

- name: Restart SSH
  service:
    name: sshd
    state: restarted

JINJA2 TEMPLATE (SSHD)#

# roles/ssh-hardening/templates/sshd_config.j2
# SSH Configuration - Managed by Ansible
# {{ ansible_managed }}

Port {{ ssh_port | default(22) }}
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
MaxSessions 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
PermitEmptyPasswords no

{% if allowed_users is defined %}
AllowUsers {{ allowed_users | join(' ') }}
{% endif %}

{% if ssh_banner is defined %}
Banner {{ ssh_banner }}
{% endif %}

# Ciphers (strong only)
Ciphers aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
KexAlgorithms curve25519-sha256,diffie-hellman-group16-sha512

FIREWALL HARDENING#


            

# roles/firewall/tasks/main.yml#

- name: Install UFW
  apt:
    name: ufw
    state: present

- name: Set default deny incoming
  ufw:
    default: deny
    direction: incoming

- name: Set default allow outgoing
  ufw:
    default: allow
    direction: outgoing

- name: Allow SSH
  ufw:
    rule: allow
    port: "{{ ssh_port }}"
    proto: tcp

- name: Allow specific ports
  ufw:
    rule: allow
    port: "{{ item.port }}"
    proto: "{{ item.proto }}"
    src: "{{ item.src | default('any') }}"
  loop: "{{ firewall_rules }}"
  when: firewall_rules is defined

- name: Enable UFW
  ufw:
    state: enabled

# Vars example:
# firewall_rules:
#   - { port: 80, proto: tcp }
#   - { port: 443, proto: tcp }
#   - { port: 22, proto: tcp, src: "10.10.10.0/24" }

AUDIT & LOGGING#


            

# roles/auditd/tasks/main.yml#

- name: Install auditd
  apt:
    name: auditd
    state: present

- name: Deploy audit rules
  template:
    src: audit.rules.j2
    dest: /etc/audit/rules.d/hardening.rules
  notify: Restart auditd

- name: Enable syslog forwarding
  lineinfile:
    path: /etc/rsyslog.conf
    line: "*.* @@{{ syslog_server }}:514"
  when: syslog_server is defined
  notify: Restart rsyslog

# templates/audit.rules.j2
# Monitor authentication
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers

# Monitor SSH keys
-w /root/.ssh -p wa -k ssh_keys
-w /home -p wa -k home_changes

# Monitor cron
-w /etc/crontab -p wa -k cron
-w /var/spool/cron -p wa -k cron

# Monitor privilege escalation
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -k priv_esc

# Monitor network configuration
-w /etc/hosts -p wa -k network
-w /etc/resolv.conf -p wa -k network

PASSWORD POLICY#

---
- name: Install libpam-pwquality
  apt:
    name: libpam-pwquality
    state: present

- name: Configure password complexity
  lineinfile:
    path: /etc/security/pwquality.conf
    regexp: "^{{ item.key }}"
    line: "{{ item.key }} = {{ item.value }}"
  loop:
    - { key: "minlen", value: "14" }
    - { key: "dcredit", value: "-1" }
    - { key: "ucredit", value: "-1" }
    - { key: "lcredit", value: "-1" }
    - { key: "ocredit", value: "-1" }
    - { key: "maxrepeat", value: "3" }

- name: Set password aging
  lineinfile:
    path: /etc/login.defs
    regexp: "^{{ item.key }}"
    line: "{{ item.key }}    {{ item.value }}"
  loop:
    - { key: "PASS_MAX_DAYS", value: "90" }
    - { key: "PASS_MIN_DAYS", value: "1" }
    - { key: "PASS_WARN_AGE", value: "14" }

KERNEL HARDENING#

---
- name: Apply sysctl hardening
  sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    sysctl_set: yes
    state: present
    reload: yes
  loop:
    - { key: "net.ipv4.conf.all.send_redirects", value: "0" }
    - { key: "net.ipv4.conf.all.accept_redirects", value: "0" }
    - { key: "net.ipv4.conf.all.accept_source_route", value: "0" }
    - { key: "net.ipv4.conf.all.log_martians", value: "1" }
    - { key: "net.ipv4.icmp_echo_ignore_broadcasts", value: "1" }
    - { key: "net.ipv4.tcp_syncookies", value: "1" }
    - { key: "net.ipv4.ip_forward", value: "0" }
    - { key: "kernel.randomize_va_space", value: "2" }
    - { key: "fs.protected_hardlinks", value: "1" }
    - { key: "fs.protected_symlinks", value: "1" }
    - { key: "kernel.sysrq", value: "0" }
    - { key: "kernel.core_uses_pid", value: "1" }

FILE PERMISSIONS#

---
- name: Set secure permissions on sensitive files
  file:
    path: "{{ item.path }}"
    owner: root
    group: root
    mode: "{{ item.mode }}"
  loop:
    - { path: "/etc/passwd", mode: "0644" }
    - { path: "/etc/shadow", mode: "0600" }
    - { path: "/etc/group", mode: "0644" }
    - { path: "/etc/gshadow", mode: "0600" }
    - { path: "/etc/crontab", mode: "0600" }
    - { path: "/boot/grub/grub.cfg", mode: "0600" }

- name: Remove world-writable files
  shell: find / -xdev -perm -0002 -type f -exec chmod o-w {} \;
  changed_when: false

- name: Find SUID binaries
  shell: find / -xdev -perm -4000 -type f
  register: suid_files
  changed_when: false

- name: Display SUID files for review
  debug:
    var: suid_files.stdout_lines

CIS BENCHMARK ROLES#

# Use community roles for CIS compliance
ansible-galaxy install dev-sec.os-hardening
ansible-galaxy install dev-sec.ssh-hardening

# Usage in playbook
- hosts: all
  roles:
    - dev-sec.os-hardening
    - dev-sec.ssh-hardening

# Popular hardening collections:
# dev-sec.os-hardening          # OS-level CIS
# dev-sec.ssh-hardening         # SSH CIS
# dev-sec.mysql-hardening       # MySQL CIS
# dev-sec.nginx-hardening       # Nginx CIS
# geerlingguy.firewall          # Firewall management

JINJA2 QUICK REFERENCE#

# Variables
{{ variable }}
{{ variable | default("fallback") }}

# Filters
{{ list | join(', ') }}                     # Join list
{{ string | upper }}                        # Uppercase
{{ string | lower }}                        # Lowercase
{{ string | hash('sha256') }}               # Hash
{{ path | basename }}                       # Filename from path
{{ dict | to_json }}                        # To JSON
{{ number | int }}                          # To integer
{{ list | length }}                         # List length
{{ list | unique }}                         # Unique items
{{ list | sort }}                           # Sort list

# Conditionals
{% if condition %}
content
{% elif other_condition %}
other content
{% else %}
fallback
{% endif %}

# Loops
{% for item in list %}
{{ item }}
{% endfor %}

{% for key, value in dict.items() %}
{{ key }} = {{ value }}
{% endfor %}

# Comments
{# This is a comment #}

COMPLIANCE SCANNING#

# After hardening, verify with:
ansible-playbook hardening.yml --check --diff  # Dry run
# Or use:
# Lynis (host-based audit)
# OpenSCAP (SCAP compliance)
# InSpec (compliance as code)

TIPS#

  - Always use --check first (dry run)
  - Use --diff to see what changes will be made
  - Ansible Vault for encrypting secrets: ansible-vault encrypt vars.yml
  - Use roles for reusable hardening modules
  - dev-sec.* roles implement CIS benchmarks
  - Template validation prevents broken configs (validate parameter)
  - Handlers ensure services restart only when needed
  - Tags allow running specific hardening sections
  - Idempotent — safe to run multiple times
  - Use molecule for testing roles before deployment

Defensive reference on CyberRamen. Offensive / red-team sheets live on OffensiveRamen.com.