Skip to content
🔵Entwurf (gut)75%
Vollständigkeit:
95%
Korrektheit:
93%
⏳ Noch nicht geprüft

Installationsphasen und Abnahme

Ziel

Dieses Dokument legt die Phasenstruktur des CIVITAS/CORE-Installationsskripts fest und definiert die Abnahmekriterien je Phase. Es dient als verbindliche Grundlage für den Skriptbau (skriptarchitektur.md) und als Checkliste für die manuelle oder automatisierte Verifikation nach einem Installationsdurchlauf.

Die beschriebene Lösung ist als funktionierender Prototyp für Entwicklung und Evaluation konzipiert. Abnahmekriterien sind bewusst auf Nachweisbarkeit ausgelegt, nicht auf Produktionsreife.


Rahmenbedingungen

  • Zielplattform: Dedizierte Proxmox-VM auf dem Knoten civitas (12 vCPU, 40 GiB RAM, 300 GiB Disk, Debian 13 (Trixie) als Gast-OS)
  • Kubernetes-Distribution: k3s (Single-Node, SQLite Data Store)
  • Deployment-Werkzeug: cc-cli (CIVITAS/CORE CLI)
  • Pflichtkomponenten: cert-manager, nginx-Ingress, RWO Storage Class (local-path-provisioner via k3s)
  • Kein öffentlicher DNS: Alle Endpunkte (idm.*, portal.*) sind intern im SOHO-VLAN erreichbar; DNS-Einträge werden manuell in der Hetzner-WebGUI gesetzt, bevor Phase 2 ausgeführt wird
  • Ausführungskontext: Das Skript kann auf dem Proxmox-Host oder in der Ziel-VM gestartet werden. Auf dem Proxmox-Host wird Phase -1 ausgeführt (VM-Provisionierung), danach wird die weitere Ausführung in der VM empfohlen. Innerhalb der VM beginnen die Phasen ab Phase 0.

Phasenübersicht

Das Installationsskript gliedert sich in drei Hauptphasen plus einer vorgelagerten Vorbedingungsprüfung:

PhaseNameInhalt
-1VM-ProvisionierungVM auf Proxmox-Host erstellen (Cloud-Image, Cloud-Init)
0VorbedingungenSystemprüfung, Konnektivität, Variablen
1Kubernetes-Clusterk3s installieren, Add-ons deployen
2.0Repository-KlonCIVITAS/CORE-Repository nach /opt/civitas-core-v1 klonen, Symlink setzen
2CIVITAS/CORE-Plattformcc-cli installieren, Plattform deployen
3VerifikationEnd-to-End-Abnahme, Fehlerreport

Phase -1 — VM-Provisionierung

Zweck

Die CIVITAS/CORE-VM auf dem Proxmox-Knoten "civitas" aus einem Debian-13-Cloud-Image erzeugen. Dieser Schritt läuft nur auf dem Proxmox-Host und wird übersprungen, wenn die VM bereits existiert (Idempotenz).

Voraussetzungen

  • Ausführung auf dem Proxmox-Host (qm, pvesh, pvesm verfügbar)
  • ROOT_PASSWORD als Umgebungsvariable gesetzt
  • Internetzugriff für Cloud-Image-Download

Schritte

SchrittAktionIdempotenz-Prüfung
-1.1Cloud-Image herunterladen (24h-Cache mit Altersprüfung)Datei im Cache (/var/lib/vz/template/qcow/) vorhanden und < 24h alt
-1.2VM mit qm create anlegen (VM_ID=2010, 12 vCPU, 40 GiB RAM, Bridge vmbr0, QEMU-GA, serielle Konsole)qm status ${VM_ID} — VM existiert
-1.3Disk aus Cloud-Image importieren (300 GiB, ZFS-thin)qm config ${VM_ID} — Disk zugewiesen
-1.4Cloud-Init konfigurieren (root, SSH-Key, statische IPv4/IPv6)qm config ${VM_ID} — ciuser, sshkeys, ipconfig0 gesetzt
-1.5VM startenqm status ${VM_ID} → running
-1.6Warten auf SSH-Erreichbarkeit unter der konfigurierten statischen VM-IPssh root@${VM_IP_STATIC} true erreichbar
-1.7Anleitung für nächste Schritte ausgeben
-1.8Skript-Dateien, Module und Templates per scp in die VM kopieren (nach ${VM_REMOTE_INSTALL_DIR})Dateien existieren in der VM
-1.9Nur .env.local (falls vorhanden) per scp in die VM kopierenDatei .env.local im Skript-Verzeichnis
-1.9ale-certs-backup.yaml (falls vorhanden) per scp in die VM kopierenDatei ${SCRIPT_DIR}/le-certs-backup.yaml vorhanden
-1.10SSH-Hop: Skript in der VM mit CIVITAS_CONTEXT=vm neu starten (Secrets aus .env.local werden gesourct)

Abnahmekriterien Phase -1

bash
# VM existiert und läuft
qm status VM_ID
# Erwartung: VM-ID im Status "running"

# VM-Konfiguration prüfen
qm config VM_ID | grep -E 'memory|cores|name'
# Erwartung: speicher 40960, cores 12, name civitas-core

# SSH-Zugang funktioniert
ssh -o StrictHostKeyChecking=no root@VM_IP 'hostnamectl'
# Erwartung: Debian GNU/Linux 13 (Trixie)

# Installationsskript in VM verfügbar
ssh root@VM_IP 'ls -la install_civitas_core.sh'

Abnahme Phase -1 bestanden, wenn die VM läuft, per SSH erreichbar ist und der SSH-Hop (CIVITAS_CONTEXT=vm) das Installationsskript erfolgreich in der VM gestartet hat.

Konfigurationsvariablen Phase -1

Die folgenden Variablen werden im Konfigurationsmodul des Skripts externalisiert:

VariableBeschreibungBeispielwert
VM_IDProxmox VM-ID2010
VM_NAMEAnzeigename in Proxmoxcivitas-core
VM_RAM_MBRAM in MiB40960
VM_CORESvCPUs12
VM_DISK_GBDisk-Größe in GiB300
VM_BRIDGEBridge-Netzwerkvmbr0
PROXMOX_STORAGEProxmox-Storage für VM-Disklocal-zfs-civitas
CLOUD_IMAGE_URLURL zum Debian-13-Cloud-Imagesiehe Quellcode
VM_IP_STATICStatische IPv4-Adresse der VM192.168.12.139
VM_IP_PREFIXIPv4-Präfixlänge24
VM_GWIPv4-Gateway192.168.12.1
VM_IP6_STATICStatische IPv6-Adresse der VMfd01:1:1:1::139
VM_IP6_PREFIXIPv6-Präfixlänge64
VM_GW6IPv6-Gatewayfd01:1:1:1:de39:6fff:febe:9962
SSH_PUBKEY_PATHPfad zum SSH-Public-Key für root-Zugang${HOME}/.ssh/authorized_keys
VM_REMOTE_INSTALL_DIRZielverzeichnis in der VM für scp/SSH/root/civitas-install

Hinweis: ROOT_PASSWORD wird ausschließlich als Umgebungsvariable übergeben und nie hartcodiert. Das Skript bricht ab, wenn die Variable nicht gesetzt ist.

Secrets aus .env.local: Liegt die Datei .env.local im Skript-Verzeichnis, wird sie beim SSH-Hop (Schritt -1.9) automatisch in die VM übertragen und dort vor dem Skriptstart gesourct. Ohne .env.local müssen alle Secrets separat als Umgebungsvariablen gesetzt werden.


Phase 0 — Vorbedingungen

Zweck

Sicherstellen, dass alle Voraussetzungen für eine erfolgreiche Installation erfüllt sind, bevor irreversible Aktionen ausgeführt werden.

Prüfungen

PrüfpunktErwarteter ZustandBefehl / MethodeFehlerverhalten
apt-Lock (cloud-init)Kein laufender apt-Prozess (lock frei)fuser /var/lib/apt/lists/lock /var/lib/dpkg/lock /var/lib/dpkg/lock-frontend >/dev/null 2>&1; Warte max. 120sAbbruch
BetriebssystemDebian 13 (Trixie), x86_64/etc/os-release (ID=debian, VERSION_ID=13)Abbruch
vCPU≥ 4 (empfohlen: 12)nprocAbbruch
RAM≥ 16384 MiB (empfohlen: 40 GiB)free -m (numerischer Vergleich)Abbruch
Disk (freier Platz k3s-Pfad)≥ 100 GiBdf -h /var/lib/rancher/k3s 2>/dev/null || df -h /Abbruch
SwapDeaktiviertswapon --show muss leer seinAbbruch
Netzwerk (intern)VM erreicht SOHO-Gatewayping -c2 <gateway>Abbruch
DNS idm.<domain> / portal.<domain>Auflösbardig +short idm.$DOMAINWarnung (kein Abbruch, DNS wird manuell vor Phase 2 gesetzt)
ZeitzoneEurope/Berlin gesetzttimedatectl show --property=Timezone --value – wird automatisch gesetzt falls abweichendWird korrigiert (kein Abbruch)
inotify-Limits (fsnotify)fs.inotify.max_user_watches ≥ 524288, fs.inotify.max_user_instances ≥ 1024/etc/sysctl.d/99-inotify.conf – Kernel-Limits werden automatisch gesetzt falls unterschrittenAbbruch wenn aktiver Wert nach Setzen nicht dem Zielwert entspricht
curl vorhandenBinary verfügbarcommand -v curlAbbruch
python3 / pip3 vorhandenBinaries verfügbarcommand -v python3 && command -v pip3Abbruch
wg (wireguard-tools)Binary verfügbarcommand -v wgAbbruch (wird automatisch installiert)
SMTP erreichbarTCP-Verbindung zu $SMTP_HOST:$SMTP_PORTnc -z -w5 $SMTP_HOST $SMTP_PORTAbbruch
k3s / kubectlNoch nicht installiert ODER bereits korrekte VersionVersionsvergleich gegen $K3S_VERSIONAbbruch bei falscher Version
Pflicht-Env-VarsAlle mandatory Secrets gesetztPrüfung in 01_config.sh via ${VAR:?}Abbruch
PBS-StoragePBS $PBS_STORAGE im Proxmox-Host konfiguriertpvesm status | grep $PBS_STORAGE (nur auf Proxmox-Host)Warnung (kein Abbruch – Backup auf Host-Ebene prüfen)

Hinweis DNS: Die DNS-Prüfung in Phase 0 gibt eine Warnung aus, bricht aber nicht ab. Hintergrund: Die DNS-Einträge für idm.<domain> und portal.<domain> können erst nach Vergabe der VM-IP in der Hetzner-WebGUI gesetzt werden. Phase 2 prüft DNS erneut — dort ist Auflösbarkeit eine harte Voraussetzung (Abbruch bei Fehler).

