diff --git a/nebula_cert_renewal.yml b/nebula_cert_renewal.yml new file mode 100644 index 0000000..1b82d7f --- /dev/null +++ b/nebula_cert_renewal.yml @@ -0,0 +1,387 @@ +--- +# ============================================================================= +# Nebula Certificate Renewal Playbook +# ============================================================================= +# +# Dieses Playbook erneuert abgelaufene (oder bald ablaufende) Nebula-Zertifikate. +# Es funktioniert auch für Nodes, die NUR über die Nebula-IP erreichbar sind. +# +# STRATEGIE für Remote-Only-Nodes: +# Ansible erreicht remote-only Nodes über die Nebula-IP. Das bedeutet: Nebula +# muss während des Rollouts LAUFEN bleiben. Das neue Zertifikat wird auf dem +# Lighthouse signiert, dann auf den Node kopiert und erst dann per Hot-Reload +# aktiviert (SIGHUP statt Neustart). So bleibt die Verbindung während des +# gesamten Vorgangs stabil. +# +# ABLAUF: +# 1. Prüfen welche Certs ablaufen (innerhalb von nebula_cert_renew_threshold_days) +# 2. Alte Certs auf dem Lighthouse sichern +# 3. Neue Certs auf dem Lighthouse signieren +# 4. Neue Certs auf die Nodes kopieren +# 5. Nebula per SIGHUP neu laden (kein Verbindungsabbruch) +# +# VERWENDUNG: +# +# Alle Nodes prüfen und bei Bedarf erneuern: +# ansible-playbook -i inventory nebula_cert_renew.yml +# +# Nur bestimmte Nodes erneuern: +# ansible-playbook -i inventory nebula_cert_renew.yml --limit web01,db01 +# +# Alle Certs erzwungen erneuern (egal ob abgelaufen oder nicht): +# ansible-playbook -i inventory nebula_cert_renew.yml -e nebula_cert_force_renew=true +# +# Schwellwert anpassen (Standard: 30 Tage vor Ablauf): +# ansible-playbook -i inventory nebula_cert_renew.yml -e nebula_cert_renew_threshold_days=60 +# +# VARIABLEN (können in group_vars oder per -e übergeben werden): +# nebula_cert_renew_threshold_days: 30 # Erneuerung X Tage vor Ablauf +# nebula_cert_force_renew: false # true = immer erneuern +# nebula_client_cert_duration: "43800h0m0s" # Laufzeit der neuen Certs (5 Jahre) +# nebula_network_cidr: 24 +# +# INVENTORY-VORAUSSETZUNGEN: +# - Gruppe [nebula_lighthouse] muss existieren +# - groups['nebula_lighthouse'][0] ist der Primary Lighthouse (CA-Schlüssel) +# - Nodes haben nebula_internal_ip_addr gesetzt +# - Remote-only Nodes: ansible_host auf die Nebula-IP setzen +# +# BEISPIEL INVENTORY: +# [nebula_lighthouse] +# lighthouse01.example.com nebula_internal_ip_addr=10.43.0.1 +# +# [servers] +# web01.example.com nebula_internal_ip_addr=10.43.0.2 +# # Nur über Nebula erreichbar - ansible_host auf Nebula-IP: +# remote01 ansible_host=10.43.0.5 nebula_internal_ip_addr=10.43.0.5 +# +# ============================================================================= + +# ----------------------------------------------------------------------------- +# PHASE 1: Zertifikate prüfen und ggf. auf dem Lighthouse neu signieren +# Läuft auf dem Primary Lighthouse (er besitzt den CA-Key) +# ----------------------------------------------------------------------------- +- name: "Nebula Cert Renewal - Phase 1: Prüfen & Signieren auf Primary Lighthouse" + hosts: "{{ groups['nebula_lighthouse'][0] }}" + gather_facts: false + become: true + + 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 + # Alle Nodes aus dem Inventory ermitteln (Lighthouse selbst + alle anderen) + _all_nebula_nodes: >- + {{ + (groups['nebula_lighthouse'] + groups.get('servers', []) + groups.get('nebula_nodes', [])) + | unique + }} + + tasks: + + - name: Sicherstellen dass python3-dateutil installiert ist (für Datumsvergleich) + package: + name: python3-dateutil + state: present + ignore_errors: true + + - name: Zertifikats-Ablaufdaten für alle Nodes ermitteln + command: > + {{ nebula_cert_dir }}/nebula-cert print -json + -path {{ nebula_cert_dir }}/{{ item }}.crt + register: _cert_info_raw + loop: "{{ _all_nebula_nodes }}" + changed_when: false + ignore_errors: true + # Fehler ignorieren falls Cert noch nicht existiert + + - name: Ablaufstatus pro Node berechnen + set_fact: + _cert_status: >- + {{ + _cert_status | default({}) | combine({ + item.item: { + 'exists': item.rc == 0, + 'expired_or_missing': (item.rc != 0) or ( + (item.stdout | from_json).details.notAfter + | int < (ansible_date_time.epoch | int + nebula_cert_renew_threshold_days * 86400) + ), + 'not_after': (item.rc == 0) | ternary( + (item.stdout | from_json).details.notAfter | int | strftime('%Y-%m-%d'), + 'N/A' + ) + } + }) + }} + loop: "{{ _cert_info_raw.results }}" + loop_control: + label: "{{ item.item }}" + vars: + ansible_date_time: + epoch: "{{ lookup('pipe', 'date +%s') }}" + + - name: Status-Übersicht anzeigen + debug: + msg: >- + {{ item.key }}: + Ablauf={{ _cert_status[item.key].not_after }}, + Erneuerung nötig={{ _cert_status[item.key].expired_or_missing or nebula_cert_force_renew | bool }} + loop: "{{ _cert_status | dict2items }}" + loop_control: + label: "{{ item.key }}" + + - name: Backup-Verzeichnis anlegen + file: + path: "{{ nebula_cert_dir }}/cert_backup_{{ lookup('pipe', 'date +%Y%m%d_%H%M%S') }}" + state: directory + owner: root + group: root + mode: '0700' + register: _backup_dir + + - name: Backup der bestehenden Certs erstellen + copy: + src: "{{ nebula_cert_dir }}/{{ item }}.crt" + dest: "{{ _backup_dir.path }}/{{ item }}.crt" + remote_src: true + owner: root + group: root + mode: '0600' + loop: "{{ _all_nebula_nodes }}" + when: _cert_status[item].exists + ignore_errors: true + + - name: Ablaufende/fehlende Certs auf Lighthouse löschen (damit nebula-cert neu signiert) + file: + path: "{{ nebula_cert_dir }}/{{ item }}" + state: absent + loop: >- + {{ + _cert_status | dict2items + | selectattr('value.expired_or_missing', 'equalto', true) + | map(attribute='key') + | product(['.crt', '.key']) + | map('join') + | list + }} + when: nebula_cert_force_renew | bool or _cert_status[item.split('.crt')[0].split('.key')[0]].expired_or_missing + # Lighthouse-eigenes Cert separat behandelt (unten) + + - name: Neue Certs für Lighthouse selbst signieren (falls nötig) + command: > + {{ nebula_cert_dir }}/nebula-cert sign + -name "{{ item }}" + -ip "{{ hostvars[item].nebula_internal_ip_addr }}/{{ nebula_network_cidr }}" + -duration "{{ nebula_client_cert_duration }}" + args: + chdir: "{{ nebula_cert_dir }}" + creates: "{{ nebula_cert_dir }}/{{ item }}.crt" + loop: "{{ groups['nebula_lighthouse'] }}" + when: > + nebula_cert_force_renew | bool or + (_cert_status[item].expired_or_missing | default(true)) + + - name: Neue Certs für alle anderen Nodes signieren (falls nötig) + command: > + {{ nebula_cert_dir }}/nebula-cert sign + -name "{{ item }}" + -ip "{{ hostvars[item].nebula_internal_ip_addr }}/{{ nebula_network_cidr }}" + -duration "{{ nebula_client_cert_duration }}" + args: + chdir: "{{ nebula_cert_dir }}" + creates: "{{ nebula_cert_dir }}/{{ item }}.crt" + loop: >- + {{ + _all_nebula_nodes + | difference(groups['nebula_lighthouse']) + }} + when: > + nebula_cert_force_renew | bool or + (_cert_status[item].expired_or_missing | default(true)) + + - name: Neue Ablaufdaten zur Bestätigung anzeigen + command: > + {{ nebula_cert_dir }}/nebula-cert print -json + -path {{ nebula_cert_dir }}/{{ item }}.crt + register: _new_cert_info + loop: "{{ _all_nebula_nodes }}" + changed_when: false + ignore_errors: true + + - name: Neue Cert-Laufzeiten anzeigen + debug: + msg: >- + {{ item.item }}: + gültig bis {{ (item.stdout | from_json).details.notAfter | int | strftime('%Y-%m-%d %H:%M') }} + loop: "{{ _new_cert_info.results }}" + loop_control: + label: "{{ item.item }}" + when: item.rc == 0 + + +# ----------------------------------------------------------------------------- +# PHASE 2: Certs auf Lighthouse-Nodes selbst deployen +# Nebula auf dem Lighthouse neu laden (SIGHUP - kein Verbindungsabbruch!) +# ----------------------------------------------------------------------------- +- name: "Nebula Cert Renewal - Phase 2: Certs auf Lighthouse deployen" + hosts: nebula_lighthouse + gather_facts: false + become: true + serial: 1 # Lighthouses nacheinander - immer einer aktiv + + vars: + nebula_cert_dir: /opt/nebula + nebula_cert_renew_threshold_days: 30 + nebula_cert_force_renew: false + + tasks: + + - name: Cert-Dateien vom Primary Lighthouse einlesen + slurp: + src: "{{ nebula_cert_dir }}/{{ item }}" + register: _lh_cert_files + delegate_to: "{{ groups['nebula_lighthouse'][0] }}" + loop: + - "{{ inventory_hostname }}.crt" + - "{{ inventory_hostname }}.key" + - ca.crt + + - name: Neue Cert/Key/CA auf Lighthouse-Node schreiben + copy: + dest: "{{ nebula_cert_dir }}/{{ item.item }}" + content: "{{ item.content | b64decode }}" + owner: root + group: root + mode: '0600' + loop: "{{ _lh_cert_files.results }}" + loop_control: + label: "{{ item.item }}" + register: _lh_cert_written + notify: nebula lighthouse hot-reload + + # WICHTIG: SIGHUP statt Neustart - Nebula lädt Certs ohne Verbindungsabbruch + handlers: + - name: nebula lighthouse hot-reload + command: pkill -HUP -f "nebula.*config.yml" + # Alternativ falls systemd verwendet wird: + # systemd: name=lighthouse state=reloaded + # Nebula reagiert auf SIGHUP mit Reload der Certs seit v1.6+ + ignore_errors: true + + +# ----------------------------------------------------------------------------- +# PHASE 3: Certs auf alle regulären Nodes deployen +# Remote-only Nodes: Verbindung über Nebula-IP bleibt durch SIGHUP-Strategie stabil +# ----------------------------------------------------------------------------- +- name: "Nebula Cert Renewal - Phase 3: Certs auf Nodes deployen" + hosts: "{{ groups.get('servers', []) + groups.get('nebula_nodes', []) | unique }}" + gather_facts: false + become: true + serial: 5 # 5 Nodes gleichzeitig - anpassen nach Bedarf + + vars: + nebula_cert_dir: /opt/nebula + nebula_cert_renew_threshold_days: 30 + nebula_cert_force_renew: false + + tasks: + + - name: Prüfen ob dieser Node ein Cert-Renewal benötigt + command: > + {{ nebula_cert_dir }}/nebula-cert print -json + -path {{ nebula_cert_dir }}/{{ inventory_hostname }}.crt + register: _local_cert_check + changed_when: false + ignore_errors: true + delegate_to: "{{ groups['nebula_lighthouse'][0] }}" + # Wir prüfen auf dem Lighthouse, nicht lokal - der hat das neue Cert + + - name: Cert-Erneuerung überspringen (kein Renewal nötig) + meta: end_host + when: > + not (nebula_cert_force_renew | bool) and + _local_cert_check.rc == 0 and + ((_local_cert_check.stdout | from_json).details.notAfter | int) > + (lookup('pipe', 'date +%s') | int + nebula_cert_renew_threshold_days * 86400) + + # Cert-Dateien vom Primary Lighthouse per slurp holen (in-memory, kein temp-file) + - name: Cert-Dateien vom Primary Lighthouse einlesen + slurp: + src: "{{ nebula_cert_dir }}/{{ item }}" + register: _node_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: "{{ _node_cert_files.results }}" + loop_control: + label: "{{ item.item }}" + register: _node_cert_written + notify: nebula node hot-reload + + # SIGHUP: Nebula lädt das neue Cert ohne die bestehende Tunnel-Verbindung zu trennen. + # Das ist der entscheidende Trick für remote-only Nodes: + # Die SSH-Verbindung über Nebula bleibt aktiv während das Cert neu geladen wird. + handlers: + - name: nebula node hot-reload + command: pkill -HUP -f "nebula.*config.yml" + # Alternativ: + # systemd: name=nebula state=reloaded + ignore_errors: true + + +# ----------------------------------------------------------------------------- +# PHASE 4: Abschlussbericht +# ----------------------------------------------------------------------------- +- name: "Nebula Cert Renewal - Phase 4: Abschlussbericht" + hosts: "{{ groups['nebula_lighthouse'][0] }}" + gather_facts: false + become: true + + vars: + nebula_cert_dir: /opt/nebula + _all_nebula_nodes: >- + {{ + (groups['nebula_lighthouse'] + groups.get('servers', []) + groups.get('nebula_nodes', [])) + | unique + }} + + tasks: + + - name: Finale Cert-Ablaufdaten ermitteln + command: > + {{ nebula_cert_dir }}/nebula-cert print -json + -path {{ nebula_cert_dir }}/{{ item }}.crt + register: _final_cert_info + loop: "{{ _all_nebula_nodes }}" + changed_when: false + ignore_errors: true + + - name: Abschlussbericht + debug: + msg: >- + {{ item.item | ljust(40) }} + gültig bis: {{ (item.stdout | from_json).details.notAfter | int | strftime('%Y-%m-%d') }} + loop: "{{ _final_cert_info.results }}" + loop_control: + label: "{{ item.item }}" + when: item.rc == 0 + + - name: Nodes mit Fehler (Cert nicht lesbar) + debug: + msg: "WARNUNG: Cert für {{ item.item }} konnte nicht gelesen werden!" + loop: "{{ _final_cert_info.results }}" + loop_control: + label: "{{ item.item }}" + when: item.rc != 0 \ No newline at end of file