Files
Nebula-Ansible-Role/nebula_cert_renewal.yml
2026-04-12 22:22:15 +02:00

391 lines
14 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
# =============================================================================
# Nebula Certificate Renewal Playbook
# =============================================================================
#
# Erneuert Nebula-Zertifikate die ablaufen oder bereits abgelaufen sind.
# Funktioniert auch für Nodes die NUR über die Nebula-IP erreichbar sind
# (remote-only), da Nebula per SIGHUP neu geladen wird ohne den Tunnel zu
# trennen.
#
# VERWENDUNG in Semaphore:
# Playbook: nebula_cert_renewal.yml
# Inventory: dein bestehendes Nebula-Inventory
#
# EXTRA VARS (optional):
# nebula_cert_renew_threshold_days=30 Erneuerung wenn < X Tage bis Ablauf
# nebula_cert_force_renew=true Alle Certs erzwungen erneuern
# nebula_client_cert_duration=43800h0m0s Laufzeit neuer Certs (5 Jahre)
#
# REMOTE-ONLY NODES:
# Nodes die nur per Nebula erreichbar sind im Inventory so eintragen:
# remoteserver ansible_host=10.43.0.5 nebula_internal_ip_addr=10.43.0.5
# Nebula wird per SIGHUP neu geladen die SSH-Verbindung bleibt stabil.
#
# ABLAUF:
# Play 1 (Primary Lighthouse): Certs prüfen, Backup erstellen, neu signieren
# Play 2 (alle Nodes): Neue Certs verteilen, Nebula per SIGHUP neu laden
# Play 3 (Primary Lighthouse): Abschlussbericht
# =============================================================================
# =============================================================================
# PLAY 1 Auf dem Primary Lighthouse: prüfen, sichern, neu signieren
# =============================================================================
- name: "Nebula Cert Renewal - Schritt 1: Certs auf Primary Lighthouse erneuern"
hosts: nebula_lighthouse[0]
gather_facts: no
become: yes
vars:
nebula_cert_renew_threshold_days: 30
nebula_cert_force_renew: false
nebula_client_cert_duration: "43800h0m0s"
nebula_network_cidr: 24
nebula_cert_dir: /opt/nebula
tasks:
# -------------------------------------------------------------------------
# Ablaufdaten ermitteln
# -------------------------------------------------------------------------
- name: Aktuellen Unix-Timestamp ermitteln
command: date +%s
register: _now_ts
changed_when: false
- name: Ablaufdatum CA-Zertifikat prüfen
command: "{{ nebula_cert_dir }}/nebula-cert print -json -path {{ nebula_cert_dir }}/ca.crt"
register: _ca_cert_info
changed_when: false
ignore_errors: yes
- name: CA-Ablaufstatus auswerten
set_fact:
_ca_expires_soon: >-
{{
_ca_cert_info.rc != 0 or
((_ca_cert_info.stdout | from_json).details.notAfter | int) <
(_now_ts.stdout | int + nebula_cert_renew_threshold_days | int * 86400)
}}
- name: CA-Zertifikat Ablaufdatum anzeigen
debug:
msg: >-
CA-Zertifikat:
{% if _ca_cert_info.rc == 0 %}
gültig bis {{ (_ca_cert_info.stdout | from_json).details.notAfter | int | strftime('%Y-%m-%d') }},
Erneuerung nötig: {{ _ca_expires_soon }}
{% else %}
NICHT LESBAR / FEHLT
{% endif %}
- name: ABBRUCH wenn CA-Zertifikat abläuft
fail:
msg: >-
Das CA-Zertifikat läuft ab oder fehlt!
Führe zuerst einen vollständigen Re-Deploy durch:
1. rm /opt/nebula/ca.crt /opt/nebula/ca.key /opt/nebula/*.crt /opt/nebula/*.key
2. ansible-playbook -i inventory nebula.yml
Siehe cert_howto.md für Details.
when: _ca_expires_soon | bool
# -------------------------------------------------------------------------
# Alle Nodes prüfen (Lighthouse selbst + alle anderen Gruppen)
# -------------------------------------------------------------------------
- name: Cert-Ablaufdaten für alle Nodes ermitteln
command: >
{{ nebula_cert_dir }}/nebula-cert print -json
-path {{ nebula_cert_dir }}/{{ item }}.crt
register: _all_cert_info
loop: "{{ groups['all'] | select('ne', inventory_hostname) | list + [inventory_hostname] }}"
changed_when: false
ignore_errors: yes
- name: Ablaufstatus pro Node berechnen
set_fact:
_nodes_needing_renewal: >-
{{
_all_cert_info.results
| selectattr('rc', 'ne', 0)
| map(attribute='item')
| list
+
_all_cert_info.results
| selectattr('rc', 'equalto', 0)
| selectattr('stdout', 'ne', '')
| select('callback', lambda x:
(x.stdout | from_json).details.notAfter | int <
_now_ts.stdout | int + nebula_cert_renew_threshold_days | int * 86400
)
| map(attribute='item')
| list
}}
# Vereinfachte Variante ohne Lambda wir nutzen json_query:
- name: Nodes mit ablaufenden Certs bestimmen (vereinfacht)
set_fact:
_renewal_needed: >-
{{
nebula_cert_force_renew | bool
| ternary(
groups['all'],
_all_cert_info.results
| selectattr('rc', 'ne', 0)
| map(attribute='item') | list
| union(
_all_cert_info.results
| selectattr('rc', 'equalto', 0)
| selectattr('stdout', 'ne', '')
| rejectattr('stdout', 'equalto', '')
| map(attribute='item') | list
| select('search', '.')
| list
)
)
}}
# Hinweis: Die echte Datumsfilterung kommt im nächsten Schritt per Loop
# Einfacher und robuster: Cert-Status per Loop mit when-Bedingung
- name: Liste der zu erneuernden Nodes erstellen
set_fact:
_renew_list: "{{ _renew_list | default([]) + [item.item] }}"
loop: "{{ _all_cert_info.results }}"
loop_control:
label: "{{ item.item }}"
when: >
nebula_cert_force_renew | bool or
item.rc != 0 or
item.stdout == '' or
(item.stdout | from_json).details.notAfter | int <
(_now_ts.stdout | int + nebula_cert_renew_threshold_days | int * 86400)
- name: Übersicht anzeigen
debug:
msg: "Certs zur Erneuerung ({{ _renew_list | default([]) | length }}): {{ _renew_list | default([]) | join(', ') or 'keine' }}"
- name: Playbook beenden wenn keine Certs erneuert werden müssen
meta: end_play
when: (_renew_list | default([])) | length == 0
# -------------------------------------------------------------------------
# Backup der bestehenden Certs
# -------------------------------------------------------------------------
- name: Backup-Verzeichnis anlegen
file:
path: "{{ nebula_cert_dir }}/cert_backup_{{ _now_ts.stdout }}"
state: directory
owner: root
group: root
mode: '0700'
register: _backup_dir
- name: Certs sichern
copy:
src: "{{ nebula_cert_dir }}/{{ item }}.crt"
dest: "{{ _backup_dir.path }}/{{ item }}.crt"
remote_src: yes
owner: root
group: root
mode: '0600'
loop: "{{ _renew_list | default([]) }}"
ignore_errors: yes
- name: Backup-Pfad anzeigen
debug:
msg: "Backup erstellt in: {{ _backup_dir.path }}"
# -------------------------------------------------------------------------
# Alte Certs löschen damit nebula-cert neu signiert (creates: Guard)
# -------------------------------------------------------------------------
- name: Alte Cert-Dateien auf Lighthouse löschen
file:
path: "{{ nebula_cert_dir }}/{{ item[0] }}.{{ item[1] }}"
state: absent
loop: "{{ _renew_list | default([]) | product(['crt', 'key']) | list }}"
loop_control:
label: "{{ item[0] }}.{{ item[1] }}"
# -------------------------------------------------------------------------
# Neue Certs signieren
# -------------------------------------------------------------------------
- name: Neue Certs für alle betroffenen Nodes signieren
command: >
{{ nebula_cert_dir }}/nebula-cert sign
-name "{{ item }}"
-ip "{{ hostvars[item].nebula_internal_ip_addr }}/{{ hostvars[item].nebula_network_cidr | default(nebula_network_cidr) }}"
-duration "{{ nebula_client_cert_duration }}"
args:
chdir: "{{ nebula_cert_dir }}"
creates: "{{ nebula_cert_dir }}/{{ item }}.crt"
loop: "{{ _renew_list | default([]) }}"
register: _sign_results
- name: Signing-Ergebnis anzeigen
debug:
msg: "{{ item.item }}: {{ 'neu signiert' if item.changed else 'übersprungen (Cert existiert bereits)' }}"
loop: "{{ _sign_results.results }}"
loop_control:
label: "{{ item.item }}"
# -------------------------------------------------------------------------
# Neue Ablaufdaten zur Kontrolle anzeigen
# -------------------------------------------------------------------------
- name: Neue Ablaufdaten prüfen
command: >
{{ nebula_cert_dir }}/nebula-cert print -json
-path {{ nebula_cert_dir }}/{{ item }}.crt
register: _new_cert_check
loop: "{{ _renew_list | default([]) }}"
changed_when: false
ignore_errors: yes
- name: Neue Ablaufdaten anzeigen
debug:
msg: >-
{{ item.item }}:
neues Cert gültig bis
{{ (item.stdout | from_json).details.notAfter | int | strftime('%Y-%m-%d %H:%M UTC') }}
loop: "{{ _new_cert_check.results }}"
loop_control:
label: "{{ item.item }}"
when: item.rc == 0
# =============================================================================
# PLAY 2 Auf allen Nodes: neue Certs deployen, Nebula per SIGHUP neu laden
#
# Strategie für remote-only Nodes (nur per Nebula-IP erreichbar):
# - Certs werden per slurp vom Lighthouse geholt (kein SCP, kein temp-file)
# - Nebula wird per SIGHUP neu geladen (kein systemd stop/start)
# - SIGHUP lässt bestehende Tunnel aktiv → SSH-Verbindung bleibt stabil
# - serial: 1 stellt sicher dass Lighthouses nie gleichzeitig neu laden
# =============================================================================
- name: "Nebula Cert Renewal - Schritt 2: Neue Certs auf alle Nodes deployen"
hosts: all
gather_facts: no
become: yes
# Lighthouses zuerst und einzeln, dann alle anderen gleichzeitig
# Ansible sortiert: nebula_lighthouse kommt vor servers/anderen Gruppen
serial:
- 1 # Erste Runde: 1 Node (Primary Lighthouse)
- 1 # Zweite Runde: 1 Node (Secondary Lighthouse falls vorhanden)
- "100%" # Rest: alle gleichzeitig
vars:
nebula_cert_dir: /opt/nebula
nebula_cert_renew_threshold_days: 30
nebula_cert_force_renew: false
tasks:
# Prüfen ob dieser Node auf der Renewal-Liste des Lighthouse steht
# Wir lesen die Variable vom Lighthouse via hostvars
- name: Prüfen ob dieser Node ein Cert-Renewal benötigt
set_fact:
_this_node_needs_renewal: >-
{{
inventory_hostname in
(hostvars[groups['nebula_lighthouse'][0]]._renew_list | default([]))
}}
- name: Node überspringen wenn kein Renewal nötig
debug:
msg: "{{ inventory_hostname }}: kein Cert-Renewal nötig, überspringe."
when: not _this_node_needs_renewal | bool
- meta: end_host
when: not _this_node_needs_renewal | bool
# Cert-Dateien vom Primary Lighthouse per slurp holen (in-memory, sicher)
- name: Neue Cert-Dateien vom Primary Lighthouse einlesen
slurp:
src: "{{ nebula_cert_dir }}/{{ item }}"
register: _cert_files
delegate_to: "{{ groups['nebula_lighthouse'][0] }}"
loop:
- "{{ inventory_hostname }}.crt"
- "{{ inventory_hostname }}.key"
- ca.crt
- name: Neue Cert/Key/CA auf Node schreiben
copy:
dest: "{{ nebula_cert_dir }}/{{ item.item }}"
content: "{{ item.content | b64decode }}"
owner: root
group: root
mode: '0600'
loop: "{{ _cert_files.results }}"
loop_control:
label: "{{ item.item }}"
notify: nebula hot reload
- name: Bestätigung
debug:
msg: "{{ inventory_hostname }}: neue Certs geschrieben, SIGHUP wird gesendet."
handlers:
# SIGHUP statt Neustart: Nebula lädt das neue Cert ohne Tunnel zu trennen.
# Das ist der Schlüssel für remote-only Nodes: die SSH-Session über Nebula
# bleibt aktiv während das Cert gewechselt wird.
- name: nebula hot reload
shell: |
# Lighthouse oder normaler Node?
if systemctl is-active --quiet lighthouse 2>/dev/null; then
pkill -HUP -f "nebula.*config.yml" || true
echo "SIGHUP an lighthouse gesendet"
elif systemctl is-active --quiet nebula 2>/dev/null; then
pkill -HUP -f "nebula.*config.yml" || true
echo "SIGHUP an nebula gesendet"
else
echo "Kein aktiver Nebula-Prozess gefunden"
fi
register: _reload_result
changed_when: true
- name: SIGHUP-Ergebnis anzeigen
debug:
msg: "{{ _reload_result.stdout }}"
# =============================================================================
# PLAY 3 Abschlussbericht auf dem Primary Lighthouse
# =============================================================================
- name: "Nebula Cert Renewal - Schritt 3: Abschlussbericht"
hosts: nebula_lighthouse[0]
gather_facts: no
become: yes
vars:
nebula_cert_dir: /opt/nebula
tasks:
- name: Finale Cert-Ablaufdaten für alle Nodes prüfen
command: >
{{ nebula_cert_dir }}/nebula-cert print -json
-path {{ nebula_cert_dir }}/{{ item }}.crt
register: _final_certs
loop: "{{ groups['all'] }}"
changed_when: false
ignore_errors: yes
- name: Abschlussbericht
debug:
msg: >-
{{ '%-45s' | format(item.item) }}
{% if item.rc == 0 %}
gültig bis: {{ (item.stdout | from_json).details.notAfter | int | strftime('%Y-%m-%d') }}
{% else %}
*** CERT NICHT LESBAR ***
{% endif %}
loop: "{{ _final_certs.results }}"
loop_control:
label: "{{ item.item }}"
- name: Hinweis auf Backup
debug:
msg: >-
Alte Zertifikate gesichert in:
{{ nebula_cert_dir }}/cert_backup_{{ hostvars[inventory_hostname]._now_ts.stdout | default('siehe Lighthouse') }}