From 75a11f76504d0735c17595a681e101ffe647defc Mon Sep 17 00:00:00 2001 From: Mateusz Suski Date: Wed, 6 May 2026 08:45:33 +0000 Subject: [PATCH] Add RHEL 9 CIS-inspired hardening playbook --- infra-run/ansible/ansible.cfg | 9 ++ .../ansible/collections/requirements.yml | 4 + .../ansible/inventory/group_vars/all.yml | 18 ++++ infra-run/ansible/inventory/hosts.yml | 5 ++ .../ansible/playbooks/cis-rhel9-hardening.yml | 20 +++++ .../roles/cis-rhel9-hardening/README.md | 83 +++++++++++++++++ .../cis-rhel9-hardening/defaults/main.yml | 80 +++++++++++++++++ .../cis-rhel9-hardening/handlers/main.yml | 24 +++++ .../roles/cis-rhel9-hardening/tasks/audit.yml | 38 ++++++++ .../cis-rhel9-hardening/tasks/filesystem.yml | 36 ++++++++ .../cis-rhel9-hardening/tasks/logging.yml | 24 +++++ .../roles/cis-rhel9-hardening/tasks/main.yml | 54 ++++++++++++ .../cis-rhel9-hardening/tasks/packages.yml | 24 +++++ .../cis-rhel9-hardening/tasks/postcheck.yml | 79 +++++++++++++++++ .../cis-rhel9-hardening/tasks/precheck.yml | 54 ++++++++++++ .../cis-rhel9-hardening/tasks/services.yml | 36 ++++++++ .../roles/cis-rhel9-hardening/tasks/ssh.yml | 88 +++++++++++++++++++ .../roles/cis-rhel9-hardening/tasks/sudo.yml | 18 ++++ .../cis-rhel9-hardening/tasks/sysctl.yml | 11 +++ .../roles/cis-rhel9-hardening/vars/main.yml | 6 ++ 20 files changed, 711 insertions(+) create mode 100644 infra-run/ansible/ansible.cfg create mode 100644 infra-run/ansible/collections/requirements.yml create mode 100644 infra-run/ansible/inventory/group_vars/all.yml create mode 100644 infra-run/ansible/inventory/hosts.yml create mode 100644 infra-run/ansible/playbooks/cis-rhel9-hardening.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/README.md create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/defaults/main.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/handlers/main.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/audit.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/filesystem.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/logging.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/main.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/packages.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/postcheck.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/precheck.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/services.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/ssh.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/sudo.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/tasks/sysctl.yml create mode 100644 infra-run/ansible/roles/cis-rhel9-hardening/vars/main.yml diff --git a/infra-run/ansible/ansible.cfg b/infra-run/ansible/ansible.cfg new file mode 100644 index 0000000..9c1c917 --- /dev/null +++ b/infra-run/ansible/ansible.cfg @@ -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 diff --git a/infra-run/ansible/collections/requirements.yml b/infra-run/ansible/collections/requirements.yml new file mode 100644 index 0000000..92a0244 --- /dev/null +++ b/infra-run/ansible/collections/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: ansible.posix + - name: community.general diff --git a/infra-run/ansible/inventory/group_vars/all.yml b/infra-run/ansible/inventory/group_vars/all.yml new file mode 100644 index 0000000..7d3c230 --- /dev/null +++ b/infra-run/ansible/inventory/group_vars/all.yml @@ -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 diff --git a/infra-run/ansible/inventory/hosts.yml b/infra-run/ansible/inventory/hosts.yml new file mode 100644 index 0000000..5e1fb88 --- /dev/null +++ b/infra-run/ansible/inventory/hosts.yml @@ -0,0 +1,5 @@ +--- +linux: + hosts: + localhost: + ansible_connection: local diff --git a/infra-run/ansible/playbooks/cis-rhel9-hardening.yml b/infra-run/ansible/playbooks/cis-rhel9-hardening.yml new file mode 100644 index 0000000..cf428f5 --- /dev/null +++ b/infra-run/ansible/playbooks/cis-rhel9-hardening.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/README.md b/infra-run/ansible/roles/cis-rhel9-hardening/README.md new file mode 100644 index 0000000..65b9810 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/README.md @@ -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. diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/defaults/main.yml b/infra-run/ansible/roles/cis-rhel9-hardening/defaults/main.yml new file mode 100644 index 0000000..8acc147 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/defaults/main.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/handlers/main.yml b/infra-run/ansible/roles/cis-rhel9-hardening/handlers/main.yml new file mode 100644 index 0000000..c267736 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/handlers/main.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/audit.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/audit.yml new file mode 100644 index 0000000..19f71ba --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/audit.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/filesystem.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/filesystem.yml new file mode 100644 index 0000000..ef306c4 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/filesystem.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/logging.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/logging.yml new file mode 100644 index 0000000..9479c03 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/logging.yml @@ -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.' }} diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/main.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/main.yml new file mode 100644 index 0000000..33e8b74 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/main.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/packages.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/packages.yml new file mode 100644 index 0000000..e083b67 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/packages.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/postcheck.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/postcheck.yml new file mode 100644 index 0000000..58b2da3 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/postcheck.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/precheck.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/precheck.yml new file mode 100644 index 0000000..78d2971 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/precheck.yml @@ -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." diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/services.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/services.yml new file mode 100644 index 0000000..590e58a --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/services.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/ssh.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/ssh.yml new file mode 100644 index 0000000..69cffba --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/ssh.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/sudo.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/sudo.yml new file mode 100644 index 0000000..553d7e5 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/sudo.yml @@ -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 }}" diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/tasks/sysctl.yml b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/sysctl.yml new file mode 100644 index 0000000..d4cccb9 --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/tasks/sysctl.yml @@ -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 }}" diff --git a/infra-run/ansible/roles/cis-rhel9-hardening/vars/main.yml b/infra-run/ansible/roles/cis-rhel9-hardening/vars/main.yml new file mode 100644 index 0000000..f4b327d --- /dev/null +++ b/infra-run/ansible/roles/cis-rhel9-hardening/vars/main.yml @@ -0,0 +1,6 @@ +--- +cis_supported_distributions: + - RedHat + - OracleLinux + +cis_supported_major_version: "9"