Compare commits

..
12 Commits
Author SHA1 Message Date
volker.raschek d3fa0d0178 fix(tasks): avoid logging of private ssh keys
Ansible Linter / ansible-lint (push) Successful in 1m59s
Lint Markdown files / markdown-lint (push) Successful in 7s
Molecule / Molecule (push) Successful in 6m5s
Release Ansible Role / Release Ansible Role (push) Successful in 1m39s
2026-10-19 21:18:40 +02:00
volker.raschekandCopilot ff11874008 test(ssh): cover key rotation with Molecule
Ansible Linter / ansible-lint (push) Successful in 2m18s
Lint Markdown files / markdown-lint (push) Successful in 15s
Release Ansible Role / Release Ansible Role (push) Successful in 3m25s
Molecule / Molecule (push) Successful in 6m1s
Replace a private key while retaining its public key file to detect stale derived keys.
Verify the replacement public key, ownership and file permissions.

Co-authored-by: Copilot <copilot@github.com>
2026-10-18 15:10:40 +02:00
volker.raschekandCopilot 7683306d5f fix(ssh): refresh public keys after key rotation
Recompute public keys from the configured private keys so existing public key files cannot become stale.
Use copy to preserve idempotence, ownership and permissions.

Co-authored-by: Copilot <copilot@github.com>
2026-10-18 15:10:40 +02:00
volker.raschekandCopilot 8efe3d6ee0 fix(ci): use renamed ansible-lint configuration
Ansible Linter / ansible-lint (push) Successful in 2m21s
Lint Markdown files / markdown-lint (push) Successful in 11s
Molecule / Molecule (push) Successful in 5m53s
Point the Gitea lint workflow at .ansible-lint.yml so it can load the renamed configuration.

Co-authored-by: Copilot <copilot@github.com>
2026-10-18 14:51:01 +02:00
volker.raschekandCopilot 5ff6db1c79 chore: rename ansible-lint configuration
Ansible Linter / ansible-lint (push) Failing after 1m40s
Lint Markdown files / markdown-lint (push) Successful in 11s
Molecule / Molecule (push) Successful in 5m20s
Use a YAML extension for explicit file type detection.

