188 lines
6.2 KiB
YAML
188 lines
6.2 KiB
YAML
---
|
|
# SSH Mesh Key Distribution & Verification
|
|
#
|
|
# Distributes SSH public keys across all managed hosts so every host can SSH
|
|
# to every other host. Also verifies connectivity.
|
|
#
|
|
# Usage:
|
|
# ansible-playbook -i inventory.yml playbooks/ssh_mesh.yml
|
|
# ansible-playbook -i inventory.yml playbooks/ssh_mesh.yml --tags verify
|
|
# ansible-playbook -i inventory.yml playbooks/ssh_mesh.yml --tags distribute
|
|
# ansible-playbook -i inventory.yml playbooks/ssh_mesh.yml -e "generate_missing=true"
|
|
|
|
- name: SSH Mesh — Collect Keys
|
|
hosts: ssh_mesh
|
|
gather_facts: false
|
|
tags: [collect, distribute]
|
|
|
|
tasks:
|
|
- name: Check if ed25519 key exists
|
|
stat:
|
|
path: "~/.ssh/id_ed25519.pub"
|
|
register: ed25519_key
|
|
|
|
- name: Check if RSA key exists (fallback)
|
|
stat:
|
|
path: "~/.ssh/id_rsa.pub"
|
|
register: rsa_key
|
|
when: not ed25519_key.stat.exists
|
|
|
|
- name: Generate ed25519 key if missing
|
|
command: ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -C "{{ ansible_user }}@{{ inventory_hostname }}"
|
|
args:
|
|
creates: ~/.ssh/id_ed25519
|
|
when:
|
|
- not ed25519_key.stat.exists
|
|
- not (rsa_key.stat.exists | default(false))
|
|
- generate_missing | default(false) | bool
|
|
|
|
- name: Re-check for ed25519 key after generation
|
|
stat:
|
|
path: "~/.ssh/id_ed25519.pub"
|
|
register: ed25519_key_recheck
|
|
when:
|
|
- not ed25519_key.stat.exists
|
|
- generate_missing | default(false) | bool
|
|
|
|
- name: Read ed25519 public key
|
|
slurp:
|
|
src: "~/.ssh/id_ed25519.pub"
|
|
register: pubkey_ed25519
|
|
when: ed25519_key.stat.exists or (ed25519_key_recheck.stat.exists | default(false))
|
|
|
|
- name: Read RSA public key (fallback)
|
|
slurp:
|
|
src: "~/.ssh/id_rsa.pub"
|
|
register: pubkey_rsa
|
|
when:
|
|
- not ed25519_key.stat.exists
|
|
- not (ed25519_key_recheck.stat.exists | default(false))
|
|
- rsa_key.stat.exists | default(false)
|
|
|
|
- name: Set public key fact
|
|
set_fact:
|
|
ssh_pubkey: >-
|
|
{{ (pubkey_ed25519.content | default(pubkey_rsa.content) | b64decode | trim) }}
|
|
ssh_key_comment: "{{ inventory_hostname }}"
|
|
when: pubkey_ed25519 is not skipped or pubkey_rsa is not skipped
|
|
|
|
- name: Warn if no key found
|
|
debug:
|
|
msg: "WARNING: No SSH key on {{ inventory_hostname }}. Run with -e generate_missing=true to create one."
|
|
when: ssh_pubkey is not defined
|
|
|
|
- name: SSH Mesh — Distribute Keys
|
|
hosts: ssh_mesh
|
|
gather_facts: false
|
|
tags: [distribute]
|
|
|
|
tasks:
|
|
- name: Build list of all mesh public keys
|
|
set_fact:
|
|
all_mesh_keys: >-
|
|
{{ groups['ssh_mesh']
|
|
| map('extract', hostvars)
|
|
| selectattr('ssh_pubkey', 'defined')
|
|
| map(attribute='ssh_pubkey')
|
|
| list }}
|
|
|
|
- name: Include admin key
|
|
set_fact:
|
|
all_mesh_keys: >-
|
|
{{ all_mesh_keys + [admin_key] }}
|
|
when: admin_key is defined
|
|
|
|
- name: Ensure .ssh directory exists
|
|
file:
|
|
path: "~/.ssh"
|
|
state: directory
|
|
mode: "0700"
|
|
|
|
- name: Ensure authorized_keys exists
|
|
file:
|
|
path: "~/.ssh/authorized_keys"
|
|
state: touch
|
|
mode: "0600"
|
|
changed_when: false
|
|
|
|
- name: Add missing keys to authorized_keys
|
|
lineinfile:
|
|
path: "~/.ssh/authorized_keys"
|
|
line: "{{ item }}"
|
|
state: present
|
|
loop: "{{ all_mesh_keys }}"
|
|
loop_control:
|
|
label: "{{ item.split()[-1] | default('unknown') }}"
|
|
|
|
- name: SSH Mesh — Verify Connectivity
|
|
hosts: localhost
|
|
gather_facts: false
|
|
connection: local
|
|
tags: [verify]
|
|
|
|
tasks:
|
|
- name: Build mesh host list
|
|
set_fact:
|
|
mesh_hosts: >-
|
|
{{ groups['ssh_mesh']
|
|
| map('extract', hostvars)
|
|
| list }}
|
|
|
|
- name: Test SSH from localhost to each mesh host
|
|
shell: |
|
|
ssh -o BatchMode=yes \
|
|
-o ConnectTimeout=5 \
|
|
-o StrictHostKeyChecking=accept-new \
|
|
-i ~/.ssh/id_ed25519 \
|
|
-p {{ item.ansible_port | default(22) }} \
|
|
{{ item.ansible_user }}@{{ item.ansible_host }} \
|
|
"echo ok" 2>&1
|
|
register: ssh_tests
|
|
loop: "{{ mesh_hosts }}"
|
|
loop_control:
|
|
label: "localhost -> {{ item.inventory_hostname | default(item.ansible_host) }}"
|
|
failed_when: false
|
|
changed_when: false
|
|
|
|
- name: Display connectivity matrix
|
|
debug:
|
|
msg: |
|
|
SSH Mesh Verification (from localhost):
|
|
{% for result in ssh_tests.results %}
|
|
{{ '✓' if result.rc == 0 and 'ok' in (result.stdout | default('')) else '✗' }} -> {{ result.item.inventory_hostname | default(result.item.ansible_host) }}{% if result.rc != 0 or 'ok' not in (result.stdout | default('')) %} ({{ result.stdout_lines[-1] | default('unknown error') }}){% endif %}
|
|
|
|
{% endfor %}
|
|
{{ ssh_tests.results | selectattr('rc', 'equalto', 0) | list | length }}/{{ ssh_tests.results | length }} hosts reachable
|
|
|
|
- name: Test cross-host SSH (sample pairs)
|
|
shell: |
|
|
results=""
|
|
{% for pair in cross_test_pairs | default([]) %}
|
|
src_user="{{ pair.src_user }}"
|
|
src_host="{{ pair.src_host }}"
|
|
src_port="{{ pair.src_port | default(22) }}"
|
|
dst_user="{{ pair.dst_user }}"
|
|
dst_host="{{ pair.dst_host }}"
|
|
dst_port="{{ pair.dst_port | default(22) }}"
|
|
out=$(ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no \
|
|
-p ${src_port} ${src_user}@${src_host} \
|
|
"ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
|
|
-i ~/.ssh/id_ed25519 -p ${dst_port} ${dst_user}@${dst_host} 'echo ok'" 2>&1)
|
|
if echo "$out" | grep -q "ok"; then
|
|
results="${results}✓ {{ pair.label }}\n"
|
|
else
|
|
results="${results}✗ {{ pair.label }} ($(echo "$out" | tail -1))\n"
|
|
fi
|
|
{% endfor %}
|
|
echo -e "$results"
|
|
register: cross_tests
|
|
when: cross_test_pairs is defined
|
|
changed_when: false
|
|
|
|
- name: Display cross-host results
|
|
debug:
|
|
msg: |
|
|
Cross-Host SSH Tests:
|
|
{{ cross_tests.stdout }}
|
|
when: cross_tests is not skipped and cross_tests.stdout is defined
|