Verify SSH key rotation, file permissions, optional file removal and password management on Arch Linux, Debian and Fedora. Keep container dependencies scoped to the test scenario. Co-authored-by: Copilot <copilot@github.com>
189 lines
5.7 KiB
YAML
189 lines
5.7 KiB
YAML
---
|
|
- name: Verify
|
|
hosts: all
|
|
vars:
|
|
_expected_modes:
|
|
/root/.ssh: "0700"
|
|
/root/.ssh/authorized_keys: "0600"
|
|
/root/.ssh/config: "0644"
|
|
/root/.ssh/molecule.ed25519: "0600"
|
|
/root/.ssh/molecule.ed25519.pub: "0644"
|
|
vars_files:
|
|
- vars/main.yml
|
|
tasks:
|
|
- name: Stat the SSH directory and files
|
|
ansible.builtin.stat:
|
|
path: "{{ item.key }}"
|
|
loop: "{{ _expected_modes | dict2items }}"
|
|
loop_control:
|
|
label: "{{ item.key }}"
|
|
register: _root_files
|
|
|
|
- name: Assert SSH ownership and permissions
|
|
ansible.builtin.assert:
|
|
that:
|
|
- item.stat.exists
|
|
- item.stat.mode == item.item.value
|
|
- item.stat.pw_name == 'root'
|
|
- item.stat.gr_name == 'root'
|
|
loop: "{{ _root_files.results }}"
|
|
loop_control:
|
|
label: "{{ item.item.key }}"
|
|
|
|
- name: Read the SSH files
|
|
ansible.builtin.slurp:
|
|
src: "{{ item }}"
|
|
loop:
|
|
- /root/.ssh/authorized_keys
|
|
- /root/.ssh/config
|
|
- /root/.ssh/molecule.ed25519
|
|
- /root/.ssh/molecule.ed25519.pub
|
|
no_log: true
|
|
register: _root_contents
|
|
|
|
- name: Assert SSH file contents
|
|
vars:
|
|
_original_public_key: '{{ lookup("ansible.builtin.file", molecule_ephemeral_directory + "/original.ed25519.pub") }}'
|
|
_replacement_public_key: '{{ lookup("ansible.builtin.file", molecule_ephemeral_directory + "/replacement.ed25519.pub") }}'
|
|
ansible.builtin.assert:
|
|
that:
|
|
- (('command="/usr/bin/true" ' + _original_public_key) in (_root_contents.results[0].content | b64decode).splitlines())
|
|
- _replacement_public_key in (_root_contents.results[0].content | b64decode).splitlines()
|
|
- "'Host *' in (_root_contents.results[1].content | b64decode).splitlines()"
|
|
- "' StrictHostKeyChecking no' in (_root_contents.results[1].content | b64decode).splitlines()"
|
|
- (_root_contents.results[2].content | b64decode) == _root_configuration.ssh.private_keys[0].content
|
|
- (_root_contents.results[3].content | b64decode).split()[1] == _original_public_key.split()[1]
|
|
no_log: true
|
|
|
|
- name: Verify SSH key rotation
|
|
hosts: all
|
|
vars:
|
|
root:
|
|
ssh:
|
|
authorized_keys: "{{ _root_configuration.ssh.authorized_keys }}"
|
|
config: "{{ _root_configuration.ssh.config }}"
|
|
private_keys:
|
|
- content: '{{ lookup("ansible.builtin.file", molecule_ephemeral_directory + "/replacement.ed25519", rstrip=false) }}'
|
|
filename: molecule.ed25519
|
|
vars_files:
|
|
- vars/main.yml
|
|
tasks:
|
|
- name: Replace the private key while keeping the public key file
|
|
ansible.builtin.include_role:
|
|
name: root
|
|
|
|
- name: Read the public key after rotation
|
|
ansible.builtin.slurp:
|
|
src: /root/.ssh/molecule.ed25519.pub
|
|
register: _rotated_public_key
|
|
|
|
- name: Assert the existing public key was updated
|
|
vars:
|
|
_expected_public_key: '{{ lookup("ansible.builtin.file", molecule_ephemeral_directory + "/replacement.ed25519.pub") }}'
|
|
ansible.builtin.assert:
|
|
that:
|
|
- (_rotated_public_key.content | b64decode).split()[1] == _expected_public_key.split()[1]
|
|
- _rotated_public_key.content != _root_contents.results[3].content
|
|
|
|
- name: Verify root password and empty authorized keys
|
|
hosts: all
|
|
vars:
|
|
root:
|
|
password: molecule-root-password
|
|
ssh:
|
|
authorized_keys: []
|
|
tasks:
|
|
- name: Set the root password and remove empty optional SSH files
|
|
ansible.builtin.include_role:
|
|
name: root
|
|
|
|
- name: Read the root password entry
|
|
ansible.builtin.getent:
|
|
database: shadow
|
|
key: root
|
|
no_log: true
|
|
|
|
- name: Hash the test password using the stored salt and rounds
|
|
ansible.builtin.command:
|
|
argv:
|
|
- openssl
|
|
- passwd
|
|
- -6
|
|
- -salt
|
|
- "{{ ansible_facts.getent_shadow.root[0].split('$')[2:-1] | join('$') }}"
|
|
- -stdin
|
|
stdin: molecule-root-password
|
|
changed_when: false
|
|
no_log: true
|
|
register: _expected_password_hash
|
|
|
|
- name: Assert the root password was set and unlocked
|
|
ansible.builtin.assert:
|
|
that:
|
|
- ansible_facts.getent_shadow.root[0].startswith('$6$')
|
|
- ansible_facts.getent_shadow.root[0] == _expected_password_hash.stdout
|
|
- ansible_become_pass == 'molecule-root-password'
|
|
no_log: true
|
|
|
|
- name: Stat removed optional SSH files
|
|
ansible.builtin.stat:
|
|
path: "{{ item }}"
|
|
loop:
|
|
- /root/.ssh/authorized_keys
|
|
- /root/.ssh/config
|
|
register: _removed_files
|
|
|
|
- name: Assert empty and undefined optional SSH files were removed
|
|
ansible.builtin.assert:
|
|
that: not item.stat.exists
|
|
loop: "{{ _removed_files.results }}"
|
|
loop_control:
|
|
label: "{{ item.item }}"
|
|
|
|
- name: Restore SSH settings
|
|
hosts: all
|
|
vars:
|
|
root: "{{ _root_configuration }}"
|
|
vars_files:
|
|
- vars/main.yml
|
|
tasks:
|
|
- name: Restore optional SSH files before testing omitted SSH settings
|
|
ansible.builtin.include_role:
|
|
name: root
|
|
|
|
- name: Verify root locking and omitted SSH settings
|
|
hosts: all
|
|
vars:
|
|
root:
|
|
password: ""
|
|
tasks:
|
|
- name: Lock root and remove omitted optional SSH files
|
|
ansible.builtin.include_role:
|
|
name: root
|
|
|
|
- name: Read the locked root password entry
|
|
ansible.builtin.getent:
|
|
database: shadow
|
|
key: root
|
|
no_log: true
|
|
|
|
- name: Assert root login is locked
|
|
ansible.builtin.assert:
|
|
that: ansible_facts.getent_shadow.root[0].startswith('!')
|
|
no_log: true
|
|
|
|
- name: Stat optional SSH files after omitting SSH settings
|
|
ansible.builtin.stat:
|
|
path: "{{ item }}"
|
|
loop:
|
|
- /root/.ssh/authorized_keys
|
|
- /root/.ssh/config
|
|
register: _omitted_files
|
|
|
|
- name: Assert omitted optional SSH files were removed
|
|
ansible.builtin.assert:
|
|
that: not item.stat.exists
|
|
loop: "{{ _omitted_files.results }}"
|
|
loop_control:
|
|
label: "{{ item.item }}"
|