--- # ============================================================================= # 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