Abnahmekriterium Phase 0

Alle Pflichtprüfungen bestanden, oder bereits laufende Komponenten entsprechen der konfigurierten Zielversion (Idempotenz). DNS-Warnung ist zulässig. Das Skript bricht bei jeder anderen fehlgeschlagenen Pflichtprüfung mit klarer Fehlermeldung und Exit-Code ≠ 0 ab.


Phase 1 — Kubernetes-Cluster

Zweck

Einen lauffähigen, einsatzbereiten k3s-Single-Node-Cluster bereitstellen, inklusive aller für CIVITAS/CORE erforderlichen Add-ons.

Schritte

SchrittAktionIdempotenz-Prüfung
1.1k3s installieren — --disable traefik muss beim Start gesetzt werden, nicht nachträglich: curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="$K3S_VERSION" INSTALL_K3S_EXEC="--disable traefik" sh -systemctl is-active k3s + Versionsvergleich gegen $K3S_VERSION
1.1aWarten auf k3s-API: kubeconfig-Datei /etc/rancher/k3s/k3s.yaml vorhanden (max. 60s, 2s-Intervall)test -f /etc/rancher/k3s/k3s.yaml
1.1bWarten auf Node-Registrierung: kubectl get nodes liefert Eintrag (max. 60s, 3s-Intervall)kubectl get nodes --no-headers – mind. ein Node vorhanden
1.2kubeconfig nach ~/.kube/config kopierenDatei vorhanden und korrekt (kubectl cluster-info)
1.3helm-CLI installieren (separat — k3s bringt helm-controller, nicht die helm-CLI)command -v helm + Versionsvergleich gegen $HELM_VERSION
1.3aKubernetes Gateway API CRDs installieren (Voraussetzung für cert-manager Gateway-Support)kubectl get crd gateways.gateway.networking.k8s.io
1.4cert-manager via Helm deployen (Namespace cert-manager, mit --set config.enableGatewayAPI=true)kubectl get pods -n cert-manager → alle Running
1.5aBootstrap-ClusterIssuer anlegen (Namespace cert-manager)kubectl get clusterissuer civitas-bootstrap-selfsigned
1.5bRoot-CA-Certificate anlegen (Namespace cert-manager)kubectl get certificate civitas-core-ca -n cert-manager → READY=True
1.5cProduktiver ClusterIssuer selfsigned-issuer mit CA-Referenzkubectl get clusterissuer selfsigned-issuer → READY=True
1.5dCA-Trust: cert in System-Store + certifi im venvcurl -sf https://idm.${DOMAIN}/ ohne --insecure
1.6nginx-Ingress via Helm deployen (Namespace ingress-nginx)kubectl get pods -n ingress-nginx → controller Running
1.7Storage Class prüfen (local-path-provisioner durch k3s mitgeliefert, nicht deaktivieren)kubectl get storageclasslocal-path als Default
1.8Gateway-Ressource civitas-gateway im Namespace ingress-nginx anlegen (Listener HTTP Port 80, GatewayClass nginx)kubectl get gateway civitas-gateway -n ingress-nginx → READY=True

Wichtig (Reihenfolge): Die Gateway-API-CRDs (Schritt 1.3a) werden vor cert-manager (Schritt 1.4) installiert, da cert-manager die CRDs beim Start erwartet. cert-manager und ClusterIssuer (Schritte 1.4–1.5d) werden vor nginx-Ingress (Schritt 1.6) installiert, damit der Ingress-Controller bei Bedarf sofort TLS-fähig ist. Die Gateway-Ressource (Schritt 1.8) wird nach nginx-Ingress installiert, da der nginx-Ingress-Controller die GatewayClass nginx erst nach der Installation registriert.

Wichtig (local-path-provisioner): Der local-path-provisioner ist Bestandteil von k3s und stellt die Default-StorageClass bereit. Er darf nicht über --disable local-storage deaktiviert werden. Deaktiviert werden ausschließlich: traefik (durch nginx ersetzt), optional servicelb und metrics-server nach Absprache.

Konfigurationsvariablen Phase 1

Die folgenden Variablen werden im Konfigurationsmodul des Skripts externalisiert und gelten phasenübergreifend:

VariableBeschreibungBeispielwert
K3S_VERSIONk3s-Release-Version (Pinning)v1.32.3+k3s1
HELM_VERSIONHelm-CLI-Versionv3.17.0
GATEWAY_API_VERSIONKubernetes Gateway API CRDs (standard channel)v1.2.1

Die Versionen werden beim ersten Skriptbau aus der aktuellen Release-Dokumentation von k3s und Helm ermittelt und im Konfigurationsmodul fixiert.

Abnahmekriterien Phase 1

Nach Abschluss von Phase 1 müssen alle folgenden Prüfungen positiv sein:

bash
# Cluster-Status
kubectl get nodes
# Erwartung: 1 Node, Status "Ready"

# System-Pods
kubectl get pods -A
# Erwartung: Alle Pods "Running" oder "Completed",
#            kein "Error" / "CrashLoopBackOff"

# cert-manager
kubectl get pods -n cert-manager
# Erwartung: cert-manager, cert-manager-cainjector,
#            cert-manager-webhook jeweils "Running"

# Gateway API CRDs
kubectl get crd gateways.gateway.networking.k8s.io
# Erwartung: CRD vorhanden (kein "NotFound")

# ClusterIssuer
kubectl get clusterissuer
# Erwartung: Issuer vorhanden, READY = True

# nginx-Ingress
kubectl get pods -n ingress-nginx
# Erwartung: ingress-nginx-controller "Running"

# Storage Class
kubectl get storageclass
# Erwartung: "local-path" vorhanden, als Default markiert
#   (Annotation: storageclass.kubernetes.io/is-default-class=true)

# CA-Issuer-DN nicht leer
openssl x509 -in /usr/local/share/ca-certificates/civitas-core-ca.crt \
  -noout -issuer | grep "CN=civitas-core-ca"

# CA-ClusterIssuer READY
kubectl get clusterissuer selfsigned-issuer \
  -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' | grep True

Abnahme Phase 1 bestanden, wenn alle Prüfungen den beschriebenen Zustand aufweisen. Das Skript protokolliert die Ergebnisse und bricht bei Abweichungen ab. Die neu hinzugekommene Gateway-API-CRD-Prüfung (gateways.gateway.networking.k8s.io) ist für den Betrieb von cert-manager mit Gateway-API-Support und für die korrekte Verarbeitung von Gateway-Routen durch cc_cli erforderlich.


mc-Client für RustFS (Phase 1b)

Zweck

Den MinIO-Client (mc) für die S3-Kommunikation mit RustFS bereitstellen. mc wird für das Hochladen von Masterportal-Konfigurationsdateien (config.json, services.json, rest-services.json) in den Bucket portal-config benötigt. Die Installation erfolgt in Phase 1b (05_addons.sh), da es sich um eine reine Infrastruktur-Abhängigkeit handelt — das Tool muss vor Phase 2 (cc_cli) verfügbar sein.

Platzierung im Phasenmodell

AspektFestlegung
Modul05_addons.sh (Phase 1b), Funktion setup_mc_client()
AufrufInnerhalb von install_addons(), nach setup_ca_trust() und vor install_nginx_ingress()
Begründungmc ist ein allgemeines Infrastruktur-Tool (analog zu helm), nicht Teil der CIVITAS/CORE-Deployment-Logik. Die RustFS-Zugangsdaten liegen als Env-Vars vor.

Schritte

SchrittAktionIdempotenz-Prüfung
1.5e.1mc-Binary von https://dl.min.io/client/mc/release/linux-amd64/mc nach /usr/local/bin/mc herunterladen und ausführbar machen (chmod +x)command -v mc → Binary existiert und ist ausführbar; Version via mc --version loggen
1.5e.2RustFS-Endpoint als mc-Alias registrieren: mc alias set ${MC_ALIAS_NAME} ${RUSTFS_ENDPOINT} ${RUSTFS_ACCESS_KEY} ${RUSTFS_SECRET_KEY}mc alias list | grep -q "${MC_ALIAS_NAME}" → Alias bereits konfiguriert
1.5e.3Bucket portal-config anlegen: mc mb --ignore-existing ${MC_ALIAS_NAME}/${MC_BUCKET_NAME}mc mb --ignore-existing ist nativ idempotent (kein Fehler bei existierendem Bucket)

Konfigurationsvariablen

VariableBeschreibungDefault
MC_ALIAS_NAMEAlias-Name für den RustFS-Endpointcivitas-rustfs
MC_BUCKET_NAMES3-Bucket-Name für portal-backend-Konfigurationportal-config

Die RustFS-Zugangsdaten werden aus den bereits bestehenden Env-Vars RUSTFS_ENDPOINT, RUSTFS_ACCESS_KEY, RUSTFS_SECRET_KEY bezogen (siehe 01_config.sh).

Fehlerverhalten

SzenarioReaktion
mc-Download fehlschlägt (kein Internetzugang)Abbruch mit Exit 1 — ohne mc sind nachfolgende S3-Operationen nicht möglich
mc alias set fehlschlägt (falsche Zugangsdaten, Endpoint nicht erreichbar)log_warn, Funktion fährt fort — Alias kann bei erneutem Skriptlauf oder manuell nachgeholt werden
mc mb fehlschlägt (Bucket nicht erstellbar)log_warn, Funktion fährt fort — Bucket kann bei erneutem Skriptlauf oder manuell angelegt werden

Abnahmekriterien

bash
# mc ist installiert und ausführbar
command -v mc && mc --version
# Erwartung: Ausgabe der mc-Version, z. B. "mc version RELEASE.2024-..."

# Alias ist konfiguriert
mc alias list | grep -q "${MC_ALIAS_NAME}"
echo $?  # Erwartung: 0

# Bucket existiert
mc ls "${MC_ALIAS_NAME}/${MC_BUCKET_NAME}" >/dev/null 2>&1
echo $?  # Erwartung: 0

Abnahme bestanden, wenn alle drei Prüfungen den beschriebenen Zustand aufweisen.


Phase 2.0 — Repository-Klon

Zweck

Das CIVITAS/CORE-Repository mit den Ansible-Playbooks auf die Ziel-VM klonen. cc_cli exec sucht playbook.yml relativ zum CWD — das Repository muss daher vor dem Aufruf bereitstehen.

Schritte

