Compare commits

...
9 Commits
Author SHA1 Message Date
volker.raschek eb44f89b22 docs(readme): fix Ansible Galaxy badge
Ansible Linter / ansible-lint (push) Successful in 2m9s
Lint Markdown files / markdown-lint (push) Successful in 13s
Molecule / Molecule (push) Successful in 6m11s
2026-10-24 17:06:58 +02:00
volker.raschek f483c7066b fix(tasks): avoid logging of environment variables and functions
Lint Markdown files / markdown-lint (push) Successful in 7s
Ansible Linter / ansible-lint (push) Successful in 3m11s
Molecule / Molecule (push) Successful in 5m51s
Release Ansible Role / Release Ansible Role (push) Successful in 3m16s
2026-10-19 21:25:21 +02:00
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
9 changed files with 109 additions and 79 deletions

No files matched your search

File renamed without changes.
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Run ansible-lint
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"
+5 -7
View File
@@ -1,6 +1,6 @@
# volker-raschek.unix-users
![Ansible Role](https://img.shields.io/ansible/role/d/volker-raschek/unix-users)
![Ansible Role](https://img.shields.io/ansible/role/d/volker-raschek/unix_users)
The ansible role `volker-raschek.unix-users` create and manage users on Linux based distributions. For example for Arch
Linux, Fedora and Ubuntu. Furthermore, the role can also be used to create groups, `~/.forward`, `~/.netrc` and to
@@ -138,11 +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`.
Private and public keys are configured directly using `filename` and `content`. The role writes each key to
`~/.ssh/<filename>`. Store private key content encrypted with Ansible Vault.
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.
Entries in `authorized_keys` are also added to `~/.ssh/authorized_keys`, including their optional command and environment
settings. Public keys are still automatically extracted from private keys into `~/.ssh/<filename>.pub`.
settings.
```yaml
unix_users:
@@ -157,13 +157,11 @@ unix_users:
StrictHostKeyChecking: "no"
UserKnownHostFile: /dev/null
authorized_keys:
- filename: claire@claire-pc.pub
content: ssh-ed25519 AAAAC3NzaC... claire
- 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:
- filename: toor@toor-pc.ed25519.key
+19 -20
View File
@@ -8,26 +8,25 @@ unix_groups: {}
# gid: 1001
# state: absent
unix_users:
alice:
state: "present"
name: "Alice"
uid: "1000"
ssh:
config:
- Host: "*"
StrictHostKeyChecking: "no"
UserKnownHostFile: /dev/null
authorized_keys:
- filename: alice@alice-pc.pub
content: ssh-ed25519 AAAAC3NzaC... alice
# command: "" # optional
private_keys:
- filename: alice@alice-pc.ed25519.key
content: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
unix_users: {}
# alice:
# state: "present"
# name: "Alice"
# uid: "1000"
# ssh:
# config:
# - Host: "*"
# StrictHostKeyChecking: "no"
# UserKnownHostFile: /dev/null
# authorized_keys:
# - content: ssh-ed25519 AAAAC3NzaC... alice
# # command: "" # optional
# private_keys:
# - filename: alice@alice-pc.ed25519.key
# content: |
# -----BEGIN OPENSSH PRIVATE KEY-----
# ...
# -----END OPENSSH PRIVATE KEY-----
# home: /home/alice
# btrfs: false
# netrc:
+2 -4
View File
@@ -28,14 +28,12 @@
- Host: "*"
StrictHostKeyChecking: "no"
authorized_keys:
- filename: alice.pub
content: "{{ lookup('ansible.builtin.file', molecule_scenario_directory + '/files/ssh/authorized_keys/molecule.pub', rstrip=false) }}"
- 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
- filename: alice-login.pub
content: "{{ lookup('ansible.builtin.file', molecule_scenario_directory + '/files/ssh/authorized_keys/molecule.pub', rstrip=false) }}"
- content: "{{ lookup('ansible.builtin.file', molecule_scenario_directory + '/files/ssh/authorized_keys/molecule.pub', rstrip=false) }}"
private_keys:
- filename: alice.ed25519.key
content: "{{ lookup('ansible.builtin.file', molecule_scenario_directory + '/files/ssh/private_keys/molecule.ed25519.key', rstrip=false) }}"
+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:
+54 -24
View File
@@ -11,8 +11,6 @@
/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"
@@ -88,28 +86,6 @@
- (_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 }}"
@@ -166,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 }}"
+11 -20
View File
@@ -95,17 +95,6 @@
state: absent
when: unix_user.value.ssh.authorized_keys is not defined or unix_user.value.ssh.authorized_keys | length <= 0
- name: "Create public SSH keys for unix user: {{ unix_user.key }}"
ansible.builtin.copy:
content: "{{ item.content }}"
dest: "{{ _unix_users_home }}/.ssh/{{ item.filename }}"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0644"
loop: "{{ unix_user.value.ssh.authorized_keys | default([]) }}"
loop_control:
label: "{{ item.filename }}"
- name: "Create private SSH keys for unix user: {{ unix_user.key }}"
ansible.builtin.copy:
content: "{{ item.content }}"
@@ -119,25 +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.filename) | quote }} > {{ (_unix_users_home + '/.ssh/' + item.filename + '.pub') | quote }}"
creates: "{{ _unix_users_home }}/.ssh/{{ item.filename }}.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.filename }}.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"
loop_control:
label: "{{ item.filename }}"
with_items:
- "{{ unix_user.value.ssh.private_keys }}"
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 }}"
@@ -190,6 +180,7 @@
- "{{ unix_user.value.shell_rc_files }}"
loop_control:
loop_var: shell_rc_file
no_log: true
- name: "Create .netrc file"
when: unix_user.value.netrc is defined and unix_user.value.netrc | length > 0
+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