Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6c6e65a34
|
||
|
|
c7be4a7999
|
||
|
|
7c1939b54a
|
||
|
|
d7ab7e34d1
|
||
|
|
c2f50c4423
|
||
|
|
44716aa722
|
||
|
|
7fa94793d9
|
No files matched your search
File renamed without changes.
@@ -16,5 +16,7 @@ jobs:
|
||||
- name: Run ansible-lint
|
||||
uses: ansible/ansible-lint@v25.2.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"
|
||||
File renamed without changes.
@@ -0,0 +1,29 @@
|
||||
name: Molecule
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ "opened", "reopened", "synchronize" ]
|
||||
push:
|
||||
branches: [ '**' ]
|
||||
tags-ignore: [ '**' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
molecule:
|
||||
name: Molecule
|
||||
runs-on: ubuntu-latest-amd64
|
||||
steps:
|
||||
# The scenario includes the role by its directory name, whose parent is used as roles path.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
path: root
|
||||
- name: Install molecule
|
||||
run: |
|
||||
apt update --yes
|
||||
apt install --yes python3-pip
|
||||
pip3 install --break-system-packages molecule docker
|
||||
- name: Run molecule
|
||||
run: molecule test
|
||||
working-directory: root
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,3 +1,5 @@
|
||||
---
|
||||
|
||||
#
|
||||
# Documentation:
|
||||
# https://yamllint.readthedocs.io/en/stable/
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,4 @@
|
||||
---
|
||||
|
||||
collections:
|
||||
- name: community.docker
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Converge
|
||||
hosts: all
|
||||
vars:
|
||||
root: "{{ _root_configuration }}"
|
||||
vars_files:
|
||||
- vars/main.yml
|
||||
tasks:
|
||||
- name: Include the role root
|
||||
ansible.builtin.include_role:
|
||||
name: root
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
- name: Create
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Start a container per platform
|
||||
community.docker.docker_container:
|
||||
name: "{{ item.name }}"
|
||||
image: "{{ item.image }}"
|
||||
command: sleep infinity
|
||||
state: started
|
||||
loop: "{{ molecule_yml.platforms }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Write the instance config
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
{% for platform in molecule_yml.platforms %}
|
||||
- instance: {{ platform.name }}
|
||||
connection: community.docker.docker
|
||||
{% endfor %}
|
||||
dest: "{{ molecule_instance_config }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: Generate temporary SSH key pairs
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ssh-keygen
|
||||
- -t
|
||||
- ed25519
|
||||
- -N
|
||||
- ""
|
||||
- -C
|
||||
- molecule-root
|
||||
- -f
|
||||
- "{{ molecule_ephemeral_directory }}/{{ item }}"
|
||||
creates: "{{ molecule_ephemeral_directory }}/{{ item }}"
|
||||
loop:
|
||||
- original.ed25519
|
||||
- replacement.ed25519
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
- name: Destroy
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Remove the container of every platform
|
||||
community.docker.docker_container:
|
||||
name: "{{ item.name }}"
|
||||
state: absent
|
||||
loop: "{{ molecule_yml.platforms }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Remove temporary SSH key pairs
|
||||
ansible.builtin.file:
|
||||
path: "{{ molecule_ephemeral_directory }}/{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- original.ed25519
|
||||
- original.ed25519.pub
|
||||
- replacement.ed25519
|
||||
- replacement.ed25519.pub
|
||||
|
||||
- name: Empty the instance config
|
||||
ansible.builtin.copy:
|
||||
content: "[]"
|
||||
dest: "{{ molecule_instance_config }}"
|
||||
mode: "0600"
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
|
||||
driver:
|
||||
name: default
|
||||
options:
|
||||
managed: true
|
||||
login_cmd_template: "docker exec --interactive --tty {instance} bash"
|
||||
|
||||
platforms:
|
||||
- name: root-archlinux
|
||||
image: docker.io/library/archlinux:base
|
||||
- name: root-debian
|
||||
image: docker.io/library/debian:13
|
||||
- name: root-fedora
|
||||
image: registry.fedoraproject.org/fedora:43
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
env:
|
||||
ANSIBLE_ROLES_PATH: "${MOLECULE_PROJECT_DIRECTORY}/.."
|
||||
config_options:
|
||||
defaults:
|
||||
interpreter_python: auto_silent
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
- name: Prepare
|
||||
hosts: all
|
||||
gather_facts: false
|
||||
vars:
|
||||
_bootstrap: |
|
||||
set -eu
|
||||
if command -v pacman > /dev/null; then
|
||||
pacman --sync --refresh --noconfirm openssh openssl python shadow
|
||||
elif command -v apt-get > /dev/null; then
|
||||
apt-get update
|
||||
apt-get install --yes openssh-client openssl passwd python3
|
||||
else
|
||||
dnf install --assumeyes openssh-clients openssl python3 shadow-utils
|
||||
fi
|
||||
tasks:
|
||||
- name: Bootstrap Python and the tools required by the role
|
||||
ansible.builtin.raw: "/bin/sh -c {{ _bootstrap | quote }}"
|
||||
changed_when: true
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
|
||||
roles: []
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
|
||||
_root_configuration:
|
||||
ssh:
|
||||
authorized_keys:
|
||||
- content: '{{ lookup("ansible.builtin.file", molecule_ephemeral_directory + "/original.ed25519.pub") }}'
|
||||
command: /usr/bin/true
|
||||
- content: '{{ lookup("ansible.builtin.file", molecule_ephemeral_directory + "/replacement.ed25519.pub") }}'
|
||||
config:
|
||||
- Host: "*"
|
||||
StrictHostKeyChecking: "no"
|
||||
private_keys:
|
||||
- content: '{{ lookup("ansible.builtin.file", molecule_ephemeral_directory + "/original.ed25519", rstrip=false) }}'
|
||||
filename: molecule.ed25519
|
||||
@@ -0,0 +1,188 @@
|
||||
---
|
||||
- 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 }}"
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
|
||||
collections: []
|
||||
@@ -52,27 +52,6 @@
|
||||
root.ssh.authorized_keys is not defined or
|
||||
root.ssh is not defined
|
||||
|
||||
- name: "Generate private and public SSH key"
|
||||
when: root.ssh is defined and
|
||||
(
|
||||
root.ssh.private_keys is not defined or
|
||||
root.ssh.private_keys | length <= 0
|
||||
)
|
||||
block:
|
||||
- name: "Check if private SSH key already exists"
|
||||
ansible.builtin.stat:
|
||||
path: "/root/.ssh/{{ inventory_hostname_short }}.key"
|
||||
register: stat_result
|
||||
- name: "Generate private SSH key"
|
||||
community.crypto.openssh_keypair:
|
||||
path: "/root/.ssh/{{ inventory_hostname_short }}.key"
|
||||
type: ed25519
|
||||
size: 512
|
||||
state: present
|
||||
comment: "root@{{ inventory_hostname_short }}"
|
||||
force: false
|
||||
when: not stat_result.stat.exists
|
||||
|
||||
- name: "Create private and public SSH key"
|
||||
when: root.ssh is defined and
|
||||
root.ssh.private_keys is defined and
|
||||
@@ -90,18 +69,25 @@
|
||||
label: "{{ item.filename }}"
|
||||
|
||||
- name: "Extract public SSH keys from private keys"
|
||||
ansible.builtin.shell:
|
||||
args:
|
||||
executable: /bin/bash
|
||||
cmd: "ssh-keygen -y -f {{ ('/root/.ssh/' + item.filename) | quote }} > {{ ('/root/.ssh/' + item.filename + '.pub') | quote }}"
|
||||
creates: "/root/.ssh/{{ item.filename }}.pub"
|
||||
ansible.builtin.command:
|
||||
argv: [ "ssh-keygen", "-y", "-f", "/root/.ssh/{{ item.filename }}" ]
|
||||
register: _root_ssh_pub_key_extraction
|
||||
changed_when: _root_ssh_pub_key_extraction.rc == 0
|
||||
failed_when: _root_ssh_pub_key_extraction.rc > 0
|
||||
changed_when: false
|
||||
loop: "{{ root.ssh.private_keys }}"
|
||||
loop_control:
|
||||
label: "{{ item.filename }}"
|
||||
|
||||
- name: "Write derived public SSH keys"
|
||||
ansible.builtin.copy:
|
||||
content: "{{ item.stdout }}\n"
|
||||
dest: "/root/.ssh/{{ item.item.filename }}.pub"
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0644"
|
||||
loop: "{{ _root_ssh_pub_key_extraction.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.filename }}"
|
||||
|
||||
- name: "Create custom SSH client config"
|
||||
ansible.builtin.template:
|
||||
src: root/.ssh/config.j2
|
||||
Reference in new issue
Block a user