From 92b61155a0b325701634e21f082f81ca4654860a Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 5 Feb 2026 17:58:33 +0100 Subject: [PATCH 1/5] feat: testing of stig concept --- .gitignore | 1 + playbooks/stig_cis_rocky9_v2_0_0.yml | 48 +++++++++ roles/crypto_policy/defaults/main.yml | 2 +- roles/sshd/README.md | 52 +++++++-- roles/sshd/defaults/main.yml | 74 ++++++++++--- roles/sshd/tasks/main.yml | 27 ++++- .../tasks/private-host-key-permissions.yml | 16 +++ .../tasks/public-host-key-permissions.yml | 16 +++ .../templates/etc/ssh/Debian12-sshd_config.j2 | 40 +++++-- .../templates/etc/ssh/Fedora40-sshd_config.j2 | 42 ++++++-- .../templates/etc/ssh/RedHat10-sshd_config.j2 | 42 ++++++-- .../templates/etc/ssh/RedHat8-sshd_config.j2 | 39 +++++-- .../templates/etc/ssh/RedHat9-sshd_config.j2 | 44 +++++--- stig/audit.csv | 1 + stig/create-db | 101 ++++++++++++++++++ stig/dump-db | 58 ++++++++++ stig/lib | 1 + stig/profile.csv | 38 +++++++ stig/remediation.csv | 31 ++++++ 19 files changed, 595 insertions(+), 78 deletions(-) create mode 100644 playbooks/stig_cis_rocky9_v2_0_0.yml create mode 100644 roles/sshd/tasks/private-host-key-permissions.yml create mode 100644 roles/sshd/tasks/public-host-key-permissions.yml create mode 100644 stig/audit.csv create mode 100755 stig/create-db create mode 100755 stig/dump-db create mode 120000 stig/lib create mode 100644 stig/profile.csv create mode 100644 stig/remediation.csv diff --git a/.gitignore b/.gitignore index 3bb399875..913be28e6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ playbooks/test.yml roles/test context/ particle/.vagrant +stig/stig.db diff --git a/playbooks/stig_cis_rocky9_v2_0_0.yml b/playbooks/stig_cis_rocky9_v2_0_0.yml new file mode 100644 index 000000000..cb96266eb --- /dev/null +++ b/playbooks/stig_cis_rocky9_v2_0_0.yml @@ -0,0 +1,48 @@ +- name: 'Playbook linuxfabrik.lfops.stig_cis_rocky9_v2_0_0' + hosts: + - 'lfops_stig_cis_rocky9_v2_0_0' + + pre_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-start.yml' + tags: + - 'always' + + # TODO + # - ansible.builtin.import_role: + # name: 'shared' + # tasks_from: 'stig.yml' + # tags: + # - 'always' + + # * create sqlite from csv files + # * queries the db + # * templates the vars/stig.yml to localhost + # * include_vars against vars/stig.yml + + # - name: 'Load default values for cis' + # ansible.builtin.include_vars: '../vars/cis-todo.yml' + # # when: 'test__cis' + + roles: + + - role: 'linuxfabrik.lfops.crypto_policy' + when: + - 'not stig_cis_rocky9_v2_0_0__skip_crypto_policy' + + - role: 'linuxfabrik.lfops.policycoreutils' + when: + - 'ansible_facts["os_family"] == "RedHat"' + - 'not stig_cis_rocky9_v2_0_0__skip_policycoreutils' + + - role: 'linuxfabrik.lfops.sshd' + when: + - 'not stig_cis_rocky9_v2_0_0__skip_sshd' + + post_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-end.yml' + tags: + - 'always' diff --git a/roles/crypto_policy/defaults/main.yml b/roles/crypto_policy/defaults/main.yml index b05d4472e..e09e4a3c1 100644 --- a/roles/crypto_policy/defaults/main.yml +++ b/roles/crypto_policy/defaults/main.yml @@ -1,4 +1,4 @@ -crypto_policy__policy: '{{ crypto_policy__policy__role_var[ansible_facts["os_family"] ~ ansible_facts["distribution_major_version"]] }}' +crypto_policy__policy: '{{ crypto_policy__policy__cis_var | d(crypto_policy__policy__role_var[ansible_facts["os_family"] ~ ansible_facts["distribution_major_version"]]) }}' crypto_policy__policy__role_var: RedHat8: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20' diff --git a/roles/sshd/README.md b/roles/sshd/README.md index cd9c42b53..31da755ac 100644 --- a/roles/sshd/README.md +++ b/roles/sshd/README.md @@ -22,26 +22,58 @@ Note that the role does not make use of `/etc/ssh/sshd_config.d/` since not all | Variable | Description | Default Value | | -------- | ----------- | ------------- | -| `sshd__address_family` | Specifies which address family should be used. Possible options: `any`, `inet` (use IPv4 only) or `inet6` (use IPv6 only). | `'any'` | -| `sshd__gssapi_authentication` | Specifies whether user authentication based on GSSAPI is allowed | `true` | +| `sshd__address_family` | String. Specifies which address family should be used. Possible options: `any`, `inet` (use IPv4 only) or `inet6` (use IPv6 only). | `'any'` | +| `sshd__allow_groups__host_var` / `sshd__allow_groups__group_var` | List of dictionaries. If specified, login is allowed only for users whose primary group or supplementary group list matches one of the patterns. Subkeys:
* `name`: Mandatory, string. The group name or pattern.
* `state`: Optional, string. `present` or `absent`. Defaults to `present`. | `[]` | +| `sshd__allow_users__host_var` / `sshd__allow_users__group_var` | List of dictionaries. If specified, login is allowed only for user names that match one of the patterns. Subkeys:
* `name`: Mandatory, string. The user name or pattern.
* `state`: Optional, string. `present` or `absent`. Defaults to `present`. | `[]` | +| `sshd__client_alive_count_max` | Number. Sets the number of client alive messages which may be sent without sshd receiving any messages back from the client. | `3` | +| `sshd__client_alive_interval` | Number. Sets a timeout interval in seconds after which if no data has been received from the client, sshd will send a message through the encrypted channel to request a response from the client. | `15` | +| `sshd__deny_groups__host_var` / `sshd__deny_groups__group_var` | List of dictionaries. Login is disallowed for users whose primary group or supplementary group list matches one of the patterns. Subkeys:
* `name`: Mandatory, string. The group name or pattern.
* `state`: Optional, string. `present` or `absent`. Defaults to `present`. | `[]` | +| `sshd__deny_users__host_var` / `sshd__deny_users__group_var` | List of dictionaries. Login is disallowed for user names that match one of the patterns. Subkeys:
* `name`: Mandatory, string. The user name or pattern.
* `state`: Optional, string. `present` or `absent`. Defaults to `present`. | `[]` | +| `sshd__disable_forwarding` | Bool. Disables all forwarding features, including X11, ssh-agent, TCP and StreamLocal. | `true` | +| `sshd__gssapi_authentication` | Bool. Specifies whether user authentication based on GSSAPI is allowed. | `true` | | `sshd__log_level` | Sets the log level | `'INFO'` | -| `sshd__password_authentication` | Specifies whether password authentication is allowed. | `false` | -| `sshd__permit_root_login` | Specifies whether root can log in using ssh. Possible options:
* `yes`
* `prohibit-password`
* `forced-commands-only`
* `no` | `'yes'` | -| `sshd__port` | Which port the sshd server should use. | `22` | -| `sshd__raw` | Raw (user-defined) SSH-Config. Will be placed at the end of the `/etc/ssh/sshd_config` file. Useful for `Match` directives. | unset | -| `sshd__service_enabled` | Enables or disables the sshd service, analogous to `systemctl enable/disable`. | `true` | -| `sshd__service_state` | Changes the state of the sshd service, analogous to `systemctl start/stop/restart/reload`. Possible options:
* `started`
* `stopped`
* `restarted`
* `reloaded` | `'started'` | -| `sshd__sftp_subsystem` | Which command should be used for the sftp subsystem. | RHEL: `'/usr/libexec/openssh/sftp-server'`, Debian: `/usr/lib/openssh/sftp-server` | -| `sshd__use_dns` | Specifies whether sshd should look up the remote hostname, and to check that the resolved host name for the remote IP address maps back to the very same IP address. | `false` | +| `sshd__login_grace_time` | Number. The time in seconds after which the server disconnects if the user has not successfully logged in. | `60` | +| `sshd__max_auth_tries` | Number. Specifies the maximum number of authentication attempts permitted per connection. | `4` | +| `sshd__max_sessions` | Number. Specifies the maximum number of open shell, login or subsystem (e.g. sftp) sessions permitted per network connection. | `10` | +| `sshd__max_startups` | String. Specifies the maximum number of concurrent unauthenticated connections to the SSH daemon. Format: `start:rate:full` - randomly refuse connections with probability `rate/100` once there are `start` unauthenticated connections, up to a maximum of `full` at which point all new connections are refused. | `'10:30:60'` | +| `sshd__password_authentication` | Bool. Specifies whether password authentication is allowed. | `false` | +| `sshd__permit_empty_passwords` | Bool. Specifies whether the server allows login to accounts with empty password strings. | `false` | +| `sshd__permit_root_login` | String. Specifies whether root can log in using ssh. Possible options:
* `yes`
* `prohibit-password`
* `forced-commands-only`
* `no` | `'yes'` | +| `sshd__permit_user_environment` | Bool. Specifies whether `~/.ssh/environment` and `environment=` options in `~/.ssh/authorized_keys` are processed by sshd. | `false` | +| `sshd__port` | Number. Which port the sshd server should use. | `22` | +| `sshd__raw` | String. Raw (user-defined) SSH-Config. Will be placed at the end of the `/etc/ssh/sshd_config` file. Useful for `Match` directives. | unset | +| `sshd__service_enabled` | Bool. Enables or disables the sshd service, analogous to `systemctl enable/disable`. | `true` | +| `sshd__service_state` | String. Changes the state of the sshd service, analogous to `systemctl start/stop/restart/reload`. Possible options:
* `started`
* `stopped`
* `restarted`
* `reloaded` | `'started'` | +| `sshd__sftp_subsystem` | String. Which command should be used for the sftp subsystem. | RHEL: `'/usr/libexec/openssh/sftp-server'`, Debian: `/usr/lib/openssh/sftp-server` | +| `sshd__use_dns` | Bool. Specifies whether sshd should look up the remote hostname, and to check that the resolved host name for the remote IP address maps back to the very same IP address. | `false` | Example: ```yaml # optional sshd__address_family: 'inet' +sshd__allow_groups__host_var: + - name: 'wheel' + - name: 'sshusers' +sshd__allow_users__host_var: + - name: 'admin' + - name: 'deploy' +sshd__client_alive_count_max: 3 +sshd__client_alive_interval: 15 +sshd__deny_groups__host_var: + - name: 'nobody' +sshd__deny_users__host_var: + - name: 'root' +sshd__disable_forwarding: false sshd__gssapi_authentication: false sshd__log_level: 'INFO' +sshd__login_grace_time: 60 +sshd__max_auth_tries: 4 +sshd__max_sessions: 10 +sshd__max_startups: '10:30:60' sshd__password_authentication: false +sshd__permit_empty_passwords: false sshd__permit_root_login: 'yes' +sshd__permit_user_environment: false sshd__port: 22 sshd__raw: |- Match Group sftpusers diff --git a/roles/sshd/defaults/main.yml b/roles/sshd/defaults/main.yml index 855848a35..75299a3e7 100644 --- a/roles/sshd/defaults/main.yml +++ b/roles/sshd/defaults/main.yml @@ -1,19 +1,67 @@ sshd__address_family: 'any' -sshd__gssapi_authentication: true -sshd__password_authentication: false -sshd__permit_root_login: 'yes' -sshd__port: 22 -sshd__service_enabled: true -sshd__service_state: 'started' -sshd__use_dns: false - -sshd__log_level__dependent_var: '' -sshd__log_level__group_var: '' -sshd__log_level__host_var: '' -sshd__log_level__role_var: 'INFO' +sshd__allow_groups__combined_var: '{{ + sshd__allow_groups__role_var + + sshd__allow_groups__dependent_var + + sshd__allow_groups__group_var + + sshd__allow_groups__host_var + }}' +sshd__allow_groups__dependent_var: [] +sshd__allow_groups__group_var: [] +sshd__allow_groups__host_var: [] +sshd__allow_groups__role_var: [] +sshd__allow_users__combined_var: '{{ + sshd__allow_users__role_var + + sshd__allow_users__dependent_var + + sshd__allow_users__group_var + + sshd__allow_users__host_var + }}' +sshd__allow_users__dependent_var: [] +sshd__allow_users__group_var: [] +sshd__allow_users__host_var: [] +sshd__allow_users__role_var: [] +sshd__client_alive_count_max: '{{ sshd__client_alive_count_max__cis_var | d(3) }}' +sshd__client_alive_interval: '{{ sshd__client_alive_interval__cis_var | d(15) }}' +sshd__deny_groups__combined_var: '{{ + sshd__deny_groups__role_var + + sshd__deny_groups__dependent_var + + sshd__deny_groups__group_var + + sshd__deny_groups__host_var + }}' +sshd__deny_groups__dependent_var: [] +sshd__deny_groups__group_var: [] +sshd__deny_groups__host_var: [] +sshd__deny_groups__role_var: [] +sshd__deny_users__combined_var: '{{ + sshd__deny_users__role_var + + sshd__deny_users__dependent_var + + sshd__deny_users__group_var + + sshd__deny_users__host_var + }}' +sshd__deny_users__dependent_var: [] +sshd__deny_users__group_var: [] +sshd__deny_users__host_var: [] +sshd__deny_users__role_var: [] +sshd__disable_forwarding: '{{ sshd__disable_forwarding__cis_var | d(true) }}' +sshd__gssapi_authentication: '{{ sshd__gssapi_authentication__cis_var | d(true) }}' sshd__log_level__combined_var: '{{ sshd__log_level__host_var if (sshd__log_level__host_var | string | length) else sshd__log_level__group_var if (sshd__log_level__group_var | string | length) else sshd__log_level__dependent_var if (sshd__log_level__dependent_var | string | length) else - sshd__log_level__role_var + (sshd__log_level__cis_var | d(sshd__log_level__role_var) }}' +sshd__log_level__dependent_var: '' +sshd__log_level__group_var: '' +sshd__log_level__host_var: '' +sshd__log_level__role_var: 'INFO' +sshd__login_grace_time: '{{ sshd__login_grace_time__cis_var | d(60) }}' +sshd__max_auth_tries: '{{ sshd__max_auth_tries__cis_var | d(4) }}' +sshd__max_sessions: '{{ sshd__max_sessions__cis_var | d(10) }}' +sshd__max_startups: '{{ sshd__max_startups__cis_var | d("10:30:60") }}' +sshd__password_authentication: false +sshd__permit_empty_passwords: '{{ sshd__permit_empty_passwords__cis_var | d(false) }}' +sshd__permit_root_login: '{{ sshd__permit_root_login__cis_var | d("yes") }}' +sshd__permit_user_environment: '{{ sshd__permit_user_environment__cis_var | d(false) }}' +sshd__port: 22 +sshd__service_enabled: true +sshd__service_state: 'started' +sshd__use_dns: false diff --git a/roles/sshd/tasks/main.yml b/roles/sshd/tasks/main.yml index 90bad5d5b..b0afeec57 100644 --- a/roles/sshd/tasks/main.yml +++ b/roles/sshd/tasks/main.yml @@ -11,7 +11,7 @@ dest: '/etc/ssh/sshd_config' owner: 'root' group: 'root' - mode: 0o600 + mode: 0o600 # covers sshd_config_permissions STIG vars: __task_file: files: @@ -21,6 +21,31 @@ - '{{ role_path }}/templates' notify: 'sshd: reload sshd' + - name: 'Find all .conf files in /etc/ssh/sshd_config.d' + ansible.builtin.find: + paths: '/etc/ssh/sshd_config.d' + patterns: '*.conf' + register: '__sshd_config_d_conf_files' + + # covers sshd_config_permissions STIG + - name: 'Ensure correct permissions on /etc/ssh/sshd_config.d/*.conf files' + ansible.builtin.file: + path: '{{ item["path"] }}' + owner: 'root' + group: 'root' + mode: 0o0600 + loop: '{{ __sshd_config_d_conf_files["files"] }}' + loop_control: + label: '{{ item["path"] }}' + + # covers sshd_private_host_key_permissions STIG + - name: 'Ensure correct permissions on SSH private host key files' + ansible.builtin.include_tasks: 'private-host-key-permissions.yml' + + # covers sshd_public_host_key_permissions STIG + - name: 'Ensure correct permissions on SSH public host key files' + ansible.builtin.include_tasks: 'public-host-key-permissions.yml' + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' ansible.builtin.include_role: name: 'shared' diff --git a/roles/sshd/tasks/private-host-key-permissions.yml b/roles/sshd/tasks/private-host-key-permissions.yml new file mode 100644 index 000000000..2943d3f7a --- /dev/null +++ b/roles/sshd/tasks/private-host-key-permissions.yml @@ -0,0 +1,16 @@ +- name: 'Find SSH private key files in /etc/ssh' + ansible.builtin.find: + paths: '/etc/ssh' + patterns: 'ssh_host_*_key' + file_type: 'file' + register: '__sshd_private_key_files' + +- name: 'Ensure correct permissions on SSH private key files' + ansible.builtin.file: + path: '{{ item["path"] }}' + owner: 'root' + group: 'ssh_keys' + mode: 0o0640 + loop: '{{ __sshd_private_key_files["files"] }}' + loop_control: + label: '{{ item["path"] }}' diff --git a/roles/sshd/tasks/public-host-key-permissions.yml b/roles/sshd/tasks/public-host-key-permissions.yml new file mode 100644 index 000000000..7df81565b --- /dev/null +++ b/roles/sshd/tasks/public-host-key-permissions.yml @@ -0,0 +1,16 @@ +- name: 'Find SSH public key files in /etc/ssh' + ansible.builtin.find: + paths: '/etc/ssh' + patterns: 'ssh_host_*_key.pub' + file_type: 'file' + register: '__sshd_public_key_files' + +- name: 'Ensure correct permissions on SSH public key files' + ansible.builtin.file: + path: '{{ item["path"] }}' + owner: 'root' + group: 'root' + mode: 0o0644 + loop: '{{ __sshd_public_key_files["files"] }}' + loop_control: + label: '{{ item["path"] }}' diff --git a/roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2 b/roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2 index d5a45725a..eb6fb9db2 100644 --- a/roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2 @@ -31,11 +31,27 @@ LogLevel {{ sshd__log_level__combined_var }} # Authentication: -#LoginGraceTime 2m +LoginGraceTime {{ sshd__login_grace_time }} PermitRootLogin {{ sshd__permit_root_login }} #StrictModes yes -#MaxAuthTries 6 -#MaxSessions 10 +MaxAuthTries {{ sshd__max_auth_tries }} +MaxSessions {{ sshd__max_sessions }} +{% set allow_users = (sshd__allow_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_users__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_users | length %} +AllowUsers {{ allow_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set allow_groups = (sshd__allow_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_groups | length %} +AllowGroups {{ allow_groups | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_users = (sshd__deny_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_users__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_users | length %} +DenyUsers {{ deny_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_groups = (sshd__deny_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_groups | length %} +DenyGroups {{ deny_groups | map(attribute="name") | join(' ') }} +{% endif %} #PubkeyAuthentication yes @@ -48,16 +64,18 @@ PermitRootLogin {{ sshd__permit_root_login }} #AuthorizedKeysCommandUser nobody # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts -#HostbasedAuthentication no +# disabling HostbasedAuthentication covers sshd_host_based_authentication STIG +HostbasedAuthentication no # Change to yes if you don't trust ~/.ssh/known_hosts for # HostbasedAuthentication #IgnoreUserKnownHosts no # Don't read the user's ~/.rhosts and ~/.shosts files -#IgnoreRhosts yes +# covers sshd_ignore_rhosts STIG +IgnoreRhosts yes # To disable tunneled clear text passwords, change to no here! PasswordAuthentication {{ sshd__password_authentication | bool | ternary("yes", "no") }} -#PermitEmptyPasswords no +PermitEmptyPasswords {{ sshd__permit_empty_passwords | bool | ternary("yes", "no") }} # Change to yes to enable challenge-response passwords (beware issues with # some PAM modules and threads) @@ -84,10 +102,12 @@ GSSAPIAuthentication {{ sshd__gssapi_authentication | bool | ternary("yes", "no" # If you just want the PAM account and session checks to run without # PAM authentication, then enable this but set PasswordAuthentication # and KbdInteractiveAuthentication to 'no'. +# covers sshd_use_pam STIG UsePAM yes #AllowAgentForwarding yes #AllowTcpForwarding yes +DisableForwarding {{ sshd__disable_forwarding | bool | ternary("yes", "no") }} #GatewayPorts no X11Forwarding yes #X11DisplayOffset 10 @@ -96,13 +116,13 @@ X11Forwarding yes PrintMotd no #PrintLastLog yes #TCPKeepAlive yes -#PermitUserEnvironment no +PermitUserEnvironment {{ sshd__permit_user_environment | bool | ternary("yes", "no") }} #Compression delayed -#ClientAliveInterval 0 -#ClientAliveCountMax 3 +ClientAliveInterval {{ sshd__client_alive_interval }} +ClientAliveCountMax {{ sshd__client_alive_count_max }} UseDNS {{ sshd__use_dns | bool | ternary("yes", "no") }} #PidFile /run/sshd.pid -#MaxStartups 10:30:100 +MaxStartups {{ sshd__max_startups }} #PermitTunnel no #ChrootDirectory none #VersionAddendum none diff --git a/roles/sshd/templates/etc/ssh/Fedora40-sshd_config.j2 b/roles/sshd/templates/etc/ssh/Fedora40-sshd_config.j2 index 85e0e950c..2f51083cb 100644 --- a/roles/sshd/templates/etc/ssh/Fedora40-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/Fedora40-sshd_config.j2 @@ -39,11 +39,27 @@ LogLevel {{ sshd__log_level__combined_var }} # Authentication: -#LoginGraceTime 2m +LoginGraceTime {{ sshd__login_grace_time }} PermitRootLogin {{ sshd__permit_root_login }} #StrictModes yes -#MaxAuthTries 6 -#MaxSessions 10 +MaxAuthTries {{ sshd__max_auth_tries }} +MaxSessions {{ sshd__max_sessions }} +{% set allow_users = (sshd__allow_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_users__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_users | length %} +AllowUsers {{ allow_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set allow_groups = (sshd__allow_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_groups | length %} +AllowGroups {{ allow_groups | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_users = (sshd__deny_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_users__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_users | length %} +DenyUsers {{ deny_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_groups = (sshd__deny_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_groups | length %} +DenyGroups {{ deny_groups | map(attribute="name") | join(' ') }} +{% endif %} #PubkeyAuthentication yes @@ -57,16 +73,18 @@ AuthorizedKeysFile .ssh/authorized_keys #AuthorizedKeysCommandUser nobody # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts -#HostbasedAuthentication no +# disabling HostbasedAuthentication covers sshd_host_based_authentication STIG +HostbasedAuthentication no # Change to yes if you don't trust ~/.ssh/known_hosts for # HostbasedAuthentication #IgnoreUserKnownHosts no # Don't read the user's ~/.rhosts and ~/.shosts files -#IgnoreRhosts yes +# covers sshd_ignore_rhosts STIG +IgnoreRhosts yes # To disable tunneled clear text passwords, change to no here! PasswordAuthentication {{ sshd__password_authentication | bool | ternary("yes", "no") }} -#PermitEmptyPasswords no +PermitEmptyPasswords {{ sshd__permit_empty_passwords | bool | ternary("yes", "no") }} # Change to no to disable s/key passwords #KbdInteractiveAuthentication yes @@ -96,10 +114,12 @@ GSSAPIAuthentication {{ sshd__gssapi_authentication | bool | ternary("yes", "no" # and KbdInteractiveAuthentication to 'no'. # WARNING: 'UsePAM no' is not supported in Fedora and may cause several # problems. -#UsePAM no +# covers sshd_use_pam STIG +UsePAM yes #AllowAgentForwarding yes #AllowTcpForwarding yes +DisableForwarding {{ sshd__disable_forwarding | bool | ternary("yes", "no") }} #GatewayPorts no #X11Forwarding no #X11DisplayOffset 10 @@ -108,13 +128,13 @@ GSSAPIAuthentication {{ sshd__gssapi_authentication | bool | ternary("yes", "no" #PrintMotd yes #PrintLastLog yes #TCPKeepAlive yes -#PermitUserEnvironment no +PermitUserEnvironment {{ sshd__permit_user_environment | bool | ternary("yes", "no") }} #Compression delayed -#ClientAliveInterval 0 -#ClientAliveCountMax 3 +ClientAliveInterval {{ sshd__client_alive_interval }} +ClientAliveCountMax {{ sshd__client_alive_count_max }} UseDNS {{ sshd__use_dns | bool | ternary("yes", "no") }} #PidFile /var/run/sshd.pid -#MaxStartups 10:30:100 +MaxStartups {{ sshd__max_startups }} #PermitTunnel no #ChrootDirectory none #VersionAddendum none diff --git a/roles/sshd/templates/etc/ssh/RedHat10-sshd_config.j2 b/roles/sshd/templates/etc/ssh/RedHat10-sshd_config.j2 index 08acb208b..35040014e 100644 --- a/roles/sshd/templates/etc/ssh/RedHat10-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/RedHat10-sshd_config.j2 @@ -39,11 +39,27 @@ LogLevel {{ sshd__log_level__combined_var }} # Authentication: -#LoginGraceTime 2m +LoginGraceTime {{ sshd__login_grace_time }} PermitRootLogin {{ sshd__permit_root_login }} #StrictModes yes -#MaxAuthTries 6 -#MaxSessions 10 +MaxAuthTries {{ sshd__max_auth_tries }} +MaxSessions {{ sshd__max_sessions }} +{% set allow_users = (sshd__allow_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_users__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_users | length %} +AllowUsers {{ allow_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set allow_groups = (sshd__allow_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_groups | length %} +AllowGroups {{ allow_groups | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_users = (sshd__deny_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_users__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_users | length %} +DenyUsers {{ deny_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_groups = (sshd__deny_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_groups | length %} +DenyGroups {{ deny_groups | map(attribute="name") | join(' ') }} +{% endif %} #PubkeyAuthentication yes @@ -57,16 +73,18 @@ AuthorizedKeysFile .ssh/authorized_keys #AuthorizedKeysCommandUser nobody # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts -#HostbasedAuthentication no +# disabling HostbasedAuthentication covers sshd_host_based_authentication STIG +HostbasedAuthentication no # Change to yes if you don't trust ~/.ssh/known_hosts for # HostbasedAuthentication #IgnoreUserKnownHosts no # Don't read the user's ~/.rhosts and ~/.shosts files -#IgnoreRhosts yes +# covers sshd_ignore_rhosts STIG +IgnoreRhosts yes # To disable tunneled clear text passwords, change to no here! PasswordAuthentication {{ sshd__password_authentication | bool | ternary("yes", "no") }} -#PermitEmptyPasswords no +PermitEmptyPasswords {{ sshd__permit_empty_passwords | bool | ternary("yes", "no") }} # Change to no to disable s/key passwords #KbdInteractiveAuthentication yes @@ -96,10 +114,12 @@ GSSAPIAuthentication {{ sshd__gssapi_authentication | bool | ternary("yes", "no" # and KbdInteractiveAuthentication to 'no'. # WARNING: 'UsePAM no' is not supported in this build and may cause several # problems. -#UsePAM no +# covers sshd_use_pam STIG +UsePAM yes #AllowAgentForwarding yes #AllowTcpForwarding yes +DisableForwarding {{ sshd__disable_forwarding | bool | ternary("yes", "no") }} #GatewayPorts no #X11Forwarding no #X11DisplayOffset 10 @@ -108,13 +128,13 @@ GSSAPIAuthentication {{ sshd__gssapi_authentication | bool | ternary("yes", "no" #PrintMotd yes #PrintLastLog yes #TCPKeepAlive yes -#PermitUserEnvironment no +PermitUserEnvironment {{ sshd__permit_user_environment | bool | ternary("yes", "no") }} #Compression delayed -#ClientAliveInterval 0 -#ClientAliveCountMax 3 +ClientAliveInterval {{ sshd__client_alive_interval }} +ClientAliveCountMax {{ sshd__client_alive_count_max }} UseDNS {{ sshd__use_dns | bool | ternary("yes", "no") }} #PidFile /var/run/sshd.pid -#MaxStartups 10:30:100 +MaxStartups {{ sshd__max_startups }} #PermitTunnel no #ChrootDirectory none #VersionAddendum none diff --git a/roles/sshd/templates/etc/ssh/RedHat8-sshd_config.j2 b/roles/sshd/templates/etc/ssh/RedHat8-sshd_config.j2 index 040eed957..6d87748a7 100644 --- a/roles/sshd/templates/etc/ssh/RedHat8-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/RedHat8-sshd_config.j2 @@ -43,11 +43,27 @@ LogLevel {{ sshd__log_level__combined_var }} # Authentication: -#LoginGraceTime 2m +LoginGraceTime {{ sshd__login_grace_time }} PermitRootLogin {{ sshd__permit_root_login }} #StrictModes yes -#MaxAuthTries 6 -#MaxSessions 10 +MaxAuthTries {{ sshd__max_auth_tries }} +MaxSessions {{ sshd__max_sessions }} +{% set allow_users = (sshd__allow_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_users__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_users | length %} +AllowUsers {{ allow_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set allow_groups = (sshd__allow_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_groups | length %} +AllowGroups {{ allow_groups | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_users = (sshd__deny_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_users__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_users | length %} +DenyUsers {{ deny_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_groups = (sshd__deny_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_groups | length %} +DenyGroups {{ deny_groups | map(attribute="name") | join(' ') }} +{% endif %} #PubkeyAuthentication yes @@ -61,17 +77,20 @@ AuthorizedKeysFile .ssh/authorized_keys #AuthorizedKeysCommandUser nobody # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts -#HostbasedAuthentication no +# disabling HostbasedAuthentication covers sshd_host_based_authentication STIG +HostbasedAuthentication no # Change to yes if you don't trust ~/.ssh/known_hosts for # HostbasedAuthentication #IgnoreUserKnownHosts no # Don't read the user's ~/.rhosts and ~/.shosts files -#IgnoreRhosts yes +# covers sshd_ignore_rhosts STIG +IgnoreRhosts yes # To disable tunneled clear text passwords, change to no here! #PasswordAuthentication yes #PermitEmptyPasswords no PasswordAuthentication {{ sshd__password_authentication | bool | ternary("yes", "no") }} +PermitEmptyPasswords {{ sshd__permit_empty_passwords | bool | ternary("yes", "no") }} # Change to no to disable s/key passwords #ChallengeResponseAuthentication yes @@ -102,10 +121,12 @@ GSSAPICleanupCredentials no # and ChallengeResponseAuthentication to 'no'. # WARNING: 'UsePAM no' is not supported in RHEL and may cause several # problems. +# covers sshd_use_pam STIG UsePAM yes #AllowAgentForwarding yes #AllowTcpForwarding yes +DisableForwarding {{ sshd__disable_forwarding | bool | ternary("yes", "no") }} #GatewayPorts no X11Forwarding yes #X11DisplayOffset 10 @@ -118,13 +139,13 @@ PrintMotd no #PrintLastLog yes #TCPKeepAlive yes -#PermitUserEnvironment no +PermitUserEnvironment {{ sshd__permit_user_environment | bool | ternary("yes", "no") }} #Compression delayed -#ClientAliveInterval 0 -#ClientAliveCountMax 3 +ClientAliveInterval {{ sshd__client_alive_interval }} +ClientAliveCountMax {{ sshd__client_alive_count_max }} UseDNS {{ sshd__use_dns | bool | ternary("yes", "no") }} #PidFile /var/run/sshd.pid -#MaxStartups 10:30:100 +MaxStartups {{ sshd__max_startups }} #PermitTunnel no #ChrootDirectory none #VersionAddendum none diff --git a/roles/sshd/templates/etc/ssh/RedHat9-sshd_config.j2 b/roles/sshd/templates/etc/ssh/RedHat9-sshd_config.j2 index fdd5c8ce6..c5a6f8c3a 100644 --- a/roles/sshd/templates/etc/ssh/RedHat9-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/RedHat9-sshd_config.j2 @@ -39,11 +39,27 @@ LogLevel {{ sshd__log_level__combined_var }} # Authentication: -#LoginGraceTime 2m +LoginGraceTime {{ sshd__login_grace_time }} PermitRootLogin {{ sshd__permit_root_login }} #StrictModes yes -#MaxAuthTries 6 -#MaxSessions 10 +MaxAuthTries {{ sshd__max_auth_tries }} +MaxSessions {{ sshd__max_sessions }} +{% set allow_users = (sshd__allow_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_users__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_users | length %} +AllowUsers {{ allow_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set allow_groups = (sshd__allow_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__allow_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if allow_groups | length %} +AllowGroups {{ allow_groups | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_users = (sshd__deny_users__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_users__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_users | length %} +DenyUsers {{ deny_users | map(attribute="name") | join(' ') }} +{% endif %} +{% set deny_groups = (sshd__deny_groups__combined_var | selectattr("state", "defined") | selectattr("state", "ne", "absent") | list) + (sshd__deny_groups__combined_var | selectattr("state", "undefined") | list) %} +{% if deny_groups | length %} +DenyGroups {{ deny_groups | map(attribute="name") | join(' ') }} +{% endif %} #PubkeyAuthentication yes @@ -57,16 +73,18 @@ AuthorizedKeysFile .ssh/authorized_keys #AuthorizedKeysCommandUser nobody # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts -#HostbasedAuthentication no +# disabling HostbasedAuthentication covers sshd_host_based_authentication STIG +HostbasedAuthentication no # Change to yes if you don't trust ~/.ssh/known_hosts for # HostbasedAuthentication #IgnoreUserKnownHosts no # Don't read the user's ~/.rhosts and ~/.shosts files -#IgnoreRhosts yes +# covers sshd_ignore_rhosts STIG +IgnoreRhosts yes # To disable tunneled clear text passwords, change to no here! PasswordAuthentication {{ sshd__password_authentication | bool | ternary("yes", "no") }} -#PermitEmptyPasswords no +PermitEmptyPasswords {{ sshd__permit_empty_passwords | bool | ternary("yes", "no") }} # Change to no to disable s/key passwords #KbdInteractiveAuthentication yes @@ -96,10 +114,12 @@ GSSAPIAuthentication {{ sshd__gssapi_authentication | bool | ternary("yes", "no" # and KbdInteractiveAuthentication to 'no'. # WARNING: 'UsePAM no' is not supported in RHEL and may cause several # problems. -#UsePAM no +# covers sshd_use_pam STIG +UsePAM yes #AllowAgentForwarding yes #AllowTcpForwarding yes +DisableForwarding {{ sshd__disable_forwarding | bool | ternary("yes", "no") }} #GatewayPorts no #X11Forwarding no #X11DisplayOffset 10 @@ -108,18 +128,18 @@ GSSAPIAuthentication {{ sshd__gssapi_authentication | bool | ternary("yes", "no" #PrintMotd yes #PrintLastLog yes #TCPKeepAlive yes -#PermitUserEnvironment no +PermitUserEnvironment {{ sshd__permit_user_environment | bool | ternary("yes", "no") }} #Compression delayed -#ClientAliveInterval 0 -#ClientAliveCountMax 3 +ClientAliveInterval {{ sshd__client_alive_interval }} +ClientAliveCountMax {{ sshd__client_alive_count_max }} UseDNS {{ sshd__use_dns | bool | ternary("yes", "no") }} #PidFile /var/run/sshd.pid -#MaxStartups 10:30:100 +MaxStartups {{ sshd__max_startups }} #PermitTunnel no #ChrootDirectory none #VersionAddendum none -# default banner path +# default banner path. covers sshd_banner STIG Banner /etc/issue.net # override default of no subsystems diff --git a/stig/audit.csv b/stig/audit.csv new file mode 100644 index 000000000..90923535b --- /dev/null +++ b/stig/audit.csv @@ -0,0 +1 @@ +id,exec_order,audit_name diff --git a/stig/create-db b/stig/create-db new file mode 100755 index 000000000..fffe15160 --- /dev/null +++ b/stig/create-db @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch +# The Unlicense (see LICENSE or https://unlicense.org/) + +import argparse +import csv +import os +import sys + +import lib.db_sqlite + + +def main(): + parser = argparse.ArgumentParser(description='Create the STIG database.') + parser.add_argument( + '--force', + action='store_true', + help='Remove existing database and re-create it.', + ) + args = parser.parse_args() + + script_dir = os.path.dirname(os.path.abspath(__file__)) + db_path = os.path.join(script_dir, 'stig.db') + + if os.path.exists(db_path): + if args.force: + os.remove(db_path) + print(f'Removed existing database: {db_path}') + else: + print(f'Database already exists: {db_path}. Use --force to re-create it.') + sys.exit(1) + + success, conn = lib.db_sqlite.connect(path=os.path.dirname(db_path), filename=os.path.basename(db_path)) + if not success: + print(f'Failed to connect to database: {conn}') + sys.exit(1) + + lib.db_sqlite.create_table( + conn, + table='profile', + definition=''' + "id" TEXT, + "profile_name" TEXT NOT NULL, + "profile_version" TEXT NOT NULL, + "control_name" TEXT NOT NULL, + "enabled" INTEGER DEFAULT 1, + "automated" INTEGER DEFAULT 1, + "server_level" INTEGER DEFAULT 1, + "workstation_level" INTEGER DEFAULT 1, + PRIMARY KEY("id") + ''', + ) + + lib.db_sqlite.create_table( + conn, + table='audit', + definition=''' + "id" TEXT, + "exec_order" INTEGER DEFAULT NULL, + "audit_name" TEXT DEFAULT NULL, + FOREIGN KEY("id") REFERENCES "profile"("id") + ''', + ) + + lib.db_sqlite.create_table( + conn, + table='remediation', + definition=''' + "id" TEXT, + "remediation_variable" TEXT DEFAULT NULL, + "mandatory" INTEGER DEFAULT 0, + "comment" TEXT DEFAULT NULL, + FOREIGN KEY("id") REFERENCES "profile"("id") + ''', + ) + + # Import data from CSV files + # Note: We don't use lib.db_sqlite.import_csv() here because it creates the table itself, + # but we need to create the tables with specific constraints (foreign keys, defaults) first. + tables = ['profile', 'audit', 'remediation'] + for table in tables: + csv_path = os.path.join(script_dir, f'{table}.csv') + if os.path.exists(csv_path): + with open(csv_path, newline='') as f: + reader = csv.DictReader(f) + for row in reader: + success, result = lib.db_sqlite.insert(conn, row, table=table) + if not success: + print(f'Failed to insert into {table}: {result}') + sys.exit(1) + print(f'Imported {csv_path}') + + lib.db_sqlite.commit(conn) + lib.db_sqlite.close(conn) + print(f'Created database: {db_path}') + + +if __name__ == '__main__': + main() diff --git a/stig/dump-db b/stig/dump-db new file mode 100755 index 000000000..a8977c49f --- /dev/null +++ b/stig/dump-db @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch +# The Unlicense (see LICENSE or https://unlicense.org/) + +import csv +import os +import sys + +import lib.db_sqlite + + +def main(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + db_path = os.path.join(script_dir, 'stig.db') + + if not os.path.exists(db_path): + print(f'Database does not exist: {db_path}') + sys.exit(1) + + success, conn = lib.db_sqlite.connect(path=os.path.dirname(db_path), filename=os.path.basename(db_path)) + if not success: + print(f'Failed to connect to database: {conn}') + sys.exit(1) + + tables = ['profile', 'audit', 'remediation'] + + for table in tables: + csv_path = os.path.join(script_dir, f'{table}.csv') + success, rows = lib.db_sqlite.select(conn, f'SELECT * FROM {table}') + if not success: + print(f'Failed to select from {table}: {rows}') + sys.exit(1) + + if not rows: + # Write empty file with just headers + success, cols = lib.db_sqlite.select(conn, f'PRAGMA table_info({table})') + if success and cols: + fieldnames = [col['name'] for col in cols] + with open(csv_path, 'w', newline='') as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + print(f'Dumped {table} (0 rows) to {csv_path}') + continue + + fieldnames = list(rows[0].keys()) + with open(csv_path, 'w', newline='') as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + print(f'Dumped {table} ({len(rows)} rows) to {csv_path}') + + lib.db_sqlite.close(conn) + + +if __name__ == '__main__': + main() diff --git a/stig/lib b/stig/lib new file mode 120000 index 000000000..dc598c56d --- /dev/null +++ b/stig/lib @@ -0,0 +1 @@ +../lib \ No newline at end of file diff --git a/stig/profile.csv b/stig/profile.csv new file mode 100644 index 000000000..899868d76 --- /dev/null +++ b/stig/profile.csv @@ -0,0 +1,38 @@ +id,profile_name,profile_version,control_name,enabled,automated,server_level,workstation_level +cramfs_off,CIS Rocky Linux 9,v2.0.0,1.1.1.1 Ensure cramfs kernel module is not available,1,1,1,1 +sshd_config_permissions,CIS Rocky Linux 9,v2.0.0,5.1.1 Ensure permissions on /etc/ssh/sshd_config are configured,1,1,1,1 +sshd_private_host_key_permissions,CIS Rocky Linux 9,v2.0.0,"5.1.2 Ensure permissions on SSH private host key files are +configured",1,1,1,1 +sshd_public_host_key_permissions,CIS Rocky Linux 9,v2.0.0,"5.1.3 Ensure permissions on SSH public host key files are +configured",1,1,1,1 +sshd_ciphers,CIS Rocky Linux 9,v2.0.0,5.1.4 Ensure sshd Ciphers are configured,1,1,1,1 +sshd_kex_algorithms,CIS Rocky Linux 9,v2.0.0,5.1.5 Ensure sshd KexAlgorithms is configured,1,1,1,1 +sshd_macs,CIS Rocky Linux 9,v2.0.0,5.1.6 Ensure sshd MACs are configured,1,1,1,1 +sshd_access,CIS Rocky Linux 9,v2.0.0,5.1.7 Ensure sshd access is configured,1,1,1,1 +sshd_banner,CIS Rocky Linux 9,v2.0.0,5.1.8 Ensure sshd Banner is configured,1,1,1,1 +sshd_client_alive,CIS Rocky Linux 9,v2.0.0,"5.1.9 Ensure sshd ClientAliveInterval and ClientAliveCountMax +are configured",1,1,1,1 +sshd_disable_forwarding,CIS Rocky Linux 9,v2.0.0,5.1.10 Ensure sshd DisableForwarding is enabled,1,1,2,1 +sshd_gssapi_authentication,CIS Rocky Linux 9,v2.0.0,5.1.11 Ensure sshd GSSAPIAuthentication is disabled,1,1,2,1 +sshd_host_based_authentication,CIS Rocky Linux 9,v2.0.0,5.1.12 Ensure sshd HostbasedAuthentication is disabled,1,1,1,1 +sshd_ignore_rhosts,CIS Rocky Linux 9,v2.0.0,5.1.13 Ensure sshd IgnoreRhosts is enabled,1,1,1,1 +sshd_login_grace_time,CIS Rocky Linux 9,v2.0.0,5.1.14 Ensure sshd LoginGraceTime is configured,1,1,1,1 +sshd_log_level,CIS Rocky Linux 9,v2.0.0,5.1.15 Ensure sshd LogLevel is configured,1,1,1,1 +sshd_max_auth_tries,CIS Rocky Linux 9,v2.0.0,5.1.16 Ensure sshd MaxAuthTries is configured,1,1,1,1 +sshd_max_startups,CIS Rocky Linux 9,v2.0.0,5.1.17 Ensure sshd MaxStartups is configured,1,1,1,1 +sshd_max_sessions,CIS Rocky Linux 9,v2.0.0,5.1.18 Ensure sshd MaxSessions is configured,1,1,1,1 +sshd_permit_empty_passwords,CIS Rocky Linux 9,v2.0.0,5.1.19 Ensure sshd PermitEmptyPasswords is disabled,1,1,1,1 +sshd__permit_root_login,CIS Rocky Linux 9,v2.0.0,5.1.20 Ensure sshd PermitRootLogin is disabled,1,1,1,1 +sshd_permit_user_environment,CIS Rocky Linux 9,v2.0.0,5.1.21 Ensure sshd PermitUserEnvironment is disabled,1,1,1,1 +sshd_use_pam,CIS Rocky Linux 9,v2.0.0,5.1.22 Ensure sshd UsePAM is enabled,1,1,1,1 +crypto_policy_not_legacy,CIS Rocky Linux 9,v2.0.0,1.6.1 Ensure system wide crypto policy is not set to legacy,1,1,1,1 +crypto_policy_not_sshd,CIS Rocky Linux 9,v2.0.0,"1.6.2 Ensure system wide crypto policy is not set in sshd +configuration",1,1,1,1 +crypto_policy_disable_sha1,CIS Rocky Linux 9,v2.0.0,"1.6.3 Ensure system wide crypto policy disables sha1 hash and +signature support",1,1,1,1 +crypto_policy_disable_mac_128_bits,CIS Rocky Linux 9,v2.0.0,"1.6.4 Ensure system wide crypto policy disables macs less than +128 bits",1,1,1,1 +crypto_policy_disable_ssh_cbc,CIS Rocky Linux 9,v2.0.0,1.6.5 Ensure system wide crypto policy disables cbc for ssh,1,1,1,1 +crypto_policy_disable_ssh_chacha20_poly1305,CIS Rocky Linux 9,v2.0.0,"1.6.6 Ensure system wide crypto policy disables chacha20- +poly1305 for ssh",1,0,1,1 +crypto_policy_disable_ssh_etm,CIS Rocky Linux 9,v2.0.0,1.6.7 Ensure system wide crypto policy disables EtM for ssh,1,0,1,1 diff --git a/stig/remediation.csv b/stig/remediation.csv new file mode 100644 index 000000000..0c0072d71 --- /dev/null +++ b/stig/remediation.csv @@ -0,0 +1,31 @@ +id,remediation_variable,mandatory,comment +sshd_client_alive,sshd__client_alive_count_max__cis_var: 3,0, +sshd_client_alive,sshd__client_alive_interval__cis_var: 15,0, +sshd_disable_forwarding,sshd__disable_forwarding__cis_var: true,0, +sshd_gssapi_authentication,sshd__gssapi_authentication__cis_var: false,0, +sshd_login_grace_time,sshd__login_grace_time__cis_var: 60,0, +sshd_log_level,sshd__log_level__cis_var: 'VERBOSE',0, +sshd_max_auth_tries,sshd__max_auth_tries__cis_var: 4,0, +sshd_banner,,0,"This also requires the linuxfabrik.lfops.motd role to be run, since this deploys the banner content." +sshd_access,,1,"Manually set at least one of the following variables: `sshd__allow_groups__*_var`, `sshd__allow_users__*_var`, `sshd__deny_groups__*_var`, `sshd__deny_users__*_var`" +sshd_max_startups,sshd__max_startups__cis_var: '10:30:60',0, +sshd_max_sessions,sshd__max_sessions__cis_var: 10,0, +sshd_permit_empty_passwords,sshd__permit_empty_passwords__cis_var: false,0, +sshd__permit_root_login,sshd__permit_root_login__cis_var: 'no',0, +sshd_permit_user_environment,sshd__permit_user_environment__cis_var: false,0, +sshd_host_based_authentication,,0,"""HostbasedAuthentication no"" is hardcoded in the linuxfabrik.lfops.sshd role." +sshd_ignore_rhosts,,0,"""IgnoreRhosts yes"" is hardcoded in the linuxfabrik.lfops.sshd role." +sshd_use_pam,,0,"""UsePAM yes"" is hardcoded in the linuxfabrik.lfops.sshd role." +sshd_config_permissions,,0,This is done by default in the linuxfabrik.lfops.sshd role. +sshd_private_host_key_permissions,,0,This is done by default in the linuxfabrik.lfops.sshd role. +sshd_public_host_key_permissions,,0,This is done by default in the linuxfabrik.lfops.sshd role. +sshd_ciphers,,0,Handled by linuxfabrik.lfops.crypto_policy with LINUXFABRIK-SSH-NO-CBC and LINUXFABRIK-SSH-NO-CHACHA20 modules. +sshd_kex_algorithms,,0,Handled by linuxfabrik.lfops.crypto_policy (DEFAULT policy provides secure kex algorithms). +sshd_macs,,0,Handled by linuxfabrik.lfops.crypto_policy with LINUXFABRIK-NO-WEAKMAC and LINUXFABRIK-SSH-NO-ETM modules. +crypto_policy_not_legacy,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not set `LEGACY` inside `crypto_policy__policy`. +crypto_policy_not_sshd,,0,"This is the case by default. Note that the `/etc/sysconfig/sshd` path in the CIS document is wrong, the relevant config is `/etc/ssh/sshd_config.d/50-redhat.conf`." +crypto_policy_disable_sha1,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-SHA1` from `crypto_policy__policy`. +crypto_policy_disable_mac_128_bits,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-WEAKMAC` from `crypto_policy__policy`. +crypto_policy_disable_ssh_cbc,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-CBC` from `crypto_policy__policy`. +crypto_policy_disable_ssh_chacha20_poly1305,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-CHACHA20` from `crypto_policy__policy`. +crypto_policy_disable_ssh_etm,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-ETM` from `crypto_policy__policy`. From 5fa7537a58930797f0642944b0f8c6b7c8d535e9 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 20 Mar 2026 16:14:10 +0100 Subject: [PATCH 2/5] fix: rename `__cis_var` to `__stig_var` since that's a more universal name --- roles/crypto_policy/defaults/main.yml | 2 +- roles/sshd/defaults/main.yml | 24 +++++++++--------- stig/remediation.csv | 36 +++++++++++++-------------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/roles/crypto_policy/defaults/main.yml b/roles/crypto_policy/defaults/main.yml index e09e4a3c1..217254556 100644 --- a/roles/crypto_policy/defaults/main.yml +++ b/roles/crypto_policy/defaults/main.yml @@ -1,4 +1,4 @@ -crypto_policy__policy: '{{ crypto_policy__policy__cis_var | d(crypto_policy__policy__role_var[ansible_facts["os_family"] ~ ansible_facts["distribution_major_version"]]) }}' +crypto_policy__policy: '{{ crypto_policy__policy__stig_var | d(crypto_policy__policy__role_var[ansible_facts["os_family"] ~ ansible_facts["distribution_major_version"]]) }}' crypto_policy__policy__role_var: RedHat8: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20' diff --git a/roles/sshd/defaults/main.yml b/roles/sshd/defaults/main.yml index 75299a3e7..aeb26dd69 100644 --- a/roles/sshd/defaults/main.yml +++ b/roles/sshd/defaults/main.yml @@ -19,8 +19,8 @@ sshd__allow_users__dependent_var: [] sshd__allow_users__group_var: [] sshd__allow_users__host_var: [] sshd__allow_users__role_var: [] -sshd__client_alive_count_max: '{{ sshd__client_alive_count_max__cis_var | d(3) }}' -sshd__client_alive_interval: '{{ sshd__client_alive_interval__cis_var | d(15) }}' +sshd__client_alive_count_max: '{{ sshd__client_alive_count_max__stig_var | d(3) }}' +sshd__client_alive_interval: '{{ sshd__client_alive_interval__stig_var | d(15) }}' sshd__deny_groups__combined_var: '{{ sshd__deny_groups__role_var + sshd__deny_groups__dependent_var + @@ -41,26 +41,26 @@ sshd__deny_users__dependent_var: [] sshd__deny_users__group_var: [] sshd__deny_users__host_var: [] sshd__deny_users__role_var: [] -sshd__disable_forwarding: '{{ sshd__disable_forwarding__cis_var | d(true) }}' -sshd__gssapi_authentication: '{{ sshd__gssapi_authentication__cis_var | d(true) }}' +sshd__disable_forwarding: '{{ sshd__disable_forwarding__stig_var | d(true) }}' +sshd__gssapi_authentication: '{{ sshd__gssapi_authentication__stig_var | d(true) }}' sshd__log_level__combined_var: '{{ sshd__log_level__host_var if (sshd__log_level__host_var | string | length) else sshd__log_level__group_var if (sshd__log_level__group_var | string | length) else sshd__log_level__dependent_var if (sshd__log_level__dependent_var | string | length) else - (sshd__log_level__cis_var | d(sshd__log_level__role_var) + (sshd__log_level__stig_var | d(sshd__log_level__role_var)) }}' sshd__log_level__dependent_var: '' sshd__log_level__group_var: '' sshd__log_level__host_var: '' sshd__log_level__role_var: 'INFO' -sshd__login_grace_time: '{{ sshd__login_grace_time__cis_var | d(60) }}' -sshd__max_auth_tries: '{{ sshd__max_auth_tries__cis_var | d(4) }}' -sshd__max_sessions: '{{ sshd__max_sessions__cis_var | d(10) }}' -sshd__max_startups: '{{ sshd__max_startups__cis_var | d("10:30:60") }}' +sshd__login_grace_time: '{{ sshd__login_grace_time__stig_var | d(60) }}' +sshd__max_auth_tries: '{{ sshd__max_auth_tries__stig_var | d(4) }}' +sshd__max_sessions: '{{ sshd__max_sessions__stig_var | d(10) }}' +sshd__max_startups: '{{ sshd__max_startups__stig_var | d("10:30:60") }}' sshd__password_authentication: false -sshd__permit_empty_passwords: '{{ sshd__permit_empty_passwords__cis_var | d(false) }}' -sshd__permit_root_login: '{{ sshd__permit_root_login__cis_var | d("yes") }}' -sshd__permit_user_environment: '{{ sshd__permit_user_environment__cis_var | d(false) }}' +sshd__permit_empty_passwords: '{{ sshd__permit_empty_passwords__stig_var | d(false) }}' +sshd__permit_root_login: '{{ sshd__permit_root_login__stig_var | d("yes") }}' +sshd__permit_user_environment: '{{ sshd__permit_user_environment__stig_var | d(false) }}' sshd__port: 22 sshd__service_enabled: true sshd__service_state: 'started' diff --git a/stig/remediation.csv b/stig/remediation.csv index 0c0072d71..2d8bce2ae 100644 --- a/stig/remediation.csv +++ b/stig/remediation.csv @@ -1,18 +1,18 @@ id,remediation_variable,mandatory,comment -sshd_client_alive,sshd__client_alive_count_max__cis_var: 3,0, -sshd_client_alive,sshd__client_alive_interval__cis_var: 15,0, -sshd_disable_forwarding,sshd__disable_forwarding__cis_var: true,0, -sshd_gssapi_authentication,sshd__gssapi_authentication__cis_var: false,0, -sshd_login_grace_time,sshd__login_grace_time__cis_var: 60,0, -sshd_log_level,sshd__log_level__cis_var: 'VERBOSE',0, -sshd_max_auth_tries,sshd__max_auth_tries__cis_var: 4,0, +sshd_client_alive,sshd__client_alive_count_max__stig_var: 3,0, +sshd_client_alive,sshd__client_alive_interval__stig_var: 15,0, +sshd_disable_forwarding,sshd__disable_forwarding__stig_var: true,0, +sshd_gssapi_authentication,sshd__gssapi_authentication__stig_var: false,0, +sshd_login_grace_time,sshd__login_grace_time__stig_var: 60,0, +sshd_log_level,sshd__log_level__stig_var: 'VERBOSE',0, +sshd_max_auth_tries,sshd__max_auth_tries__stig_var: 4,0, sshd_banner,,0,"This also requires the linuxfabrik.lfops.motd role to be run, since this deploys the banner content." sshd_access,,1,"Manually set at least one of the following variables: `sshd__allow_groups__*_var`, `sshd__allow_users__*_var`, `sshd__deny_groups__*_var`, `sshd__deny_users__*_var`" -sshd_max_startups,sshd__max_startups__cis_var: '10:30:60',0, -sshd_max_sessions,sshd__max_sessions__cis_var: 10,0, -sshd_permit_empty_passwords,sshd__permit_empty_passwords__cis_var: false,0, -sshd__permit_root_login,sshd__permit_root_login__cis_var: 'no',0, -sshd_permit_user_environment,sshd__permit_user_environment__cis_var: false,0, +sshd_max_startups,sshd__max_startups__stig_var: '10:30:60',0, +sshd_max_sessions,sshd__max_sessions__stig_var: 10,0, +sshd_permit_empty_passwords,sshd__permit_empty_passwords__stig_var: false,0, +sshd__permit_root_login,sshd__permit_root_login__stig_var: 'no',0, +sshd_permit_user_environment,sshd__permit_user_environment__stig_var: false,0, sshd_host_based_authentication,,0,"""HostbasedAuthentication no"" is hardcoded in the linuxfabrik.lfops.sshd role." sshd_ignore_rhosts,,0,"""IgnoreRhosts yes"" is hardcoded in the linuxfabrik.lfops.sshd role." sshd_use_pam,,0,"""UsePAM yes"" is hardcoded in the linuxfabrik.lfops.sshd role." @@ -22,10 +22,10 @@ sshd_public_host_key_permissions,,0,This is done by default in the linuxfabrik.l sshd_ciphers,,0,Handled by linuxfabrik.lfops.crypto_policy with LINUXFABRIK-SSH-NO-CBC and LINUXFABRIK-SSH-NO-CHACHA20 modules. sshd_kex_algorithms,,0,Handled by linuxfabrik.lfops.crypto_policy (DEFAULT policy provides secure kex algorithms). sshd_macs,,0,Handled by linuxfabrik.lfops.crypto_policy with LINUXFABRIK-NO-WEAKMAC and LINUXFABRIK-SSH-NO-ETM modules. -crypto_policy_not_legacy,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not set `LEGACY` inside `crypto_policy__policy`. +crypto_policy_not_legacy,crypto_policy__policy__stig_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not set `LEGACY` inside `crypto_policy__policy`. crypto_policy_not_sshd,,0,"This is the case by default. Note that the `/etc/sysconfig/sshd` path in the CIS document is wrong, the relevant config is `/etc/ssh/sshd_config.d/50-redhat.conf`." -crypto_policy_disable_sha1,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-SHA1` from `crypto_policy__policy`. -crypto_policy_disable_mac_128_bits,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-WEAKMAC` from `crypto_policy__policy`. -crypto_policy_disable_ssh_cbc,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-CBC` from `crypto_policy__policy`. -crypto_policy_disable_ssh_chacha20_poly1305,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-CHACHA20` from `crypto_policy__policy`. -crypto_policy_disable_ssh_etm,crypto_policy__policy__cis_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-ETM` from `crypto_policy__policy`. +crypto_policy_disable_sha1,crypto_policy__policy__stig_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-SHA1` from `crypto_policy__policy`. +crypto_policy_disable_mac_128_bits,crypto_policy__policy__stig_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-WEAKMAC` from `crypto_policy__policy`. +crypto_policy_disable_ssh_cbc,crypto_policy__policy__stig_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-CBC` from `crypto_policy__policy`. +crypto_policy_disable_ssh_chacha20_poly1305,crypto_policy__policy__stig_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-CHACHA20` from `crypto_policy__policy`. +crypto_policy_disable_ssh_etm,crypto_policy__policy__stig_var: 'DEFAULT:LINUXFABRIK-NO-SHA1:LINUXFABRIK-NO-WEAKMAC:LINUXFABRIK-SSH-NO-CBC:LINUXFABRIK-SSH-NO-CHACHA20:LINUXFABRIK-SSH-NO-ETM',0,Do not remove `LINUXFABRIK-NO-ETM` from `crypto_policy__policy`. From 6fe83b937ec97583334146cc3fa950fdd053dd4c Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 20 Mar 2026 16:15:40 +0100 Subject: [PATCH 3/5] fix(roles/sshd): update timestamp in templates --- roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2 | 2 +- roles/sshd/templates/etc/ssh/Fedora40-sshd_config.j2 | 2 +- roles/sshd/templates/etc/ssh/RedHat10-sshd_config.j2 | 2 +- roles/sshd/templates/etc/ssh/RedHat8-sshd_config.j2 | 2 +- roles/sshd/templates/etc/ssh/RedHat9-sshd_config.j2 | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2 b/roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2 index eb6fb9db2..19a1fa693 100644 --- a/roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2025031401 +# 2026032001 # This is the sshd server system-wide configuration file. See # sshd_config(5) for more information. diff --git a/roles/sshd/templates/etc/ssh/Fedora40-sshd_config.j2 b/roles/sshd/templates/etc/ssh/Fedora40-sshd_config.j2 index 2f51083cb..322a040b4 100644 --- a/roles/sshd/templates/etc/ssh/Fedora40-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/Fedora40-sshd_config.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024080201 +# 2026032001 # $OpenBSD: sshd_config,v 1.104 2021/07/02 05:11:21 dtucker Exp $ diff --git a/roles/sshd/templates/etc/ssh/RedHat10-sshd_config.j2 b/roles/sshd/templates/etc/ssh/RedHat10-sshd_config.j2 index 35040014e..bfd13ff44 100644 --- a/roles/sshd/templates/etc/ssh/RedHat10-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/RedHat10-sshd_config.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026010801 +# 2026032001 # $OpenBSD: sshd_config,v 1.104 2021/07/02 05:11:21 dtucker Exp $ diff --git a/roles/sshd/templates/etc/ssh/RedHat8-sshd_config.j2 b/roles/sshd/templates/etc/ssh/RedHat8-sshd_config.j2 index 6d87748a7..e79327316 100644 --- a/roles/sshd/templates/etc/ssh/RedHat8-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/RedHat8-sshd_config.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024062301 +# 2026032001 # $OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $ diff --git a/roles/sshd/templates/etc/ssh/RedHat9-sshd_config.j2 b/roles/sshd/templates/etc/ssh/RedHat9-sshd_config.j2 index c5a6f8c3a..baba68bbd 100644 --- a/roles/sshd/templates/etc/ssh/RedHat9-sshd_config.j2 +++ b/roles/sshd/templates/etc/ssh/RedHat9-sshd_config.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024050901 +# 2026032001 # $OpenBSD: sshd_config,v 1.104 2021/07/02 05:11:21 dtucker Exp $ From c1fcefafc59353876f27f05bbad08dbef8bf239b Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 20 Mar 2026 16:50:10 +0100 Subject: [PATCH 4/5] refactor(roles/sshd): soften non-stig default --- roles/sshd/README.md | 4 ++-- roles/sshd/defaults/main.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/sshd/README.md b/roles/sshd/README.md index 31da755ac..eb2a62caf 100644 --- a/roles/sshd/README.md +++ b/roles/sshd/README.md @@ -26,10 +26,10 @@ Note that the role does not make use of `/etc/ssh/sshd_config.d/` since not all | `sshd__allow_groups__host_var` / `sshd__allow_groups__group_var` | List of dictionaries. If specified, login is allowed only for users whose primary group or supplementary group list matches one of the patterns. Subkeys:
* `name`: Mandatory, string. The group name or pattern.
* `state`: Optional, string. `present` or `absent`. Defaults to `present`. | `[]` | | `sshd__allow_users__host_var` / `sshd__allow_users__group_var` | List of dictionaries. If specified, login is allowed only for user names that match one of the patterns. Subkeys:
* `name`: Mandatory, string. The user name or pattern.
* `state`: Optional, string. `present` or `absent`. Defaults to `present`. | `[]` | | `sshd__client_alive_count_max` | Number. Sets the number of client alive messages which may be sent without sshd receiving any messages back from the client. | `3` | -| `sshd__client_alive_interval` | Number. Sets a timeout interval in seconds after which if no data has been received from the client, sshd will send a message through the encrypted channel to request a response from the client. | `15` | +| `sshd__client_alive_interval` | Number. Sets a timeout interval in seconds after which if no data has been received from the client, sshd will send a message through the encrypted channel to request a response from the client. | `0` | | `sshd__deny_groups__host_var` / `sshd__deny_groups__group_var` | List of dictionaries. Login is disallowed for users whose primary group or supplementary group list matches one of the patterns. Subkeys:
* `name`: Mandatory, string. The group name or pattern.
* `state`: Optional, string. `present` or `absent`. Defaults to `present`. | `[]` | | `sshd__deny_users__host_var` / `sshd__deny_users__group_var` | List of dictionaries. Login is disallowed for user names that match one of the patterns. Subkeys:
* `name`: Mandatory, string. The user name or pattern.
* `state`: Optional, string. `present` or `absent`. Defaults to `present`. | `[]` | -| `sshd__disable_forwarding` | Bool. Disables all forwarding features, including X11, ssh-agent, TCP and StreamLocal. | `true` | +| `sshd__disable_forwarding` | Bool. Disables all forwarding features, including X11, ssh-agent, TCP and StreamLocal. | `false` | | `sshd__gssapi_authentication` | Bool. Specifies whether user authentication based on GSSAPI is allowed. | `true` | | `sshd__log_level` | Sets the log level | `'INFO'` | | `sshd__login_grace_time` | Number. The time in seconds after which the server disconnects if the user has not successfully logged in. | `60` | diff --git a/roles/sshd/defaults/main.yml b/roles/sshd/defaults/main.yml index aeb26dd69..ddcfa55fd 100644 --- a/roles/sshd/defaults/main.yml +++ b/roles/sshd/defaults/main.yml @@ -20,7 +20,7 @@ sshd__allow_users__group_var: [] sshd__allow_users__host_var: [] sshd__allow_users__role_var: [] sshd__client_alive_count_max: '{{ sshd__client_alive_count_max__stig_var | d(3) }}' -sshd__client_alive_interval: '{{ sshd__client_alive_interval__stig_var | d(15) }}' +sshd__client_alive_interval: '{{ sshd__client_alive_interval__stig_var | d(0) }}' sshd__deny_groups__combined_var: '{{ sshd__deny_groups__role_var + sshd__deny_groups__dependent_var + @@ -41,7 +41,7 @@ sshd__deny_users__dependent_var: [] sshd__deny_users__group_var: [] sshd__deny_users__host_var: [] sshd__deny_users__role_var: [] -sshd__disable_forwarding: '{{ sshd__disable_forwarding__stig_var | d(true) }}' +sshd__disable_forwarding: '{{ sshd__disable_forwarding__stig_var | d(false) }}' sshd__gssapi_authentication: '{{ sshd__gssapi_authentication__stig_var | d(true) }}' sshd__log_level__combined_var: '{{ sshd__log_level__host_var if (sshd__log_level__host_var | string | length) else From 3a0cc426aedf9e99acbcba430e693f2c0970b06e Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 20 Mar 2026 16:51:45 +0100 Subject: [PATCH 5/5] docs: keep CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b3af8d2..3da84cda1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,19 @@ Note: Always add new entries to the top of the section, even if this results in ### Added +Playbook:stig_cis_rocky9_v2_0_0 +* Add playbook for CIS Rocky Linux 9 v2.0.0 STIG compliance + +Role:sshd +* Add `sshd__allow_groups`, `sshd__allow_users`, `sshd__deny_groups`, `sshd__deny_users` variables using the injection system +* Add `sshd__client_alive_count_max`, `sshd__client_alive_interval`, `sshd__disable_forwarding`, `sshd__login_grace_time`, `sshd__max_auth_tries`, `sshd__max_sessions`, `sshd__max_startups`, `sshd__permit_empty_passwords`, `sshd__permit_user_environment` variables +* Hardcode `HostbasedAuthentication no`, `IgnoreRhosts yes`, `UsePAM yes` for STIG compliance +* Add tasks for SSH host key file permissions +* Add `__stig_var` override support for STIG-driven variable injection + +Role:crypto_policy +* Add `__stig_var` override support for STIG-driven variable injection + Role:fail2ban * Make `bantime` configurable for the sshd and portscan jails