Files
ansible-role-unix-users/molecule/default/verify.yml
T
volker.raschekandCopilot de280ef620
Lint Markdown files / markdown-lint (push) Successful in 7s
Ansible Linter / ansible-lint (push) Successful in 2m15s
Molecule / Molecule (push) Successful in 5m45s
Release Ansible Role / Release Ansible Role (push) Successful in 1m35s
feat(ssh)!: configure keys with filename and content
Accept SSH key content directly so callers no longer need role-specific
source directories. Update Molecule coverage and documentation.

BREAKING CHANGE: private_keys entries must be objects with filename and
content instead of file names. authorized_keys entries must provide
content alongside filename. Public keys are also written to ~/.ssh.

Co-authored-by: Copilot <copilot@github.com>
2026-10-17 21:41:41 +02:00

169 lines
7.1 KiB
YAML

---
- name: Verify
hosts: all
vars:
_alice_home: /home/molecule-alice
_bob_home: /home/molecule-bob
_expected_modes:
/home/molecule-alice/.ssh: "0700"
/home/molecule-alice/.ssh/authorized_keys: "0600"
/home/molecule-alice/.ssh/config: "0644"
/home/molecule-alice/.ssh/alice.ed25519.key: "0600"
/home/molecule-alice/.ssh/alice.ed25519.key.pub: "0644"
/home/molecule-alice/.ssh/alice.pub: "0644"
/home/molecule-alice/.ssh/alice-login.pub: "0644"
/home/molecule-alice/.forward: "0644"
/home/molecule-alice/.netrc: "0600"
/home/molecule-alice/.bashrc.d: "0755"
/home/molecule-alice/.bashrc.d/molecule.bashrc: "0644"
/home/molecule-alice/.cache: "0755"
/home/molecule-alice/.config: "0755"
/home/molecule-alice/.local/share: "0755"
/home/molecule-alice/.local/state: "0755"
# Only files the role writes are listed, .bashrc originates from the lineinfile task.
_expected_contents:
/home/molecule-alice/.forward: "alice@example.local"
/home/molecule-alice/.netrc: "machine hostname.local login alice password secret"
/home/molecule-alice/.ssh/config: " StrictHostKeyChecking no"
/home/molecule-alice/.bashrc.d/molecule.bashrc: "alias dcd='docker compose down'"
/home/molecule-alice/.bashrc: "source \"/home/molecule-alice/.bashrc.d/molecule.bashrc\""
tasks:
- name: Stat the files of molecule-alice
ansible.builtin.stat:
path: "{{ item.key }}"
register: _alice_files
loop: "{{ _expected_modes | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: Assert that the files of molecule-alice exist with the expected mode and owner
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.mode == item.item.value
- item.stat.pw_name == 'molecule-alice'
- item.stat.gr_name == 'molecule-alice'
fail_msg: >-
{{ item.item.key }} has mode {{ item.stat.mode | default('none') }} and owner
{{ item.stat.pw_name | default('none') }}:{{ item.stat.gr_name | default('none') }}
instead of {{ item.item.value }} and molecule-alice:molecule-alice
loop: "{{ _alice_files.results }}"
loop_control:
label: "{{ item.item.key }}"
- name: Read the files of molecule-alice
ansible.builtin.slurp:
src: "{{ item.key }}"
register: _alice_contents
loop: "{{ _expected_contents | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: Assert that the files of molecule-alice hold the configured values
ansible.builtin.assert:
that: item.item.value in (item.content | b64decode).splitlines()
fail_msg: "{{ item.item.key }} does not contain the line {{ item.item.value }}"
loop: "{{ _alice_contents.results }}"
loop_control:
label: "{{ item.item.key }}"
- name: Read the authorized_keys and the extracted public key of molecule-alice
ansible.builtin.slurp:
src: "{{ item }}"
register: _alice_keys
loop:
- "{{ _alice_home }}/.ssh/authorized_keys"
- "{{ _alice_home }}/.ssh/alice.ed25519.key.pub"
- name: Assert that the authorized key carries its options and matches the extracted public key
vars:
_authorized_key: "{{ (_alice_keys.results[0].content | b64decode).splitlines() | select('search', 'ssh-ed25519') | first }}"
_public_key: "{{ _alice_keys.results[1].content | b64decode | trim }}"
ansible.builtin.assert:
that:
- _authorized_key.startswith('command="/usr/bin/true",environment="EDITOR=vi" ')
- _public_key.startswith('ssh-ed25519 ')
- _public_key.split()[1] == _authorized_key.split()[2]
- (_alice_keys.results[0].content | b64decode).splitlines() | select('match', '^ssh-ed25519 ') | list | length == 1
fail_msg: "the authorized key of molecule-alice does not match the key extracted from its private key"
- name: Read the configured private and public SSH key files
ansible.builtin.slurp:
src: "{{ _alice_home }}/.ssh/{{ item }}"
loop:
- alice.ed25519.key
- alice.pub
- alice-login.pub
no_log: true
register: _alice_key_contents
- name: Assert that SSH key files contain the exact configured content
vars:
_private_key_content: "{{ lookup('ansible.builtin.file', molecule_scenario_directory + '/files/ssh/private_keys/molecule.ed25519.key', rstrip=false) }}"
_public_key_content: "{{ lookup('ansible.builtin.file', molecule_scenario_directory + '/files/ssh/authorized_keys/molecule.pub', rstrip=false) }}"
ansible.builtin.assert:
that:
- (_alice_key_contents.results[0].content | b64decode) == _private_key_content
- (_alice_key_contents.results[1].content | b64decode) == _public_key_content
- (_alice_key_contents.results[2].content | b64decode) == _public_key_content
- _public_key_content | trim in (_alice_keys.results[0].content | b64decode).splitlines()
no_log: true
- name: Read the user and group database
ansible.builtin.getent:
database: "{{ item }}"
loop:
- passwd
- group
- shadow
- name: Assert that molecule-alice was created as declared
vars:
_entry: "{{ ansible_facts['getent_passwd']['molecule-alice'] }}"
ansible.builtin.assert:
that:
- _entry[1] == '4242'
- _entry[2] == '4242'
- _entry[3] == 'Alice'
- _entry[4] == _alice_home
- _entry[5] == '/bin/bash'
fail_msg: "molecule-alice was created as {{ _entry }}"
- name: Assert that the integer gid of the group molecule-alice was applied
ansible.builtin.assert:
that: ansible_facts['getent_group']['molecule-alice'][1] == '4242'
fail_msg: "the group molecule-alice has gid {{ ansible_facts['getent_group']['molecule-alice'][1] }}"
# A random salt would produce a new hash on every run, which the idempotence step would report as a change.
- name: Assert that the password hash is derived from a deterministic salt
vars:
_expected_hash: "{{ 'alice' | password_hash('sha512', 'molecule-alice' | hash('sha512') | truncate(16, true, '')) }}"
ansible.builtin.assert:
that: ansible_facts['getent_shadow']['molecule-alice'][0] == _expected_hash
fail_msg: "the password hash of molecule-alice is not reproducible and therefore changes on every run"
- name: Assert that molecule-dave and the group molecule-obsolete were removed
ansible.builtin.assert:
that:
- "'molecule-dave' not in ansible_facts['getent_passwd']"
- "'molecule-obsolete' not in ansible_facts['getent_group']"
fail_msg: "the removal of molecule-dave or of the group molecule-obsolete did not happen"
- name: Stat the home of molecule-bob and the files he did not ask for
ansible.builtin.stat:
path: "{{ item }}"
register: _bob_files
loop:
- "{{ _bob_home }}"
- "{{ _bob_home }}/.ssh"
- "{{ _bob_home }}/.forward"
- "{{ _bob_home }}/.netrc"
- name: Assert that molecule-bob got a home but none of the optional files
ansible.builtin.assert:
that:
- _bob_files.results[0].stat.exists
- not _bob_files.results[1:] | map(attribute='stat.exists') | select | list
fail_msg: "molecule-bob has files that were never declared for him"