Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ playbooks/test.yml
roles/test
context/
particle/.vagrant
stig/stig.db
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 48 additions & 0 deletions playbooks/stig_cis_rocky9_v2_0_0.yml
Original file line number Diff line number Diff line change
@@ -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'
2 changes: 1 addition & 1 deletion roles/crypto_policy/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -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__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'
Expand Down
52 changes: 42 additions & 10 deletions roles/sshd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<br> * `name`: Mandatory, string. The group name or pattern.<br> * `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:<br> * `name`: Mandatory, string. The user name or pattern.<br> * `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. | `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:<br> * `name`: Mandatory, string. The group name or pattern.<br> * `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:<br> * `name`: Mandatory, string. The user name or pattern.<br> * `state`: Optional, string. `present` or `absent`. Defaults to `present`. | `[]` |
| `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__password_authentication` | Specifies whether password authentication is allowed. | `false` |
| `sshd__permit_root_login` | Specifies whether root can log in using ssh. Possible options:<br> * `yes`<br> * `prohibit-password`<br> * `forced-commands-only`<br> * `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:<br> * `started`<br> * `stopped`<br> * `restarted`<br> * `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:<br> * `yes`<br> * `prohibit-password`<br> * `forced-commands-only`<br> * `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:<br> * `started`<br> * `stopped`<br> * `restarted`<br> * `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
Expand Down
74 changes: 61 additions & 13 deletions roles/sshd/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -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__stig_var | d(3) }}'
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 +
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__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
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__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__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__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'
sshd__use_dns: false
27 changes: 26 additions & 1 deletion roles/sshd/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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'
Expand Down
16 changes: 16 additions & 0 deletions roles/sshd/tasks/private-host-key-permissions.yml
Original file line number Diff line number Diff line change
@@ -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"] }}'
16 changes: 16 additions & 0 deletions roles/sshd/tasks/public-host-key-permissions.yml
Original file line number Diff line number Diff line change
@@ -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"] }}'
42 changes: 31 additions & 11 deletions roles/sshd/templates/etc/ssh/Debian12-sshd_config.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# {{ ansible_managed }}
# 2025031401
# 2026032001

# This is the sshd server system-wide configuration file. See
# sshd_config(5) for more information.
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading