Add RHEL 9 CIS-inspired hardening playbook

This commit is contained in:
Mateusz Suski
2026-05-06 08:45:33 +00:00
parent 1e2db3e125
commit 75a11f7650
20 changed files with 711 additions and 0 deletions
@@ -0,0 +1,38 @@
---
- name: Ensure audit rules directory exists
ansible.builtin.file:
path: /etc/audit/rules.d
state: directory
owner: root
group: root
mode: "0750"
- name: Configure audit backlog limit
ansible.builtin.lineinfile:
path: /etc/audit/audit.rules
regexp: '^-b\s+'
line: "-b {{ cis_audit_backlog_limit }}"
create: true
owner: root
group: root
mode: "0640"
notify: restart auditd
- name: Install baseline audit rules
ansible.builtin.lineinfile:
path: "{{ cis_audit_rules_path }}"
line: "{{ item }}"
create: true
owner: root
group: root
mode: "0640"
loop: "{{ cis_audit_rules }}"
loop_control:
label: "{{ item }}"
notify: restart auditd
- name: Ensure auditd is enabled and running
ansible.builtin.systemd:
name: auditd
enabled: true
state: started
@@ -0,0 +1,36 @@
---
- name: Gather current mount facts
ansible.builtin.set_fact:
cis_current_mount_paths: "{{ ansible_mounts | map(attribute='mount') | list }}"
- name: Report filesystem mount option mode
ansible.builtin.debug:
msg: >-
{{ 'OK: Mount option management is enabled for configured targets.'
if cis_manage_mount_options | bool
else 'WARNING: Mount option management is disabled. No production filesystems will be remounted.' }}
- name: Show configured mount option recommendations
ansible.builtin.debug:
msg: "Review {{ item.path }} for options: {{ item.options | join(',') }}"
loop: "{{ cis_mount_option_targets }}"
loop_control:
label: "{{ item.path }}"
when: not cis_manage_mount_options | bool
- name: Persist configured mount options without remounting
ansible.posix.mount:
path: "{{ item.path }}"
src: "{{ cis_mount_fact.device }}"
fstype: "{{ cis_mount_fact.fstype }}"
state: present
opts: "{{ ((cis_mount_fact.options | default('defaults')).split(',') + item.options) | unique | join(',') }}"
loop: "{{ cis_mount_option_targets }}"
loop_control:
label: "{{ item.path }}"
vars:
cis_mount_fact: "{{ ansible_mounts | selectattr('mount', 'equalto', item.path) | list | first | default({}) }}"
when:
- cis_manage_mount_options | bool
- item.path in cis_current_mount_paths
register: cis_mount_option_results
@@ -0,0 +1,24 @@
---
- name: Ensure rsyslog is installed
ansible.builtin.package:
name: rsyslog
state: present
when: cis_enable_rsyslog | bool
- name: Ensure rsyslog is enabled and running
ansible.builtin.systemd:
name: rsyslog
enabled: true
state: started
when: cis_enable_rsyslog | bool
- name: Validate journald configuration file presence
ansible.builtin.stat:
path: /etc/systemd/journald.conf
register: cis_journald_conf
- name: Report journald configuration status
ansible.builtin.debug:
msg: >-
{{ 'OK: /etc/systemd/journald.conf is present.'
if cis_journald_conf.stat.exists else 'WARNING: /etc/systemd/journald.conf was not found.' }}
@@ -0,0 +1,54 @@
---
- name: Run platform safety prechecks
ansible.builtin.import_tasks: precheck.yml
tags:
- always
- precheck
- name: Manage packages
ansible.builtin.import_tasks: packages.yml
tags:
- packages
- name: Harden SSH daemon configuration
ansible.builtin.import_tasks: ssh.yml
tags:
- ssh
- name: Apply kernel network hardening
ansible.builtin.import_tasks: sysctl.yml
when: cis_enable_sysctl_hardening | bool
tags:
- sysctl
- name: Manage baseline services
ansible.builtin.import_tasks: services.yml
tags:
- services
- name: Configure Linux audit controls
ansible.builtin.import_tasks: audit.yml
when: cis_install_auditd | bool
tags:
- audit
- name: Configure sudo controls
ansible.builtin.import_tasks: sudo.yml
tags:
- sudo
- name: Configure logging controls
ansible.builtin.import_tasks: logging.yml
tags:
- logging
- name: Review filesystem mount options
ansible.builtin.import_tasks: filesystem.yml
tags:
- filesystem
- name: Run validation postchecks
ansible.builtin.import_tasks: postcheck.yml
tags:
- always
- postcheck
@@ -0,0 +1,24 @@
---
- name: Remove legacy network packages
ansible.builtin.package:
name: "{{ cis_legacy_packages }}"
state: absent
when: cis_remove_legacy_packages | bool
- name: Install chrony when enabled
ansible.builtin.package:
name: chrony
state: present
when: cis_enable_chrony | bool
- name: Install auditd when enabled
ansible.builtin.package:
name: audit
state: present
when: cis_install_auditd | bool
- name: Install rsyslog when enabled
ansible.builtin.package:
name: rsyslog
state: present
when: cis_enable_rsyslog | bool
@@ -0,0 +1,79 @@
---
- name: Validate sshd effective configuration syntax
ansible.builtin.command: sshd -t
register: cis_sshd_validate
changed_when: false
check_mode: false
- name: Read sysctl values for validation
ansible.builtin.command: "sysctl -n {{ item.key }}"
loop: "{{ cis_sysctl_settings | dict2items }}"
loop_control:
label: "{{ item.key }}"
register: cis_sysctl_validation
changed_when: false
failed_when: false
check_mode: false
when: cis_enable_sysctl_hardening | bool
- name: Gather final service facts
ansible.builtin.service_facts:
- name: Build service state summary
ansible.builtin.set_fact:
cis_service_state_summary:
chronyd: "{{ ansible_facts.services['chronyd.service'].state | default('not-found') }}"
auditd: "{{ ansible_facts.services['auditd.service'].state | default('not-found') }}"
rsyslog: "{{ ansible_facts.services['rsyslog.service'].state | default('not-found') }}"
- name: Build sysctl validation summary
ansible.builtin.set_fact:
cis_sysctl_validation_summary: "{{ cis_sysctl_validation_summary | default({}) | combine({item.item.key: item.stdout | default('unreadable')}) }}"
loop: "{{ cis_sysctl_validation.results | default([]) }}"
loop_control:
label: "{{ item.item.key }}"
when: cis_enable_sysctl_hardening | bool
- name: Build mount option change summary
ansible.builtin.set_fact:
cis_mount_option_summary: >-
{{
cis_mount_option_results.results
| default([])
| selectattr('changed', 'defined')
| selectattr('changed')
| map(attribute='item.path')
| list
}}
- name: Publish validation summary
ansible.builtin.set_fact:
cis_validation_summary:
benchmark: "CIS RHEL 9 Benchmark {{ cis_benchmark_version }} inspired controls"
sshd_config: "{{ 'OK' if cis_sshd_validate.rc == 0 else 'CRITICAL' }}"
services: "{{ cis_service_state_summary }}"
sysctl: "{{ cis_sysctl_validation_summary | default({}) }}"
mount_option_updates: "{{ cis_mount_option_summary | default([]) }}"
applied_controls:
- ssh
- packages
- sysctl
- services
- audit
- sudo
- logging
- filesystem
- name: Show service states
ansible.builtin.debug:
var: cis_service_state_summary
- name: Show changed mount options
ansible.builtin.debug:
msg: >-
{{ cis_mount_option_summary | default([]) if cis_mount_option_summary | default([]) | length > 0
else 'OK: No mount option changes were applied.' }}
- name: Show applied control summary
ansible.builtin.debug:
var: cis_validation_summary
@@ -0,0 +1,54 @@
---
- name: Determine root filesystem free space
ansible.builtin.set_fact:
cis_root_mount: "{{ ansible_mounts | selectattr('mount', 'equalto', '/') | list | first | default({}) }}"
- name: Calculate root filesystem free space in MB
ansible.builtin.set_fact:
cis_root_free_mb: "{{ ((cis_root_mount.size_available | default(0) | int) / 1024 / 1024) | round(0, 'floor') | int }}"
- name: Detect containerized runtime
ansible.builtin.set_fact:
cis_container_detected: >-
{{
ansible_virtualization_type | default('') in cis_container_virtualization_types
or ansible_env.container | default('') | length > 0
}}
- name: Report platform precheck status
ansible.builtin.debug:
msg:
- "OK: Facts gathered for {{ ansible_distribution }} {{ ansible_distribution_version }}."
- "OK: Root filesystem free space is {{ cis_root_free_mb }} MB."
- >-
{{ 'WARNING: Containerized environment detected; service and kernel controls may be limited.'
if cis_container_detected else 'OK: No containerized runtime detected from Ansible facts.' }}
- >-
{{ 'OK: systemd service manager detected.'
if ansible_service_mgr == 'systemd' else 'CRITICAL: systemd service manager is required.' }}
- name: Fail when operating system is unsupported
ansible.builtin.assert:
that:
- ansible_distribution in cis_supported_distributions
- ansible_distribution_major_version == cis_supported_major_version
fail_msg: >-
CRITICAL: This role supports only RHEL 9 / Oracle Linux 9 compatible systems.
Detected {{ ansible_distribution }} {{ ansible_distribution_version }}.
success_msg: "OK: Supported RHEL 9 compatible platform detected."
- name: Fail when systemd is unavailable
ansible.builtin.assert:
that:
- ansible_service_mgr == 'systemd'
fail_msg: "CRITICAL: systemd is required for this operational hardening role."
success_msg: "OK: systemd is available."
- name: Fail when root filesystem free space is below safety threshold
ansible.builtin.assert:
that:
- cis_root_free_mb | int >= cis_min_root_free_mb | int
fail_msg: >-
CRITICAL: Root filesystem has {{ cis_root_free_mb }} MB free.
Minimum required free space is {{ cis_min_root_free_mb }} MB.
success_msg: "OK: Root filesystem free space meets the safety threshold."
@@ -0,0 +1,36 @@
---
- name: Enable chronyd service
ansible.builtin.systemd:
name: chronyd
enabled: true
state: started
when: cis_enable_chrony | bool
- name: Enable rsyslog service
ansible.builtin.systemd:
name: rsyslog
enabled: true
state: started
when: cis_enable_rsyslog | bool
- name: Enable auditd service
ansible.builtin.systemd:
name: auditd
enabled: true
state: started
when: cis_install_auditd | bool
- name: Gather service facts
ansible.builtin.service_facts:
- name: Disable unnecessary legacy services when present
ansible.builtin.systemd:
name: "{{ item }}"
enabled: false
state: stopped
loop: "{{ cis_legacy_services }}"
loop_control:
label: "{{ item }}"
when:
- cis_remove_legacy_packages | bool
- item in ansible_facts.services
@@ -0,0 +1,88 @@
---
- name: Ensure sshd drop-in directory exists
ansible.builtin.file:
path: "{{ cis_ssh_dropin_path | dirname }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Ensure sshd hardening drop-in exists
ansible.builtin.file:
path: "{{ cis_ssh_dropin_path }}"
state: touch
owner: root
group: root
mode: "0644"
modification_time: preserve
access_time: preserve
- name: Configure SSH root login
ansible.builtin.lineinfile:
path: "{{ cis_ssh_dropin_path }}"
regexp: '^PermitRootLogin\s+'
line: "PermitRootLogin {{ 'no' if cis_disable_root_login | bool else 'prohibit-password' }}"
validate: sshd -t -f %s
notify:
- validate sshd
- reload sshd
- name: Configure SSH empty password restriction
ansible.builtin.lineinfile:
path: "{{ cis_ssh_dropin_path }}"
regexp: '^PermitEmptyPasswords\s+'
line: "PermitEmptyPasswords no"
validate: sshd -t -f %s
notify:
- validate sshd
- reload sshd
- name: Configure SSH password authentication
ansible.builtin.lineinfile:
path: "{{ cis_ssh_dropin_path }}"
regexp: '^PasswordAuthentication\s+'
line: "PasswordAuthentication {{ 'no' if cis_disable_password_auth | bool else 'yes' }}"
validate: sshd -t -f %s
notify:
- validate sshd
- reload sshd
- name: Configure SSH MaxAuthTries
ansible.builtin.lineinfile:
path: "{{ cis_ssh_dropin_path }}"
regexp: '^MaxAuthTries\s+'
line: "MaxAuthTries {{ cis_ssh_max_auth_tries }}"
validate: sshd -t -f %s
notify:
- validate sshd
- reload sshd
- name: Configure SSH LoginGraceTime
ansible.builtin.lineinfile:
path: "{{ cis_ssh_dropin_path }}"
regexp: '^LoginGraceTime\s+'
line: "LoginGraceTime {{ cis_ssh_login_grace_time }}"
validate: sshd -t -f %s
notify:
- validate sshd
- reload sshd
- name: Configure SSH ClientAliveInterval
ansible.builtin.lineinfile:
path: "{{ cis_ssh_dropin_path }}"
regexp: '^ClientAliveInterval\s+'
line: "ClientAliveInterval {{ cis_ssh_client_alive_interval }}"
validate: sshd -t -f %s
notify:
- validate sshd
- reload sshd
- name: Configure SSH ClientAliveCountMax
ansible.builtin.lineinfile:
path: "{{ cis_ssh_dropin_path }}"
regexp: '^ClientAliveCountMax\s+'
line: "ClientAliveCountMax {{ cis_ssh_client_alive_count_max }}"
validate: sshd -t -f %s
notify:
- validate sshd
- reload sshd
@@ -0,0 +1,18 @@
---
- name: Configure sudo hardening drop-in
ansible.builtin.lineinfile:
path: "{{ cis_sudoers_dropin_path }}"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
create: true
owner: root
group: root
mode: "0440"
validate: /usr/sbin/visudo -cf %s
loop:
- regexp: '^Defaults\s+use_pty'
line: "Defaults use_pty"
- regexp: '^Defaults\s+logfile='
line: 'Defaults logfile="{{ cis_sudo_logfile }}"'
loop_control:
label: "{{ item.line }}"
@@ -0,0 +1,11 @@
---
- name: Apply CIS-inspired sysctl settings
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
sysctl_file: "{{ cis_sysctl_config_file }}"
state: present
reload: true
loop: "{{ cis_sysctl_settings | dict2items }}"
loop_control:
label: "{{ item.key }}"