From 2fd9c0b5ef57540bb6548c3d2832a8274121c718 Mon Sep 17 00:00:00 2001 From: Mateusz Suski Date: Wed, 6 May 2026 08:56:45 +0000 Subject: [PATCH] Add Debian 13 and Ubuntu 26.04 CIS-inspired hardening playbook --- .../playbooks/cis-debian-ubuntu-hardening.yml | 20 ++++ .../cis-debian-ubuntu-hardening/README.md | 90 ++++++++++++++++ .../defaults/main.yml | 90 ++++++++++++++++ .../handlers/main.yml | 30 ++++++ .../tasks/audit.yml | 39 +++++++ .../tasks/filesystem.yml | 36 +++++++ .../tasks/logging.yml | 28 +++++ .../tasks/main.yml | 54 ++++++++++ .../tasks/packages.yml | 48 +++++++++ .../tasks/postcheck.yml | 101 ++++++++++++++++++ .../tasks/precheck.yml | 73 +++++++++++++ .../tasks/services.yml | 30 ++++++ .../cis-debian-ubuntu-hardening/tasks/ssh.yml | 99 +++++++++++++++++ .../tasks/sudo.yml | 23 ++++ .../tasks/sysctl.yml | 17 +++ 15 files changed, 778 insertions(+) create mode 100644 infra-run/ansible/playbooks/cis-debian-ubuntu-hardening.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/README.md create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/defaults/main.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/handlers/main.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/audit.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/filesystem.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/logging.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/main.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/packages.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/postcheck.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/precheck.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/services.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/ssh.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/sudo.yml create mode 100644 infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/sysctl.yml diff --git a/infra-run/ansible/playbooks/cis-debian-ubuntu-hardening.yml b/infra-run/ansible/playbooks/cis-debian-ubuntu-hardening.yml new file mode 100644 index 0000000..39271c2 --- /dev/null +++ b/infra-run/ansible/playbooks/cis-debian-ubuntu-hardening.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/README.md b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/README.md new file mode 100644 index 0000000..df07dc1 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/README.md @@ -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 +``` diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/defaults/main.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/defaults/main.yml new file mode 100644 index 0000000..c35c51e --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/defaults/main.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/handlers/main.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/handlers/main.yml new file mode 100644 index 0000000..c26d5c7 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/handlers/main.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/audit.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/audit.yml new file mode 100644 index 0000000..a4afce2 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/audit.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/filesystem.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/filesystem.yml new file mode 100644 index 0000000..ef306c4 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-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-debian-ubuntu-hardening/tasks/logging.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/logging.yml new file mode 100644 index 0000000..acd7646 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/logging.yml @@ -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.' }} diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/main.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/main.yml new file mode 100644 index 0000000..33e8b74 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-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-debian-ubuntu-hardening/tasks/packages.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/packages.yml new file mode 100644 index 0000000..bfbb29e --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/packages.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/postcheck.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/postcheck.yml new file mode 100644 index 0000000..3f7be17 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/postcheck.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/precheck.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/precheck.yml new file mode 100644 index 0000000..3c4a2b5 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/precheck.yml @@ -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." diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/services.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/services.yml new file mode 100644 index 0000000..66c27e8 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/services.yml @@ -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" diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/ssh.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/ssh.yml new file mode 100644 index 0000000..676044d --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/ssh.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/sudo.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/sudo.yml new file mode 100644 index 0000000..fc81309 --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/sudo.yml @@ -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 }}" diff --git a/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/sysctl.yml b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/sysctl.yml new file mode 100644 index 0000000..6ac7cad --- /dev/null +++ b/infra-run/ansible/roles/cis-debian-ubuntu-hardening/tasks/sysctl.yml @@ -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