JINJA2 & ANSIBLE FOR HARDENING
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
JINJA2 & ANSIBLE FOR HARDENING CHEATSHEET
============================================
Source: https://cheatsheet.johlem.net
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.