feat: manage ssh certificates (#252)

* Role configured to accept SSH connection via SSH certificates
* Works with or without principals and ansible-lint updated
* add test for SSH certificates authentication with principals
* Add configuration to run tests for SSH certificates authentication with principals
* tasks to use SSH certificates grouped into one file
* Update README.md
This commit is contained in:
EmyLIEUTAUD 2023-09-11 15:39:03 +02:00 committed by GitHub
parent d54f51f32a
commit 0bc6d8f40b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 303 additions and 0 deletions

View file

@ -282,6 +282,62 @@ Default path to the sftp server binary.
This variable is set to *true* after the role was successfully executed.
## Configure SSH certificate authentication
To configure SSH certificate authentication on your SSH server, you need to provide at least the trusted user CA key, which will be used to validate client certificates against.
This is done with the `sshd_trusted_user_ca_keys_list` variable.
If you need to map some of the authorized principals to system users, you can do that using the `sshd_principals` variable.
### Additional variables
#### sshd_trusted_user_ca_keys_list
List of the trusted user CA public keys in OpenSSH (one-line) format (mandatory).
#### sshd_trustedusercakeys_directory_owner, shsd_trustedusercakeys_directory_group, sshd_trustedusercakeys_directory_mode
Use these variables to set the ownership and permissions for the Trusted User CA Keys directory. Defaults are respectively *root*, *root* and *0755*.
#### sshd_trustedusercakeys_file_owner, shsd_trustedusercakeys_file_group, sshd_trustedusercakeys_file_mode
Use these variables to set the ownership and permissions for the Trusted User CA Keys file. Defaults are respectively *root*, *root* and *0640*.
#### sshd_principals
A dict containing principals for users in the os (optional). e.g.
```yaml
sshd_principals:
admin:
- frontend-admin
- backend-admin
somelinuxuser:
- some-principal-defined-in-certificate
```
#### sshd_authorizedprincipals_directory_owner, shsd_authorizedprincipals_directory_group, sshd_authorizedprincipals_directory_mode
Use these variables to set the ownership and permissions for the Authorized Principals directory. Defaults are respectively *root*, *root* and *0755*.
#### sshd_authorizedprincipals_file_owner, shsd_authorizedprincipals_file_group, sshd_authorizedprincipals_file_mode
Use these variables to set the ownership and permissions for the Authorized Principals file. Defaults are respectively *root*, *root* and *0644*.
### Additional configuration
The SSH server needs this information stored in files so in addition to the above variables, respective configuration options `TrustedUserCAKeys` (mandatory) and `AuthorizedPrincipalsFile` (optional) need to be present the `sshd` dictionary when invoking the role. For example:
```yaml
sshd:
TrustedUserCAKeys: /etc/ssh/path-to-trusted-user-ca-keys/trusted-user-ca-keys.pub
AuthorizedPrincipalsFile: "/etc/ssh/path-to-auth-principals/auth_principals/%u"
```
To learn more about SSH Certificates, here is a [nice tutorial to pure SSH certificates, from wikibooks](https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Certificate-based_Authentication).
To understand principals and to set up SSH certificates with Vault, this is a [well-explained tutorial from Hashicorp](https://www.hashicorp.com/blog/managing-ssh-access-at-scale-with-hashicorp-vault).
## Dependencies
None

View file

@ -44,6 +44,12 @@ sshd: {}
# configuration file snippet or configuring second sshd service
sshd_config_file: "{{ __sshd_config_file }}"
# If not empty, list of trusted CA keys
sshd_trusted_user_ca_keys_list: []
# If not empty, dict containing principals for users in the os
sshd_principals: {}
### VARS DEFAULTS
### The following are defaults for OS specific configuration in var files in
### this role. They should not be set directly by role users, unless they know
@ -60,6 +66,20 @@ sshd_sftp_server: "{{ __sshd_sftp_server }}"
sshd_drop_in_dir_mode: "{{ __sshd_drop_in_dir_mode }}"
sshd_main_config_file: "{{ __sshd_main_config_file }}"
sshd_trustedusercakeys_directory_owner: "{{ __sshd_trustedusercakeys_directory_owner }}"
sshd_trustedusercakeys_directory_group: "{{ __sshd_trustedusercakeys_directory_group }}"
sshd_trustedusercakeys_directory_mode: "{{ __sshd_trustedusercakeys_directory_mode }}"
sshd_trustedusercakeys_file_owner: "{{ __sshd_trustedusercakeys_file_owner }}"
sshd_trustedusercakeys_file_group: "{{ __sshd_trustedusercakeys_file_group }}"
sshd_trustedusercakeys_file_mode: "{{ __sshd_trustedusercakeys_file_mode }}"
sshd_authorizedprincipals_directory_owner: "{{ __sshd_authorizedprincipals_directory_owner }}"
sshd_authorizedprincipals_directory_group: "{{ __sshd_authorizedprincipals_directory_group }}"
sshd_authorizedprincipals_directory_mode: "{{ __sshd_authorizedprincipals_directory_mode }}"
sshd_authorizedprincipals_file_owner: "{{ __sshd_authorizedprincipals_file_owner }}"
sshd_authorizedprincipals_file_group: "{{ __sshd_authorizedprincipals_file_group }}"
sshd_authorizedprincipals_file_mode: "{{ __sshd_authorizedprincipals_file_mode }}"
# This lists by default all hostkeys as rendered in the generated configuration
# file ("auto"). Before attempting to run sshd (either for verification of
# configuration or restarting), we make sure the keys exist and have correct

View file

@ -0,0 +1,23 @@
---
- name: Use SSH certificates
hosts: all
tasks:
- name: Configure sshd to enable SSH Certificate login
ansible.builtin.include_role:
name: ansible-sshd
vars:
sshd:
# Disable password authentication, use SSH Certificates and configure authorized principals
PasswordAuthentication: false
TrustedUserCAKeys: /etc/ssh/trusted-user-ca-keys.pub
AuthorizedPrincipalsFile: "/etc/ssh/auth_principals/%u"
# List of trusted user CA keys
sshd_trusted_user_ca_keys_list:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICwqRjI9gAwkQF9iIylhRVAOFy2Joodh3fXJ7CbGWqUd
# Key is the user in the os, values are *Principals* defined in the certificate
sshd_principals:
admin:
- frontend-admin
- backend-admin
somelinuxuser:
- some-principal-defined-in-certificate

54
tasks/certificates.yml Normal file
View file

@ -0,0 +1,54 @@
---
- name: Configure Trusted user CA Keys
vars:
# The explicit to_json filter is needed for Python 2 compatibility
__sshd_trustedusercakeys_from_config: >-
{% if sshd_TrustedUserCAKeys is defined %}
{{ sshd_TrustedUserCAKeys | to_json }}
{% else %}
{{ sshd['TrustedUserCAKeys'] | to_json }}
{% endif %}
block:
- name: Create Trusted user CA Keys directory
ansible.builtin.file:
path: "{{ (__sshd_trustedusercakeys_from_config | from_json) | dirname }}"
state: directory
owner: "{{ sshd_trustedusercakeys_directory_owner }}"
group: "{{ sshd_trustedusercakeys_directory_group }}"
mode: "{{ sshd_trustedusercakeys_directory_mode }}"
- name: Copy Trusted user CA Keys
ansible.builtin.template:
src: "trusted-user-ca-keys.pub.j2"
dest: "{{ __sshd_trustedusercakeys_from_config | from_json }}"
owner: "{{ sshd_trustedusercakeys_file_owner }}"
group: "{{ sshd_trustedusercakeys_file_group }}"
mode: "{{ sshd_trustedusercakeys_file_mode }}"
- name: Configure Principals
vars:
# The explicit to_json filter is needed for Python 2 compatibility
__sshd_authorizedprincipalsfile_from_config: >-
{% if sshd_AuthorizedPrincipalsFile is defined %}
{{ sshd_AuthorizedPrincipalsFile | to_json }}
{% else %}
{{ sshd['AuthorizedPrincipalsFile'] | to_json }}
{% endif %}
when: sshd_principals != {}
block:
- name: Create Principals directory
ansible.builtin.file:
path: "{{ (__sshd_authorizedprincipalsfile_from_config | from_json) | dirname }}"
state: directory
owner: "{{ sshd_authorizedprincipals_directory_owner }}"
group: "{{ sshd_authorizedprincipals_directory_group }}"
mode: "{{ sshd_authorizedprincipals_directory_mode }}"
- name: Copy Principals files
ansible.builtin.template:
src: "auth_principals.j2"
dest: "{{ (__sshd_authorizedprincipalsfile_from_config | from_json) | dirname }}/{{ item.key }}"
owner: "{{ sshd_authorizedprincipals_file_owner }}"
group: "{{ sshd_authorizedprincipals_file_group }}"
mode: "{{ sshd_authorizedprincipals_file_mode }}"
with_dict: "{{ sshd_principals }}"

View file

@ -147,6 +147,10 @@
ansible.builtin.include_tasks: install_namespace.yml
when: sshd_config_namespace is not none
- name: Configure sshd to use SSH certificates
ansible.builtin.include_tasks: certificates.yml
when: sshd_trusted_user_ca_keys_list != []
rescue:
- name: Re-raise the error
ansible.builtin.fail:

View file

@ -0,0 +1,5 @@
{{ ansible_managed | comment }}
{{ "willshersystems:ansible-sshd" | comment(prefix="", postfix="") }}
{% for principal in item.value %}
{{ principal }}
{% endfor %}

View file

@ -0,0 +1,5 @@
{{ ansible_managed | comment }}
{{ "willshersystems:ansible-sshd" | comment(prefix="", postfix="") }}
{% for key in sshd_trusted_user_ca_keys_list %}
{{ key }}
{% endfor %}

View file

@ -0,0 +1,124 @@
---
- name: Test SSH certificates options
hosts: all
vars:
__sshd_test_backup_files:
- /etc/ssh/sshd_config
- /etc/ssh/sshd_config.d/00-ansible_system_role.conf
tasks:
- name: "Backup configuration files"
ansible.builtin.include_tasks: tasks/backup.yml
- name: Ensure group 'nobody' exists
ansible.builtin.group:
name: nobody
- name: Ensure the user 'nobody' exists
ansible.builtin.user:
name: nobody
group: nobody
comment: nobody
create_home: false
shell: /sbin/nologin
- name: Configure sshd
ansible.builtin.include_role:
name: ansible-sshd
vars:
sshd:
PasswordAuthentication: false
TrustedUserCAKeys: /etc/ssh/ca-keys/trusted-user-ca-keys.pub
AuthorizedPrincipalsFile: "/etc/ssh/auth_principals/%u"
sshd_trusted_user_ca_keys_list:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICwqRjI9gAwkQF9iIylhRVAOFy2Joodh3fXJ7CbGWqUd
# Key is the user in the os, values are *Principals* defined in the certificate
sshd_principals:
user:
- principal
sshd_config_file: /etc/ssh/sshd_config
# very BAD example
sshd_trustedusercakeys_directory_owner: "nobody"
sshd_trustedusercakeys_directory_group: "nobody"
sshd_trustedusercakeys_directory_mode: "0770"
sshd_trustedusercakeys_file_owner: "nobody"
sshd_trustedusercakeys_file_group: "nobody"
sshd_trustedusercakeys_file_mode: "0750"
sshd_authorizedprincipals_directory_owner: "nobody"
sshd_authorizedprincipals_directory_group: "nobody"
sshd_authorizedprincipals_directory_mode: "0777"
sshd_authorizedprincipals_file_owner: "nobody"
sshd_authorizedprincipals_file_group: "nobody"
sshd_authorizedprincipals_file_mode: "0755"
- name: Verify the options are correctly set
tags: tests::verify
block:
- name: Flush handlers
ansible.builtin.meta: flush_handlers
- name: Print current configuration file
ansible.builtin.slurp:
src: /etc/ssh/sshd_config
register: config
- name: Check the options are in configuration file
ansible.builtin.assert:
that:
- "'PasswordAuthentication no' in config.content | b64decode"
- "'TrustedUserCAKeys /etc/ssh/ca-keys/trusted-user-ca-keys.pub' in config.content | b64decode"
- "'AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u' in config.content | b64decode"
- name: Get trusted user CA keys directory stat
ansible.builtin.stat:
path: /etc/ssh/ca-keys
register: trustedusercakeys_directory_stat
- name: Check trusted user CA keys directory has requested properties
ansible.builtin.assert:
that:
- trustedusercakeys_directory_stat.stat.isdir
- trustedusercakeys_directory_stat.stat.pw_name == "nobody"
- trustedusercakeys_directory_stat.stat.gr_name == "nobody"
- trustedusercakeys_directory_stat.stat.mode == "0770"
- name: Get trusted user CA keys file stat
ansible.builtin.stat:
path: /etc/ssh/ca-keys/trusted-user-ca-keys.pub
register: trustedusercakeys_file_stat
- name: Check trusted user CA keys file has requested properties
ansible.builtin.assert:
that:
- trustedusercakeys_file_stat.stat.exists
- trustedusercakeys_file_stat.stat.pw_name == "nobody"
- trustedusercakeys_file_stat.stat.gr_name == "nobody"
- trustedusercakeys_file_stat.stat.mode == "0750"
- name: Get authorized principals directory stat
ansible.builtin.stat:
path: /etc/ssh/auth_principals
register: authorizedprincipals_directory_stat
- name: Check authorized principals directory has requested properties
ansible.builtin.assert:
that:
- authorizedprincipals_directory_stat.stat.isdir
- authorizedprincipals_directory_stat.stat.pw_name == "nobody"
- authorizedprincipals_directory_stat.stat.gr_name == "nobody"
- authorizedprincipals_directory_stat.stat.mode == "0777"
- name: Get authorized principals file stat
ansible.builtin.stat:
path: /etc/ssh/auth_principals/user
register: authorizedprincipals_file_stat
- name: Check authorized principals file has requested properties
ansible.builtin.assert:
that:
- authorizedprincipals_file_stat.stat.exists
- authorizedprincipals_file_stat.stat.pw_name == "nobody"
- authorizedprincipals_file_stat.stat.gr_name == "nobody"
- authorizedprincipals_file_stat.stat.mode == "0755"
- name: "Restore configuration files"
ansible.builtin.include_tasks: tasks/restore.yml

View file

@ -5,6 +5,18 @@ __sshd_config_mode: "0600"
__sshd_hostkey_owner: "root"
__sshd_hostkey_group: "root"
__sshd_hostkey_mode: "0600"
__sshd_trustedusercakeys_directory_owner: "root"
__sshd_trustedusercakeys_directory_group: "root"
__sshd_trustedusercakeys_directory_mode: "0755"
__sshd_trustedusercakeys_file_owner: "root"
__sshd_trustedusercakeys_file_group: "root"
__sshd_trustedusercakeys_file_mode: "0640"
__sshd_authorizedprincipals_directory_owner: "root"
__sshd_authorizedprincipals_directory_group: "root"
__sshd_authorizedprincipals_directory_mode: "0755"
__sshd_authorizedprincipals_file_owner: "root"
__sshd_authorizedprincipals_file_group: "root"
__sshd_authorizedprincipals_file_mode: "0644"
# The OpenSSH 5.3 in RHEL6 does not support "Match all" so we need a workaround
__sshd_compat_match_all: Match all
# The hostkeys not supported in FIPS mode, if applicable