From 02a51f72f9be49511091e7140422ebf34bd8ad4a Mon Sep 17 00:00:00 2001 From: Mateusz Suski Date: Wed, 6 May 2026 09:21:15 +0000 Subject: [PATCH] Add IBM AIX 7 CIS-inspired hardening playbook --- infra-run/ansible/inventory/hosts.yml | 3 + .../ansible/playbooks/cis-aix7-hardening.yml | 21 +++ .../roles/cis-aix7-hardening/README.md | 67 ++++++++ .../cis-aix7-hardening/defaults/main.yml | 98 ++++++++++++ .../cis-aix7-hardening/handlers/main.yml | 44 ++++++ .../roles/cis-aix7-hardening/tasks/audit.yml | 32 ++++ .../roles/cis-aix7-hardening/tasks/cron.yml | 49 ++++++ .../cis-aix7-hardening/tasks/filesystem.yml | 60 +++++++ .../cis-aix7-hardening/tasks/logging.yml | 40 +++++ .../roles/cis-aix7-hardening/tasks/main.yml | 65 ++++++++ .../cis-aix7-hardening/tasks/network.yml | 65 ++++++++ .../tasks/password_policy.yml | 66 ++++++++ .../cis-aix7-hardening/tasks/postcheck.yml | 58 +++++++ .../cis-aix7-hardening/tasks/precheck.yml | 147 ++++++++++++++++++ .../cis-aix7-hardening/tasks/services.yml | 51 ++++++ .../roles/cis-aix7-hardening/tasks/ssh.yml | 42 +++++ .../roles/cis-aix7-hardening/tasks/sudo.yml | 50 ++++++ .../roles/cis-aix7-hardening/tasks/users.yml | 51 ++++++ 18 files changed, 1009 insertions(+) create mode 100644 infra-run/ansible/playbooks/cis-aix7-hardening.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/README.md create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/defaults/main.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/handlers/main.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/audit.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/cron.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/filesystem.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/logging.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/main.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/network.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/password_policy.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/postcheck.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/precheck.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/services.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/ssh.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/sudo.yml create mode 100644 infra-run/ansible/roles/cis-aix7-hardening/tasks/users.yml diff --git a/infra-run/ansible/inventory/hosts.yml b/infra-run/ansible/inventory/hosts.yml index 5e1fb88..0d6c739 100644 --- a/infra-run/ansible/inventory/hosts.yml +++ b/infra-run/ansible/inventory/hosts.yml @@ -3,3 +3,6 @@ linux: hosts: localhost: ansible_connection: local + +aix: + hosts: {} diff --git a/infra-run/ansible/playbooks/cis-aix7-hardening.yml b/infra-run/ansible/playbooks/cis-aix7-hardening.yml new file mode 100644 index 0000000..08ef6c4 --- /dev/null +++ b/infra-run/ansible/playbooks/cis-aix7-hardening.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-aix7-hardening/README.md b/infra-run/ansible/roles/cis-aix7-hardening/README.md new file mode 100644 index 0000000..f19c845 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/README.md @@ -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. diff --git a/infra-run/ansible/roles/cis-aix7-hardening/defaults/main.yml b/infra-run/ansible/roles/cis-aix7-hardening/defaults/main.yml new file mode 100644 index 0000000..81eb8b6 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/defaults/main.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-aix7-hardening/handlers/main.yml b/infra-run/ansible/roles/cis-aix7-hardening/handlers/main.yml new file mode 100644 index 0000000..c79fba2 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/handlers/main.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/audit.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/audit.yml new file mode 100644 index 0000000..ef595d4 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/audit.yml @@ -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('') }}" diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/cron.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/cron.yml new file mode 100644 index 0000000..917aad4 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/cron.yml @@ -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.' }} diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/filesystem.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/filesystem.yml new file mode 100644 index 0000000..d803037 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/filesystem.yml @@ -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([]) }}" diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/logging.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/logging.yml new file mode 100644 index 0000000..ce4d78d --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/logging.yml @@ -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.' }} diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/main.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/main.yml new file mode 100644 index 0000000..0d9c963 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/main.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/network.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/network.yml new file mode 100644 index 0000000..b3fc93c --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/network.yml @@ -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.' }} diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/password_policy.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/password_policy.yml new file mode 100644 index 0000000..828d5f0 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/password_policy.yml @@ -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') }}" diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/postcheck.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/postcheck.yml new file mode 100644 index 0000000..0527aa7 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/postcheck.yml @@ -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 }}" diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/precheck.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/precheck.yml new file mode 100644 index 0000000..600bd67 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/precheck.yml @@ -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." diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/services.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/services.yml new file mode 100644 index 0000000..23bd2de --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/services.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/ssh.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/ssh.yml new file mode 100644 index 0000000..62c1f0d --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/ssh.yml @@ -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 diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/sudo.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/sudo.yml new file mode 100644 index 0000000..03b614e --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/sudo.yml @@ -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.' }} diff --git a/infra-run/ansible/roles/cis-aix7-hardening/tasks/users.yml b/infra-run/ansible/roles/cis-aix7-hardening/tasks/users.yml new file mode 100644 index 0000000..ce19f58 --- /dev/null +++ b/infra-run/ansible/roles/cis-aix7-hardening/tasks/users.yml @@ -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([]) }}"