Compare commits

..

4 Commits

Author SHA1 Message Date
Mateusz Suski deb12a0b4f Update docs for Ansible hardening roles 2026-05-06 09:25:43 +00:00
Mateusz Suski 02a51f72f9 Add IBM AIX 7 CIS-inspired hardening playbook 2026-05-06 09:21:15 +00:00
Mateusz Suski 2fd9c0b5ef Add Debian 13 and Ubuntu 26.04 CIS-inspired hardening playbook 2026-05-06 08:56:45 +00:00
Mateusz Suski 75a11f7650 Add RHEL 9 CIS-inspired hardening playbook 2026-05-06 08:45:33 +00:00
59 changed files with 2574 additions and 17 deletions
+19
View File
@@ -1,5 +1,24 @@
# Changelog
## [Unreleased]
### Added
- CIS-inspired Ansible hardening automation:
- RHEL 9 role and playbook.
- Debian 13 / Ubuntu 26.04 role and playbook.
- IBM AIX 7 role and playbook.
- Shared sanitized Ansible inventory defaults for Linux and AIX examples.
- Role-level task structure covering pre-checks, SSH, sudo, auditing, logging, services, filesystem controls, platform-specific settings, handlers, and post-check validation.
### Changed
- Updated repository, `infra-run`, and Ansible README files to describe the new hardening automation instead of placeholder-only Ansible structure.
### Notes
- Hardening content is CIS-inspired and intended for portfolio/lab use; production use requires environment-specific review and validation.
## [Initial Version]
### Added
+8
View File
@@ -13,6 +13,7 @@ flowchart TD
B --> B2["docs"]
B --> B3["runbooks"]
B --> B4["scripts"]
B1 --> B11["hardening roles"]
B4 --> B41["bash"]
B4 --> B42["python"]
C --> C1["storage"]
@@ -65,6 +66,12 @@ Veritas VxVM and VCS storage expansion workflow covering new LUN detection, VxVM
GPFS / IBM Spectrum Scale filesystem expansion workflow covering cluster validation, candidate disk discovery, NSD stanza planning, NSD creation, filesystem expansion, optional rebalance, post-checks, and change reporting.
### Ansible Hardening Toolkit
[infra-run/ansible/](./infra-run/ansible/)
CIS-inspired Ansible automation for repeatable operating system hardening across RHEL 9, Debian 13 / Ubuntu 26.04, and IBM AIX 7 targets. The roles are organized around pre-checks, configurable safeguards, SSH and sudo policy, auditing, logging, services, filesystem controls, platform-specific system settings, handlers, and post-change validation.
## Repository Structure
- `infra-run` - core operational automation, scripts, runbooks, and infrastructure operations examples.
@@ -77,6 +84,7 @@ GPFS / IBM Spectrum Scale filesystem expansion workflow covering cluster validat
- Pre-check, change, and post-check workflow.
- Real-world scenarios, not tutorials.
- Minimal but practical tooling.
- Configurable automation with sanitized defaults and explicit overrides.
## Notes
+9 -2
View File
@@ -16,12 +16,19 @@ flowchart TD
## Scope
- `ansible` - placeholder structure for infrastructure automation and testing.
- `ansible` - infrastructure automation with CIS-inspired hardening roles and playbooks.
- `docs` - supporting technical notes and written documentation.
- `runbooks` - procedural operational guides.
- `scripts` - executable tooling for operations and diagnostics.
## Current Automation
- RHEL 9 CIS-inspired hardening role and playbook.
- Debian 13 / Ubuntu 26.04 CIS-inspired hardening role and playbook.
- IBM AIX 7 CIS-inspired hardening role and playbook.
- Shared sanitized inventory defaults for Linux and AIX examples.
## Notes
- This folder reflects the structure of a production-oriented operations repository.
- Current implementation is strongest in the Bash tooling under `scripts/bash`.
- Current implementation includes Bash operational toolkits and Ansible hardening automation.
+14 -7
View File
@@ -1,6 +1,6 @@
# infra-run/ansible
This directory reserves the Ansible automation area for future infrastructure-as-code content. It is organized around the standard separation of inventory, roles, playbooks, collections, and tests.
This directory contains Ansible automation for infrastructure operations and OS hardening. It is organized around the standard separation of inventory, roles, playbooks, collections, and tests.
## Diagram
@@ -17,13 +17,20 @@ flowchart TD
## Scope
- `collections` - vendored or custom Ansible collections.
- `inventory` - environment inventory definitions and variables.
- `playbooks` - executable playbooks for repeatable operations.
- `roles` - reusable automation roles.
- `collections` - collection requirements for supported automation targets.
- `inventory` - sanitized Linux and AIX inventory examples with shared defaults.
- `playbooks` - executable CIS-inspired hardening playbooks.
- `roles` - reusable hardening roles for supported operating systems.
- `tests` - validation and test harnesses for Ansible content.
## Hardening Coverage
- `cis-rhel9-hardening` - RHEL 9 baseline tasks for packages, services, SSH, sudo, sysctl, auditing, logging, filesystem controls, and validation.
- `cis-debian-ubuntu-hardening` - Debian 13 and Ubuntu 26.04 baseline tasks for apt packages, services, SSH, sudo, sysctl, auditing, logging, filesystem controls, and validation.
- `cis-aix7-hardening` - IBM AIX 7 baseline tasks for SSH, sudo, audit, logging, cron, users, password policy, network settings, filesystem controls, services, and validation.
## Notes
- The directory layout is already prepared for growth even where content is still placeholder-only.
- This keeps the repository ready for automation expansion alongside the existing script toolkits.
- Roles are CIS-inspired examples intended for portfolio and lab use, not a drop-in compliance certification.
- Defaults are sanitized and configurable through inventory or `--extra-vars`.
- Run platform-specific playbooks against appropriate test hosts before adapting them to production environments.
+9
View File
@@ -0,0 +1,9 @@
[defaults]
inventory = inventory/hosts.yml
roles_path = roles
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
[privilege_escalation]
become = True
@@ -0,0 +1,4 @@
---
collections:
- name: ansible.posix
- name: community.general
+9 -2
View File
@@ -16,8 +16,15 @@ flowchart TD
- `group_vars` - variables applied at group or environment level.
- `host_vars` - variables tailored to individual nodes.
- `hosts.yml` - sanitized example groups for Linux and AIX hardening targets.
## Current Inventory Shape
- `linux` - local example host for Linux hardening playbooks.
- `aix` - empty sanitized group ready for AIX host definitions.
- `group_vars/all.yml` - shared hardening defaults such as NTP servers, SSH behavior, audit/logging toggles, sysctl hardening, and optional mount management.
## Notes
- The structure is present even though the repository currently keeps this area sanitized and mostly empty.
- This is the natural companion to future playbooks and roles under `infra-run/ansible`.
- Inventory values are intentionally sanitized.
- Override defaults per host, per group, or per run before applying any hardening playbook.
@@ -0,0 +1,18 @@
---
timezone: UTC
cis_ntp_servers:
- 0.rhel.pool.ntp.org
- 1.rhel.pool.ntp.org
- 2.rhel.pool.ntp.org
- 3.rhel.pool.ntp.org
# Operational defaults. Override per run with --extra-vars or inventory when needed.
cis_disable_root_login: true
cis_disable_password_auth: false
cis_install_auditd: true
cis_enable_chrony: true
cis_enable_rsyslog: true
cis_remove_legacy_packages: true
cis_enable_sysctl_hardening: true
cis_manage_mount_options: false
+8
View File
@@ -0,0 +1,8 @@
---
linux:
hosts:
localhost:
ansible_connection: local
aix:
hosts: {}
+5 -3
View File
@@ -1,6 +1,6 @@
# infra-run/ansible/playbooks
This directory is intended for executable Ansible playbooks that coordinate roles, inventories, and operational tasks. In the current portfolio state it acts as a prepared entry point for future automation runs.
This directory contains executable Ansible playbooks that coordinate roles, inventories, and operational hardening tasks.
## Diagram
@@ -14,5 +14,7 @@ flowchart TD
## Notes
- Playbooks belong here when the repository expands beyond script-first operations.
- The directory currently contains only placeholder content.
- `cis-rhel9-hardening.yml` applies the RHEL 9 CIS-inspired hardening role to Linux inventory targets.
- `cis-debian-ubuntu-hardening.yml` applies the Debian 13 / Ubuntu 26.04 CIS-inspired hardening role to Linux inventory targets.
- `cis-aix7-hardening.yml` applies the IBM AIX 7 CIS-inspired hardening role to AIX inventory targets.
- Use the sanitized inventory under `../inventory/` as a starting point and override defaults per environment.
@@ -0,0 +1,21 @@
---
- name: Apply CIS-inspired IBM AIX 7 hardening controls
hosts: aix
become: true
gather_facts: true
roles:
- role: cis-aix7-hardening
tags:
- cis
- aix7
- hardening
post_tasks:
- name: Show AIX hardening validation summary
ansible.builtin.debug:
var: cis_aix_validation_summary
when: cis_aix_validation_summary is defined
tags:
- always
- postcheck
@@ -0,0 +1,20 @@
---
- name: Apply CIS-inspired Debian and Ubuntu hardening controls
hosts: linux
become: true
gather_facts: true
roles:
- role: cis-debian-ubuntu-hardening
tags:
- cis
- hardening
post_tasks:
- name: Show validation summary
ansible.builtin.debug:
var: cis_validation_summary
when: cis_validation_summary is defined
tags:
- always
- postcheck
@@ -0,0 +1,20 @@
---
- name: Apply CIS-inspired RHEL 9 hardening controls
hosts: linux
become: true
gather_facts: true
roles:
- role: cis-rhel9-hardening
tags:
- cis
- hardening
post_tasks:
- name: Show validation summary
ansible.builtin.debug:
var: cis_validation_summary
when: cis_validation_summary is defined
tags:
- always
- postcheck
+12 -3
View File
@@ -1,6 +1,6 @@
# infra-run/ansible/roles
This folder is reserved for reusable Ansible roles. Roles make it possible to organize configuration logic into predictable, testable units that can be shared across playbooks.
This folder contains reusable Ansible roles. Roles organize configuration logic into predictable, testable units that can be shared across playbooks.
## Diagram
@@ -10,9 +10,18 @@ flowchart TD
A --> C["monitoring"]
A --> D["storage"]
A --> E["security"]
E --> E1["cis-rhel9-hardening"]
E --> E2["cis-debian-ubuntu-hardening"]
E --> E3["cis-aix7-hardening"]
```
## Current Roles
- `cis-rhel9-hardening` - CIS-inspired RHEL 9 baseline with package, service, SSH, sudo, sysctl, audit, logging, filesystem, and validation tasks.
- `cis-debian-ubuntu-hardening` - CIS-inspired Debian 13 and Ubuntu 26.04 baseline with apt, service, SSH, sudo, sysctl, audit, logging, filesystem, and validation tasks.
- `cis-aix7-hardening` - CIS-inspired IBM AIX 7 baseline with SSH, sudo, audit, logging, cron, user, password, network, filesystem, service, and validation tasks.
## Notes
- The role layout is not yet populated, but the structure is in place for future automation modules.
- Keeping a README here documents intent even before role code exists.
- Each role includes defaults, task includes, handlers where needed, and role-specific README guidance.
- The hardening content is sanitized for portfolio use and should be reviewed against site policy before production use.
@@ -0,0 +1,67 @@
# cis-aix7-hardening
Operational IBM AIX 7.x hardening role inspired by CIS Benchmark 1.2.0 and common enterprise Unix security practices.
Reference: https://www.cisecurity.org/benchmark/aix
This role is intended for infrastructure and security operations teams that manage production AIX estates. It favors readable, conservative controls over broad benchmark coverage.
## Supported OS
- IBM AIX 7.x
## Implemented Areas
- Platform prechecks for AIX 7.x, SRC, SSH, audit tooling, required commands, disk safety, and baseline security state.
- SSH daemon hardening in `/etc/ssh/sshd_config` with validation through `sshd -t`.
- Account and password controls through AIX-native `lssec`, `chsec`, and `pwdadm`.
- Network tunable validation and optional hardening through `no`, with optional `nfso` support.
- SRC-aware service checks and safe inetd legacy service disablement.
- Filesystem review for JFS2, world-writable directories, and invalid owners or groups.
- Syslog and audit validation, with audit enablement disabled by default.
- Cron and at permission hardening under `/var/adm/cron`.
- Sudo defaults with validation through `visudo -cf` when sudo is present.
- Postcheck reporting for SSH, services, network values, and password policy.
## AIX Operational Notes
AIX is not Linux. This role does not assume systemd, sysctl, Linux package managers, or Linux service paths. Service operations use SRC commands such as `lssrc`, `startsrc`, `stopsrc`, and `refresh`.
AIX environments vary heavily between enterprises. Filesystem layout, OpenSSH source, sudo packaging, audit classes, NFS tuning, and security policy ownership should be validated before production rollout.
## Safety Philosophy
- Defaults are conservative.
- Audit enablement is opt-in with `cis_enable_audit`.
- Filesystem mount option management is opt-in with `cis_manage_mount_options`.
- SSH password authentication is not disabled by default.
- Native AIX security files are updated with targeted `chsec` calls instead of wholesale replacement.
- Check mode is supported where practical, though AIX command modules may still need read-only probes for validation.
## Check Mode Examples
```bash
ansible-playbook playbooks/cis-aix7-hardening.yml --check
```
```bash
ansible-playbook playbooks/cis-aix7-hardening.yml --check --tags precheck,ssh,postcheck
```
## Tag Examples
```bash
ansible-playbook playbooks/cis-aix7-hardening.yml --tags precheck
```
```bash
ansible-playbook playbooks/cis-aix7-hardening.yml --tags ssh,password_policy,network
```
```bash
ansible-playbook playbooks/cis-aix7-hardening.yml --tags audit -e cis_enable_audit=true
```
## Important Warning
This is not a full CIS certification implementation and does not implement the entire CIS AIX benchmark. It is a practical CIS-inspired baseline that should be reviewed by infrastructure, security, and application owners before production enforcement.
@@ -0,0 +1,98 @@
---
cis_benchmark_version: "1.2.0"
cis_disable_root_login: true
cis_disable_password_auth: false
cis_enable_network_hardening: true
cis_enable_password_policy: true
cis_enable_audit: false
cis_manage_mount_options: false
cis_ssh_max_auth_tries: 4
cis_ssh_login_grace_time: 60
cis_ssh_client_alive_interval: 300
cis_ssh_client_alive_count_max: 3
cis_ssh_config_path: /etc/ssh/sshd_config
cis_sshd_test_command: sshd -t
cis_min_root_free_mb: 1024
cis_password_minlen: 14
cis_password_histsize: 10
cis_password_maxage_weeks: 12
cis_password_minalpha: 1
cis_password_minother: 1
cis_password_maxrepeats: 2
cis_password_minage_weeks: 1
cis_login_retries: 5
cis_login_lockout: 30
cis_required_commands:
- lsattr
- chdev
- lssrc
- chsec
- lssec
- pwdadm
- "no"
- audit
- cron
cis_ssh_candidate_paths:
- /usr/sbin/sshd
- /usr/bin/sshd
- /opt/freeware/sbin/sshd
- /opt/freeware/bin/sshd
cis_network_no_settings:
ipforwarding: "0"
ipsendredirects: "0"
ipignoreredirects: "1"
ipsrcrouteforward: "0"
clean_partial_conns: "1"
tcp_pmtu_discover: "0"
cis_network_nfso_settings: {}
cis_legacy_inetd_services:
- telnet
- shell
- login
- exec
- comsat
- talk
- ntalk
- tftp
- uucp
- finger
cis_src_subsystems:
- sshd
- inetd
- syslogd
- audit
cis_mount_option_targets:
- path: /tmp
options:
- nosuid
- path: /var/tmp
options:
- nosuid
cis_manage_sudo: true
cis_sudoers_path: /etc/sudoers
cis_sudo_logfile: /var/log/sudo.log
cis_sudo_use_pty: true
cis_cron_allow_path: /var/adm/cron/cron.allow
cis_cron_deny_path: /var/adm/cron/cron.deny
cis_at_allow_path: /var/adm/cron/at.allow
cis_at_deny_path: /var/adm/cron/at.deny
cis_cron_directories:
- /var/adm/cron
- /var/spool/cron
- /var/spool/cron/crontabs
cis_syslog_config_path: /etc/syslog.conf
cis_audit_config_path: /etc/security/audit/config
@@ -0,0 +1,44 @@
---
- name: Validate sshd configuration
ansible.builtin.command: "{{ cis_sshd_test_command }}"
changed_when: false
listen: validate sshd
- name: Restart sshd using SRC
ansible.builtin.shell: |
set -o pipefail
if lssrc -s sshd >/dev/null 2>&1; then
stopsrc -s sshd >/dev/null 2>&1 || true
startsrc -s sshd
fi
args:
executable: /bin/ksh
changed_when: true
listen: restart sshd
- name: Refresh inetd
ansible.builtin.command: refresh -s inetd
changed_when: true
failed_when: false
listen: refresh inetd
- name: Refresh syslog
ansible.builtin.command: refresh -s syslogd
changed_when: true
failed_when: false
listen: refresh syslog
- name: Restart audit subsystem
ansible.builtin.shell: |
set -o pipefail
if lssrc -s audit >/dev/null 2>&1; then
stopsrc -s audit >/dev/null 2>&1 || true
startsrc -s audit
else
audit start
fi
args:
executable: /bin/ksh
changed_when: true
when: cis_enable_audit | bool
listen: restart audit
@@ -0,0 +1,32 @@
---
- name: Validate AIX audit configuration file
ansible.builtin.stat:
path: "{{ cis_audit_config_path }}"
register: cis_aix_audit_config
- name: Collect AIX audit query status
ansible.builtin.command: audit query
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_audit_status
- name: Enable AIX audit subsystem when explicitly configured
ansible.builtin.command: audit start
changed_when: true
when:
- cis_enable_audit | bool
- cis_aix_audit_config.stat.exists
- cis_aix_audit_status.rc != 0 or 'auditing off' in (cis_aix_audit_status.stdout | default('') | lower)
notify: restart audit
- name: Report audit status
ansible.builtin.debug:
msg:
- >-
{{ 'OK: AIX audit configuration file exists.'
if cis_aix_audit_config.stat.exists else 'WARNING: AIX audit configuration file was not found.' }}
- >-
{{ 'OK: Audit enablement is explicitly allowed by cis_enable_audit.'
if cis_enable_audit | bool else 'WARNING: Audit enablement is disabled by default; validation only was performed.' }}
- "OK: audit query rc={{ cis_aix_audit_status.rc }} output={{ cis_aix_audit_status.stdout | default('') }}"
@@ -0,0 +1,49 @@
---
- name: Ensure cron and at control files exist with safe ownership
ansible.builtin.file:
path: "{{ item }}"
state: touch
owner: root
group: cron
mode: "0600"
modification_time: preserve
access_time: preserve
loop:
- "{{ cis_cron_allow_path }}"
- "{{ cis_at_allow_path }}"
- name: Ensure deny files are not world readable when present
ansible.builtin.file:
path: "{{ item }}"
owner: root
group: cron
mode: "0600"
loop:
- "{{ cis_cron_deny_path }}"
- "{{ cis_at_deny_path }}"
failed_when: false
- name: Secure cron directories when present
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: cron
mode: "0750"
loop: "{{ cis_cron_directories }}"
failed_when: false
- name: Validate cron SRC state
ansible.builtin.command: lssrc -s cron
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_cron_state
- name: Report cron and at hardening status
ansible.builtin.debug:
msg:
- "OK: cron.allow and at.allow ownership and permissions are managed."
- >-
{{ 'OK: cron SRC subsystem exists.'
if cis_aix_cron_state.rc == 0 else 'WARNING: cron SRC subsystem was not found.' }}
@@ -0,0 +1,60 @@
---
- name: Build mounted filesystem list from gathered facts
ansible.builtin.set_fact:
cis_aix_mount_points: "{{ ansible_mounts | map(attribute='mount') | list }}"
- name: Validate JFS2 filesystems
ansible.builtin.shell: |
set -o pipefail
lsfs -q | awk '/vfs[[:space:]]*=[[:space:]]*jfs2/{print prev} {prev=$0}'
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_jfs2_filesystems
- name: Review configured mount option targets
ansible.builtin.debug:
msg: >-
OK: Mount option management is disabled by default.
Review target {{ item.path }} for options {{ item.options | join(', ') }} before production rollout.
loop: "{{ cis_mount_option_targets }}"
when: not cis_manage_mount_options | bool
- name: Apply configured mount options only when explicitly enabled
ansible.builtin.command: "chfs -a options={{ item.options | join(',') }} {{ item.path }}"
changed_when: true
loop: "{{ cis_mount_option_targets }}"
when:
- cis_manage_mount_options | bool
- item.path in cis_aix_mount_points
- name: Identify world-writable directories on local filesystems
ansible.builtin.shell: |
set -o pipefail
find / -xdev -type d -perm -0002 -print 2>/dev/null | head -200
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_world_writable_dirs
- name: Identify files without valid owner or group on local filesystems
ansible.builtin.shell: |
set -o pipefail
find / -xdev \( -nouser -o -nogroup \) -print 2>/dev/null | head -200
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_unowned_files
- name: Report filesystem review findings
ansible.builtin.debug:
msg:
- "OK: JFS2 filesystem review completed."
- "WARNING: World-writable directories found: {{ cis_aix_world_writable_dirs.stdout_lines | default([]) }}"
- "WARNING: Files without valid owner/group found: {{ cis_aix_unowned_files.stdout_lines | default([]) }}"
@@ -0,0 +1,40 @@
---
- name: Collect syslog SRC state
ansible.builtin.command: lssrc -s syslogd
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_syslog_state
- name: Ensure syslog configuration exists
ansible.builtin.stat:
path: "{{ cis_syslog_config_path }}"
register: cis_aix_syslog_config
- name: Start syslogd when installed but inactive
ansible.builtin.command: startsrc -s syslogd
changed_when: true
when:
- cis_aix_syslog_state.rc == 0
- "'active' not in cis_aix_syslog_state.stdout"
- name: Validate syslog configuration has active entries
ansible.builtin.shell: "awk 'NF && $1 !~ /^#/ {found=1} END {exit found ? 0 : 1}' {{ cis_syslog_config_path }}"
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_syslog_has_rules
when: cis_aix_syslog_config.stat.exists
- name: Report logging status
ansible.builtin.debug:
msg:
- >-
{{ 'OK: syslogd SRC subsystem exists.'
if cis_aix_syslog_state.rc == 0 else 'WARNING: syslogd SRC subsystem was not found.' }}
- >-
{{ 'OK: syslog configuration has active rules.'
if cis_aix_syslog_has_rules.rc | default(1) == 0
else 'WARNING: syslog configuration has no active rules or could not be validated.' }}
@@ -0,0 +1,65 @@
---
- name: Run AIX platform safety prechecks
ansible.builtin.import_tasks: precheck.yml
tags:
- always
- precheck
- name: Harden AIX SSH daemon configuration
ansible.builtin.import_tasks: ssh.yml
tags:
- ssh
- name: Apply AIX user account controls
ansible.builtin.import_tasks: users.yml
tags:
- users
- name: Apply AIX password policy controls
ansible.builtin.import_tasks: password_policy.yml
when: cis_enable_password_policy | bool
tags:
- password_policy
- name: Apply AIX network hardening controls
ansible.builtin.import_tasks: network.yml
when: cis_enable_network_hardening | bool
tags:
- network
- name: Manage AIX baseline services
ansible.builtin.import_tasks: services.yml
tags:
- services
- name: Review AIX filesystem controls
ansible.builtin.import_tasks: filesystem.yml
tags:
- filesystem
- name: Validate AIX logging controls
ansible.builtin.import_tasks: logging.yml
tags:
- logging
- name: Validate AIX audit controls
ansible.builtin.import_tasks: audit.yml
tags:
- audit
- name: Harden AIX cron and at controls
ansible.builtin.import_tasks: cron.yml
tags:
- cron
- name: Harden sudo configuration
ansible.builtin.import_tasks: sudo.yml
when: cis_manage_sudo | bool
tags:
- sudo
- name: Run AIX validation postchecks
ansible.builtin.import_tasks: postcheck.yml
tags:
- always
- postcheck
@@ -0,0 +1,65 @@
---
- name: Collect current AIX network tunables
ansible.builtin.command: no -a
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_no_current
- name: Query configured AIX network tunables
ansible.builtin.command: "no -o {{ item.key }}"
changed_when: false
failed_when: false
check_mode: false
loop: "{{ cis_network_no_settings | dict2items }}"
register: cis_aix_no_query
- name: Apply configured AIX network tunables
ansible.builtin.command: "no -p -o {{ item.item.key }}={{ item.item.value }}"
changed_when: true
loop: "{{ cis_aix_no_query.results }}"
when:
- item.rc == 0
- item.stdout is not search('=\\s*' ~ (item.item.value | string) ~ '\\b')
- name: Warn about unsupported AIX network tunables
ansible.builtin.debug:
msg: "WARNING: AIX network tunable {{ item.item.key }} is not supported on this host."
loop: "{{ cis_aix_no_query.results }}"
when: item.rc != 0
- name: Check nfso availability
ansible.builtin.shell: "command -v nfso >/dev/null 2>&1 || whence nfso >/dev/null 2>&1"
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_nfso_available
- name: Query configured AIX NFS tunables
ansible.builtin.command: "nfso -o {{ item.key }}"
changed_when: false
failed_when: false
check_mode: false
loop: "{{ cis_network_nfso_settings | dict2items }}"
register: cis_aix_nfso_query
when:
- cis_aix_nfso_available.rc == 0
- cis_network_nfso_settings | length > 0
- name: Apply configured AIX NFS tunables
ansible.builtin.command: "nfso -p -o {{ item.item.key }}={{ item.item.value }}"
changed_when: true
loop: "{{ cis_aix_nfso_query.results | default([]) }}"
when:
- item.rc == 0
- item.stdout is not search('=\\s*' ~ (item.item.value | string) ~ '\\b')
- name: Report network hardening status
ansible.builtin.debug:
msg:
- "OK: AIX network tunables were validated before changes."
- >-
{{ 'OK: nfso is available for optional NFS network tunables.'
if cis_aix_nfso_available.rc == 0 else 'WARNING: nfso was not found; NFS tunables were skipped.' }}
@@ -0,0 +1,66 @@
---
- name: Collect current default password policy
ansible.builtin.command: lssec -f /etc/security/user -s default -a minlen histsize maxage minage minalpha minother maxrepeats loginretries
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_password_policy_current
- name: Collect current default login policy
ansible.builtin.command: lssec -f /etc/security/login.cfg -s usw -a logindisable logininterval loginreenable
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_login_policy_current
- name: Manage default password security attributes
ansible.builtin.command: "chsec -f /etc/security/user -s default -a {{ item.key }}={{ item.value }}"
changed_when: true
loop:
- key: minlen
value: "{{ cis_password_minlen }}"
- key: histsize
value: "{{ cis_password_histsize }}"
- key: maxage
value: "{{ cis_password_maxage_weeks }}"
- key: minage
value: "{{ cis_password_minage_weeks }}"
- key: minalpha
value: "{{ cis_password_minalpha }}"
- key: minother
value: "{{ cis_password_minother }}"
- key: maxrepeats
value: "{{ cis_password_maxrepeats }}"
- key: loginretries
value: "{{ cis_login_retries }}"
when: >-
(item.key ~ '=' ~ (item.value | string))
not in (cis_aix_password_policy_current.stdout | default(''))
- name: Manage login lockout interval
ansible.builtin.command: "chsec -f /etc/security/login.cfg -s usw -a loginreenable={{ cis_login_lockout }}"
changed_when: true
when: >-
('loginreenable=' ~ (cis_login_lockout | string))
not in (cis_aix_login_policy_current.stdout | default(''))
- name: Collect updated default password policy
ansible.builtin.command: lssec -f /etc/security/user -s default -a minlen histsize maxage minage minalpha minother maxrepeats loginretries
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_password_policy_updated
- name: Validate password database state
ansible.builtin.command: pwdadm -q root
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_pwdadm_root
- name: Report password policy status
ansible.builtin.debug:
msg:
- "OK: Password policy managed through AIX chsec defaults, without replacing security files."
- "OK: Current default policy: {{ cis_aix_password_policy_updated.stdout | default('unavailable') }}"
- "OK: pwdadm root status: {{ cis_aix_pwdadm_root.stdout | default('unavailable') }}"
@@ -0,0 +1,58 @@
---
- name: Validate sshd configuration after hardening
ansible.builtin.command: "{{ cis_sshd_test_command }}"
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_post_sshd
- name: Show selected AIX network security values
ansible.builtin.command: "no -o {{ item.key }}"
changed_when: false
failed_when: false
check_mode: false
loop: "{{ cis_network_no_settings | dict2items }}"
register: cis_aix_post_network
- name: Show key SRC service states
ansible.builtin.command: "lssrc -s {{ item }}"
changed_when: false
failed_when: false
check_mode: false
loop:
- sshd
- syslogd
- audit
register: cis_aix_post_services
- name: Show password policy summary
ansible.builtin.command: lssec -f /etc/security/user -s default -a minlen histsize maxage minage minalpha minother loginretries
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_post_password
- name: Build AIX hardening validation summary
ansible.builtin.set_fact:
cis_aix_validation_summary:
oslevel: "{{ cis_aix_oslevel.stdout | default('unavailable') }}"
sshd_config_valid: "{{ cis_aix_post_sshd.rc == 0 }}"
sshd_validation_output: "{{ cis_aix_post_sshd.stderr | default(cis_aix_post_sshd.stdout | default('')) }}"
network_values: "{{ cis_aix_post_network.results | map(attribute='stdout') | list }}"
service_states: "{{ cis_aix_post_services.results | map(attribute='stdout') | list }}"
password_policy: "{{ cis_aix_post_password.stdout | default('unavailable') }}"
recommendations:
- "Validate SSH access from a second privileged session before enforcing passwordless-only access."
- "Review audit classes and events with security operations before setting cis_enable_audit=true."
- "Keep cis_manage_mount_options=false until filesystem owners approve remount or chfs behavior."
- name: Print AIX operational postcheck recommendations
ansible.builtin.debug:
msg:
- >-
{{ 'OK: sshd configuration validates.'
if cis_aix_post_sshd.rc == 0 else 'CRITICAL: sshd validation failed; review SSH config before restarting sessions.' }}
- "OK: Service states: {{ cis_aix_validation_summary.service_states }}"
- "OK: Password policy summary: {{ cis_aix_validation_summary.password_policy }}"
- "WARNING: This role is CIS-inspired and does not represent a complete CIS certification implementation."
- "{{ cis_aix_validation_summary.recommendations }}"
@@ -0,0 +1,147 @@
---
- name: Determine root filesystem free space
ansible.builtin.set_fact:
cis_aix_root_mount: "{{ ansible_mounts | selectattr('mount', 'equalto', '/') | list | first | default({}) }}"
- name: Calculate root filesystem free space in MB
ansible.builtin.set_fact:
cis_aix_root_free_mb: "{{ ((cis_aix_root_mount.size_available | default(0) | int) / 1024 / 1024) | round(0, 'floor') | int }}"
- name: Collect AIX maintenance level
ansible.builtin.command: oslevel -s
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_oslevel
- name: Check required AIX commands
ansible.builtin.shell: "command -v {{ item | quote }} >/dev/null 2>&1 || whence {{ item | quote }} >/dev/null 2>&1"
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
loop: "{{ cis_required_commands }}"
register: cis_aix_required_command_checks
- name: Build missing required command list
ansible.builtin.set_fact:
cis_aix_missing_required_commands: >-
{{
cis_aix_required_command_checks.results
| selectattr('rc', 'ne', 0)
| map(attribute='item')
| list
}}
- name: Locate sshd binary
ansible.builtin.stat:
path: "{{ item }}"
loop: "{{ cis_ssh_candidate_paths }}"
register: cis_aix_sshd_path_checks
- name: Store detected sshd binary
ansible.builtin.set_fact:
cis_aix_sshd_path: >-
{{
(
cis_aix_sshd_path_checks.results
| selectattr('stat.exists')
| map(attribute='item')
| list
| first
)
| default('')
}}
- name: Validate SRC subsystem availability
ansible.builtin.command: lssrc -a
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_src_summary
- name: Validate audit subsystem availability
ansible.builtin.command: audit query
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_audit_query
- name: Collect LPAR summary when available
ansible.builtin.shell: "command -v lparstat >/dev/null 2>&1 && lparstat -i || true"
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_lparstat
- name: Collect current network tunable summary
ansible.builtin.command: no -a
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_network_summary
- name: Collect default AIX user security summary
ansible.builtin.command: lssec -f /etc/security/user -s default -a ALL
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_security_user_summary
- name: Report AIX precheck status
ansible.builtin.debug:
msg:
- >-
OK: Facts gathered for {{ ansible_distribution | default(ansible_system | default('unknown')) }}
{{ ansible_distribution_version | default(ansible_kernel | default('unknown')) }}.
- "OK: oslevel -s reports {{ cis_aix_oslevel.stdout | default('unavailable') }}."
- "OK: Root filesystem free space is {{ cis_aix_root_free_mb }} MB."
- >-
{{ 'OK: sshd binary detected at ' ~ cis_aix_sshd_path
if cis_aix_sshd_path | length > 0 else 'CRITICAL: sshd binary was not found in expected AIX paths.' }}
- >-
{{ 'OK: SRC subsystem commands are functional.'
if cis_aix_src_summary.rc == 0 else 'CRITICAL: lssrc failed; SRC is unavailable or not usable.' }}
- >-
{{ 'OK: AIX audit subsystem responded to audit query.'
if cis_aix_audit_query.rc == 0 else 'WARNING: audit query did not complete; audit may be disabled or unconfigured.' }}
- >-
{{ 'OK: Required commands are present.'
if cis_aix_missing_required_commands | length == 0
else 'CRITICAL: Missing required commands: ' ~ (cis_aix_missing_required_commands | join(', ')) }}
- name: Fail when operating system is unsupported
ansible.builtin.assert:
that:
- ansible_system | default(ansible_distribution | default('')) == 'AIX'
- ansible_distribution_version | default('') is match('^7\\.')
fail_msg: >-
CRITICAL: This role supports IBM AIX 7.x only.
Detected {{ ansible_distribution | default(ansible_system | default('unknown')) }}
{{ ansible_distribution_version | default('unknown') }}.
success_msg: "OK: Supported IBM AIX 7.x platform detected."
- name: Fail when root filesystem free space is below safety threshold
ansible.builtin.assert:
that:
- cis_aix_root_free_mb | int >= cis_min_root_free_mb | int
fail_msg: >-
CRITICAL: Root filesystem has {{ cis_aix_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."
- name: Fail when critical AIX commands are missing
ansible.builtin.assert:
that:
- cis_aix_missing_required_commands | length == 0
- cis_aix_src_summary.rc == 0
- cis_aix_sshd_path | length > 0
fail_msg: >-
CRITICAL: Required AIX hardening prerequisites are missing.
Missing commands={{ cis_aix_missing_required_commands | join(', ') | default('none', true) }},
SRC rc={{ cis_aix_src_summary.rc }},
sshd={{ cis_aix_sshd_path | default('not found', true) }}.
success_msg: "OK: Critical AIX hardening prerequisites are available."
@@ -0,0 +1,51 @@
---
- name: Collect SRC subsystem states
ansible.builtin.command: "lssrc -s {{ item }}"
changed_when: false
failed_when: false
check_mode: false
loop: "{{ cis_src_subsystems }}"
register: cis_aix_src_service_states
- name: Validate inetd configuration exists
ansible.builtin.stat:
path: /etc/inetd.conf
register: cis_aix_inetd_config
- name: Read inetd configuration
ansible.builtin.slurp:
src: /etc/inetd.conf
register: cis_aix_inetd_conf_content
when: cis_aix_inetd_config.stat.exists
- name: Disable insecure inetd services when present
ansible.builtin.lineinfile:
path: /etc/inetd.conf
regexp: '^(?!#)({{ item }})\s+'
line: '# \1 disabled by cis-aix7-hardening'
backrefs: true
backup: true
loop: "{{ cis_legacy_inetd_services }}"
when: cis_aix_inetd_config.stat.exists
notify: refresh inetd
- name: Report inetd configuration status
ansible.builtin.debug:
msg:
- >-
{{ 'OK: /etc/inetd.conf exists and legacy entries were reviewed.'
if cis_aix_inetd_config.stat.exists else 'WARNING: /etc/inetd.conf was not found; inetd review skipped.' }}
- "OK: SRC states collected for {{ cis_src_subsystems | join(', ') }}."
- name: Stop inactive legacy SRC subsystems when present
ansible.builtin.command: "stopsrc -s {{ item }}"
changed_when: true
failed_when: false
loop:
- routed
- gated
- named
when: >-
cis_aix_src_summary.stdout is defined
and item in cis_aix_src_summary.stdout
and 'active' in cis_aix_src_summary.stdout
@@ -0,0 +1,42 @@
---
- name: Ensure sshd configuration exists
ansible.builtin.stat:
path: "{{ cis_ssh_config_path }}"
register: cis_aix_sshd_config
- name: Fail when sshd configuration is missing
ansible.builtin.assert:
that:
- cis_aix_sshd_config.stat.exists
fail_msg: "CRITICAL: {{ cis_ssh_config_path }} was not found; refusing to manage SSH hardening."
success_msg: "OK: {{ cis_ssh_config_path }} exists."
- name: Set sshd validation command from detected binary
ansible.builtin.set_fact:
cis_sshd_test_command: "{{ cis_aix_sshd_path }} -t"
when: cis_aix_sshd_path is defined and cis_aix_sshd_path | length > 0
- name: Apply managed AIX sshd hardening block
ansible.builtin.blockinfile:
path: "{{ cis_ssh_config_path }}"
marker: "# {mark} ANSIBLE MANAGED BLOCK cis-aix7-hardening"
owner: root
group: system
mode: "0600"
backup: true
validate: "{{ cis_sshd_test_command }} -f %s"
block: |
PermitRootLogin {{ 'no' if cis_disable_root_login | bool else 'prohibit-password' }}
PermitEmptyPasswords no
PasswordAuthentication {{ 'no' if cis_disable_password_auth | bool else 'yes' }}
MaxAuthTries {{ cis_ssh_max_auth_tries }}
LoginGraceTime {{ cis_ssh_login_grace_time }}
ClientAliveInterval {{ cis_ssh_client_alive_interval }}
ClientAliveCountMax {{ cis_ssh_client_alive_count_max }}
notify:
- validate sshd
- restart sshd
- name: Validate effective sshd configuration
ansible.builtin.command: "{{ cis_sshd_test_command }}"
changed_when: false
@@ -0,0 +1,50 @@
---
- name: Check sudoers file availability
ansible.builtin.stat:
path: "{{ cis_sudoers_path }}"
register: cis_aix_sudoers
- name: Check visudo availability
ansible.builtin.shell: "command -v visudo >/dev/null 2>&1 || whence visudo >/dev/null 2>&1"
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_visudo_available
- name: Manage sudo use_pty default when supported
ansible.builtin.lineinfile:
path: "{{ cis_sudoers_path }}"
regexp: '^Defaults\s+use_pty\b'
line: "Defaults use_pty"
validate: "visudo -cf %s"
when:
- cis_sudo_use_pty | bool
- cis_aix_sudoers.stat.exists
- cis_aix_visudo_available.rc == 0
- name: Manage sudo logfile default
ansible.builtin.lineinfile:
path: "{{ cis_sudoers_path }}"
regexp: '^Defaults\s+logfile='
line: 'Defaults logfile="{{ cis_sudo_logfile }}"'
validate: "visudo -cf %s"
when:
- cis_aix_sudoers.stat.exists
- cis_aix_visudo_available.rc == 0
- name: Validate sudoers syntax
ansible.builtin.command: "visudo -cf {{ cis_sudoers_path }}"
changed_when: false
when:
- cis_aix_sudoers.stat.exists
- cis_aix_visudo_available.rc == 0
- name: Report sudo hardening status
ansible.builtin.debug:
msg:
- >-
{{ 'OK: sudoers exists and visudo validation is available.'
if cis_aix_sudoers.stat.exists and cis_aix_visudo_available.rc == 0
else 'WARNING: sudo or visudo was not found; sudo controls were skipped.' }}
@@ -0,0 +1,51 @@
---
- name: Collect root account security attributes
ansible.builtin.command: lssec -f /etc/security/user -s root -a account_locked login rlogin su sugroups
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_root_security
- name: Collect accounts with administrative UID
ansible.builtin.shell: "awk -F: '$3 == 0 {print $1}' /etc/passwd"
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_uid_zero_accounts
- name: Report administrative account review
ansible.builtin.debug:
msg:
- >-
{{ 'OK: Only root has UID 0.'
if cis_aix_uid_zero_accounts.stdout_lines | default([]) | length == 1
else 'WARNING: Multiple UID 0 accounts detected: ' ~ (cis_aix_uid_zero_accounts.stdout_lines | default([]) | join(', ')) }}
- "OK: Root security attributes: {{ cis_aix_root_security.stdout | default('unavailable') }}"
- name: Ensure root remote login is disabled when requested
ansible.builtin.command: chsec -f /etc/security/user -s root -a rlogin=false
changed_when: true
when:
- cis_disable_root_login | bool
- "'rlogin=false' not in (cis_aix_root_security.stdout | default(''))"
- name: Collect locked or administratively disabled accounts
ansible.builtin.shell: |
set -o pipefail
awk -F: '{print $1}' /etc/passwd | while read user; do
lsuser -a account_locked "$user" 2>/dev/null
done
args:
executable: /bin/ksh
changed_when: false
failed_when: false
check_mode: false
register: cis_aix_account_lock_summary
- name: Report account lock summary
ansible.builtin.debug:
msg:
- "OK: Collected account lock status for local users."
- "{{ cis_aix_account_lock_summary.stdout_lines | default([]) }}"
@@ -0,0 +1,90 @@
# CIS-Inspired Debian and Ubuntu Hardening
This role applies a small, practical set of CIS-inspired operational hardening controls for Debian and Ubuntu servers. It is intentionally readable, conservative, and suitable as a baseline for production environments that still need local review.
## Supported OS
- Debian 13 Trixie
- Ubuntu Server 26.04 LTS
Unsupported distributions and versions fail during precheck before hardening tasks run.
## Implemented Areas
- SSH daemon hardening with a validated drop-in configuration
- Legacy network package removal
- Optional installation and enablement of `auditd`, `chrony`, `rsyslog`, and `sudo`
- Kernel network sysctl hardening
- Basic audit rule examples, disabled by default
- Sudo `use_pty` and optional sudo logfile configuration
- Logging service checks without replacing existing logging configuration
- Filesystem mount option recommendations, disabled by default
## Safety Philosophy
The defaults are intended to be operationally safe:
- Check mode is supported.
- SSH password authentication remains enabled by default.
- Filesystem mount option management is disabled by default.
- Audit rules are not written unless explicitly enabled.
- Services are enabled only when the matching feature is enabled and the service exists.
- Existing logging configuration is not replaced.
This role does not implement the full CIS benchmark and is not a CIS certification implementation.
## Usage
Run in check mode first:
```bash
ansible-playbook playbooks/cis-debian-ubuntu-hardening.yml --check --diff
```
Apply the full baseline:
```bash
ansible-playbook playbooks/cis-debian-ubuntu-hardening.yml
```
Run only selected areas:
```bash
ansible-playbook playbooks/cis-debian-ubuntu-hardening.yml --tags precheck,ssh,postcheck
ansible-playbook playbooks/cis-debian-ubuntu-hardening.yml --tags packages,services
ansible-playbook playbooks/cis-debian-ubuntu-hardening.yml --tags sudo,logging
```
## Key Variables
```yaml
cis_disable_root_login: true
cis_disable_password_auth: false
cis_install_auditd: true
cis_enable_chrony: true
cis_enable_rsyslog: true
cis_remove_legacy_packages: true
cis_enable_sysctl_hardening: true
cis_manage_mount_options: false
cis_manage_audit_rules: false
cis_ssh_max_auth_tries: 4
cis_ssh_login_grace_time: 60
cis_ssh_client_alive_interval: 300
cis_ssh_client_alive_count_max: 3
cis_sudo_use_pty: true
cis_sudo_logfile: /var/log/sudo.log
```
Enable audit rules only after reviewing the examples:
```yaml
cis_manage_audit_rules: true
```
Enable mount option persistence only after reviewing each filesystem target:
```yaml
cis_manage_mount_options: true
```
@@ -0,0 +1,90 @@
---
cis_disable_root_login: true
cis_disable_password_auth: false
cis_install_auditd: true
cis_enable_chrony: true
cis_enable_rsyslog: true
cis_remove_legacy_packages: true
cis_enable_sysctl_hardening: true
cis_manage_mount_options: false
cis_manage_audit_rules: false
cis_ssh_max_auth_tries: 4
cis_ssh_login_grace_time: 60
cis_ssh_client_alive_interval: 300
cis_ssh_client_alive_count_max: 3
cis_sudo_use_pty: true
cis_sudo_logfile: /var/log/sudo.log
cis_min_root_free_mb: 1024
cis_supported_debian_major_version: "13"
cis_supported_ubuntu_version: "26.04"
cis_ssh_service_name: ssh
cis_ssh_dropin_path: /etc/ssh/sshd_config.d/50-cis-debian-ubuntu-hardening.conf
cis_ssh_main_config_path: /etc/ssh/sshd_config
cis_hardening_packages:
- chrony
- rsyslog
- sudo
cis_audit_packages:
- auditd
- audispd-plugins
cis_legacy_packages:
- telnet
- rsh-client
- rsh-server
- talk
- talkd
- nis
cis_sysctl_settings:
net.ipv4.ip_forward: 0
net.ipv4.conf.all.send_redirects: 0
net.ipv4.conf.default.send_redirects: 0
net.ipv4.conf.all.accept_source_route: 0
net.ipv4.conf.default.accept_source_route: 0
net.ipv4.conf.all.accept_redirects: 0
net.ipv4.conf.default.accept_redirects: 0
net.ipv4.tcp_syncookies: 1
cis_sysctl_config_file: /etc/sysctl.d/60-cis-debian-ubuntu-hardening.conf
cis_audit_rules_path: /etc/audit/rules.d/50-cis-debian-ubuntu-hardening.rules
cis_audit_rules:
- "-w /etc/passwd -p wa -k identity"
- "-w /etc/shadow -p wa -k identity"
- "-w /etc/group -p wa -k identity"
- "-w /etc/gshadow -p wa -k identity"
- "-w /etc/sudoers -p wa -k scope"
- "-w /etc/sudoers.d/ -p wa -k scope"
cis_sudoers_dropin_path: /etc/sudoers.d/50-cis-debian-ubuntu-hardening
cis_mount_option_targets:
- path: /tmp
options:
- nodev
- nosuid
- noexec
- path: /var/tmp
options:
- nodev
- nosuid
- noexec
- path: /home
options:
- nodev
cis_container_virtualization_types:
- container
- docker
- lxc
- podman
- containerd
- systemd-nspawn
@@ -0,0 +1,30 @@
---
- name: Validate ssh configuration
ansible.builtin.command: sshd -t
changed_when: false
listen: validate ssh
- name: Restart ssh service safely
ansible.builtin.service:
name: "{{ cis_ssh_service_name }}"
state: restarted
listen: restart ssh
- name: Restart auditd
ansible.builtin.service:
name: auditd
state: restarted
use: service
listen: restart auditd
- name: Restart rsyslog
ansible.builtin.service:
name: rsyslog
state: restarted
listen: restart rsyslog
- name: Restart chrony
ansible.builtin.service:
name: chrony
state: restarted
listen: restart chrony
@@ -0,0 +1,39 @@
---
- name: Ensure audit rules directory exists
ansible.builtin.file:
path: /etc/audit/rules.d
state: directory
owner: root
group: root
mode: "0750"
- name: Report audit rules management mode
ansible.builtin.debug:
msg: >-
{{ 'OK: Baseline audit rule management is enabled.'
if cis_manage_audit_rules | bool
else 'WARNING: Audit rules are not managed because cis_manage_audit_rules is false.' }}
- name: Install baseline audit rules when explicitly enabled
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 }}"
when: cis_manage_audit_rules | bool
notify: restart auditd
- name: Ensure auditd is enabled and running
ansible.builtin.systemd:
name: auditd
enabled: true
state: started
when:
- cis_install_auditd | bool
- "'auditd.service' in ansible_facts.services"
- not cis_container_detected | default(false) | bool
@@ -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,28 @@
---
- name: Ensure rsyslog is installed
ansible.builtin.apt:
name: rsyslog
state: present
update_cache: true
cache_valid_time: 3600
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
- not cis_container_detected | default(false) | 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,48 @@
---
- name: Remove legacy network packages
ansible.builtin.apt:
name: "{{ cis_legacy_packages }}"
state: absent
purge: false
when: cis_remove_legacy_packages | bool
- name: Build enabled hardening package list
ansible.builtin.set_fact:
cis_enabled_hardening_packages: >-
{{
['sudo']
+ (['chrony'] if cis_enable_chrony | bool else [])
+ (['rsyslog'] if cis_enable_rsyslog | bool else [])
}}
- name: Install baseline hardening packages
ansible.builtin.apt:
name: "{{ cis_enabled_hardening_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Install auditd when enabled
ansible.builtin.apt:
name: auditd
state: present
update_cache: true
cache_valid_time: 3600
when: cis_install_auditd | bool
- name: Install audispd plugins when available
ansible.builtin.apt:
name: audispd-plugins
state: present
update_cache: true
cache_valid_time: 3600
register: cis_audispd_plugins_install
failed_when: false
when: cis_install_auditd | bool
- name: Report audispd plugins availability
ansible.builtin.debug:
msg: "WARNING: audispd-plugins was not installed; package may be unavailable for this release."
when:
- cis_install_auditd | bool
- cis_audispd_plugins_install is failed
@@ -0,0 +1,101 @@
---
- name: Validate ssh 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
- not cis_container_detected | default(false) | bool
- name: Gather installed package facts
ansible.builtin.package_facts:
manager: auto
- name: Gather final service facts
ansible.builtin.service_facts:
- name: Build service state summary
ansible.builtin.set_fact:
cis_service_state_summary:
ssh: "{{ ansible_facts.services['ssh.service'].state | default('not-found') }}"
chrony: "{{ ansible_facts.services['chrony.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 package validation summary
ansible.builtin.set_fact:
cis_package_validation_summary:
legacy_absent: "{{ cis_legacy_packages | difference(ansible_facts.packages.keys() | list) }}"
hardening_present: "{{ (cis_enabled_hardening_packages | default(cis_hardening_packages)) | intersect(ansible_facts.packages.keys() | list) }}"
audit_present: "{{ cis_audit_packages | intersect(ansible_facts.packages.keys() | list) }}"
- 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
- not cis_container_detected | default(false) | 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-inspired controls for Debian 13 Trixie and Ubuntu Server 26.04 LTS"
sshd_config: "{{ 'OK' if cis_sshd_validate.rc == 0 else 'CRITICAL' }}"
services: "{{ cis_service_state_summary }}"
packages: "{{ cis_package_validation_summary }}"
sysctl: "{{ cis_sysctl_validation_summary | default({}) }}"
mount_option_updates: "{{ cis_mount_option_summary | default([]) }}"
audit_rules_managed: "{{ cis_manage_audit_rules | bool }}"
applied_controls:
- ssh
- packages
- sysctl
- services
- audit
- sudo
- logging
- filesystem
- name: Show service states
ansible.builtin.debug:
var: cis_service_state_summary
- name: Show package validation
ansible.builtin.debug:
var: cis_package_validation_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,73 @@
---
- 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: Check for apt
ansible.builtin.stat:
path: /usr/bin/apt-get
register: cis_apt_check
- 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."
- >-
{{ 'OK: apt package manager detected.'
if cis_apt_check.stat.exists else 'CRITICAL: apt package manager was not found.' }}
- >-
{{ 'OK: systemd service manager detected.'
if ansible_service_mgr == 'systemd' else 'CRITICAL: systemd service manager is required.' }}
- >-
{{ 'WARNING: Containerized environment detected; service and kernel controls may be limited.'
if cis_container_detected else 'OK: No containerized runtime detected from Ansible facts.' }}
- name: Fail when operating system is unsupported
ansible.builtin.assert:
that:
- >-
(ansible_distribution == 'Debian'
and ansible_distribution_major_version == cis_supported_debian_major_version)
or
(ansible_distribution == 'Ubuntu'
and ansible_distribution_version is version(cis_supported_ubuntu_version, '=='))
fail_msg: >-
CRITICAL: This role supports only Debian 13 / Trixie and Ubuntu Server 26.04 LTS.
Detected {{ ansible_distribution }} {{ ansible_distribution_version }}.
success_msg: "OK: Supported Debian/Ubuntu 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 apt is unavailable
ansible.builtin.assert:
that:
- cis_apt_check.stat.exists
fail_msg: "CRITICAL: apt-get is required for this Debian/Ubuntu hardening role."
success_msg: "OK: apt-get 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,30 @@
---
- name: Gather service facts
ansible.builtin.service_facts:
- name: Enable chrony service when present and enabled
ansible.builtin.systemd:
name: chrony
enabled: true
state: started
when:
- cis_enable_chrony | bool
- "'chrony.service' in ansible_facts.services"
- name: Enable rsyslog service when present and enabled
ansible.builtin.systemd:
name: rsyslog
enabled: true
state: started
when:
- cis_enable_rsyslog | bool
- "'rsyslog.service' in ansible_facts.services"
- name: Enable auditd service when present and enabled
ansible.builtin.systemd:
name: auditd
enabled: true
state: started
when:
- cis_install_auditd | bool
- "'auditd.service' in ansible_facts.services"
@@ -0,0 +1,99 @@
---
- 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: Ensure sshd drop-in directory is included
ansible.builtin.lineinfile:
path: "{{ cis_ssh_main_config_path }}"
regexp: '^Include\s+/etc/ssh/sshd_config\.d/\*\.conf'
line: "Include /etc/ssh/sshd_config.d/*.conf"
insertbefore: BOF
validate: sshd -t -f %s
notify:
- validate ssh
- restart ssh
- 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 ssh
- restart ssh
- 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 ssh
- restart ssh
- 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 ssh
- restart ssh
- 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 ssh
- restart ssh
- 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 ssh
- restart ssh
- 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 ssh
- restart ssh
- 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 ssh
- restart ssh
@@ -0,0 +1,23 @@
---
- name: Build sudo hardening directives
ansible.builtin.set_fact:
cis_sudo_directives: >-
{{
([{'regexp': '^Defaults\s+use_pty', 'line': 'Defaults use_pty'}]
if cis_sudo_use_pty | bool else [])
+ [{'regexp': '^Defaults\s+logfile=', 'line': 'Defaults logfile="' ~ cis_sudo_logfile ~ '"'}]
}}
- 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: "{{ cis_sudo_directives }}"
loop_control:
label: "{{ item.line }}"
@@ -0,0 +1,17 @@
---
- 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 }}"
when: not cis_container_detected | default(false) | bool
- name: Report skipped sysctl hardening inside containers
ansible.builtin.debug:
msg: "WARNING: Sysctl hardening skipped because a containerized environment was detected."
when: cis_container_detected | default(false) | bool
@@ -0,0 +1,83 @@
# CIS-Inspired RHEL 9 Hardening Role
This role provides a practical, production-style hardening baseline for RHEL 9 and Oracle Linux 9 systems. It is inspired by CIS Benchmark controls for Red Hat Enterprise Linux 9 version 2.0.0, but it is intentionally scoped to common operational controls that infrastructure and security operations teams frequently automate.
This is not a full CIS certification implementation.
## Supported Platforms
- Red Hat Enterprise Linux 9
- Oracle Linux 9
The role fails safely on unsupported operating systems or unsupported major versions.
## Implemented Controls
- SSH daemon hardening for root login, empty passwords, password authentication, retry limits, login grace time, and client keepalive behavior.
- Removal of selected legacy network packages such as telnet, rsh-server, and ypbind.
- Optional installation and enablement of chrony, auditd, and rsyslog.
- CIS-inspired IPv4 network sysctl settings.
- Service enablement for chronyd, auditd, and rsyslog.
- Safe disabling of known legacy services when they are present.
- Basic audit backlog and audit rule examples.
- Sudo defaults for `use_pty` and a configurable sudo logfile.
- Rsyslog service validation and journald configuration presence checks.
- Optional filesystem mount option persistence for selected paths.
## Safety Philosophy
The defaults are conservative. The role supports Ansible check mode and avoids destructive production behavior by default. Filesystem mount option management is disabled unless `cis_manage_mount_options` is explicitly enabled, and even then the role persists configured options without remounting live filesystems.
Review variables before using this role in production.
## Common Variables
```yaml
cis_disable_root_login: true
cis_disable_password_auth: false
cis_install_auditd: true
cis_enable_chrony: true
cis_enable_rsyslog: true
cis_remove_legacy_packages: true
cis_enable_sysctl_hardening: true
cis_manage_mount_options: false
```
## Check Mode
Run a full safety preview:
```bash
ansible-playbook playbooks/cis-rhel9-hardening.yml --check --diff
```
Run only SSH controls in check mode:
```bash
ansible-playbook playbooks/cis-rhel9-hardening.yml --check --diff --tags ssh
```
## Tags
Useful tags include:
- `precheck`
- `packages`
- `ssh`
- `sysctl`
- `services`
- `audit`
- `sudo`
- `logging`
- `filesystem`
- `postcheck`
Example:
```bash
ansible-playbook playbooks/cis-rhel9-hardening.yml --tags precheck,ssh,postcheck
```
## Production Rollout Notes
This role is a hardening starting point for internal infrastructure teams. It should be reviewed against local access patterns, break-glass procedures, compliance requirements, monitoring expectations, and host build standards before rollout.
@@ -0,0 +1,80 @@
---
cis_benchmark_version: "2.0.0"
cis_disable_root_login: true
cis_disable_password_auth: false
cis_install_auditd: true
cis_enable_chrony: true
cis_enable_rsyslog: true
cis_remove_legacy_packages: true
cis_enable_sysctl_hardening: true
cis_manage_mount_options: false
cis_ssh_max_auth_tries: 4
cis_ssh_login_grace_time: 60
cis_ssh_client_alive_interval: 300
cis_ssh_client_alive_count_max: 3
cis_ssh_dropin_path: /etc/ssh/sshd_config.d/50-cis-rhel9-hardening.conf
cis_min_root_free_mb: 1024
cis_legacy_packages:
- telnet
- rsh-server
- ypbind
cis_legacy_services:
- telnet.socket
- rsh.socket
- rexec.socket
- rlogin.socket
- ypbind.service
cis_sysctl_settings:
net.ipv4.ip_forward: 0
net.ipv4.conf.all.send_redirects: 0
net.ipv4.conf.default.send_redirects: 0
net.ipv4.conf.all.accept_source_route: 0
net.ipv4.conf.default.accept_source_route: 0
net.ipv4.conf.all.accept_redirects: 0
net.ipv4.conf.default.accept_redirects: 0
net.ipv4.tcp_syncookies: 1
cis_sysctl_config_file: /etc/sysctl.d/60-cis-rhel9-hardening.conf
cis_audit_rules_path: /etc/audit/rules.d/50-cis-rhel9-hardening.rules
cis_audit_backlog_limit: 8192
cis_audit_rules:
- "-w /etc/passwd -p wa -k identity"
- "-w /etc/shadow -p wa -k identity"
- "-w /etc/group -p wa -k identity"
- "-w /etc/gshadow -p wa -k identity"
- "-w /etc/sudoers -p wa -k scope"
- "-w /etc/sudoers.d/ -p wa -k scope"
- "-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change"
cis_sudoers_dropin_path: /etc/sudoers.d/50-cis-rhel9-hardening
cis_sudo_logfile: /var/log/sudo.log
cis_mount_option_targets:
- path: /tmp
options:
- nodev
- nosuid
- noexec
- path: /var/tmp
options:
- nodev
- nosuid
- noexec
- path: /home
options:
- nodev
cis_container_virtualization_types:
- container
- docker
- lxc
- podman
- containerd
- systemd-nspawn
@@ -0,0 +1,24 @@
---
- name: Validate sshd configuration
ansible.builtin.command: sshd -t
changed_when: false
listen: validate sshd
- name: Reload sshd
ansible.builtin.service:
name: sshd
state: reloaded
listen: reload sshd
- name: Restart auditd
ansible.builtin.service:
name: auditd
state: restarted
use: service
listen: restart auditd
- name: Restart rsyslog
ansible.builtin.service:
name: rsyslog
state: restarted
listen: restart rsyslog
@@ -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 }}"
@@ -0,0 +1,6 @@
---
cis_supported_distributions:
- RedHat
- OracleLinux
cis_supported_major_version: "9"