Co-authored-by: Copilot <copilot@github.com>
2026-10-18 14:44:27 +02:00
volker.raschek efce92c31e fix(defaults): disable alice
Lint Markdown files / markdown-lint (push) Successful in 8s
Ansible Linter / ansible-lint (push) Successful in 2m8s
Molecule / Molecule (push) Successful in 5m12s
Release Ansible Role / Release Ansible Role (push) Successful in 3m13s
2026-10-17 23:16:13 +02:00
volker.raschekandCopilot 89e36b89d6 fix(ssh): remove authorized_keys filename for 2.1.0
Molecule / Molecule (push) Successful in 5m8s
Lint Markdown files / markdown-lint (push) Successful in 8s
Ansible Linter / ansible-lint (push) Successful in 2m9s
Release Ansible Role / Release Ansible Role (push) Successful in 3m17s
Co-authored-by: Copilot <copilot@github.com>
2026-10-17 22:21:39 +02:00
volker.raschekandCopilot de280ef620 feat(ssh)!: configure keys with filename and content
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
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
CSRBot 6d42e7cc41 Merge pull request 'chore(deps): update ansible/ansible-lint action to v26.10.1' (#65) from renovate/ansible-ansible-lint-26.x into master
Ansible Linter / ansible-lint (push) Successful in 5m4s
Lint Markdown files / markdown-lint (push) Successful in 16s
Molecule / Molecule (push) Successful in 6m42s
2026-10-13 13:49:07 +00:00
CSRBot 0de969302c chore(deps): update ansible/ansible-lint action to v26.10.1
Ansible Linter / ansible-lint (push) Successful in 4m15s
Ansible Linter / ansible-lint (pull_request) Successful in 4m45s
Lint Markdown files / markdown-lint (pull_request) Successful in 16s
Molecule / Molecule (push) Successful in 9m10s
Molecule / Molecule (pull_request) Successful in 6m59s
Lint Markdown files / markdown-lint (push) Successful in 14s
2026-10-13 13:15:01 +00:00
CSRBot b6e5adfbbe Merge pull request 'chore(deps): update ansible/ansible-lint action to v26.10.0' (#64) from renovate/ansible-ansible-lint-26.x into master
Ansible Linter / ansible-lint (push) Successful in 7m5s
Lint Markdown files / markdown-lint (push) Successful in 14s
Molecule / Molecule (push) Successful in 11m32s
2026-10-12 10:42:28 +00:00
CSRBot 95192b7690 chore(deps): update ansible/ansible-lint action to v26.10.0
Ansible Linter / ansible-lint (pull_request) Successful in 4m51s
Ansible Linter / ansible-lint (push) Successful in 3m50s
Lint Markdown files / markdown-lint (pull_request) Successful in 14s
Lint Markdown files / markdown-lint (push) Successful in 13s
Molecule / Molecule (pull_request) Successful in 8m16s
Molecule / Molecule (push) Successful in 7m43s
2026-10-12 10:11:13 +00:00
10 changed files with 115 additions and 31 deletions

No files matched your search

File renamed without changes.
+2 -2
View File
@@ -14,9 +14,9 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Run ansible-lint
uses: ansible/ansible-lint@e7f397ad6dfa20d274afa17cd7bbedd84ed136f5 # v26.9.0
uses: ansible/ansible-lint@9bce2bfb8f4a984e6b13767bc0d6bf0a49492164 # v26.10.1
with:
args: "--config-file .ansible-lint"
args: "--config-file .ansible-lint.yml"
# The molecule scenario is linted as well, so its collections are required beside the ones of the role.
requirements_file: "molecule/default/collections.yml"
setup_python: "true"
+11 -6
View File
@@ -138,10 +138,11 @@ environment variables can be espcilitly defined, to consume it during execution
> [!IMPORTANT]
> To allow consuming environment variables must be set `PermitUserEnvironment yes` in `/etc/ssh/sshd_config`.
The private key `toor@toor-pc.ed25519.key` must be stored in `ssh/private_keys`. The public key will be automatically
extracted from the private key.
Private keys are configured using `filename` and `content`; the role writes them to `~/.ssh/<filename>` and extracts
their public keys to `~/.ssh/<filename>.pub`. Store private key content encrypted with Ansible Vault.
The public keys `claire@claire-pc.pub` as well as `bob@bob-pc.pub` must be stored in `ssh/authorized_keys`.
Entries in `authorized_keys` are also added to `~/.ssh/authorized_keys`, including their optional command and environment
settings.
```yaml
unix_users:
@@ -156,14 +157,18 @@ unix_users:
StrictHostKeyChecking: "no"
UserKnownHostFile: /dev/null
authorized_keys:
- filename: claire@claire-pc.pub
- content: ssh-ed25519 AAAAC3NzaC... claire
- command: /usr/local/bin/upload-file.sh
envs:
- key: SSH_KEY_NAME
value: bob@bob-pc
filename: bob@bob-pc.pub
content: ssh-ed25519 AAAAC3NzaC... bob
private_keys:
- toor@toor-pc.ed25519.key
- filename: toor@toor-pc.ed25519.key
content: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
shell: /bin/bash
password: toor
group: toor
+6 -2
View File
@@ -19,10 +19,14 @@ unix_users: {}
# StrictHostKeyChecking: "no"
# UserKnownHostFile: /dev/null
# authorized_keys:
# - filename: alice@alice-pc.pub
# - content: ssh-ed25519 AAAAC3NzaC... alice
# # command: "" # optional
# private_keys:
# - alice@alice-pc.ed25519.key
# - filename: alice@alice-pc.ed25519.key
# content: |
# -----BEGIN OPENSSH PRIVATE KEY-----
# ...
# -----END OPENSSH PRIVATE KEY-----
# home: /home/alice
# btrfs: false
# netrc:
+4 -2
View File
@@ -28,13 +28,15 @@
- Host: "*"
StrictHostKeyChecking: "no"
authorized_keys:
- filename: molecule.pub
- content: "{{ lookup('ansible.builtin.file', molecule_scenario_directory + '/files/ssh/authorized_keys/molecule.pub', rstrip=false) }}"
command: "/usr/bin/true"
envs:
- key: EDITOR
value: vi
- content: "{{ lookup('ansible.builtin.file', molecule_scenario_directory + '/files/ssh/authorized_keys/molecule.pub', rstrip=false) }}"
private_keys:
- molecule.ed25519.key
- filename: alice.ed25519.key
content: "{{ lookup('ansible.builtin.file', molecule_scenario_directory + '/files/ssh/private_keys/molecule.ed25519.key', rstrip=false) }}"
netrc:
- machine: hostname.local
login: alice
+15 -3
View File
@@ -37,10 +37,22 @@
- "{{ _authorized_key }}"
# The key pair is generated instead of committed, private key material does not belong into a repository.
- name: Generate the ssh key pair the role is fed with
- name: Generate the SSH key pairs for initial configuration and rotation
ansible.builtin.command:
cmd: "ssh-keygen -t ed25519 -N '' -C molecule -f {{ _private_key }}"
creates: "{{ _private_key }}"
argv:
- ssh-keygen
- -t
- ed25519
- -N
- ''
- -C
- molecule
- -f
- "{{ item }}"
creates: "{{ item }}"
loop:
- "{{ _private_key }}"
- "{{ _private_key }}.replacement"
- name: Offer the public key as authorized key fixture
ansible.builtin.copy:
+58 -3
View File
@@ -9,8 +9,8 @@
/home/molecule-alice/.ssh: "0700"
/home/molecule-alice/.ssh/authorized_keys: "0600"
/home/molecule-alice/.ssh/config: "0644"
/home/molecule-alice/.ssh/molecule.ed25519.key: "0600"
/home/molecule-alice/.ssh/molecule.ed25519.key.pub: "0644"
/home/molecule-alice/.ssh/alice.ed25519.key: "0600"
/home/molecule-alice/.ssh/alice.ed25519.key.pub: "0644"
/home/molecule-alice/.forward: "0644"
/home/molecule-alice/.netrc: "0600"
/home/molecule-alice/.bashrc.d: "0755"
@@ -72,7 +72,7 @@
register: _alice_keys
loop:
- "{{ _alice_home }}/.ssh/authorized_keys"
- "{{ _alice_home }}/.ssh/molecule.ed25519.key.pub"
- "{{ _alice_home }}/.ssh/alice.ed25519.key.pub"
- name: Assert that the authorized key carries its options and matches the extracted public key
vars:
@@ -83,6 +83,7 @@
- _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 user and group database
@@ -141,3 +142,57 @@
- _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"
- name: Verify SSH key rotation
hosts: all
vars:
_replacement_key: "{{ molecule_scenario_directory }}/files/ssh/private_keys/molecule.ed25519.key.replacement"
unix_groups: {}
unix_users:
molecule-alice:
group: molecule-alice
password: alice
ssh:
private_keys:
- filename: alice.ed25519.key
content: "{{ lookup('ansible.builtin.file', _replacement_key, rstrip=false) }}"
state: present
tasks:
- name: Replace the private key while keeping the existing public key file
ansible.builtin.include_role:
name: unix-users
- name: Read the public key after rotation
ansible.builtin.slurp:
src: /home/molecule-alice/.ssh/alice.ed25519.key.pub
register: _rotated_public_key
- name: Assert the existing public key matches the replacement key and differs from the original
vars:
_expected_public_key: "{{ lookup('ansible.builtin.file', _replacement_key + '.pub') }}"
ansible.builtin.assert:
that:
- (_rotated_public_key.content | b64decode).split()[1] == _expected_public_key.split()[1]
- _rotated_public_key.content != _alice_keys.results[1].content
- name: Stat the rotated private and public keys
ansible.builtin.stat:
path: "{{ item.path }}"
loop:
- path: /home/molecule-alice/.ssh/alice.ed25519.key
mode: '0600'
- path: /home/molecule-alice/.ssh/alice.ed25519.key.pub
mode: '0644'
loop_control:
label: "{{ item.path }}"
register: _rotated_key_files
- name: Assert rotated keys retain their owner and permissions
ansible.builtin.assert:
that:
- item.stat.pw_name == 'molecule-alice'
- item.stat.gr_name == 'molecule-alice'
- item.stat.mode == item.item.mode
loop: "{{ _rotated_key_files.results }}"
loop_control:
label: "{{ item.item.path }}"
+15 -11
View File
@@ -95,11 +95,10 @@
state: absent
when: unix_user.value.ssh.authorized_keys is not defined or unix_user.value.ssh.authorized_keys | length <= 0
# The relative source is resolved against the files directory of the playbook, like the lookup in authorized_keys.j2.
- name: "Create private SSH keys for unix user: {{ unix_user.key }}"
ansible.builtin.copy:
src: "ssh/private_keys/{{ item }}"
dest: "{{ _unix_users_home }}/.ssh/{{ item }}"
content: "{{ item.content }}"
dest: "{{ _unix_users_home }}/.ssh/{{ item.filename }}"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0600"
@@ -109,21 +108,26 @@
when: unix_user.value.ssh.private_keys is defined and unix_user.value.ssh.private_keys | length > 0
- name: "Extract public SSH keys from private keys for unix user: {{ unix_user.key }}"
ansible.builtin.shell:
cmd: "ssh-keygen -y -f {{ _unix_users_home }}/.ssh/{{ item }} > {{ _unix_users_home }}/.ssh/{{ item }}.pub"
creates: "{{ _unix_users_home }}/.ssh/{{ item }}.pub"
ansible.builtin.command:
argv: [ "ssh-keygen", "-y", "-f", "{{ _unix_users_home }}/.ssh/{{ item.filename }}" ]
changed_when: false
register: _unix_users_ssh_pub_key_extraction
loop_control:
label: "{{ item.filename }}"
with_items:
- "{{ unix_user.value.ssh.private_keys }}"
when: unix_user.value.ssh.private_keys is defined and unix_user.value.ssh.private_keys | length > 0
- name: "Correct permissions of public SSH keys for unix user: {{ unix_user.key }}"
ansible.builtin.file:
path: "{{ _unix_users_home }}/.ssh/{{ item }}.pub"
- name: "Write derived public SSH keys for unix user: {{ unix_user.key }}"
ansible.builtin.copy:
content: "{{ item.stdout }}\n"
dest: "{{ _unix_users_home }}/.ssh/{{ item.item.filename }}.pub"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0644"
with_items:
- "{{ unix_user.value.ssh.private_keys }}"
loop_control:
label: "{{ item.item.filename }}"
loop: "{{ _unix_users_ssh_pub_key_extraction.results }}"
when: unix_user.value.ssh.private_keys is defined and unix_user.value.ssh.private_keys | length > 0
- name: "Create custom SSH client config for unix user: {{ unix_user.key }}"
+2
View File
@@ -2,6 +2,7 @@
- name: Remove unix user
ansible.builtin.include_tasks: remove_unix_user.yml
no_log: true
with_dict: "{{ unix_users }}"
loop_control:
loop_var: unix_user
@@ -22,6 +23,7 @@
- name: Create unix groups
ansible.builtin.include_tasks: create_unix_group.yml
no_log: true
with_dict: "{{ unix_groups }}"
loop_control:
loop_var: unix_group
+2 -2
View File
@@ -21,8 +21,8 @@
{% endif %}
{% endif %}
{% if _args | length > 0 %}
{{ _args | join(',') }} {{ lookup('file', 'ssh/authorized_keys/' + authorized_key.filename ) }}
{{ _args | join(',') }} {{ authorized_key.content | trim }}
{% else %}
{{ lookup('file', 'ssh/authorized_keys/' + authorized_key.filename ) }}
{{ authorized_key.content | trim }}
{% endif %}
{% endfor %}