SchrittAktionIdempotenz-Prüfung
2.0.1Repository klonen nach /opt/civitas-core-v1.git-Verzeichnis vorhanden → git pull statt clone
2.0.2Symlink /opt/civitas-core → /opt/civitas-core-v1 setzenreadlink /opt/civitas-core liefert /opt/civitas-core-v1

Konfigurationsvariablen

VariableBeschreibungWert
CC_V1_REPO_URLRepository-URLhttps://gitlab.com/civitas-connect/civitas-core/civitas-core-v1/civitas-core.git
CC_V1_REPO_PATHLokaler Pfad/opt/civitas-core-v1
CC_V1_REPO_BRANCHBranchmain

Abnahmekriterien Phase 2.0

bash
# Repository vorhanden
test -d /opt/civitas-core-v1/.git
# Playbook auffindbar
test -f /opt/civitas-core-v1/playbook.yml
# Symlink korrekt
test "$(readlink /opt/civitas-core)" = "/opt/civitas-core-v1"

Phase 2 — CIVITAS/CORE-Plattform

Zweck

Die CIVITAS/CORE-Plattform über cc-cli auf dem in Phase 1 bereitgestellten Cluster installieren. Der Aufruf von cc_cli validate und cc_cli exec erfolgt aus dem in Phase 2.0 geklonten Repository-Verzeichnis /opt/civitas-core-v1.

Voraussetzungen

  • Phase 1 vollständig abgenommen.
  • Phase 2.0 vollständig abgenommen (Repository vorhanden).
  • Gültiger kubeconfig unter ~/.kube/config.
  • DNS-Einträge für idm.$DOMAIN und portal.$DOMAIN auflösbar (hier harte Prüfung — Abbruch bei Fehler).
  • SMTP-Zugangsdaten als Umgebungsvariablen gesetzt.

Schritte

SchrittAktionIdempotenz-Prüfung
2.0DNS erneut prüfen (harter Abbruch wenn nicht auflösbar)dig +short idm.$DOMAIN und dig +short portal.$DOMAIN — beide müssen eine IP liefern
2.0bRepository klonen nach /opt/civitas-core-v1.git-Verzeichnis vorhanden, playbook.yml gefunden
2.1Overlay-Dateien einspielen: apply_overlay() kopiert Dateien aus overlay_V1/ in das geklonte Repo (z. B. Keycloak-Values mit servicePort: http). Originaldateien werden nach .overlay_backup/ gesichert.Log pro kopierter Datei; Abbruch wenn Zielverzeichnis fehlt
2.1bMasterportal-Release-Namen patchen: patch_masterportal_release_name() fügt `lowerzugd_instance.instance_name` im Helm-Release-Namen hinzu (RFC-1123: Großbuchstaben in Release-Namen ungültig)
2.1ccc-cli installieren (gepinnte Version 1.5.0) + ansible, kubernetes, openshift, jmespath im venv. Ansible-Logging wird aktiviert: ANSIBLE_LOG_PATH, ANSIBLE_VERBOSITY=3pip show cc-cli | grep Version vs. $CC_CLI_VERSION
2.1dCA-Trust im certifi-Bundle aktualisieren (venv existiert jetzt)grep "civitas-core-ca" "${certifi_bundle}"
2.1aPlaybook-URLs patchen: patch_playbook_urls() fügt follow_redirects: yes in betroffenen Playbook-Dateien ein (behebt 404-Fehler bei POST zur Keycloak-Admin-API)Keine (sed-Patch wird bei jedem Lauf neu angewandt; Duplikate werden von YAML ignoriert)
2.2Inventory cc_cli_inventory.yml aus Template erzeugen + http-Sicherheitscheck: Abbruch wenn hostname: "http:// im gerenderten InventoryDatei vorhanden, Platzhalter geprüft, kein http:// im hostname
2.3cc_cli validate ausführenExit-Code 0
2.4bWireGuard konfigurieren und Tunnel aktivieren (vor cc_cli exec)systemctl is-active wg-quick@wg0
2.4ccc_cli exec (single run, alle Komponenten). Ansible-Log unter logs/ansible_run_latest.logExit-Code 0 (404 wird toleriert)
2.4aGeoData-Ingress bereinigen: cleanup_geodata_ingress() entfernt doppelten Ingress-Eintrag (geostack vs. geostack-geostack)Überspringt die Bereinigung, wenn kein doppelter Ingress existiert
2.4dLogfile-Prüfung + Warten auf Podstest -f logs/ansible_run_latest.log; kubectl wait pods --all -n cc-prd-access-stack
2.4eStaging-Vorabprüfung für Produktionszertifikate: Vor dem ersten produktiven Let's-Encrypt-Zertifikat für einen Hostnamen MUSS zuerst ein Staging-Zertifikat per Annotation cert-manager.io/cluster-issuer=letsencrypt-staging auf der Ingress-Ressource ausgelöst werden. Die Staging-Pflicht gilt ausschließlich für NEU anzufordernde Zertifikate. Bereits vorhandene, gültige Zertifikate (aus laufendem Cluster oder Datei-Backup) sind von der Staging-Pflicht ausgenommen. Nach erfolgreicher Verifikation wird die Ingress mit civitas.io/staging-verified: "true" annotiert.kubectl get ingress <name> -n cc-prd-access-stack -o jsonpath='{.metadata.annotations.civitas\.io/staging-verified}' | grep -q "true" ODER Hostname ist bereits im Produktivbetrieb (gültiges Produktionszertifikat vorhanden) ODER ein gültiges Zertifikat für den Hostnamen liegt in einem Datei-Backup (le-certs-backup.yaml) vor UND wurde erfolgreich restauriert (notBefore-Zeitstempel vor/nach Restore identisch)
2.4fswitch_certificate_issuer(): Automatisierter Wechsel des Ausstellers für alle Ingress-Ressourcen. Setzt staging auf ALLE Ingresses, wartet auf READY, prüft Aussteller auf (STAGING), setzt dann production auf ALLE Ingresses, prüft erneut. Report mit Erfolg/Fehler pro Host, kein Abbruch bei Einzelfehlern.kubectl get ingress --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.annotations.cert-manager\.io/cluster-issuer}{"\n"}{end}' – alle Einträge müssen letsencrypt-prod lauten
2.5Admin-User im Realm erzwingen: ensure_keycloak_admin_user() legt ADMIN_EMAIL im Keycloak-Realm ${CC_ENVIRONMENT} nach, falls das Playbook die Erstellung übersprungen hatNur ausführen, wenn User im Ziel-Realm fehlt
2.5aLE-Zertifikate aus Backup wiederherstellen (falls vorhanden)kubectl apply -f le-certs-backup.yaml erfolgreich ODER Datei nicht vorhanden

Schritt 2.1a — Playbook-URLs patchen (patch_playbook_urls())

Die Ansible-Playbooks von CIVITAS/CORE verwenden für die Keycloak-Admin-API URLs ohne /auth-Prefix. Ansible's uri-Modul folgt Redirects bei POST standardmäßig nicht (follow_redirects=safe), sodass POST-Aufrufe zur Rollenzuweisung mit HTTP 404 scheitern.

Die Funktion fügt in den betroffenen Playbook-Dateien den Parameter follow_redirects: yes ein.

Betroffene Dateien:

  • tasks/geodata/configure/integrated_keycloak.yml
  • tasks/geodata/install/geoserver_setup_role_service.yml
  • tasks/access/keycloak/idm-config/keycloak_8_users.yml
  • tasks/access/keycloak/idm-config/keycloak_5_clients.yml
  • tasks/dashboard/superset.yml
  • tasks/datacatalog/piveau.yml

Idempotenz: Der sed-Patch wird bei jedem Skriptdurchlauf neu angewandt. Doppelte Zeilen verhindern keine Funktion (YAML ignoriert Duplikate).

Schritt 2.4a — GeoData-Ingress bereinigen (cleanup_geodata_ingress())

Das cc_cli-Playbook erzeugt manchmal einen doppelten GeoData-Ingress (geostack vs. geostack-geostack). Die Funktion entfernt den doppelten Eintrag.

Idempotenz: Überspringt die Bereinigung, wenn kein doppelter Ingress existiert.

Schritt 2.5 — Admin-User im Realm erzwingen (ensure_keycloak_admin_user())

Stellt sicher, dass der Admin-User (ADMIN_EMAIL) im Realm ${CC_ENVIRONMENT} existiert. Das cc_cli-Playbook legt den User nur beim ersten Durchlauf an. Bei Wiederholung überspringt das Playbook die User-Erstellung – die Funktion prift nach und legt den User bei Bedarf über die Keycloak-Admin-REST-API nach.

Idempotenz: Nur ausführen, wenn der User im Ziel-Realm fehlt.

Schritt 2.5a — LE-Zertifikate aus Backup wiederherstellen

Prüft, ob die Datei le-certs-backup.yaml im Installationsverzeichnis vorhanden ist. Wenn ja, werden die darin enthaltenen TLS-Secrets und LE-Account-Keys via kubectl apply -f in den Cluster eingespielt.

Zweck: Die Secrets existieren bereits, bevor switch_certificate_issuer() die Ingress-Annotationen setzt. cert-manager erkennt die vorhandenen Secrets und stellt keine neuen Zertifikate bei Let's Encrypt aus. Dadurch wird die Production-Phase von ~5 Minuten auf Sekunden verkürzt und das Rate-Limit geschont.

Idempotenz: Überspringt, wenn die Datei nicht vorhanden ist. Mehrmaliges Anwenden ist unschädlich (kubectl apply ist idempotent).


Hinweis Arbeitsverzeichnis: cc_cli exec wird aus /opt/civitas-core-v1 heraus aufgerufen (cd /opt/civitas-core-v1 && cc_cli exec ...). cc_cli sucht playbook.yml relativ zum CWD. Ein Aufruf aus einem anderen Verzeichnis führt zu Could not find any playbook to execute.

Hinweis cc_cli-exec (single run): cc_cli exec wird in einem einzigen Durchlauf ohne Tag-Filterung ausgeführt. Der Ansible-Playbook-Lauf installiert alle Komponenten, die im Inventory auf enable: true gesetzt sind. Ein Fehler in einer Komponente führt standardmäßig zum Abbruch des gesamten Durchlaufs. Daher werden bekannte Idempotenz-Fehler (z. B. HTTP 404 beim Löschen einer bereits entfernten Keycloak-Ressource) im Log toleriert, sodass nachfolgende Komponenten nicht blockiert werden.

Ansible-Logging: Vor dem Start von cc_cli exec werden folgende Umgebungsvariablen gesetzt:

  • ANSIBLE_LOG_PATH – Pfad zum Logfile (logs/ansible_run_latest.log)
  • ANSIBLE_VERBOSITY=3 – Detailgrad der Ansible-Ausgabe (Modul-Argumente, Return-Werte, SSH-Verbindungsdetails)

