--- # ============================================================================= # 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') }}