reference-ansible/ARCHITECTURE.md
Simon Bärlocher c67e9aac43
chore(demo-gymburgdorf): finish ACME, LDAP, DMZ routing for live inventory
- ACME via DNS-01 against internal NS (172.16.9.169) with TCP-only +
  disableANSChecks so the DMZ traefik can issue LE certs without
  reaching public NS IPs.
- Migrate single-domain vars to `*_domains` lists (authentik, nextcloud,
  collabora, garage_s3) so public + *.int.* SANs share one cert and
  server-to-server traffic stays in the LAN.
- Wire `traefik_dmz_exposed_services` per backend host (application,
  storage) with explicit `backend_host` overrides pointing at internal
  FQDNs — DMZ traefik now validates upstream certs against SAN names.
- Nextcloud notify_push setup on internal FQDN to avoid DMZ hairpin;
  collabora WOPI / authentik LDAP outpost wired to *.int.* equivalents.
2026-05-27 23:18:58 +02:00

453 lines
21 KiB
Markdown

<!-- markdownlint-disable MD013 MD060 MD051 -->
# Architektur — `reference-ansible`
Dieses Dokument beschreibt die Architektur des Repos `reference-ansible`
und nutzt die Inventory `inventories/demo-gymburgdorf/` als
durchgehendes Beispiel. Es dient sowohl als Onboarding-Doku für neue
Engineers als auch als Referenz beim Aufsetzen weiterer Demo-Mandanten.
> **Demo-only.** Alle Defaults in den Rollen (Passwörter, Tokens,
> RPC-Secrets) sind unsicher und ausschliesslich für Demo-Setups
> gedacht. Siehe [§ 8 — Security und Demo-Only-Defaults](#8-security-und-demo-only-defaults).
**Letzte Aktualisierung:** 2026-05-18 · **Owner:** @sbaerlocher
## Inhalt
- [§ 0 — Glossar](#0-glossar)
- [§ 1 — Repo-Layout und Roles-Herkunft](#1-repo-layout-und-roles-herkunft)
- [§ 2 — Setup und Voraussetzungen](#2-setup-und-voraussetzungen)
- [§ 3 — Variablen-Hierarchie](#3-variablen-hierarchie)
- [§ 4 — Inventory-Topologie (`demo-gymburgdorf`)](#4-inventory-topologie-demo-gymburgdorf)
- [§ 5 — Service-Layout und Variablen-Verortung](#5-service-layout-und-variablen-verortung)
- [§ 6 — Deploy-Flow](#6-deploy-flow)
- [§ 7 — Traefik-Modi (DMZ vs Backend)](#7-traefik-modi-dmz-vs-backend)
- [§ 8 — Security und Demo-Only-Defaults](#8-security-und-demo-only-defaults)
- [§ 9 — Variablen-Cheatsheet](#9-variablen-cheatsheet)
- [§ 10 — Walkthrough: Neuen Demo-Mandanten anlegen](#10-walkthrough-neuen-demo-mandanten-anlegen)
- [§ 11 — Bekannte Lücken und Trade-offs](#11-bekannte-lücken-und-trade-offs)
## 0. Glossar
| Begriff | Bedeutung |
|---|---|
| **OpenBao** | HashiCorp-Vault-Fork. Single Source of Truth für Secrets. Endpoint: `bao.digitalboard.ch`. |
| **Authentik** | Identity Provider. Stellt OIDC für SP-Services und LDAP via Outpost. |
| **Outpost (Authentik)** | Separater Authentik-Sidecar, der LDAP/Proxy-Protokolle für Legacy-Apps emuliert. Spricht via RPC + Token zu Authentik. |
| **WOPI** | Web Application Open Platform Interface — Protokoll, mit dem Nextcloud/Opencloud Office-Dokumente an Collabora übergeben. |
| **TSIG / RFC2136** | Authenticated DNS-Updates. Traefik nutzt TSIG-signierte `nsupdate`-Calls für ACME DNS-01-Challenges. |
| **DNS-01 (ACME)** | Let's-Encrypt-Challenge-Typ: Zertifikatsbesitz wird per TXT-Record im DNS bewiesen statt per HTTP. Erforderlich für Wildcard-Certs. |
| **CNAME-Bridge** | `_acme-challenge.<fqdn>` zeigt per CNAME in eine dedizierte Update-Zone (`demo-gymb._acme.digitalboard.ch`). So bleibt der TSIG-Key auf eine schmale Zone beschränkt. |
| **File-Provider / Docker-Provider** | Traefik-Konfigurationsquellen. File-Provider liest statische YAML, Docker-Provider liest Container-Labels via `/var/run/docker.sock`. |
| **STUN/TURN** | NAT-Traversal-Protokolle für WebRTC (z. B. für Nextcloud Talk). Läuft auf separatem Host (`turn`). |
| **Garage** | S3-kompatibler Object Store (Rust). Backend für Nextcloud/Opencloud. |
| **FQCN** | Fully Qualified Collection Name, z. B. `digitalboard.core.traefik`. Ansible-Pflicht ab 2.10. |
## 1. Repo-Layout und Roles-Herkunft
```text
reference-ansible/
├── Makefile # Deploy-Targets, OIDC-Login, OBJC-Fork-Workaround
├── ansible.cfg # collections_path, remote_user=root, hashi_vault auth_method=token
├── requirements.yml # community.hashi_vault + digitalboard.core (Git)
├── playbooks/site.yml # Play-Sequenz (14 Plays, siehe § 6)
├── collections/ # ← installiert von `make install`, gitignored
│ └── ansible_collections/
│ └── digitalboard/core/
│ └── roles/ # 🔑 HIER liegen die Rollen, NICHT im Repo-Root
└── inventories/
├── demo-gymburgdorf/ # Inventory dieses Dokuments
├── demo-mbazürich/
├── demo-phbern/
└── vagrant/ # lokale Test-Inventory mit eigener Topologie
```
> **Wichtig:** Es gibt **kein** `roles/`-Verzeichnis im Repo-Root. Alle
> Rollen kommen aus der Collection `digitalboard.core` (siehe
> [requirements.yml](requirements.yml)), installiert via `make install`
> nach `./collections/`. Plays referenzieren sie per FQCN
> `digitalboard.core.<role>`.
## 2. Setup und Voraussetzungen
**Tools auf dem Control-Node:**
- `ansible` (Core ≥ 2.15)
- `bao` CLI (OpenBao) — z. B. `sudo pacman -S openbao python-hvac` (Arch) oder Homebrew
- `python-hvac` (für `community.hashi_vault` Lookups)
- Auf macOS: `OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` (im [Makefile](Makefile) gesetzt; ohne crashen Ansible-Forks beim Bao-Lookup)
**Initial-Setup:**
```bash
git clone <repo>
cd reference-ansible
make install # Galaxy + digitalboard.core nach ./collections/
```
**Vor jedem Deploy:** Bao-Login in **derselben Shell**, in der dann `ansible-playbook` läuft:
```bash
export BAO_ADDR=https://bao.digitalboard.ch
bao login -method=oidc -path=Digitalboard
export VAULT_TOKEN=$(bao print token)
```
> ⚠️ `make bao` allein reicht **nicht**: jedes `make`-Target startet
> eine neue Shell, der dort gesetzte `VAULT_TOKEN` lebt nur
> während `make bao` selbst. Entweder die drei Befehle oben manuell
> ausführen oder `make bao deploy_site_demo_gymburgdorf` als **einen**
> Aufruf — sonst hat das Deploy keinen Token.
**Smoke-Test:**
```bash
make ping_demo # pingt alle drei Demo-Inventories
```
## 3. Variablen-Hierarchie
Ansible mergt Variablen über mehrere Quellen. Vereinfachtes Modell für
dieses Repo (vollständige Precedence siehe Ansible-Docs):
```mermaid
flowchart LR
classDef role fill:#fef3c7,stroke:#92400e,color:#000
classDef group fill:#dbeafe,stroke:#1e40af,color:#000
classDef host fill:#dcfce7,stroke:#166534,color:#000
classDef vault fill:#fee2e2,stroke:#991b1b,color:#000
R["<b>role defaults</b><br/>(niedrigste Precedence)<br/>collections/.../roles/&lt;r&gt;/defaults/main.yml"]:::role
GA["<b>group_vars/all/</b><br/>vault.yml, docker.yml"]:::group
GG["<b>group_vars/&lt;group&gt;/</b><br/>traefik_servers/, backend_servers/<br/>(parallele Gruppen, gemerged via<br/>ansible_group_priority)"]:::group
HV["<b>host_vars/&lt;host&gt;/</b><br/>(höchste der drei Inventory-Quellen)"]:::host
BAO["<b>OpenBao</b><br/>Lookup zur Laufzeit"]:::vault
R --> |"&lt;wird überschrieben von&gt;"| GA
GA --> |"&lt;wird überschrieben von&gt;"| GG
GG --> |"&lt;wird überschrieben von&gt;"| HV
HV -.community.hashi_vault.-> BAO
GG -.community.hashi_vault.-> BAO
```
**Wichtige Eigenschaften:**
- Mehrere `group_vars/<group>/` sind **parallel**, nicht hierarchisch
geschachtelt. `traefik_servers` und `backend_servers` werden nach
`ansible_group_priority` (Default 1) gemerged; bei Konflikt gewinnt
alphabetisch der spätere Gruppenname.
- `host_vars/<host>/` schlägt jede Gruppe.
- `host_vars/reverseproxy/traefik.yml: traefik_mode: dmz` überschreibt
daher den Default aus `group_vars/backend_servers/` — und zwar nur,
weil `reverseproxy` nicht Mitglied von `backend_servers` ist (sonst
ginge es sowieso nicht).
**Bao-Lookups** sind keine Precedence-Ebene, sondern **Werte** innerhalb
einer beliebigen Var-Quelle. Pattern siehe [§ 8](#8-security-und-demo-only-defaults).
## 4. Inventory-Topologie (`demo-gymburgdorf`)
```mermaid
flowchart LR
classDef dmz fill:#fee2e2,stroke:#991b1b,color:#000
classDef app fill:#dcfce7,stroke:#166534,color:#000
classDef stor fill:#dbeafe,stroke:#1e40af,color:#000
classDef turn fill:#fef9c3,stroke:#854d0e,color:#000
subgraph ALL["group: all_servers"]
direction LR
subgraph DMZ["DMZ 172.16.9.0/24"]
RP["<b>reverseproxy</b><br/>172.16.9.111<br/>traefik_mode: dmz"]:::dmz
TURN["<b>turn</b><br/>172.16.9.112<br/>(noch keine Rolle in site.yml)"]:::turn
end
subgraph BE["Backend 172.16.19.0/24<br/>group: backend_servers"]
APP["<b>application</b><br/>172.16.19.101<br/>traefik_mode: backend<br/>+ authentik, authentik_outpost_ldap,<br/> nextcloud, collabora, drawio"]:::app
ST["<b>storage</b><br/>172.16.19.102<br/>traefik_mode: backend<br/>+ garage (S3)"]:::stor
end
end
RP -.HTTPS in, HTTP out.-> APP
RP -.HTTPS in, HTTP out.-> ST
```
**Gruppen-Mitgliedschaften (aus [hosts.yml](inventories/demo-gymburgdorf/hosts.yml)):**
| Gruppe | Mitglieder | Zweck |
|---|---|---|
| `all_servers` | `reverseproxy`, `application`, `storage`, `turn` | Basis-Rolle für alle Hosts |
| `traefik_servers` | `children: all_servers` (= alle 4 Hosts) | Traefik überall; DMZ/Backend per `traefik_mode` |
| `backend_servers` | `application`, `storage` | setzt `traefik_mode: backend` per group_var |
| `garage_servers` | `storage` | Single-Host-Wrapper für Garage-Role |
| `nextcloud_servers`, `collabora_servers`, `drawio_servers`, `authentik_servers`, `authentik_outpost_ldap_servers` | je nur `application` | Single-Host-Wrapper |
> **Unterschied zur `vagrant`-Inventory:** `vagrant` strukturiert
> Traefik anders — über die Children-Gruppen `traefik_servers_dmz` und
> `traefik_servers_backend` statt über `backend_servers` +
> `host_vars`-Override. Die beiden Topologien sind **strukturell
> inkompatibel**; ein 1:1-Mapping geht nicht. Siehe [§ 10](#10-walkthrough-neuen-demo-mandanten-anlegen)
> für die empfohlene Vorlage.
## 5. Service-Layout und Variablen-Verortung
```mermaid
flowchart TB
classDef rp fill:#fee2e2,stroke:#991b1b,color:#000
classDef ap fill:#dcfce7,stroke:#166534,color:#000
classDef st fill:#dbeafe,stroke:#1e40af,color:#000
classDef ext fill:#e9d5ff,stroke:#6b21a8,color:#000
Internet((Internet))
DNS["DNS ns1.digitalboard.ch<br/>RFC2136 TSIG<br/>Zone: demo-gymb._acme.digitalboard.ch<br/>CNAME-Bridge: _acme-challenge.*.gymb.souveredu.ch"]:::ext
BAO["OpenBao<br/>bao.digitalboard.ch<br/>mount: demo-gymburgdorf"]:::ext
subgraph RP["<b>reverseproxy</b> — traefik dmz"]
TRDMZ["traefik (file provider)<br/>📍 group_vars/traefik_servers/traefik.yml<br/>📍 host_vars/reverseproxy/traefik.yml<br/> → traefik_mode: dmz<br/> → traefik_dmz_exposed_services"]:::rp
end
subgraph APP["<b>application</b> — traefik backend"]
TRA["traefik (docker provider)<br/>📍 group_vars/backend_servers/traefik.yml"]:::ap
AK["authentik (OIDC + LDAP-Outpost-Backend)<br/>📍 host_vars/application/authentik.yml"]:::ap
AKO["authentik_outpost_ldap<br/>📍 host_vars/application/authentik_outpost_ldap.yml"]:::ap
NC["nextcloud<br/>📍 host_vars/application/nextcloud.yml"]:::ap
COL["collabora<br/>📍 host_vars/application/collabora.yml"]:::ap
DRW["drawio<br/>📍 host_vars/application/drawio.yml"]:::ap
end
subgraph ST["<b>storage</b> — traefik backend"]
TRS["traefik (docker provider)"]:::st
GAR["garage (S3)<br/>📍 host_vars/storage/garage.yml"]:::st
end
Internet -->|HTTPS :443| TRDMZ
TRDMZ -->|HTTP backend| TRA
TRDMZ -->|HTTP backend| TRS
TRA --> AK & AKO & NC & COL & DRW
TRS --> GAR
NC -. S3 .-> GAR
NC -. OIDC .-> AK
NC -. WOPI .-> COL
NC -. LDAP .-> AKO
AKO -. RPC + token .-> AK
TRDMZ -. ACME DNS-01 TSIG .-> DNS
TRDMZ -. hashi_vault acme-tsig .-> BAO
AK -. hashi_vault secrets .-> BAO
NC -. hashi_vault secrets .-> BAO
GAR -. hashi_vault secrets .-> BAO
```
> **Hinweis:** `opencloud`, `send` und `openforms` sind in
> [playbooks/site.yml](playbooks/site.yml) als Plays vorhanden, haben
> aber in `demo-gymburgdorf` aktuell keine entsprechende Gruppe in
> [hosts.yml](inventories/demo-gymburgdorf/hosts.yml) — die Plays
> laufen also durch, ohne Targets zu finden. Wenn diese Services für
> einen Mandanten gewünscht sind, jeweils `<service>_servers`-Gruppe in
> `hosts.yml` und `host_vars/application/<service>.yml` ergänzen.
>
> Der Host `turn` ist in `all_servers` und damit auch in
> `traefik_servers`, aber es gibt **keine** Service-Gruppe für ihn —
> aktuell läuft auf `turn` nur die `base`- und `traefik`-Rolle.
## 6. Deploy-Flow
Reihenfolge aus [playbooks/site.yml](playbooks/site.yml):
```mermaid
sequenceDiagram
participant U as User
participant A as ansible-playbook
participant V as OpenBao
participant H as Hosts
U->>U: bao login + export VAULT_TOKEN
U->>A: make deploy_site_demo_gymburgdorf
A->>A: lade vars: role defaults → group_vars/all → group_vars/&lt;groups&gt; → host_vars/&lt;host&gt;
A->>V: community.hashi_vault Lookups<br/>(acme-tsig, service-secrets)
V-->>A: secret values
A->>H: Play 1 — base (alle Hosts)
A->>H: Play 2 — traefik (alle Hosts: dmz auf reverseproxy, backend sonst)
A->>H: Play 3 — httpbin
A->>H: Play 4 — 389ds
A->>H: Play 5 — keycloak
A->>H: Play 6 — garage (storage)
A->>H: Play 7 — collabora (application)
A->>H: Play 8 — authentik (application)
A->>H: Play 9 — authentik_outpost_ldap (application)
A->>H: Play 10 — nextcloud (application)
A->>H: Play 11 — drawio (application)
A->>H: Play 12 — send
A->>H: Play 13 — openforms
A->>H: Play 14 — opencloud
```
Plays ohne passende Gruppen-Mitglieder (`httpbin_servers`, `ds389_servers`,
`keycloak_servers`, `send_servers`, `openforms_servers`,
`opencloud_servers` in dieser Inventory) laufen no-op durch.
`--diff` ist im Target gesetzt → Änderungen pro Task sichtbar.
## 7. Traefik-Modi (DMZ vs Backend)
**`traefik_mode: dmz`** — public-facing Reverse Proxy auf `reverseproxy`:
- **file provider** mit `services.yml` für statisches Routing.
- Kein Docker-Socket gemountet, keine lokalen Container.
- Routet zu `backend_host`-Adressen anderer Maschinen.
- Backends werden über `traefik_dmz_exposed_services` (Liste in
`host_vars/reverseproxy/`) deklariert. Selektive Backend-Auswahl
zusätzlich über `traefik_backend_servers_to_proxy`.
**`traefik_mode: backend`** — application/storage:
- Mountet `/var/run/docker.sock`.
- **docker provider**: Auto-Discovery via Container-Labels (`traefik.enable=true`).
- Services werden lokal exponiert; die DMZ-Traefik routet von aussen
dorthin (Klartext-HTTP, siehe [§ 8](#8-security-und-demo-only-defaults)).
**Beide Modi** unterstützen ACME via RFC2136 DNS Challenge oder
Self-Signed (`traefik_cert_mode: acme | selfsigned`).
## 8. Security und Demo-Only-Defaults
> Dieses Repo ist explizit für **Demo-Setups** gedacht. Alle Default-
> Werte in den Rollen sind unsicher und werden in `demo-*`-Inventories
> über Bao-Lookups oder host_vars überschrieben. Für Prod-Deployments
> gilt zusätzlich der Härtungs-Block weiter unten.
### Secret-Pattern (Bao-Lookup)
```yaml
# group_vars/.../<service>.yml oder host_vars/.../<service>.yml
authentik_secret_key: "{{ lookup('community.hashi_vault.hashi_vault',
vault_mount + '/data/authentik:secret_key',
url=vault_addr) }}"
```
- `vault_mount` und `vault_addr` aus
[group_vars/all/vault.yml](inventories/demo-gymburgdorf/group_vars/all/vault.yml).
- KV-v2-Pfade brauchen explizit `/data/` im Pfad — Ansible löst das
nicht selber auf.
- `vault_mount` ist pro Inventory eindeutig (`demo-gymburgdorf`,
`demo-phbern`, …) → Mandant-Isolation in Bao via Mount + Policy.
### Demo-Only-Defaults — Override-Pflicht
Diese Defaults in `digitalboard.core` sind unsicher. In jeder
**Prod-tauglichen** Deployment müssen sie via Bao-Lookup oder
host_var überschrieben werden:
| Variable | Default | Wo überschreiben |
|---|---|---|
| `keycloak_admin_password` | `changeme` | host_vars `keycloak_servers` |
| `keycloak_postgres_password` | `changeme` | dito |
| `authentik_secret_key` | `changeme-generate-a-random-string` | `host_vars/application/authentik.yml` |
| `authentik_postgres_password` | `changeme` | dito |
| `nextcloud_admin_password` | `admin` | `host_vars/application/nextcloud.yml` |
| `nextcloud_postgres_password` | `changeme` | dito |
| `nextcloud_s3_key` / `nextcloud_s3_secret` | `changeme` / `changeme` | dito |
| `garage_webui_password` | `admin` | `host_vars/storage/garage.yml` |
| `garage_rpc_secret` | `0123…cdef` (64-hex Konstante) | dito |
| `garage_admin_token` | identisch zu `rpc_secret` | dito |
| `garage_metrics_token` | identisch zu `rpc_secret` | dito |
> **Konvention:** Jeder Wert, der oben in der Tabelle steht, **muss**
> in `demo-*/host_vars/.../...yml` einen Bao-Lookup haben, bevor das
> Inventory als deploy-fähig gilt.
### Threat-Boundaries (Stand: Demo)
| Boundary | Status | Notiz |
|---|---|---|
| DMZ ↔ Backend (172.16.9 ↔ 172.16.19) | **Klartext-HTTP** | Auth-Bearer, OIDC-Code, Session-Cookies reisen unverschlüsselt. Für Demo ok, für Prod: mTLS oder WireGuard-Overlay. |
| Host-Firewall | **fehlt** | Die `base`-Rolle installiert kein UFW/nftables. Segmentation hängt am Hypervisor/VLAN. |
| SSH | `ansible_user: root` | Kein Bastion, kein Jumphost. Key-Distribution out-of-band. |
| Authentik-SPOF | **akzeptiert** | IDP und SP-Services auf demselben Host (`application`). Authentik-Ausfall = Login-Ausfall inkl. LDAP-Outpost. Kein Break-Glass-Pfad. |
| ACME-TSIG-Key | Bao-Lookup | Eine TSIG-Key pro Demo-Zone (`acme_update_key_demo_gymb`), Zonen-isoliert. Rotation manuell. |
| Backup/DR | **out-of-scope** | Garage `replication_factor: 1` (default), kein Postgres-Backup-Job, kein Bao-Snapshot-Cron. |
### Für Prod-Adaption ergänzen
- Host-FW (`base`-Rolle erweitern oder eigene `firewall`-Rolle).
- mTLS oder WireGuard zwischen DMZ und Backend.
- Authentik auf separaten Host, mit Recovery-Admin-Token.
- Bao-Policies pro Inventory-Mount (read-only für Deploy-Token,
write-only für Bootstrap-Job).
- Backup-Cron für Postgres + Garage + Bao.
- SSH-Bastion + Key-Rotation.
## 9. Variablen-Cheatsheet
| Variable | Wohin in `demo-gymburgdorf/` | Warum |
|---|---|---|
| `vault_addr`, `vault_mount` | `group_vars/all/vault.yml` | Bao-Endpoint gilt site-weit |
| `docker_registry_mirrors` | `group_vars/all/docker.yml` | Pulls aus Mirror auf allen Hosts |
| `traefik_acme_*`, `traefik_use_ssl`, `traefik_cert_mode` | `group_vars/traefik_servers/traefik.yml` | Gilt für alle Traefik-Instanzen (dmz + backend) |
| `traefik_mode: backend` | `group_vars/backend_servers/traefik.yml` | Default für app + storage |
| `traefik_mode: dmz` | `host_vars/reverseproxy/traefik.yml` | Host-spezifischer Override |
| `traefik_dmz_exposed_services` | `host_vars/reverseproxy/` | Liste der DMZ-Backends — nur dort sinnvoll |
| `nextcloud_*`, `authentik_*`, `collabora_*`, `drawio_*` | `host_vars/application/<service>.yml` | Service läuft auf `application` |
| `garage_*` | `host_vars/storage/garage.yml` | Service läuft auf `storage` |
| Secrets (Passwords, Tokens, Keys) | inline-Variable mit `lookup('community.hashi_vault.hashi_vault', …)` | Single source of truth via Bao |
## 10. Walkthrough: Neuen Demo-Mandanten anlegen
Empfohlene Vorlage: **`demo-gymburgdorf`** (nicht `vagrant`, weil die
Gruppen-Topologie inkompatibel ist).
1. **Inventory kopieren:**
```bash
cp -r inventories/demo-gymburgdorf inventories/demo-<kunde>
```
2. **`hosts.yml` anpassen:** IPs, Hostnames pro Host.
3. **`group_vars/all/vault.yml`** — `vault_mount` auf den neuen
Mandant-Mount setzen (`demo-<kunde>`).
4. **`group_vars/traefik_servers/traefik.yml`** — `traefik_acme_dns_zone`
und `traefik_acme_tsig_*`-Lookup-Pfade auf die neue Zone /
den neuen Bao-Pfad biegen.
5. **`host_vars/application/*.yml`** und **`host_vars/storage/*.yml`**
durchgehen: FQDNs auf das neue Domain-Pattern (z. B.
`*.<kunde>.souveredu.ch`), Bao-Lookup-Pfade auf `demo-<kunde>/data/…`.
6. **OpenBao vorbereiten** (out-of-band, nicht via Ansible):
- Neuen KV-v2-Mount `demo-<kunde>` anlegen.
- Secrets schreiben: `acme-tsig`, `authentik`, `nextcloud`,
`garage`, … (siehe [§ 8](#8-security-und-demo-only-defaults)
für die Override-Pflicht-Liste).
- Policy für den Deploy-Token: read auf `demo-<kunde>/data/*`.
7. **DNS:** TSIG-Update-Zone (`demo-<kunde>._acme.digitalboard.ch`) bei
`ns1.digitalboard.ch` anlegen, CNAMEs `_acme-challenge.*.<kunde>.<tld>`
dorthin.
8. **Makefile** — neues Target nach Vorbild von
`deploy_site_demo_gymburgdorf` ergänzen und in `deploy_site_demo`
einreihen.
9. **Smoke-Test:** `ansible all -i inventories/demo-<kunde>/hosts.yml -m ping`.
10. **Deploy:** Bao-Login + `make deploy_site_demo_<kunde>`.
## 11. Bekannte Lücken und Trade-offs
- **`opencloud`-Inventory in `demo-gymburgdorf`:** keine
`opencloud_servers`-Gruppe vorhanden. Wenn benötigt, ergänzen wie in
[§ 5](#5-service-layout-und-variablen-verortung) beschrieben.
- **`turn`-Host:** in DMZ definiert, aber keine STUN/TURN-Rolle in
[playbooks/site.yml](playbooks/site.yml). Wird aktuell nur per
`base`+`traefik` provisioniert.
- **Idempotenz:** Rollen sind Docker-Compose-basiert; Re-Runs führen
ggf. Container-Restarts aus, wenn sich Compose-Inputs ändern. Kein
spezieller Rollback-Mechanismus — bei Fehlschlag manuell auf den
vorigen Stand zurücksetzen.
- **TLS-Renewal:** Erfolgt durch Traefik intern via ACME. Kein
externer Renew-Cron im Repo.
- **CI/Testing:** Aktuell nicht im Repo. Smoke-Test via `make ping_demo`.
- **Logs:** Traefik läuft in `demo-gymburgdorf` und `vagrant` mit
`traefik_log_level: DEBUG` (Role-Default ist `INFO`) — vor
Prod-Adaption auf `INFO` oder `WARN` reduzieren.