Add IBM AIX 7 CIS-inspired hardening playbook
This commit is contained in:
@@ -3,3 +3,6 @@ linux:
|
||||
hosts:
|
||||
localhost:
|
||||
ansible_connection: local
|
||||
|
||||
aix:
|
||||
hosts: {}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
- name: Apply CIS-inspired IBM AIX 7 hardening controls
|
||||
hosts: aix
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
roles:
|
||||
- role: cis-aix7-hardening
|
||||
tags:
|
||||
- cis
|
||||
- aix7
|
||||
- hardening
|
||||
|
||||
post_tasks:
|
||||
- name: Show AIX hardening validation summary
|
||||
ansible.builtin.debug:
|
||||
var: cis_aix_validation_summary
|
||||
when: cis_aix_validation_summary is defined
|
||||
tags:
|
||||
- always
|
||||
- postcheck
|
||||
@@ -0,0 +1,67 @@
|
||||
# cis-aix7-hardening
|
||||
|
||||
Operational IBM AIX 7.x hardening role inspired by CIS Benchmark 1.2.0 and common enterprise Unix security practices.
|
||||
|
||||
Reference: https://www.cisecurity.org/benchmark/aix
|
||||
|
||||
This role is intended for infrastructure and security operations teams that manage production AIX estates. It favors readable, conservative controls over broad benchmark coverage.
|
||||
|
||||
## Supported OS
|
||||
|
||||
- IBM AIX 7.x
|
||||
|
||||
## Implemented Areas
|
||||
|
||||
- Platform prechecks for AIX 7.x, SRC, SSH, audit tooling, required commands, disk safety, and baseline security state.
|
||||
- SSH daemon hardening in `/etc/ssh/sshd_config` with validation through `sshd -t`.
|
||||
- Account and password controls through AIX-native `lssec`, `chsec`, and `pwdadm`.
|
||||
- Network tunable validation and optional hardening through `no`, with optional `nfso` support.
|
||||
- SRC-aware service checks and safe inetd legacy service disablement.
|
||||
- Filesystem review for JFS2, world-writable directories, and invalid owners or groups.
|
||||
- Syslog and audit validation, with audit enablement disabled by default.
|
||||
- Cron and at permission hardening under `/var/adm/cron`.
|
||||
- Sudo defaults with validation through `visudo -cf` when sudo is present.
|
||||
- Postcheck reporting for SSH, services, network values, and password policy.
|
||||
|
||||
## AIX Operational Notes
|
||||
|
||||
AIX is not Linux. This role does not assume systemd, sysctl, Linux package managers, or Linux service paths. Service operations use SRC commands such as `lssrc`, `startsrc`, `stopsrc`, and `refresh`.
|
||||
|
||||
AIX environments vary heavily between enterprises. Filesystem layout, OpenSSH source, sudo packaging, audit classes, NFS tuning, and security policy ownership should be validated before production rollout.
|
||||
|
||||
## Safety Philosophy
|
||||
|
||||
- Defaults are conservative.
|
||||
- Audit enablement is opt-in with `cis_enable_audit`.
|
||||
- Filesystem mount option management is opt-in with `cis_manage_mount_options`.
|
||||
- SSH password authentication is not disabled by default.
|
||||
- Native AIX security files are updated with targeted `chsec` calls instead of wholesale replacement.
|
||||
- Check mode is supported where practical, though AIX command modules may still need read-only probes for validation.
|
||||
|
||||
## Check Mode Examples
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/cis-aix7-hardening.yml --check
|
||||
```
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/cis-aix7-hardening.yml --check --tags precheck,ssh,postcheck
|
||||
```
|
||||
|
||||
## Tag Examples
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/cis-aix7-hardening.yml --tags precheck
|
||||
```
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/cis-aix7-hardening.yml --tags ssh,password_policy,network
|
||||
```
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/cis-aix7-hardening.yml --tags audit -e cis_enable_audit=true
|
||||
```
|
||||
|
||||
## Important Warning
|
||||
|
||||
This is not a full CIS certification implementation and does not implement the entire CIS AIX benchmark. It is a practical CIS-inspired baseline that should be reviewed by infrastructure, security, and application owners before production enforcement.
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
cis_benchmark_version: "1.2.0"
|
||||
|
||||
cis_disable_root_login: true
|
||||
cis_disable_password_auth: false
|
||||
cis_enable_network_hardening: true
|
||||
cis_enable_password_policy: true
|
||||
cis_enable_audit: false
|
||||
cis_manage_mount_options: false
|
||||
|
||||
cis_ssh_max_auth_tries: 4
|
||||
cis_ssh_login_grace_time: 60
|
||||
cis_ssh_client_alive_interval: 300
|
||||
cis_ssh_client_alive_count_max: 3
|
||||
cis_ssh_config_path: /etc/ssh/sshd_config
|
||||
cis_sshd_test_command: sshd -t
|
||||
|
||||
cis_min_root_free_mb: 1024
|
||||
|
||||
cis_password_minlen: 14
|
||||
cis_password_histsize: 10
|
||||
cis_password_maxage_weeks: 12
|
||||
cis_password_minalpha: 1
|
||||
cis_password_minother: 1
|
||||
cis_password_maxrepeats: 2
|
||||
cis_password_minage_weeks: 1
|
||||
cis_login_retries: 5
|
||||
cis_login_lockout: 30
|
||||
|
||||
cis_required_commands:
|
||||
- lsattr
|
||||
- chdev
|
||||
- lssrc
|
||||
- chsec
|
||||
- lssec
|
||||
- pwdadm
|
||||
- "no"
|
||||
- audit
|
||||
- cron
|
||||
|
||||
cis_ssh_candidate_paths:
|
||||
- /usr/sbin/sshd
|
||||
- /usr/bin/sshd
|
||||
- /opt/freeware/sbin/sshd
|
||||
- /opt/freeware/bin/sshd
|
||||
|
||||
cis_network_no_settings:
|
||||
ipforwarding: "0"
|
||||
ipsendredirects: "0"
|
||||
ipignoreredirects: "1"
|
||||
ipsrcrouteforward: "0"
|
||||
clean_partial_conns: "1"
|
||||
tcp_pmtu_discover: "0"
|
||||
|
||||
cis_network_nfso_settings: {}
|
||||
|
||||
cis_legacy_inetd_services:
|
||||
- telnet
|
||||
- shell
|
||||
- login
|
||||
- exec
|
||||
- comsat
|
||||
- talk
|
||||
- ntalk
|
||||
- tftp
|
||||
- uucp
|
||||
- finger
|
||||
|
||||
cis_src_subsystems:
|
||||
- sshd
|
||||
- inetd
|
||||
- syslogd
|
||||
- audit
|
||||
|
||||
cis_mount_option_targets:
|
||||
- path: /tmp
|
||||
options:
|
||||
- nosuid
|
||||
- path: /var/tmp
|
||||
options:
|
||||
- nosuid
|
||||
|
||||
cis_manage_sudo: true
|
||||
cis_sudoers_path: /etc/sudoers
|
||||
cis_sudo_logfile: /var/log/sudo.log
|
||||
cis_sudo_use_pty: true
|
||||
|
||||
cis_cron_allow_path: /var/adm/cron/cron.allow
|
||||
cis_cron_deny_path: /var/adm/cron/cron.deny
|
||||
cis_at_allow_path: /var/adm/cron/at.allow
|
||||
cis_at_deny_path: /var/adm/cron/at.deny
|
||||
cis_cron_directories:
|
||||
- /var/adm/cron
|
||||
- /var/spool/cron
|
||||
- /var/spool/cron/crontabs
|
||||
|
||||
cis_syslog_config_path: /etc/syslog.conf
|
||||
cis_audit_config_path: /etc/security/audit/config
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
- name: Validate sshd configuration
|
||||
ansible.builtin.command: "{{ cis_sshd_test_command }}"
|
||||
changed_when: false
|
||||
listen: validate sshd
|
||||
|
||||
- name: Restart sshd using SRC
|
||||
ansible.builtin.shell: |
|
||||
set -o pipefail
|
||||
if lssrc -s sshd >/dev/null 2>&1; then
|
||||
stopsrc -s sshd >/dev/null 2>&1 || true
|
||||
startsrc -s sshd
|
||||
fi
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: true
|
||||
listen: restart sshd
|
||||
|
||||
- name: Refresh inetd
|
||||
ansible.builtin.command: refresh -s inetd
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
listen: refresh inetd
|
||||
|
||||
- name: Refresh syslog
|
||||
ansible.builtin.command: refresh -s syslogd
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
listen: refresh syslog
|
||||
|
||||
- name: Restart audit subsystem
|
||||
ansible.builtin.shell: |
|
||||
set -o pipefail
|
||||
if lssrc -s audit >/dev/null 2>&1; then
|
||||
stopsrc -s audit >/dev/null 2>&1 || true
|
||||
startsrc -s audit
|
||||
else
|
||||
audit start
|
||||
fi
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: true
|
||||
when: cis_enable_audit | bool
|
||||
listen: restart audit
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
- name: Validate AIX audit configuration file
|
||||
ansible.builtin.stat:
|
||||
path: "{{ cis_audit_config_path }}"
|
||||
register: cis_aix_audit_config
|
||||
|
||||
- name: Collect AIX audit query status
|
||||
ansible.builtin.command: audit query
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_audit_status
|
||||
|
||||
- name: Enable AIX audit subsystem when explicitly configured
|
||||
ansible.builtin.command: audit start
|
||||
changed_when: true
|
||||
when:
|
||||
- cis_enable_audit | bool
|
||||
- cis_aix_audit_config.stat.exists
|
||||
- cis_aix_audit_status.rc != 0 or 'auditing off' in (cis_aix_audit_status.stdout | default('') | lower)
|
||||
notify: restart audit
|
||||
|
||||
- name: Report audit status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- >-
|
||||
{{ 'OK: AIX audit configuration file exists.'
|
||||
if cis_aix_audit_config.stat.exists else 'WARNING: AIX audit configuration file was not found.' }}
|
||||
- >-
|
||||
{{ 'OK: Audit enablement is explicitly allowed by cis_enable_audit.'
|
||||
if cis_enable_audit | bool else 'WARNING: Audit enablement is disabled by default; validation only was performed.' }}
|
||||
- "OK: audit query rc={{ cis_aix_audit_status.rc }} output={{ cis_aix_audit_status.stdout | default('') }}"
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
- name: Ensure cron and at control files exist with safe ownership
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: touch
|
||||
owner: root
|
||||
group: cron
|
||||
mode: "0600"
|
||||
modification_time: preserve
|
||||
access_time: preserve
|
||||
loop:
|
||||
- "{{ cis_cron_allow_path }}"
|
||||
- "{{ cis_at_allow_path }}"
|
||||
|
||||
- name: Ensure deny files are not world readable when present
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
owner: root
|
||||
group: cron
|
||||
mode: "0600"
|
||||
loop:
|
||||
- "{{ cis_cron_deny_path }}"
|
||||
- "{{ cis_at_deny_path }}"
|
||||
failed_when: false
|
||||
|
||||
- name: Secure cron directories when present
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: cron
|
||||
mode: "0750"
|
||||
loop: "{{ cis_cron_directories }}"
|
||||
failed_when: false
|
||||
|
||||
- name: Validate cron SRC state
|
||||
ansible.builtin.command: lssrc -s cron
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_cron_state
|
||||
|
||||
- name: Report cron and at hardening status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "OK: cron.allow and at.allow ownership and permissions are managed."
|
||||
- >-
|
||||
{{ 'OK: cron SRC subsystem exists.'
|
||||
if cis_aix_cron_state.rc == 0 else 'WARNING: cron SRC subsystem was not found.' }}
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
- name: Build mounted filesystem list from gathered facts
|
||||
ansible.builtin.set_fact:
|
||||
cis_aix_mount_points: "{{ ansible_mounts | map(attribute='mount') | list }}"
|
||||
|
||||
- name: Validate JFS2 filesystems
|
||||
ansible.builtin.shell: |
|
||||
set -o pipefail
|
||||
lsfs -q | awk '/vfs[[:space:]]*=[[:space:]]*jfs2/{print prev} {prev=$0}'
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_jfs2_filesystems
|
||||
|
||||
- name: Review configured mount option targets
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
OK: Mount option management is disabled by default.
|
||||
Review target {{ item.path }} for options {{ item.options | join(', ') }} before production rollout.
|
||||
loop: "{{ cis_mount_option_targets }}"
|
||||
when: not cis_manage_mount_options | bool
|
||||
|
||||
- name: Apply configured mount options only when explicitly enabled
|
||||
ansible.builtin.command: "chfs -a options={{ item.options | join(',') }} {{ item.path }}"
|
||||
changed_when: true
|
||||
loop: "{{ cis_mount_option_targets }}"
|
||||
when:
|
||||
- cis_manage_mount_options | bool
|
||||
- item.path in cis_aix_mount_points
|
||||
|
||||
- name: Identify world-writable directories on local filesystems
|
||||
ansible.builtin.shell: |
|
||||
set -o pipefail
|
||||
find / -xdev -type d -perm -0002 -print 2>/dev/null | head -200
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_world_writable_dirs
|
||||
|
||||
- name: Identify files without valid owner or group on local filesystems
|
||||
ansible.builtin.shell: |
|
||||
set -o pipefail
|
||||
find / -xdev \( -nouser -o -nogroup \) -print 2>/dev/null | head -200
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_unowned_files
|
||||
|
||||
- name: Report filesystem review findings
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "OK: JFS2 filesystem review completed."
|
||||
- "WARNING: World-writable directories found: {{ cis_aix_world_writable_dirs.stdout_lines | default([]) }}"
|
||||
- "WARNING: Files without valid owner/group found: {{ cis_aix_unowned_files.stdout_lines | default([]) }}"
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
- name: Collect syslog SRC state
|
||||
ansible.builtin.command: lssrc -s syslogd
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_syslog_state
|
||||
|
||||
- name: Ensure syslog configuration exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ cis_syslog_config_path }}"
|
||||
register: cis_aix_syslog_config
|
||||
|
||||
- name: Start syslogd when installed but inactive
|
||||
ansible.builtin.command: startsrc -s syslogd
|
||||
changed_when: true
|
||||
when:
|
||||
- cis_aix_syslog_state.rc == 0
|
||||
- "'active' not in cis_aix_syslog_state.stdout"
|
||||
|
||||
- name: Validate syslog configuration has active entries
|
||||
ansible.builtin.shell: "awk 'NF && $1 !~ /^#/ {found=1} END {exit found ? 0 : 1}' {{ cis_syslog_config_path }}"
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_syslog_has_rules
|
||||
when: cis_aix_syslog_config.stat.exists
|
||||
|
||||
- name: Report logging status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- >-
|
||||
{{ 'OK: syslogd SRC subsystem exists.'
|
||||
if cis_aix_syslog_state.rc == 0 else 'WARNING: syslogd SRC subsystem was not found.' }}
|
||||
- >-
|
||||
{{ 'OK: syslog configuration has active rules.'
|
||||
if cis_aix_syslog_has_rules.rc | default(1) == 0
|
||||
else 'WARNING: syslog configuration has no active rules or could not be validated.' }}
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
- name: Run AIX platform safety prechecks
|
||||
ansible.builtin.import_tasks: precheck.yml
|
||||
tags:
|
||||
- always
|
||||
- precheck
|
||||
|
||||
- name: Harden AIX SSH daemon configuration
|
||||
ansible.builtin.import_tasks: ssh.yml
|
||||
tags:
|
||||
- ssh
|
||||
|
||||
- name: Apply AIX user account controls
|
||||
ansible.builtin.import_tasks: users.yml
|
||||
tags:
|
||||
- users
|
||||
|
||||
- name: Apply AIX password policy controls
|
||||
ansible.builtin.import_tasks: password_policy.yml
|
||||
when: cis_enable_password_policy | bool
|
||||
tags:
|
||||
- password_policy
|
||||
|
||||
- name: Apply AIX network hardening controls
|
||||
ansible.builtin.import_tasks: network.yml
|
||||
when: cis_enable_network_hardening | bool
|
||||
tags:
|
||||
- network
|
||||
|
||||
- name: Manage AIX baseline services
|
||||
ansible.builtin.import_tasks: services.yml
|
||||
tags:
|
||||
- services
|
||||
|
||||
- name: Review AIX filesystem controls
|
||||
ansible.builtin.import_tasks: filesystem.yml
|
||||
tags:
|
||||
- filesystem
|
||||
|
||||
- name: Validate AIX logging controls
|
||||
ansible.builtin.import_tasks: logging.yml
|
||||
tags:
|
||||
- logging
|
||||
|
||||
- name: Validate AIX audit controls
|
||||
ansible.builtin.import_tasks: audit.yml
|
||||
tags:
|
||||
- audit
|
||||
|
||||
- name: Harden AIX cron and at controls
|
||||
ansible.builtin.import_tasks: cron.yml
|
||||
tags:
|
||||
- cron
|
||||
|
||||
- name: Harden sudo configuration
|
||||
ansible.builtin.import_tasks: sudo.yml
|
||||
when: cis_manage_sudo | bool
|
||||
tags:
|
||||
- sudo
|
||||
|
||||
- name: Run AIX validation postchecks
|
||||
ansible.builtin.import_tasks: postcheck.yml
|
||||
tags:
|
||||
- always
|
||||
- postcheck
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
- name: Collect current AIX network tunables
|
||||
ansible.builtin.command: no -a
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_no_current
|
||||
|
||||
- name: Query configured AIX network tunables
|
||||
ansible.builtin.command: "no -o {{ item.key }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
loop: "{{ cis_network_no_settings | dict2items }}"
|
||||
register: cis_aix_no_query
|
||||
|
||||
- name: Apply configured AIX network tunables
|
||||
ansible.builtin.command: "no -p -o {{ item.item.key }}={{ item.item.value }}"
|
||||
changed_when: true
|
||||
loop: "{{ cis_aix_no_query.results }}"
|
||||
when:
|
||||
- item.rc == 0
|
||||
- item.stdout is not search('=\\s*' ~ (item.item.value | string) ~ '\\b')
|
||||
|
||||
- name: Warn about unsupported AIX network tunables
|
||||
ansible.builtin.debug:
|
||||
msg: "WARNING: AIX network tunable {{ item.item.key }} is not supported on this host."
|
||||
loop: "{{ cis_aix_no_query.results }}"
|
||||
when: item.rc != 0
|
||||
|
||||
- name: Check nfso availability
|
||||
ansible.builtin.shell: "command -v nfso >/dev/null 2>&1 || whence nfso >/dev/null 2>&1"
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_nfso_available
|
||||
|
||||
- name: Query configured AIX NFS tunables
|
||||
ansible.builtin.command: "nfso -o {{ item.key }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
loop: "{{ cis_network_nfso_settings | dict2items }}"
|
||||
register: cis_aix_nfso_query
|
||||
when:
|
||||
- cis_aix_nfso_available.rc == 0
|
||||
- cis_network_nfso_settings | length > 0
|
||||
|
||||
- name: Apply configured AIX NFS tunables
|
||||
ansible.builtin.command: "nfso -p -o {{ item.item.key }}={{ item.item.value }}"
|
||||
changed_when: true
|
||||
loop: "{{ cis_aix_nfso_query.results | default([]) }}"
|
||||
when:
|
||||
- item.rc == 0
|
||||
- item.stdout is not search('=\\s*' ~ (item.item.value | string) ~ '\\b')
|
||||
|
||||
- name: Report network hardening status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "OK: AIX network tunables were validated before changes."
|
||||
- >-
|
||||
{{ 'OK: nfso is available for optional NFS network tunables.'
|
||||
if cis_aix_nfso_available.rc == 0 else 'WARNING: nfso was not found; NFS tunables were skipped.' }}
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
- name: Collect current default password policy
|
||||
ansible.builtin.command: lssec -f /etc/security/user -s default -a minlen histsize maxage minage minalpha minother maxrepeats loginretries
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_password_policy_current
|
||||
|
||||
- name: Collect current default login policy
|
||||
ansible.builtin.command: lssec -f /etc/security/login.cfg -s usw -a logindisable logininterval loginreenable
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_login_policy_current
|
||||
|
||||
- name: Manage default password security attributes
|
||||
ansible.builtin.command: "chsec -f /etc/security/user -s default -a {{ item.key }}={{ item.value }}"
|
||||
changed_when: true
|
||||
loop:
|
||||
- key: minlen
|
||||
value: "{{ cis_password_minlen }}"
|
||||
- key: histsize
|
||||
value: "{{ cis_password_histsize }}"
|
||||
- key: maxage
|
||||
value: "{{ cis_password_maxage_weeks }}"
|
||||
- key: minage
|
||||
value: "{{ cis_password_minage_weeks }}"
|
||||
- key: minalpha
|
||||
value: "{{ cis_password_minalpha }}"
|
||||
- key: minother
|
||||
value: "{{ cis_password_minother }}"
|
||||
- key: maxrepeats
|
||||
value: "{{ cis_password_maxrepeats }}"
|
||||
- key: loginretries
|
||||
value: "{{ cis_login_retries }}"
|
||||
when: >-
|
||||
(item.key ~ '=' ~ (item.value | string))
|
||||
not in (cis_aix_password_policy_current.stdout | default(''))
|
||||
|
||||
- name: Manage login lockout interval
|
||||
ansible.builtin.command: "chsec -f /etc/security/login.cfg -s usw -a loginreenable={{ cis_login_lockout }}"
|
||||
changed_when: true
|
||||
when: >-
|
||||
('loginreenable=' ~ (cis_login_lockout | string))
|
||||
not in (cis_aix_login_policy_current.stdout | default(''))
|
||||
|
||||
- name: Collect updated default password policy
|
||||
ansible.builtin.command: lssec -f /etc/security/user -s default -a minlen histsize maxage minage minalpha minother maxrepeats loginretries
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_password_policy_updated
|
||||
|
||||
- name: Validate password database state
|
||||
ansible.builtin.command: pwdadm -q root
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_pwdadm_root
|
||||
|
||||
- name: Report password policy status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "OK: Password policy managed through AIX chsec defaults, without replacing security files."
|
||||
- "OK: Current default policy: {{ cis_aix_password_policy_updated.stdout | default('unavailable') }}"
|
||||
- "OK: pwdadm root status: {{ cis_aix_pwdadm_root.stdout | default('unavailable') }}"
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
- name: Validate sshd configuration after hardening
|
||||
ansible.builtin.command: "{{ cis_sshd_test_command }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_post_sshd
|
||||
|
||||
- name: Show selected AIX network security values
|
||||
ansible.builtin.command: "no -o {{ item.key }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
loop: "{{ cis_network_no_settings | dict2items }}"
|
||||
register: cis_aix_post_network
|
||||
|
||||
- name: Show key SRC service states
|
||||
ansible.builtin.command: "lssrc -s {{ item }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
loop:
|
||||
- sshd
|
||||
- syslogd
|
||||
- audit
|
||||
register: cis_aix_post_services
|
||||
|
||||
- name: Show password policy summary
|
||||
ansible.builtin.command: lssec -f /etc/security/user -s default -a minlen histsize maxage minage minalpha minother loginretries
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_post_password
|
||||
|
||||
- name: Build AIX hardening validation summary
|
||||
ansible.builtin.set_fact:
|
||||
cis_aix_validation_summary:
|
||||
oslevel: "{{ cis_aix_oslevel.stdout | default('unavailable') }}"
|
||||
sshd_config_valid: "{{ cis_aix_post_sshd.rc == 0 }}"
|
||||
sshd_validation_output: "{{ cis_aix_post_sshd.stderr | default(cis_aix_post_sshd.stdout | default('')) }}"
|
||||
network_values: "{{ cis_aix_post_network.results | map(attribute='stdout') | list }}"
|
||||
service_states: "{{ cis_aix_post_services.results | map(attribute='stdout') | list }}"
|
||||
password_policy: "{{ cis_aix_post_password.stdout | default('unavailable') }}"
|
||||
recommendations:
|
||||
- "Validate SSH access from a second privileged session before enforcing passwordless-only access."
|
||||
- "Review audit classes and events with security operations before setting cis_enable_audit=true."
|
||||
- "Keep cis_manage_mount_options=false until filesystem owners approve remount or chfs behavior."
|
||||
|
||||
- name: Print AIX operational postcheck recommendations
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- >-
|
||||
{{ 'OK: sshd configuration validates.'
|
||||
if cis_aix_post_sshd.rc == 0 else 'CRITICAL: sshd validation failed; review SSH config before restarting sessions.' }}
|
||||
- "OK: Service states: {{ cis_aix_validation_summary.service_states }}"
|
||||
- "OK: Password policy summary: {{ cis_aix_validation_summary.password_policy }}"
|
||||
- "WARNING: This role is CIS-inspired and does not represent a complete CIS certification implementation."
|
||||
- "{{ cis_aix_validation_summary.recommendations }}"
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
- name: Determine root filesystem free space
|
||||
ansible.builtin.set_fact:
|
||||
cis_aix_root_mount: "{{ ansible_mounts | selectattr('mount', 'equalto', '/') | list | first | default({}) }}"
|
||||
|
||||
- name: Calculate root filesystem free space in MB
|
||||
ansible.builtin.set_fact:
|
||||
cis_aix_root_free_mb: "{{ ((cis_aix_root_mount.size_available | default(0) | int) / 1024 / 1024) | round(0, 'floor') | int }}"
|
||||
|
||||
- name: Collect AIX maintenance level
|
||||
ansible.builtin.command: oslevel -s
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_oslevel
|
||||
|
||||
- name: Check required AIX commands
|
||||
ansible.builtin.shell: "command -v {{ item | quote }} >/dev/null 2>&1 || whence {{ item | quote }} >/dev/null 2>&1"
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
loop: "{{ cis_required_commands }}"
|
||||
register: cis_aix_required_command_checks
|
||||
|
||||
- name: Build missing required command list
|
||||
ansible.builtin.set_fact:
|
||||
cis_aix_missing_required_commands: >-
|
||||
{{
|
||||
cis_aix_required_command_checks.results
|
||||
| selectattr('rc', 'ne', 0)
|
||||
| map(attribute='item')
|
||||
| list
|
||||
}}
|
||||
|
||||
- name: Locate sshd binary
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop: "{{ cis_ssh_candidate_paths }}"
|
||||
register: cis_aix_sshd_path_checks
|
||||
|
||||
- name: Store detected sshd binary
|
||||
ansible.builtin.set_fact:
|
||||
cis_aix_sshd_path: >-
|
||||
{{
|
||||
(
|
||||
cis_aix_sshd_path_checks.results
|
||||
| selectattr('stat.exists')
|
||||
| map(attribute='item')
|
||||
| list
|
||||
| first
|
||||
)
|
||||
| default('')
|
||||
}}
|
||||
|
||||
- name: Validate SRC subsystem availability
|
||||
ansible.builtin.command: lssrc -a
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_src_summary
|
||||
|
||||
- name: Validate audit subsystem availability
|
||||
ansible.builtin.command: audit query
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_audit_query
|
||||
|
||||
- name: Collect LPAR summary when available
|
||||
ansible.builtin.shell: "command -v lparstat >/dev/null 2>&1 && lparstat -i || true"
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_lparstat
|
||||
|
||||
- name: Collect current network tunable summary
|
||||
ansible.builtin.command: no -a
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_network_summary
|
||||
|
||||
- name: Collect default AIX user security summary
|
||||
ansible.builtin.command: lssec -f /etc/security/user -s default -a ALL
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_security_user_summary
|
||||
|
||||
- name: Report AIX precheck status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- >-
|
||||
OK: Facts gathered for {{ ansible_distribution | default(ansible_system | default('unknown')) }}
|
||||
{{ ansible_distribution_version | default(ansible_kernel | default('unknown')) }}.
|
||||
- "OK: oslevel -s reports {{ cis_aix_oslevel.stdout | default('unavailable') }}."
|
||||
- "OK: Root filesystem free space is {{ cis_aix_root_free_mb }} MB."
|
||||
- >-
|
||||
{{ 'OK: sshd binary detected at ' ~ cis_aix_sshd_path
|
||||
if cis_aix_sshd_path | length > 0 else 'CRITICAL: sshd binary was not found in expected AIX paths.' }}
|
||||
- >-
|
||||
{{ 'OK: SRC subsystem commands are functional.'
|
||||
if cis_aix_src_summary.rc == 0 else 'CRITICAL: lssrc failed; SRC is unavailable or not usable.' }}
|
||||
- >-
|
||||
{{ 'OK: AIX audit subsystem responded to audit query.'
|
||||
if cis_aix_audit_query.rc == 0 else 'WARNING: audit query did not complete; audit may be disabled or unconfigured.' }}
|
||||
- >-
|
||||
{{ 'OK: Required commands are present.'
|
||||
if cis_aix_missing_required_commands | length == 0
|
||||
else 'CRITICAL: Missing required commands: ' ~ (cis_aix_missing_required_commands | join(', ')) }}
|
||||
|
||||
- name: Fail when operating system is unsupported
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_system | default(ansible_distribution | default('')) == 'AIX'
|
||||
- ansible_distribution_version | default('') is match('^7\\.')
|
||||
fail_msg: >-
|
||||
CRITICAL: This role supports IBM AIX 7.x only.
|
||||
Detected {{ ansible_distribution | default(ansible_system | default('unknown')) }}
|
||||
{{ ansible_distribution_version | default('unknown') }}.
|
||||
success_msg: "OK: Supported IBM AIX 7.x platform detected."
|
||||
|
||||
- name: Fail when root filesystem free space is below safety threshold
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- cis_aix_root_free_mb | int >= cis_min_root_free_mb | int
|
||||
fail_msg: >-
|
||||
CRITICAL: Root filesystem has {{ cis_aix_root_free_mb }} MB free.
|
||||
Minimum required free space is {{ cis_min_root_free_mb }} MB.
|
||||
success_msg: "OK: Root filesystem free space meets the safety threshold."
|
||||
|
||||
- name: Fail when critical AIX commands are missing
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- cis_aix_missing_required_commands | length == 0
|
||||
- cis_aix_src_summary.rc == 0
|
||||
- cis_aix_sshd_path | length > 0
|
||||
fail_msg: >-
|
||||
CRITICAL: Required AIX hardening prerequisites are missing.
|
||||
Missing commands={{ cis_aix_missing_required_commands | join(', ') | default('none', true) }},
|
||||
SRC rc={{ cis_aix_src_summary.rc }},
|
||||
sshd={{ cis_aix_sshd_path | default('not found', true) }}.
|
||||
success_msg: "OK: Critical AIX hardening prerequisites are available."
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
- name: Collect SRC subsystem states
|
||||
ansible.builtin.command: "lssrc -s {{ item }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
loop: "{{ cis_src_subsystems }}"
|
||||
register: cis_aix_src_service_states
|
||||
|
||||
- name: Validate inetd configuration exists
|
||||
ansible.builtin.stat:
|
||||
path: /etc/inetd.conf
|
||||
register: cis_aix_inetd_config
|
||||
|
||||
- name: Read inetd configuration
|
||||
ansible.builtin.slurp:
|
||||
src: /etc/inetd.conf
|
||||
register: cis_aix_inetd_conf_content
|
||||
when: cis_aix_inetd_config.stat.exists
|
||||
|
||||
- name: Disable insecure inetd services when present
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/inetd.conf
|
||||
regexp: '^(?!#)({{ item }})\s+'
|
||||
line: '# \1 disabled by cis-aix7-hardening'
|
||||
backrefs: true
|
||||
backup: true
|
||||
loop: "{{ cis_legacy_inetd_services }}"
|
||||
when: cis_aix_inetd_config.stat.exists
|
||||
notify: refresh inetd
|
||||
|
||||
- name: Report inetd configuration status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- >-
|
||||
{{ 'OK: /etc/inetd.conf exists and legacy entries were reviewed.'
|
||||
if cis_aix_inetd_config.stat.exists else 'WARNING: /etc/inetd.conf was not found; inetd review skipped.' }}
|
||||
- "OK: SRC states collected for {{ cis_src_subsystems | join(', ') }}."
|
||||
|
||||
- name: Stop inactive legacy SRC subsystems when present
|
||||
ansible.builtin.command: "stopsrc -s {{ item }}"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
loop:
|
||||
- routed
|
||||
- gated
|
||||
- named
|
||||
when: >-
|
||||
cis_aix_src_summary.stdout is defined
|
||||
and item in cis_aix_src_summary.stdout
|
||||
and 'active' in cis_aix_src_summary.stdout
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
- name: Ensure sshd configuration exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ cis_ssh_config_path }}"
|
||||
register: cis_aix_sshd_config
|
||||
|
||||
- name: Fail when sshd configuration is missing
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- cis_aix_sshd_config.stat.exists
|
||||
fail_msg: "CRITICAL: {{ cis_ssh_config_path }} was not found; refusing to manage SSH hardening."
|
||||
success_msg: "OK: {{ cis_ssh_config_path }} exists."
|
||||
|
||||
- name: Set sshd validation command from detected binary
|
||||
ansible.builtin.set_fact:
|
||||
cis_sshd_test_command: "{{ cis_aix_sshd_path }} -t"
|
||||
when: cis_aix_sshd_path is defined and cis_aix_sshd_path | length > 0
|
||||
|
||||
- name: Apply managed AIX sshd hardening block
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ cis_ssh_config_path }}"
|
||||
marker: "# {mark} ANSIBLE MANAGED BLOCK cis-aix7-hardening"
|
||||
owner: root
|
||||
group: system
|
||||
mode: "0600"
|
||||
backup: true
|
||||
validate: "{{ cis_sshd_test_command }} -f %s"
|
||||
block: |
|
||||
PermitRootLogin {{ 'no' if cis_disable_root_login | bool else 'prohibit-password' }}
|
||||
PermitEmptyPasswords no
|
||||
PasswordAuthentication {{ 'no' if cis_disable_password_auth | bool else 'yes' }}
|
||||
MaxAuthTries {{ cis_ssh_max_auth_tries }}
|
||||
LoginGraceTime {{ cis_ssh_login_grace_time }}
|
||||
ClientAliveInterval {{ cis_ssh_client_alive_interval }}
|
||||
ClientAliveCountMax {{ cis_ssh_client_alive_count_max }}
|
||||
notify:
|
||||
- validate sshd
|
||||
- restart sshd
|
||||
|
||||
- name: Validate effective sshd configuration
|
||||
ansible.builtin.command: "{{ cis_sshd_test_command }}"
|
||||
changed_when: false
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
- name: Check sudoers file availability
|
||||
ansible.builtin.stat:
|
||||
path: "{{ cis_sudoers_path }}"
|
||||
register: cis_aix_sudoers
|
||||
|
||||
- name: Check visudo availability
|
||||
ansible.builtin.shell: "command -v visudo >/dev/null 2>&1 || whence visudo >/dev/null 2>&1"
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_visudo_available
|
||||
|
||||
- name: Manage sudo use_pty default when supported
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ cis_sudoers_path }}"
|
||||
regexp: '^Defaults\s+use_pty\b'
|
||||
line: "Defaults use_pty"
|
||||
validate: "visudo -cf %s"
|
||||
when:
|
||||
- cis_sudo_use_pty | bool
|
||||
- cis_aix_sudoers.stat.exists
|
||||
- cis_aix_visudo_available.rc == 0
|
||||
|
||||
- name: Manage sudo logfile default
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ cis_sudoers_path }}"
|
||||
regexp: '^Defaults\s+logfile='
|
||||
line: 'Defaults logfile="{{ cis_sudo_logfile }}"'
|
||||
validate: "visudo -cf %s"
|
||||
when:
|
||||
- cis_aix_sudoers.stat.exists
|
||||
- cis_aix_visudo_available.rc == 0
|
||||
|
||||
- name: Validate sudoers syntax
|
||||
ansible.builtin.command: "visudo -cf {{ cis_sudoers_path }}"
|
||||
changed_when: false
|
||||
when:
|
||||
- cis_aix_sudoers.stat.exists
|
||||
- cis_aix_visudo_available.rc == 0
|
||||
|
||||
- name: Report sudo hardening status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- >-
|
||||
{{ 'OK: sudoers exists and visudo validation is available.'
|
||||
if cis_aix_sudoers.stat.exists and cis_aix_visudo_available.rc == 0
|
||||
else 'WARNING: sudo or visudo was not found; sudo controls were skipped.' }}
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
- name: Collect root account security attributes
|
||||
ansible.builtin.command: lssec -f /etc/security/user -s root -a account_locked login rlogin su sugroups
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_root_security
|
||||
|
||||
- name: Collect accounts with administrative UID
|
||||
ansible.builtin.shell: "awk -F: '$3 == 0 {print $1}' /etc/passwd"
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_uid_zero_accounts
|
||||
|
||||
- name: Report administrative account review
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- >-
|
||||
{{ 'OK: Only root has UID 0.'
|
||||
if cis_aix_uid_zero_accounts.stdout_lines | default([]) | length == 1
|
||||
else 'WARNING: Multiple UID 0 accounts detected: ' ~ (cis_aix_uid_zero_accounts.stdout_lines | default([]) | join(', ')) }}
|
||||
- "OK: Root security attributes: {{ cis_aix_root_security.stdout | default('unavailable') }}"
|
||||
|
||||
- name: Ensure root remote login is disabled when requested
|
||||
ansible.builtin.command: chsec -f /etc/security/user -s root -a rlogin=false
|
||||
changed_when: true
|
||||
when:
|
||||
- cis_disable_root_login | bool
|
||||
- "'rlogin=false' not in (cis_aix_root_security.stdout | default(''))"
|
||||
|
||||
- name: Collect locked or administratively disabled accounts
|
||||
ansible.builtin.shell: |
|
||||
set -o pipefail
|
||||
awk -F: '{print $1}' /etc/passwd | while read user; do
|
||||
lsuser -a account_locked "$user" 2>/dev/null
|
||||
done
|
||||
args:
|
||||
executable: /bin/ksh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
register: cis_aix_account_lock_summary
|
||||
|
||||
- name: Report account lock summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "OK: Collected account lock status for local users."
|
||||
- "{{ cis_aix_account_lock_summary.stdout_lines | default([]) }}"
|
||||
Reference in New Issue
Block a user