Das Logfile wird bei jedem Lauf überschrieben und nach dem Skriptdurchlauf auf Existenz geprüft. Bei Fehlern bleibt das Log erhalten (kein Cleanup durch Traps, da alle Traps deaktiviert sind).

Bekanntes Problem: Die Keycloak-Tenant-Konfiguration enthält einen Task "Delete piveau-hub-repo default resource", der bei Erstinstallation erwartungsgemäß HTTP 204 zurückgibt, bei Wiederholung jedoch HTTP 404 (Ressource bereits gelöscht). Dieser 404-Fehler wird vom Skript toleriert (kein Abbruch), sodass nachfolgende Komponenten nicht betroffen sind.

Hinweis cc_cli-Installation: Die Installation erfolgt in einem isolierten Python-venv unter ${CC_CLI_VENV_PATH}. Zusätzlich zu cc-cli werden die Pakete ansible, kubernetes, openshift (für k8s-Ansible-Module) und jmespath (für json_query-Filter in Playbooks) installiert. Nach der pip-Installation werden die benötigten Ansible-Collections über ansible-galaxy collection install bezogen. Ohne diese Collections schlagen Playbooks, die kubernetes.core, community.grafana oder community.mongodb verwenden, fehl.

Hinweis: certifi-CA-Bundle Das Python-venv unter ${CC_CLI_VENV_PATH} enthält certifi mit einem eigenen CA-Bundle. Dieser enthält per Default keine selbst-signierten CAs. Das Root-CA-Cert muss vor cc_cli validate / cc_cli exec in das certifi-Bundle eingetragen sein. Schritt 1.5d (Phase 1b) kann dies noch nicht leisten, da das venv zu dem Zeitpunkt noch nicht existiert. Daher wiederholt Schritt 2.1c (update_ca_trust_certifi()) den certifi-Eintrag unmittelbar nach der venv-Erstellung. Das Skript ruft ${CC_CLI_VENV_PATH}/bin/cc_cli direkt auf — das venv muss nicht per source activate aktiviert werden. Das certifi-Bundle jedoch muss das CA-Cert enthalten.

Ausführungsreihenfolge in Modul 05/06: configure_ca_trust() (Phase 1b, Modul 05) muss stets vor run_cc_cli_exec() (Phase 2, Modul 06) abgeschlossen sein. Die erweiterte Funktion trägt nicht nur die interne civitas-core-ca, sondern auch die öffentliche ISRG Root X1 in den System-Trust-Store ein (siehe §CA-Trust-Integration). update_ca_trust_certifi() in Schritt 2.1c übernimmt beide Trust-Anker aus dem System-Store in das certifi-Bundle des venv. Ohne diesen Ablauf scheitert die TLS-Verifikation bei per Backup-Restore wiederhergestellten LE-Produktionszertifikaten.

Hinweis TLS (HAProxy-Architektur): Der HAProxy auf OPNsense leitet TLS-Verbindungen für *.udp.data-dna.eu per TCP-Passthrough (Layer 4) direkt an 10.10.10.5:443 weiter. nginx in der VM terminiert TLS selbstständig mit Zertifikaten von cert-manager. Der globale ssl-redirect=true (Helm-Default) ist korrekt und erwünscht. Anders als in der früheren Caddy-Architektur wird kein Ingress-Patch mehr benötigt – die tls-Sektion in Ingress-Ressourcen bleibt erhalten und wird von nginx zur TLS-Terminierung verwendet.

Ansible-Health-Checks: Die integrierten Health-Checks von cc_cli (inv_checks) sind im Inventory-Template auf enable: true gesetzt. Die Checks rufen die externen URLs (https://idm.udp.data-dna.eu/) auf. Der Pfad führt vom venv in der VM über WireGuard → OPNsense → HAProxy → TCP-Passthrough zurück zur VM:443 → nginx (TLS-Ende) → Service. nginx antwortet mit HTTP 200, da TLS korrekt terminiert wird.

Hinweis: Namespace-Konvention: cc_cli exec legt keine Namespaces mit festen Namen an, sondern leitet diese aus dem Inventory-Feld all.vars.ENVIRONMENT (z. B. cc-prd) ab. Pro Stack-Komponente entsteht ein Namespace nach dem Muster {ENVIRONMENT}-{stack}:

NamespaceStackEnthält typischerweise
cc-prd-access-stackAccessKeycloak, APISIX, Service Portal
cc-prd-context-stackContextFrost-Server (SensorThings), QuantumLeap, Stellio
cc-prd-dashboard-stackDashboardApache Superset, Grafana
cc-prd-database-stackDatabasePostgreSQL, ggf. weitere Datenbanken
cc-prd-operation-stackOperationPrometheus, Grafana, Loki, PGAdmin, Velero

Das Installationsskript definiert in 01_config.sh das Array K8S_NAMESPACES, das aus CC_ENVIRONMENT abgeleitet wird:

bash
CC_ENVIRONMENT="${CC_ENVIRONMENT:-cc-prd}"
K8S_NAMESPACES=(
  "${CC_ENVIRONMENT}-access-stack"
  "${CC_ENVIRONMENT}-context-stack"
  "${CC_ENVIRONMENT}-dashboard-stack"
  "${CC_ENVIRONMENT}-database-stack"
  "${CC_ENVIRONMENT}-operation-stack"
)

Der frühere Einzel-Namespace K8S_NAMESPACE (z. B. civitas-core) existiert nicht mehr – alle Prüfungen in Phase 2 und Phase 3 iterieren über das K8S_NAMESPACES-Array.

Hinweis Overlay-Mechanismus (overlay_V1/): Statt per sed auf Textmuster im geklonten Upstream-Repo zu patchen (fragil bei Repo-Änderungen), werden abweichende Dateien als vollständige Kopien in civitas_einrichtung/overlay_V1/ vorgehalten. Die Funktion apply_overlay() in 06_civitas.sh kopiert nach clone_civitas_repo() alle Dateien aus overlay_V1/ in die entsprechende Zielstruktur unterhalb von CC_CLI_PLAYBOOK_DIR. Vor dem Überschreiben wird ein Backup der Originaldatei nach .overlay_backup/<relpath>.orig angelegt (nur einmalig, bei erstem Lauf). Fehlt ein Zielverzeichnis, bricht das Skript mit einem Hinweis auf eine mögliche Strukturänderung im Upstream-Repo ab.

Derzeit enthaltene Overlays:

  • overlay_V1/templates/access/keycloak/keycloak-values.yaml – setzt ingress.servicePort: http und backend-protocol: HTTP für alle Ingress- und adminIngress-Blöcke (behebt 502 Bad Gateway bei TLS-Upstream).

Hinweis Masterportal-Release-Name (RFC-1123): Der Task masterportal.yml im Upstream-Repo setzt den Helm-Release-Namen aus gd_instance.instance_name (z. B. "Standard") zusammen. Helm erlaubt aber nur RFC-1123-konforme Namen (Kleinbuchstaben). Die Funktion patch_masterportal_release_name() in 06_civitas.sh fügt nach dem Overlay den | lower-Filter ein, sodass gd_instance.instance_name | lower verwendet wird.

Hinweis http-Sicherheitscheck in render_inventory(): Nach dem Erzeugen des cc_cli_inventory.yml prüft das Skript, ob der Wert von inv_access.platform.hostname fälschlich http:// enthält. Ist dies der Fall (z. B. durch falsche Konfiguration von inv_k8s.ingress.http: false), bricht das Skript sofort ab. Der Check verhindert den 308-Permanent-Redirect- Fehler, bei dem nginx HTTP-Anfragen an die Keycloak-Admin-API per 308 auf HTTPS umleitet und Ansible's uri-Modul den Redirect als Fehler wertet.

Hinweis Ansible-Logging: Vor dem Start von run_cc_cli_exec() werden die Umgebungsvariablen ANSIBLE_LOG_PATH (gesetzt auf logs/ansible_run_latest.log), ANSIBLE_VERBOSITY=3 und ANSIBLE_DEBUG (entfernt, da es 'debug' is not a valid AnsibleEventType' verursacht) gesetzt. Alle Traps (ERR, EXIT, INT, TERM) sind deaktiviert, sodass Logfiles und temporäre Dateien bei Fehlern erhalten bleiben.

Restore aus Datei-Backup (le-certs-backup.yaml)

Zusätzlich zu den beiden bestehenden Fällen (Staging-Annotation, laufender Produktivbetrieb) existiert ein dritter Fall: Ein gültiges Let's-Encrypt-Produktionszertifikat liegt in einer Datei (le-certs-backup.yaml) vor und wird in den Cluster restauriert. In diesem Moment existiert im Cluster kein Nachweis mehr, dass das Zertifikat bereits im Produktivbetrieb war — der Nachweis liegt nur noch in der Backup-Datei selbst vor.

Vorbedingung

  • Die Datei le-certs-backup.yaml muss im Verzeichnis ${VM_REMOTE_INSTALL_DIR} existieren.
  • Die Datei enthält Kubernetes-Secret-Definitionen mit gültigen Let's-Encrypt-Produktionszertifikaten (felder: tls.crt, tls.key, ca.crt).

Backup-Erstellung

Die Backup-Datei wird durch Export aller TLS-Secrets (außer der internen CA civitas-core-ca) erzeugt. Das Skript install_civitas_core_V1.sh führt diesen Schritt automatisch vor der ersten switch_certificate_issuer()-Ausführung durch. Manuelle Erzeugung:

bash
for ns_name in $(kubectl get certificate --all-namespaces -o json \
  | jq -r '.items[] | select(.metadata.name != "civitas-core-ca") | "\(.metadata.namespace)/\(.spec.secretName)"'); do
  ns="${ns_name%%/*}"; name="${ns_name##*/}"
  kubectl get secret "$name" -n "$ns" -o yaml \
    | yq eval 'del(.metadata.resourceVersion, .metadata.uid, .metadata.creationTimestamp, .metadata.annotations["kubectl.kubernetes.io/last-applied-configuration"])' -
  echo "---"
done > "${VM_REMOTE_INSTALL_DIR}/le-certs-backup.yaml"

Die Felder resourceVersion, uid und creationTimestamp werden bereits beim Export entfernt (yq eval 'del(...)'), sodass beim späteren Restore (kubectl apply -f) kein Conflict-Fehler auftritt.

Verbindliche Schritt-Reihenfolge

  1. Backup-Secrets einspielen (kubectl apply -f le-certs-backup.yaml). Wichtig: Die Felder resourceVersion, uid und creationTimestamp müssen aus dem Backup entfernt worden sein, da kubectl apply sonst mit einem Conflict-Fehler abbricht. Dies ist bei einem YAML-Export über kubectl get secret ... -o yaml standardmäßig der Fall und muss vor dem Backup bereinigt werden.

  2. Ingress-Annotation auf den Ziel-Issuer setzen. Erst nach erfolgreichem Einspielen der Secrets wird die Annotation cert-manager.io/cluster-issuer auf den Wert letsencrypt-prod gesetzt (bzw. auf den Wert, der dem wiederhergestellten Zertifikat entspricht).

  3. Certificate-Objekte löschen (Delete+Recreate). Nach dem Setzen der Annotation werden die vorhandenen Certificate-Ressourcen gelöscht. ingress-shim erstellt sie sofort neu — jetzt mit dem korrekten issuerRef aus der Annotation. cert-manager reconcilingt, findet das bereits vorhandene, gültige Secret und markiert das Certificate als Ready — ohne neue ACME-Anfrage (CertificateRequest/Order/Challenge). (Bei NO_NEW_LE_CERT=true wird die Löschung übersprungen; in diesem Fall reicht das Annotation-Update durch ingress-shim aus, da der issuerRef im Certificate-Objekt bereits zum Secret passt.)

  4. Verifikation des notBefore-Zeitstempels. Der notBefore-Zeitstempel des wiederhergestellten Zertifikats muss mit dem Zeitstempel aus der Backup-Datei übereinstimmen. Dies beweist, dass keine Neuausstellung stattgefunden hat.

    bash
    NOT_BEFORE_BACKUP=$(yq eval 'select(.metadata.name == "idm.'"${DOMAIN}"'-tls") | .data["tls.crt"]' le-certs-backup.yaml | base64 -d | openssl x509 -noout -dates 2>/dev/null | grep notBefore | cut -d= -f2)
    NOT_BEFORE_CLUSTER=$(kubectl get secret idm.${DOMAIN}-tls -n ${CC_ENVIRONMENT}-access-stack \
      -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -dates | grep notBefore | cut -d= -f2)
    if [ "${NOT_BEFORE_BACKUP}" = "${NOT_BEFORE_CLUSTER}" ]; then
      echo "OK: notBefore identisch — keine Neuausstellung"
    else
      echo "FEHLER: notBefore abweichend — Neuausstellung trotz Restore"
    fi

Reihenfolge-Verletzung (Vorsicht)

Wird die Reihenfolge vertauscht (z. B. Annotation vor Backup-Einspielen oder Certificate-Löschung vor Backup-Einspielen), entsteht ein Zeitfenster, in dem cert-manager das fehlende oder nicht zum Issuer passende Secret bemerkt und eine reale Let's-Encrypt-Anfrage auslöst, bevor der Restore greift. Dies verbraucht unnötig Rate-Limit-Kontingent.

TLS-Zertifikatskette und Issuer-Rollen

Die TLS-Zertifikatskette in der CIVITAS/CORE-VM folgt einem dreistufigen Modell:

StufeRessourceRolle
1ClusterIssuer civitas-bootstrap-selfsigned (spec.selfSigned: {})Erzeugt die Root-CA. Dient nur zur Signatur des Root-CA-Zertifikats, nicht für Anwendungszertifikate.
2Certificate civitas-core-ca (Namespace cert-manager)Root-CA mit nicht-leerem Issuer-DN (CN=civitas-core-ca, O=civitas-core, C=DE). Liegt als Secret civitas-core-ca-secret vor.
3ClusterIssuer selfsigned-issuer (spec.ca.secretName: civitas-core-ca-secret)Produktiver CA-Issuer. Signiert alle Anwendungszertifikate (Keycloak, Portal).

Zusätzlich wird langfristig ein dedizierter CA-ClusterIssuer mit aussagekräftigem Namen empfohlen:

yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: civitas-core-ca-issuer
spec:
  ca:
    secretName: civitas-core-ca-secret

Dieser Issuer kann im Inventory unter cert_manager.issuer_name referenziert werden und macht die Rollentrennung explizit: selfsigned-issuer bleibt als Alias erhalten, aber neue Installationen sollten civitas-core-ca-issuer verwenden.

Wichtig: Ein Certificate im Namespace cc-prd-access-stack (z. B. für Keycloak) muss in seinem issuerRef den CA-Issuer referenzieren, nicht den Bootstrap-Issuer:

yaml
# Korrekt: Anwendungszertifikat mit CA-Issuer
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: idm.udp.data-dna.eu-tls
  namespace: cc-prd-access-stack
spec:
  secretName: idm.udp.data-dna.eu-tls
  commonName: idm.udp.data-dna.eu
  dnsNames:
    - idm.udp.data-dna.eu
  issuerRef:
    name: civitas-core-ca-issuer   # oder selfsigned-issuer (CA-Typ)
    kind: ClusterIssuer

Wird stattdessen fälschlich der Bootstrap-Issuer referenziert, signiert cert-manager das Zertifikat ohne CA-Bezug – der TLS-Handshake scheitert mit unknown CA.

Let's-Encrypt-ClusterIssuer (HTTP-01 per Ingress)

Für die Ausstellung öffentlich vertrauenswürdiger TLS-Zertifikate werden zwei ClusterIssuer vom Typ ACME vorgehalten:

NameServerZweck
letsencrypt-staginghttps://acme-staging-v02.api.letsencrypt.org/directoryTest (hohe Rate-Limits)
letsencrypt-prodhttps://acme-v02.api.letsencrypt.org/directoryProduktion

Beide Issuer verwenden den http01.ingress-Solver:

yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: ${ADMIN_EMAIL}
    privateKeySecretRef:
      name: letsencrypt-staging-key
    solvers:
    - http01:
        ingress:
          ingressClassName: nginx

Der Wechsel von selbstsignierten CA-Zertifikaten auf LE-Zertifikate erfolgt durch die Funktion switch_certificate_issuer() in Modul 06. Sie setzt die Annotation cert-manager.io/cluster-issuer auf allen Ingress-Ressourcen mit TLS-Block und wartet auf die Ausstellung durch Let's Encrypt.

LE_CERT-Schalter: Die Umgebungsvariable LE_CERT steuert, ob Production-Zertifikate ausgestellt werden:

LE_CERTVerhalten
false (Default)Nur Staging-Zertifikate ausstellen. Geeignet für Entwicklung/Test.
trueStaging bestehen → Production-Zertifikate ausstellen. Geeignet für produktionsnahe Umgebungen.

Bei LE_CERT=false endet switch_certificate_issuer() nach erfolgreicher Staging-Phase und überspringt die Production-Phase. Der Report zeigt "Production: NICHT GESTARTET (LE_CERT=false)".

CA-Trust-Integration (configure_ca_trust())

Die Funktion configure_ca_trust() in Phase 1b (Modul 05) trägt das Root-CA-Zertifikat (civitas-core-ca) in den System-Trust-Store und in das certifi-Bundle des Python-venv ein. Dieser Abschnitt spezifiziert die vollständige Trust-Store-Befüllung einschließlich öffentlicher Root-CAs.

Öffentliche Root-CAs für externe Zertifikate

Problem

setup_ca_trust() (Modul 05_addons.sh) trägt bislang ausschließlich die interne civitas-core-ca in den System-Trust-Store und in das certifi-Bundle des Python-venv ein. Wird durch den Backup-Restore-Mechanismus (restore_backup_and_switch_to_prod()) ein Hostname bereits mit einem öffentlich signierten LE-Zertifikat wiederhergestellt (issuer enthält z. B. C=US, O=Let's Encrypt), scheitert die TLS-Verifikation durch Ansible mit CERTIFICATE_VERIFY_FAILED, da das CA-Bundle, auf das inv_k8s.ingress.ca_path zeigt, ausschließlich die interne CA kennt.

Root Cause

Es handelt sich nicht um einen Konflikt zwischen Backup-Restore-Reihenfolge und Ansible-Build, sondern um eine unvollständige Trust-Store-Befüllung: Die Inventar-Variable ca_path zeigt auf /usr/local/share/ca-certificates/civitas-core-ca.crt. Diese Datei enthält ausschließlich die interne civitas-core-ca, nicht die öffentliche Let's-Encrypt-Root (ISRG Root X1). Ansible validiert TLS-Verbindungen gegen genau diesen ca_path — fehlt der LE-Aussteller, bricht das uri-Modul mit unable to get local issuer certificate ab.

Lösung

Der LE-Root-Trust wird als zusätzlicher Block innerhalb der bestehenden Funktion setup_ca_trust() in 05_addons.sh implementiert, und zwar direkt nach der Zeile, die die interne CA aus dem Kubernetes-Secret extrahiert und mit > in die Zieldatei schreibt. Grund: Diese Zeile überschreibt die Datei bei jedem Skriptdurchlauf neu — ein separater Schritt an anderer Stelle würde bei jedem Neuaufbau wieder verloren gehen.

bash
  # ── ISRG Root X1 (Let's Encrypt) für externe Zertifikate ─────────────
  # ca_path im Inventory zeigt auf diese Datei. Die Datei wurde unmittelbar
  # zuvor mit `>` aus dem Kubernetes-Secret überschrieben und enthält daher
  # garantiert nur die interne CA — ein Idempotenz-Check via openssl ist
  # strukturell nicht möglich. Der Download läuft bei jedem Durchlauf; bei
  # Fehlschlag wird die finale Verifikation am Funktionsende zuschlagen.
  local le_root_url="https://letsencrypt.org/certs/isrgrootx1.pem"
  if curl -fsSL "${le_root_url}" >> "${ca_cert_local}"; then
    log_ok "ISRG Root X1 ergänzt in ${ca_cert_local}"
  else
    log_warn "ISRG Root X1 konnte nicht heruntergeladen werden — ${le_root_url}"
    log_warn "  CA-Bundle enthält nur die interne CA — cc_cli exec wird später scheitern"
  fi
  # Hinweis: Das certifi-Bundle wird nicht hier befüllt, sondern durch
  # die bestehende Zeile 'cat "${ca_cert_local}" >> "${certifi_bundle}"'
  # einige Zeilen weiter unten in setup_ca_trust(). Da ca_cert_local zu
  # diesem Zeitpunkt bereits beide Trust-Anker enthält (interne CA + LE Root),
  # landen beide automatisch im certifi-Bundle — kein separater Eintrag nötig.

Wichtig: setup_ca_trust() extrahiert die interne CA mit kubectl ... | base64 -d > "${ca_cert_local}" — das > überschreibt die Datei vollständig. Der LE-Root-Block MUSS unmittelbar nach dieser Zeile stehen, damit beide Trust-Anker in derselben Datei kombiniert werden. Ein Idempotenz-Check via openssl x509 -in vor dem Download ist strukturell nicht möglich, da die Datei zu diesem Zeitpunkt immer nur die interne CA enthält.

Hinweis zu update-ca-certificates: Der nachgelagerte Aufruf von update-ca-certificates verarbeitet die Datei in den System-Store (/etc/ssl/certs/ca-certificates.crt) und bleibt als begleitende Konsistenzmaßnahme für andere TLS-Konsumenten erhalten. Der kausale Fix für Ansible ist jedoch der direkte Eintrag in der ca_path-Datei, nicht das System-Bundle.

Hinweis zur Idempotenz: Ein Idempotenz-Check via openssl x509 -in ist hier strukturell unmöglich, da die Datei zu Beginn der Funktion mit kubectl ... > "${ca_cert_local}" überschrieben wird. Die finale Verifikation via grep -c "BEGIN CERTIFICATE" am Funktionsende stellt sicher, dass beide Trust-Anker vorhanden sind. Ein erneuter Download bei jedem Durchlauf ist korrekt und beabsichtigt.

Fehlerverhalten
SzenarioErkennungReaktion
Download erfolgreichcurl -fsSL gibt Exit-Code 0 zurückISRG Root X1 wird angehängt, finale Verifikation ≥ 2 → OK
Download fehlschlägtcurl -fsSL gibt Exit-Code ≠ 0 zurücklog_warn (kein Abbruch), finale Verifikation = 1 → Abbruch mit Exit 1
Abnahmekriterium
bash
grep -c "BEGIN CERTIFICATE" /usr/local/share/ca-certificates/civitas-core-ca.crt
# Erwartung: 2 (interne civitas-core-ca + ISRG Root X1)

Die Datei enthält beide Zertifikate als PEM-Blöcke. Ist nur eines vorhanden (z. B. nur die interne CA), fehlt der öffentliche Vertrauensanker und cc_cli exec wird mit CERTIFICATE_VERIFY_FAILED scheitern.

Dieser Check wird als finale Verifikation innerhalb von setup_ca_trust() direkt nach dem certifi-Block ausgeführt. Er deckt sowohl fehlgeschlagene Downloads als auch abgebrochene Skriptläufe zuverlässig auf — unabhängig davon, ob der Download selbst als Fehler oder Warnung quittiert wurde.

Festlegung

setup_ca_trust() ist künftig verbindlich für beide Trust-Anker zuständig:

  1. Die interne civitas-core-ca aus dem Kubernetes-Secret (via >)
  2. Die öffentliche ISRG Root X1 von https://letsencrypt.org/certs/isrgrootx1.pem (via >>)

Der LE-Root-Block wird direkt nach der Secret-Extraktion innerhalb derselben Funktion platziert, nicht als separater nachgelagerter Schritt. Eine finale Verifikation via grep -c "BEGIN CERTIFICATE" am Funktionsende stellt sicher, dass beide Trust-Anker vorhanden sind. Ein manuelles Nachziehen der ISRG Root nach Backup-Restore entfällt damit vollständig.

Konfigurationsvariablen (Pflichtfelder)

Alle Variablen werden im Konfigurationsmodul des Skripts externalisiert. Passwörter und Secrets werden ausschließlich als Umgebungsvariablen übergeben — nie hartcodiert oder in Git eingecheckt.

VariableBeschreibungBeispielwert / Hinweis
DOMAIN_NAMEBasis-Domain, aus .env.localdata-dna.eu
DOMAINAbgeleitet aus DOMAIN_NAMEudp.${DOMAIN_NAME}
LE_CERTLE-Production aktivierenfalse (Default), true für Production
APISIX_DASHBOARDAPISIX-Dashboard aktivierenfalse (Default), true für apim.<DOMAIN>
SMTP_HOSTSMTP-ServerSMTP-Server-Hostname
SMTP_PORTSMTP-Port587
SMTP_USERSMTP-Absendernoreply@${DOMAIN_NAME}
SMTP_PASSSMTP-PasswortAus Umgebungsvariable $SMTP_PASS
SMTP_FROMSMTP-Absenderadresse für E-Mailsno-reply@${DOMAIN_NAME}
CC_CLI_VERSIONcc-cli-Version (Pinning)1.5.0 — nicht latest
CC_V1_REPO_URLRepository-URL des CIVITAS/CORE V1-Monoreposhttps://gitlab.com/civitas-connect/civitas-core/civitas-core-v1/civitas-core.git
CC_V1_REPO_PATHLokaler Pfad des geklonten Repositorys/opt/civitas-core-v1
CC_CLI_PLAYBOOK_DIRVerzeichnis mit playbook.yml (cc_cli CWD)${CC_V1_REPO_PATH}/core_platform
CC_V1_REPO_BRANCHGit-Branchmain
TIMEOUT_CC_CLI_EXECTimeout für cc_cli exec in Sekunden600
ADMIN_EMAILPlatform-Admin-E-Mail (auch Keycloak-master_username)admin@${DOMAIN_NAME}
ADMIN_PASSKeycloak-Master-Password + initiales platform_admin-Passwort (identisch, kein separater Wert). Muss Keycloak-Policy erfüllen: ≥12 Zeichen, 1 Ziffer, 1 Groß-/Kleinbuchstabe, 1 SonderzeichenAus .env.local
CERT_MANAGER_ISSUERClusterIssuer-Name für Anwendungszertifikatecivitas-core-ca-issuer (CA-Typ)

Hinweis SMTP: Für Keycloak (Bestandteil von CIVITAS/CORE V2) ist eine erreichbare SMTP-Konfiguration zwingend. Ohne gültige SMTP-Verbindung schlägt cc_cli validate fehl. Die SMTP-Erreichbarkeit wird bereits in Phase 0 geprüft.

Hinweis cc-cli-Version: CC_CLI_VERSION ist immer auf eine konkrete Versionsnummer zu setzen (derzeit 1.5.0), niemals latest. Breaking Changes durch neue Releases werden so vermieden.

Abnahmekriterien Phase 2

bash
# DNS-Auflösung (harte Voraussetzung)
dig +short idm.$DOMAIN
dig +short portal.$DOMAIN
# Erwartung: jeweils eine IP-Adresse

# Namespace-Prüfung über das K8S_NAMESPACES-Array
for ns in "${K8S_NAMESPACES[@]}"; do
  kubectl get namespace "$ns"
done
# Erwartung: alle drei Namespaces "Active"

# TLS-Zertifikate pro Namespace prüfen
for ns in "${K8S_NAMESPACES[@]}"; do
  kubectl get certificate -n "$ns"
done
# Erwartung: READY=True für alle Zertifikate, issuerRef zeigt auf CA-Issuer

# IssuerRef-Konsistenz prüfen (Certificates müssen CA-Issuer referenzieren)
for ns in "${K8S_NAMESPACES[@]}"; do
  kubectl get certificate -n "$ns" \
    -o jsonpath='{range .items[*]}{.metadata.name}{" → "}{.spec.issuerRef.name}{"\n"}{end}'
done
# Erwartung: issuerRef.name = selfsigned-issuer oder civitas-core-ca-issuer
#            NICHT: civitas-bootstrap-selfsigned

# TLS-Endpunkt-Prüfung (HTTPS via HAProxy-Passthrough, --cacert prüft CA-Trust)
curl -sf --max-time 10 \
  --cacert /usr/local/share/ca-certificates/civitas-core-ca.crt \
  "https://idm.${DOMAIN}/realms/master"
# Erwartung: HTTP 200 oder Keycloak-Response, KEIN "unknown CA" / "self-signed certificate"

curl -sf --max-time 10 \
  --cacert /usr/local/share/ca-certificates/civitas-core-ca.crt \
  "https://${DOMAIN}/"
# Erwartung: HTTP 200 oder Redirect, KEIN "unknown CA"


# Staging-vor-Produktion-Verifikation (Let's Encrypt)
# Wenn ein Hostname produktiv mit Let's Encrypt betrieben wird, muss
# vor dem produktiven Request ein Staging-Zertifikat erfolgreich
# ausgestellt und verifiziert worden sein. DIES GILT NUR FÜR NEU
# ANZUFORDERNDE ZERTIFIKATE. Bereits vorhandene, gültige Zertifikate
# (im laufenden Cluster oder aus Datei-Backup restauriert) sind von
# der Staging-Pflicht ausgenommen.
STAGING_ANNOTATION=$(kubectl get certificate idm.udp.data-dna.eu-tls -n cc-prd-access-stack -o jsonpath='{.metadata.annotations.civitas\.io/staging-verified}' 2>/dev/null)
if [ "${STAGING_ANNOTATION}" = "true" ]; then
  echo "Staging-Verifikation bestanden (Annotation vorhanden)"
else
  # Prüfe alternativ, ob ein gültiges Zertifikat aus Datei-Backup restauriert wurde
  NOT_BEFORE_CLUSTER=$(kubectl get secret idm.${DOMAIN}-tls -n ${CC_ENVIRONMENT}-access-stack \
    -o jsonpath='{.data.tls\.crt}' 2>/dev/null | base64 -d 2>/dev/null | openssl x509 -noout -dates 2>/dev/null | grep notBefore | cut -d= -f2)
  if [ -n "${NOT_BEFORE_CLUSTER}" ]; then
    # Vergleich mit Backup-Zeitstempel, falls Backup-Datei existiert
    BACKUP_FILE="${VM_REMOTE_INSTALL_DIR}/le-certs-backup.yaml"
    if [ -f "${BACKUP_FILE}" ]; then
      NOT_BEFORE_BACKUP=$(yq eval 'select(.metadata.name == "idm.'"${DOMAIN}"'-tls") | .data["tls.crt"]' "${BACKUP_FILE}" | base64 -d | openssl x509 -noout -dates 2>/dev/null | grep notBefore | cut -d= -f2)
      if [ "${NOT_BEFORE_CLUSTER}" = "${NOT_BEFORE_BACKUP}" ]; then
        echo "Keine Staging-Annotation, aber Zertifikat aus Backup (notBefore identisch)"
        echo "  notBefore=${NOT_BEFORE_CLUSTER}"
        echo "  (Staging-Pflicht entfällt — kein neuer ACME-Request nötig)"
      else
        echo "Staging-Verifikation nicht möglich: notBefore weicht von Backup ab"
        echo "  Cluster=${NOT_BEFORE_CLUSTER}  Backup=${NOT_BEFORE_BACKUP:--}"
        false
      fi
    else
      echo "Keine Staging-Annotation, aber gültiges Zertifikat im Cluster vorhanden"
      echo "  notBefore=${NOT_BEFORE_CLUSTER} (kein Backup-Datei zum Vergleich)"
      echo "  (Staging-Pflicht entfällt — kein neuer ACME-Request nötig)"
    fi
  else
    echo "Keine Staging-Annotation gefunden — Hostnamen ohne Produktivzertifikat"
    echo "muessen zwingend zuerst per letsencrypt-staging getestet werden."
    false
  fi
fi
# Erwartung: Annotation civitas.io/staging-verified="true" auf dem Certificate-Objekt
# (pro Hostname einzeln), ODER Hostname ist bereits im Produktivbetrieb
# (gueltiges Produktionszertifikat vorhanden), ODER Zertifikat wurde erfolgreich
# aus Datei-Backup (le-certs-backup.yaml) restauriert (notBefore-Zeitstempel
# vor/nach Restore identisch)

# WireGuard-Tunnel aktiv
systemctl is-active wg-quick@wg0
# Erwartung: active

# Konnektivität OPNsense
ping -c2 10.10.10.1
# Erwartung: 0% packet loss

Fehleranalyse bei unknown CA: Tritt dieser Fehler bei curl --cacert auf, ist das präsentierte Zertifikat nicht von der erwarteten Root-CA signiert. Maßnahme: kubectl describe certificate -n <namespace> ausführen und den issuerRef prüfen. Zeigt er auf civitas-bootstrap-selfsigned (Stufe 1), muss das Certificate gelöscht werden (cert-manager stellt es mit dem aktuellen Issuer neu aus). Zeigt er auf selfsigned-issuer (Stufe 3), prüfen ob dieser Issuer vom Typ ca: ist (nicht selfSigned:).

Abnahme Phase 2 (Zielzustand): Phase 2 gilt als bestanden, wenn alle Pods laufen, TLS-Zertifikate von der Root-CA signiert sind und beide Endpunkte per HTTPS mit --cacert civitas-core-ca.crt erreichbar sind. Hinweis: Phase 2.0 (Repository-Klon) muss vor Phase 2 abgeschlossen sein. Ohne das geklonte Repository in /opt/civitas-core-v1 scheitert Schritt 2.4 mit Could not find any playbook to execute..


Phase 3 — Verifikation und Fehlerreport

Zweck

Systematische End-to-End-Prüfung nach Abschluss beider Installationsphasen. Das Skript führt alle Abnahmetests aus Phase 1 und Phase 2 erneut aus und erzeugt einen zusammenfassenden Bericht.

Struktur des Verifikationsmoduls

verify_phase1()   → Prüft alle Phase-1-Kriterien, zählt Fehler
verify_phase2()   → Iteriert über K8S_NAMESPACES, prüft pro Namespace:
                     Existenz, Pods, Ingress-Ressourcen, TLS-Zertifikate,
                     IssuerRef-Konsistenz (kein Bootstrap-Issuer)
                   → Domain-Level-Checks: Keycloak, Portal (HTTPS mit --cacert)
                   → Infrastruktur: WireGuard-Tunnel, OPNsense-Konnektivität
report_result()   → Gibt Zusammenfassung aus (OK / FAILED + Fehlercount)
exit_with_code()  → Exit 0 bei Erfolg, Exit 1 bei ≥ 1 Fehler

Ausgabeformat (Beispiel)

[2026-06-27 00:00:00] [PHASE 1] k3s Node Ready                     ... OK
[2026-06-27 00:00:00] [PHASE 1] cert-manager Running                ... OK
[2026-06-27 00:00:00] [PHASE 1] nginx-Ingress (DaemonSet) Running   ... OK
[2026-06-27 00:00:00] [PHASE 1] Storage Class local-path (Default)  ... OK
[2026-06-27 00:00:00] [PHASE 1] CA-Issuer-DN korrekt                ... OK
[2026-06-27 00:00:10] [PHASE 2] Namespace cc-prd-access-stack       ... OK
[2026-06-27 00:00:11] [PHASE 2] cc-prd-access-stack: Pods Running   ... OK
[2026-06-27 00:00:12] [PHASE 2] Namespace cc-prd-database-stack     ... OK
[2026-06-27 00:00:13] [PHASE 2] cc-prd-database-stack: Pods Running ... OK
[2026-06-27 00:00:14] [PHASE 2] Namespace cc-prd-operation-stack    ... OK
[2026-06-27 00:00:15] [PHASE 2] cc-prd-operation-stack: Pods Running... OK
[2026-06-27 00:00:16] [PHASE 2] Keycloak https://idm.udp.data-dna.eu  ... OK
[2026-06-27 00:00:17] [PHASE 2] Portal https://udp.data-dna.eu        ... OK
[2026-06-27 00:00:18] [PHASE 2] WireGuard-Tunnel wg0 aktiv          ... OK
------------------------------------------------------------
Ergebnis: 13/13 Prüfungen bestanden. Installation erfolgreich.

Hinweis: Vor der Endprüfung wartet das Skript in Phase 2 auf den Ready-Status aller Pods in jedem Namespace aus K8S_NAMESPACES. Die Warteschleife ist als Best-Effort ausgelegt – auch bei Timeout wird Phase 3 (Verify) durchlaufen, um eine detaillierte Diagnose zu liefern:

bash
for ns in "${K8S_NAMESPACES[@]}"; do
  if ! wait_pods_ready "${ns}"; then
    log_warn "Nicht alle Pods in ${ns} wurden Ready - Details in Phase 3."
  fi
done

Namespace-Verifikation

Phase 3 prüft, dass alle von cc_cli angelegten Namespaces existieren. Es wird NICHT ein einzelner, fest benannter Namespace geprüft, sondern alle Einträge aus dem Array K8S_NAMESPACES (definiert in 01_config.sh, Muster ${CC_ENVIRONMENT}-{stack}).

Pseudocode:

text
for ns in K8S_NAMESPACES:
    exists = kubectl_get_namespace(ns) == 0
    check(f"Namespace {ns} existiert", exists)

Die Variable K8S_NAMESPACE (Singular) ist obsolet und darf nicht mehr für Existenzprüfungen verwendet werden.

TLS-Endpunkt-Prüfung in Phase 3: Ein TLS-Endpunkt gilt nur als „OK", wenn curl --cacert /usr/local/share/ca-certificates/civitas-core-ca.crt https://<domain>/ ohne unknown CA oder self-signed certificate durchläuft. Schlägt diese Prüfung fehl, ist im entsprechenden Namespace der issuerRef des Certificate-Objekts zu prüfen:

bash
kubectl describe certificate -n cc-prd-access-stack idm.udp.data-dna.eu-tls

Erwartet wird ein issuerRef.name, der auf den CA-ClusterIssuer zeigt (selfsigned-issuer oder civitas-core-ca-issuer), nicht auf den Bootstrap-Issuer civitas-bootstrap-selfsigned.

Test-Vorbereitung und -Durchführung (optional)

Nach erfolgreicher Verifikation kann das Skript optional die offiziellen CIVITAS/CORE-E2E-Tests (pytest + Playwright, siehe 02-API-UI-Tests.md im offiziellen Doku-Repo) ausführen. Die Tests werden nur aktiviert, wenn RUN_TESTS=true in .env.local gesetzt ist.

Vorbereitung (setup_tests_env()):

SchrittAktion
3a.1uv installieren (pip install uv)
3a.2uv sync im tests/-Verzeichnis des geklonten Repos
3a.3Playwright-Browser installieren
3a.4.env-Datei aus Kubernetes-Secrets generieren
3a.5GeoServer-Grundkonfiguration (pytest --only-geoserver-setup)

Durchführung (run_test_suite()):

bash
cd /opt/civitas-core-v1/tests
source .venv/bin/activate
pytest --prod-safe e2e_tests/

Integration in Phase 3:

bash
run_verification() {
  log "=== Phase 3: Verifikation ==="
  VERIFY_ERRORS=0

  verify_phase1           # Cluster, System-Pods, Add-ons
  verify_phase2           # Namespaces, Pods, Ingresses, TLS, WireGuard
  if [[ "${RUN_TESTS:-false}" == "true" ]]; then
    setup_tests_env
    run_test_suite
  fi
  report_result
}

Fehlerverhalten: Fehlschlagende Tests zählen als Fehler im Report, unterbrechen aber nicht den Ablauf. Der Admin kann nach der Installation die Test-Details einsehen:

bash
cd /opt/civitas-core-v1/tests
source .venv/bin/activate
pytest --prod-safe --headed e2e_tests/  # Mit Browser-Fenster für UI-Tests

Fehlerbehandlung

Das Skript arbeitet mit set -euo pipefail. Jede Phase wird durch eine dedizierte Funktion gekapselt. Fehler werden mit Zeitstempel und Phase protokolliert.

FehlerklasseVerhalten
Pflichtprüfung nicht erfüllt (Phase 0)Sofortiger Abbruch, keine Änderungen am System
DNS-Warnung (Phase 0)Warnung ausgeben, Ausführung fortsetzen
DNS-Fehler (Phase 2, Schritt 2.0)Abbruch mit Hinweis: DNS-Eintrag in Hetzner-WebGUI setzen
Installationsfehler (Phase 1/2)Abbruch der aktuellen Phase, Fehlermeldung mit Log-Hinweis
Timeout cc_cli execAbbruch mit Hinweis auf $TIMEOUT_CC_CLI_EXEC
Verifikationsfehler (Phase 3)Keine Systemänderung, Fehlerbericht + Exit 1
Bereits installierte Komponente (Idempotenz)Kein Fehler, Meldung „bereits vorhanden, überspringe"
HTTP 404 bei Keycloak DELETE (z.B. Default-Resource)Ressource bereits entfernt = Ziel erreicht. Bekanntes Problem bei Wiederholung nach abgebrochenem Run. Workaround: kubectl delete namespace cc-prd-access-stack und neu starten.
Test-Fehler (Phase 3)Warnung + Fehlerzähler, kein Abbruch
Test-Umgebungs-Fehler (Phase 3)Warnung + Überspringen der Tests
Playbook-URL-Patch fehlgeschlagen (Phase 2)Warnung + Fortsetzung (Skript kann ohne Patch laufen)
Admin-User-Erstellung fehlgeschlagen (Phase 2)Warnung + Fortsetzung (User muss manuell in Keycloak angelegt werden)
LE-Backup-Restore fehlgeschlagen (Phase 2)Warnung + Fortsetzung (Zertifikate müssen neu ausgestellt werden)

Offene Punkte (vor Skriptbau zu klären)

PunktStatusEntscheidung bei
Gast-OSEntschieden: Debian 13 (Trixie) – Cloud-Image und OS-Check im Codedurch Code festgelegt
Domainname: civitas.data-dna.eu oder anderer Vorschlag?OffenGeklärt: udp.data-dna.euPeter König → durch Code in 01_config.sh festgelegt
TLS-Strategie: self-signed ClusterIssuer oder interne CA?OffenGeklärt: 3-stufiges CA-Setup (Variante C)netzwerk-dns-tls.mdnetzwerk-dns-tls.md (Variante C)
Ziel-Namespace für CIVITAS/COREVorschlag: civitas-coreBestätigung Peter KönigBestätigt
cc-cli-Version (Pinning)Gepinnt auf 1.5.0 in 01_config.shdurch Code festgelegt

| servicelb und metrics-server: deaktivieren oder aktiv lassen? | Offen | skriptarchitektur.md |


Nachgelagerte Konfiguration

Folgende Schritte sind nach der automatischen Installation manuell durchzuführen. Sie sind nicht automatisierbar, da sie über die Web-UI der jeweiligen Komponenten konfiguriert werden.

GeoServer-JWT-Authentifizierung

Damit Masterportal geschützte (nicht-öffentliche) Layer anzeigen kann, muss in GeoServer ein JWT-Header-Authentifizierungsfilter eingerichtet werden. Ohne diese Konfiguration erhalten berechtigte Nutzer einen 401 Unauthorized beim Zugriff auf nicht-öffentliche Layer über Masterportal.

Schritte:

  1. In GeoServer unter Security → Authentication einen neuen Authentifizierungsfilter anlegen:

    • Typ: JWT Header (jwt-headers)
    • Name: civitas-idm-jwt
    • Request header attribute for User Name: Authorization
    • Format the Header value is in: JWT
    • JSON path for the User Name: preferred_username
    • Validate JWT (Access Token): ✅
    • Validate Token Expiry: ✅
    • Validate JWT (Access Token) Signature: ✅
    • JSON Web Key Set URL (jwks_uri): https://idm.${DOMAIN}/realms/${CC_ENVIRONMENT}/protocol/openid-connect/certs
    • Validate JWT (Access Token) Against Endpoint: ✅
    • URL (userinfo_endpoint): https://idm.${DOMAIN}/realms/${CC_ENVIRONMENT}/protocol/openid-connect/userinfo
    • Role Source: Header Containing JWT
    • Request Header attributes for Roles: Authorization
    • JSON Path: resource_access.geostack.roles
    • Role Converter Map: geoAdmin=ROLE_ADMINISTRATOR;geoAdmin=ADMIN
  2. Den Filter civitas-idm-jwt in den Filter-Chains konfigurieren:

    Filter-ChainFilterBemerkung
    restcivitas-idm-jwtREST-API-Zugriff
    gwccivitas-idm-jwtGeoWebCache
    defaultcivitas-idm-jwtStandard-Zugriff
    webcivitas-idm-oidc (unverändert)Web-UI
  3. Änderungen speichern und ggf. GeoServer neu starten (Pod-Neustart in Kubernetes: kubectl rollout restart deployment geoserver-geoserver -n ${CC_ENVIRONMENT}-geodata-stack).

Initiale Benutzer und Rollen

Nach der Installation müssen in Keycloak Benutzer angelegt und mit den passenden Rollen versehen werden. Siehe Keycloak-Admin-Guide der CIVITAS/CORE-Dokumentation für Details.

Wichtige Rollen für die Administration:

RolleDienstBerechtigung
geoAdminGeoServerGeoServer-Administration
supersetAdminSupersetSuperset-Administration
grafanaAdmin / grafanaServerAdminGrafanaGrafana-Administration
adminToolsAdminPrometheus/GrafanaMonitoring-Administration
operatorPiveauDatenkatalog-Verwaltung (falls aktiviert)

Festlegungen

  1. Vor Phase 0 kann auf dem Proxmox-Host eine Phase -1 (VM-Provisionierung) ausgeführt werden. Diese erstellt die CIVITAS/CORE-VM aus dem Debian-13- Cloud-Image und ist idempotent (bestehende VM wird übersprungen).

  2. Das Skript gliedert sich in Phase 0 (Vorbedingungen), Phase 1 (k3s + Add-ons), Phase 2 (cc-cli + Plattform) und Phase 3 (Verifikation).

  3. Jede Phase hat klar definierte, maschinell prüfbare Abnahmekriterien.

  4. Das Skript bricht bei jedem Pflichtfehler ab und gibt einen eindeutigen Exit-Code zurück.

  5. Alle Phasen sind idempotent: Ein erneuter Aufruf erzeugt keinen Fehler bei bereits korrekt installierten Komponenten.

  6. Die DNS-Prüfung in Phase 0 ist eine Warnung; in Phase 2 ist sie hart.

  7. --disable traefik wird beim k3s-Erststart gesetzt; local-path-provisioner bleibt aktiv und stellt die Default-StorageClass bereit.

  8. helm-CLI wird separat installiert; k3s bringt nur den helm-controller.

  9. CC_CLI_VERSION wird auf eine konkrete Version gepinnt, niemals latest.

  10. SMTP-Zugangsdaten werden ausschließlich als Umgebungsvariablen übergeben, niemals hartcodiert. SMTP-Erreichbarkeit wird in Phase 0 geprüft.

  11. Phase 2 ist distributionsunabhängig; der Aufwand ist für k3s, k0s und kubeadm identisch.

  12. Die gesamte Lösung bildet die erste Ausbaustufe ab: einen funktionierenden Prototyp für Entwicklung und Evaluation. Produktionsanpassungen (HA, externes etcd, Backup-Integration, öffentlicher Zugang / DMZ) bleiben einer späteren Spezifikation vorbehalten.

  13. Das CIVITAS/CORE-Repository wird in Phase 2.0 nach /opt/civitas-core-v1 geklont. Ein Symlink /opt/civitas-core → /opt/civitas-core-v1 wird gesetzt. cc_cli exec wird ausschließlich aus /opt/civitas-core-v1 heraus aufgerufen. Wird das Repository bei einem Folgeaufruf bereits vorgefunden, ersetzt git pull den git clone-Schritt (Idempotenz).


CIVITAS/CORE-Shutdown und -Start

Ziel

Einen definierten Ablauf für das saubere Herunterfahren der CIVITAS/CORE-VM festlegen, der Pods ordentlich terminiert (kubectl drain), bevor k3s gestoppt wird, sowie die automatische Wiederherstellung der Schedulability nach dem Neustart.

Shutdown-Ablauf (cico-shutdown)

Das Skript /usr/local/bin/cico-shutdown führt folgende Schritte aus:

SchrittAktionBeschreibung
1kubectl drain civitas-core --ignore-daemonsets --delete-emptydir-data --grace-period=60 --disable-evictionNode cordonen und Pods evicten; DaemonSets bleiben laufen, leere Verzeichnisse werden gelöscht
2Polling Loop: kubectl get pods -A --field-selector=spec.nodeName=civitas-coreWarten bis alle Pods terminiert sind (max. TIMEOUT Sekunden, Default 120)
3systemctl stop k3sKubernetes-Dienst beenden
4sync && shutdown -h nowDateisysteme synchronisieren und VM herunterfahren

Fehlerverhalten:

SzenarioReaktion
kubectl drain schlägt fehlWarnung, Ausführung der Folgeschritte (force-Eviction vermeiden)
Pods terminieren nicht innerhalb des TimeoutsWarnung mit Liste der verbleibenden Pods, Shutdown wird trotzdem fortgesetzt
systemctl stop k3s schlägt fehlWarnung, Shutdown wird trotzdem fortgesetzt

Konfigurationsvariablen:

VariableBeschreibungDefault
K3S_NODEKubernetes-Node-Namecivitas-core
TIMEOUTMaximale Wartezeit auf Pod-Terminierung (Sekunden)120
POLL_INTERVALPolling-Intervall (Sekunden)5

Abnahmekriterium:

bash
# Skript ist vorhanden und ausführbar
command -v cico-shutdown
# Nach Ausführung ist die VM heruntergefahren (manuelle Prüfung via Proxmox)

Hinweis: Nach einem cico-shutdown ist der Node beim nächsten Start cordoniert. Der automatische Uncordon wird durch den systemd-Dienst cico-uncordon.service sichergestellt (siehe Abschnitt „Start-Ablauf").

Start-Ablauf (Automatischer Uncordon)

Nach dem Boot der VM startet k3s automatisch (systemd). Der Node ist jedoch durch den vorherigen kubectl drain als unschedulable (cordoned) markiert. Der systemd-Dienst cico-uncordon.service hebt diese Sperre auf.

Systemd-Dienst cico-uncordon.service:

EigenschaftWert
Typoneshot
Start nachk3s.service
Befehl/usr/local/bin/cico-uncordon
ZielWantedBy=multi-user.target

Skript /usr/local/bin/cico-uncordon:

SchrittAktionIdempotenz-Prüfung
1Warten auf k3s-API: kubectl get nodes civitas-core (max. 180s)Polling loop bis API antwortet
2Prüfen ob Node cordoniert ist: kubectl get node civitas-core -o jsonpath='{.spec.unschedulable}'Wenn unschedulable != true → nichts tun, log_ok
3Uncordon: kubectl uncordon civitas-coreNur ausgeführt wenn Schritt 2 true ergab

Fehlerverhalten:

SzenarioReaktion
k3s-API nach 180s nicht verfügbarAbbruch mit Exit 1 — systemd markiert Service als failed, nächster Boot-Versuch wiederholt den Vorgang

Abnahmekriterium:

bash
# Dienst ist aktiv und aktiviert
systemctl is-enabled cico-uncordon.service
systemctl status cico-uncordon.service

# Node ist schedulable
kubectl get node civitas-core -o jsonpath='{.spec.unschedulable}'
# Erwartung: kein Output (oder "false") — Node ist nicht cordoniert

Bereitstellung

Die Skripte und der systemd-Dienst werden in Phase 1b (Modul 05_addons.sh) durch die Funktion install_cico_utils() bereitgestellt. Die Skriptinhalte sind als Here-Docs innerhalb der Funktion hinterlegt — kein separates bin/-Verzeichnis erforderlich:

SchrittAktionIdempotenz-Prüfung
1Here-Doc erzeugt /usr/local/bin/cico-shutdown (chmod +x)command -v cico-shutdown
2Here-Doc erzeugt /usr/local/bin/cico-uncordon (chmod +x)command -v cico-uncordon
3Here-Doc erzeugt /etc/systemd/system/cico-uncordon.servicesystemctl is-enabled cico-uncordon.service
4systemctl daemon-reload && systemctl enable cico-uncordon.serviceDienst ist active (exited)