diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 807f408..a98825c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,49 +1,151 @@ - -# Architekturskizze — `demo-gymburgdorf` + +# Architektur — `reference-ansible` -Diese Skizze zeigt, wie das `reference-ansible`-Repo am Beispiel der -Inventory `demo-gymburgdorf` funktioniert: welche Hosts existieren, -welche Rollen darauf laufen, wo welche Variablen hingehören und wie -Secrets aus OpenBao gelookupt werden. +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. -## 1. Variablen-Hierarchie (Ansible Precedence) +> **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). -```mermaid -flowchart TB - classDef rolelayer fill:#fef3c7,stroke:#92400e,color:#000 - classDef grouplayer fill:#dbeafe,stroke:#1e40af,color:#000 - classDef hostlayer fill:#dcfce7,stroke:#166534,color:#000 - classDef vaultlayer fill:#fee2e2,stroke:#991b1b,color:#000 +**Letzte Aktualisierung:** 2026-05-18 · **Owner:** @sbaerlocher - R["role defaults/main.yml
(niedrigste Precedence)
~180 Variablen über 12 Rollen
z.B. traefik_use_ssl: false
keycloak_admin_password: changeme"]:::rolelayer +## Inhalt - GA["group_vars/all/
docker.yml → docker_registry_mirrors
vault.yml → vault_addr, vault_mount"]:::grouplayer +- [§ 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) - GT["group_vars/traefik_servers/
traefik.yml
traefik_use_ssl, traefik_cert_mode: acme
traefik_acme_dns_zone
traefik_acme_tsig_* (Vault-Lookup!)"]:::grouplayer +## 0. Glossar - GB["group_vars/backend_servers/
traefik.yml → traefik_mode: backend"]:::grouplayer +| 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.` 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. | - HR["host_vars/reverseproxy/
traefik.yml → traefik_mode: dmz"]:::hostlayer +## 1. Repo-Layout und Roles-Herkunft - HA["host_vars/application/ (fehlt aktuell!)
FQDNs, OIDC-Clients, DB-Passwords
nextcloud_domain, authentik_domain, ..."]:::hostlayer - - HS["host_vars/storage/ (fehlt aktuell!)
garage_s3_domain, garage_*_token
traefik_dmz_exposed_services (für DMZ)"]:::hostlayer - - V["HashiCorp Vault / OpenBao
bao.digitalboard.ch
mount: demo-gymburgdorf
z.B. demo-gymburgdorf/data/acme-tsig"]:::vaultlayer - - R --> GA --> GT --> GB --> HR - GB --> HA - GB --> HS - GT -.Lookup zur Laufzeit.-> V - HA -.Lookup zur Laufzeit.-> V - HS -.Lookup zur Laufzeit.-> V +```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 ``` -Höhere Ebene überschreibt tiefere. `host_vars/reverseproxy/traefik_mode: dmz` -schlägt also `group_vars/backend_servers/traefik_mode: backend` — -möglich, weil `reverseproxy` *nicht* in `backend_servers` ist. +> **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.`. -## 2. Inventory-Topologie demo-gymburgdorf +## 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 +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["role defaults
(niedrigste Precedence)
collections/.../roles/<r>/defaults/main.yml"]:::role + GA["group_vars/all/
vault.yml, docker.yml"]:::group + GG["group_vars/<group>/
traefik_servers/, backend_servers/
(parallele Gruppen, gemerged via
ansible_group_priority)"]:::group + HV["host_vars/<host>/
(höchste der drei Inventory-Quellen)"]:::host + BAO["OpenBao
Lookup zur Laufzeit"]:::vault + + R --> |"<wird überschrieben von>"| GA + GA --> |"<wird überschrieben von>"| GG + GG --> |"<wird überschrieben von>"| HV + HV -.community.hashi_vault.-> BAO + GG -.community.hashi_vault.-> BAO +``` + +**Wichtige Eigenschaften:** + +- Mehrere `group_vars//` 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//` 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 @@ -52,33 +154,40 @@ flowchart LR classDef stor fill:#dbeafe,stroke:#1e40af,color:#000 classDef turn fill:#fef9c3,stroke:#854d0e,color:#000 - subgraph ALL["group: all_servers (alle Hosts)"] + subgraph ALL["group: all_servers"] direction LR - subgraph DMZ["DMZ-Segment 172.16.9.0/24"] + subgraph DMZ["DMZ 172.16.9.0/24"] RP["reverseproxy
172.16.9.111
traefik_mode: dmz"]:::dmz - TURN["turn
172.16.9.112
(STUN/TURN)"]:::turn + TURN["turn
172.16.9.112
(noch keine Rolle in site.yml)"]:::turn end - subgraph BE["Backend-Segment 172.16.19.0/24
(group: backend_servers)"] - APP["application
172.16.19.101
traefik_mode: backend
+ nextcloud, opencloud,
collabora, drawio,
authentik, authentik_outpost_ldap"]:::app + subgraph BE["Backend 172.16.19.0/24
group: backend_servers"] + APP["application
172.16.19.101
traefik_mode: backend
+ authentik, authentik_outpost_ldap,
nextcloud, collabora, drawio"]:::app ST["storage
172.16.19.102
traefik_mode: backend
+ garage (S3)"]:::stor end end - RP -.HTTP/HTTPS reverse proxy.-> APP - RP -.HTTP/HTTPS reverse proxy.-> ST + RP -.HTTPS in, HTTP out.-> APP + RP -.HTTPS in, HTTP out.-> ST ``` -Gruppen-Mitgliedschaften (`hosts.yml`): +**Gruppen-Mitgliedschaften (aus [hosts.yml](inventories/demo-gymburgdorf/hosts.yml)):** -- `traefik_servers` ⊇ `all_servers` → **alle 4 Hosts** bekommen Traefik - (DMZ-Modus für `reverseproxy`, Backend-Modus für `application`/`storage`). -- `backend_servers = {application, storage}` → setzt - `traefik_mode: backend` via group_vars. -- Service-Gruppen (`nextcloud_servers`, `garage_servers`, …) sind - Single-Host-Wrapper, mit denen `playbooks/site.yml` gezielt - deploybare Rollen targetet. +| 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 | -## 3. Service-Layout & Variablen-Verortung +> **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 @@ -88,120 +197,257 @@ flowchart TB classDef ext fill:#e9d5ff,stroke:#6b21a8,color:#000 Internet((Internet)) - DNS["DNS ns1.digitalboard.ch
RFC2136 TSIG (key: acme_update_key_demo_gymb)
dynamic zone: demo-gymb._acme.digitalboard.ch
CNAME-bridge: _acme-challenge.*.gymb.souveredu.ch"]:::ext + DNS["DNS ns1.digitalboard.ch
RFC2136 TSIG
Zone: demo-gymb._acme.digitalboard.ch
CNAME-Bridge: _acme-challenge.*.gymb.souveredu.ch"]:::ext BAO["OpenBao
bao.digitalboard.ch
mount: demo-gymburgdorf"]:::ext subgraph RP["reverseproxy — traefik dmz"] - TRDMZ["traefik (file provider)
📍 group_vars/traefik_servers/traefik.yml
→ acme, tsig, ssl
📍 host_vars/reverseproxy/traefik.yml
→ traefik_mode: dmz
📍 host_vars/reverseproxy/...
→ traefik_dmz_exposed_services"]:::rp + TRDMZ["traefik (file provider)
📍 group_vars/traefik_servers/traefik.yml
📍 host_vars/reverseproxy/traefik.yml
→ traefik_mode: dmz
→ traefik_dmz_exposed_services"]:::rp end - subgraph APP["application — backend"] - TRA["traefik (docker provider)
📍 group_vars/backend_servers
→ traefik_mode: backend"]:::ap - NC["nextcloud
📍 host_vars/application/nextcloud.yml
domain, postgres_pw, oidc, s3, ldap"]:::ap - OC["opencloud
📍 host_vars/application/opencloud.yml
oidc_issuer, ldap, s3"]:::ap - AK["authentik
📍 host_vars/application/authentik.yml
secret_key, postgres, ldap_apps, oidc_apps"]:::ap - AKO["authentik_outpost_ldap
📍 host_vars/application/authentik_outpost_ldap.yml
host, token"]:::ap - COL["collabora
📍 host_vars/application/collabora.yml
domain, allowed_domains"]:::ap + subgraph APP["application — traefik backend"] + TRA["traefik (docker provider)
📍 group_vars/backend_servers/traefik.yml"]:::ap + AK["authentik (OIDC + LDAP-Outpost-Backend)
📍 host_vars/application/authentik.yml"]:::ap + AKO["authentik_outpost_ldap
📍 host_vars/application/authentik_outpost_ldap.yml"]:::ap + NC["nextcloud
📍 host_vars/application/nextcloud.yml"]:::ap + COL["collabora
📍 host_vars/application/collabora.yml"]:::ap DRW["drawio
📍 host_vars/application/drawio.yml"]:::ap end - subgraph ST["storage — backend"] + subgraph ST["storage — traefik backend"] TRS["traefik (docker provider)"]:::st - GAR["garage (S3)
📍 host_vars/storage/garage.yml
s3_domain, rpc_secret,
admin_token, s3_keys"]:::st + GAR["garage (S3)
📍 host_vars/storage/garage.yml"]:::st end Internet -->|HTTPS :443| TRDMZ TRDMZ -->|HTTP backend| TRA TRDMZ -->|HTTP backend| TRS - TRA --> NC & OC & AK & COL & DRW & AKO + TRA --> AK & AKO & NC & COL & DRW TRS --> GAR NC -. S3 .-> GAR - OC -. S3 .-> GAR NC -. OIDC .-> AK - OC -. OIDC .-> AK NC -. WOPI .-> COL - OC -. WOPI .-> COL NC -. LDAP .-> AKO - OC -. LDAP .-> AKO AKO -. RPC + token .-> AK TRDMZ -. ACME DNS-01 TSIG .-> DNS - TRDMZ -. lookup acme-tsig .-> BAO - AK -. lookup secrets .-> BAO - NC -. lookup secrets .-> BAO - GAR -. lookup secrets .-> BAO + TRDMZ -. hashi_vault acme-tsig .-> BAO + AK -. hashi_vault secrets .-> BAO + NC -. hashi_vault secrets .-> BAO + GAR -. hashi_vault secrets .-> BAO ``` -## 4. Deploy-Flow +> **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 `_servers`-Gruppe in +> `hosts.yml` und `host_vars/application/.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 (make) - participant M as Makefile + participant U as User participant A as ansible-playbook participant V as OpenBao participant H as Hosts - U->>M: make bao - M->>V: bao login (OIDC) - V-->>M: VAULT_TOKEN - U->>M: make deploy_site_demo_gymburgdorf - M->>A: ansible-playbook site.yml -i inventories/demo-gymburgdorf/hosts.yml - A->>A: lade group_vars/all → group_vars/traefik_servers → group_vars/backend_servers → host_vars/* - A->>V: community.hashi_vault Lookups (acme-tsig, secrets) + 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/<groups> → host_vars/<host> + A->>V: community.hashi_vault Lookups
(acme-tsig, service-secrets) V-->>A: secret values - A->>H: Play "base" → all_servers - A->>H: Play "traefik" → traefik_servers (dmz auf reverseproxy, backend auf application/storage) - A->>H: Play "garage" → storage - A->>H: Play "authentik / nextcloud / collabora / ..." → application + 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 ``` -## 5. Variablen-Cheatsheet — wo gehört was hin? +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/.../.yml oder host_vars/.../.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` ✅ | Vault-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, welche Backend-Services die DMZ proxyt (nur dort sinnvoll) | -| `nextcloud_*`, `authentik_*`, `opencloud_*`, `collabora_*`, `drawio_*` | **`host_vars/application/.yml`** | Service läuft genau auf `application` | -| `garage_*` | **`host_vars/storage/garage.yml`** | Service läuft genau auf `storage` | -| Secrets (Passwords, Tokens, Keys) | Inline-Variable mit `lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/', url=vault_addr)` | Single source of truth; Pattern wie bei `_acme_tsig` | +| `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/.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 | -## 6. Traefik-Modi (zentral für die Architektur) +## 10. Walkthrough: Neuen Demo-Mandanten anlegen -**`traefik_mode: dmz`** (Public-facing Reverse Proxy auf `reverseproxy`): +Empfohlene Vorlage: **`demo-gymburgdorf`** (nicht `vagrant`, weil die +Gruppen-Topologie inkompatibel ist). -- Aggregiert Services von Backend-Servern via - `traefik_dmz_exposed_services` (Host-Variablen). -- Nutzt **file provider** mit `services.yml` für statisches Routing. -- Kein Docker-Socket gemountet — keine lokalen Container. -- Routet zu `backend_host` auf anderen Maschinen. -- Selektive Backend-Auswahl via `traefik_backend_servers_to_proxy`. +1. **Inventory kopieren:** -**`traefik_mode: backend`** (Application/Storage Server): + ```bash + cp -r inventories/demo-gymburgdorf inventories/demo- + ``` -- Mountet Docker-Socket (`/var/run/docker.sock`). -- Nutzt **docker provider** für Auto-Discovery lokaler Container. -- Services mit Label `traefik.enable=true` werden automatisch exponiert. -- Beide Modi unterstützen ACME (RFC2136 DNS Challenge) oder Self-Signed. +2. **`hosts.yml` anpassen:** IPs, Hostnames pro Host. -## 7. Was im aktuellen `demo-gymburgdorf` noch fehlt +3. **`group_vars/all/vault.yml`** — `vault_mount` auf den neuen + Mandant-Mount setzen (`demo-`). -Im Vergleich zur `vagrant`-Inventory (Referenz) fehlen für eine -vollständige Deployment: +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. -- `host_vars/application/main.yml` — Backbone-Vars für den Host - (FQDN-Pattern, gemeinsame Defaults). -- `host_vars/application/{nextcloud,opencloud,authentik,authentik_outpost_ldap,collabora,drawio}.yml` - — die service-spezifischen Konfigurationen. -- `host_vars/storage/{main.yml,garage.yml}` — Garage-Cluster-Setup. -- `host_vars/reverseproxy/<…>.yml` mit `traefik_dmz_exposed_services` - — sonst routet die DMZ nichts. +5. **`host_vars/application/*.yml`** und **`host_vars/storage/*.yml`** + durchgehen: FQDNs auf das neue Domain-Pattern (z. B. + `*..souveredu.ch`), Bao-Lookup-Pfade auf `demo-/data/…`. -Die `vagrant`-Inventory ist das Template: dieselbe Struktur auf -`application`/`storage` mappen, FQDNs auf `*.gymb.souveredu.ch` -umstellen, Secrets durch Bao-Lookups ersetzen. +6. **OpenBao vorbereiten** (out-of-band, nicht via Ansible): + - Neuen KV-v2-Mount `demo-` 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-/data/*`. + +7. **DNS:** TSIG-Update-Zone (`demo-._acme.digitalboard.ch`) bei + `ns1.digitalboard.ch` anlegen, CNAMEs `_acme-challenge.*..` + 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-/hosts.yml -m ping`. + +10. **Deploy:** Bao-Login + `make deploy_site_demo_`. + +## 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. diff --git a/Makefile b/Makefile index 67b2a75..ffa5afe 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ ping_demo: deploy_site_demo_gymburgdorf: echo "deploying demo site gymburgdorf" - ansible-playbook playbooks/site.yml -i inventories/demo-gymburgdorf/hosts.yml --diff + ansible-playbook playbooks/site.yml -i inventories/demo-gymburgdorf/hosts.yml --diff deploy_site_demo_mbazürich: echo "deploying demo site mbazürich" diff --git a/README.md b/README.md index 4bfdb6d..fead38b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,61 @@ # reference-ansible +Ansible-Setup für Demo-Deployments (`demo-gymburgdorf`, +`demo-mbazürich`, `demo-phbern`) und lokale Vagrant-Tests. Rollen +kommen aus der Collection +[`digitalboard.core`](https://git.digitalboard.ch/Digitalboard/digitalboard.core) +(via [requirements.yml](requirements.yml)). +> Architektur, Variablen-Hierarchie, Service-Topologie und der +> Walkthrough zum Aufsetzen neuer Mandanten: siehe +> **[ARCHITECTURE.md](ARCHITECTURE.md)**. -### Secrets -Secrets are managed using [OpenBao](https://bao.digitalboard.ch). -The bao CLI needs to be installed. e.g `sudo pacman -S openbao python-hvac` +## Voraussetzungen + +- `ansible` (Core ≥ 2.15) +- `bao` CLI ([OpenBao](https://openbao.org/)) — z. B. + `sudo pacman -S openbao python-hvac` (Arch) oder Homebrew +- `python-hvac` (für `community.hashi_vault` Lookups) + +## Setup + +```bash +make install # installiert digitalboard.core + community.hashi_vault nach ./collections/ +``` + +## Secrets (OpenBao) + +Vor jedem Deploy in **derselben Shell** authentisieren: -Authenticate and export token before running playbooks: ```bash export BAO_ADDR=https://bao.digitalboard.ch bao login -method=oidc -path=Digitalboard export VAULT_TOKEN=$(bao print token) -``` \ No newline at end of file +``` + +> ⚠️ `make bao` allein reicht **nicht** — jedes `make`-Target läuft in +> einer neuen Shell, der dort gesetzte `VAULT_TOKEN` lebt nur während +> `make bao` selbst. Entweder die drei Befehle oben manuell im Shell +> ausführen oder `make bao deploy_site_demo_gymburgdorf` als **einen** +> Aufruf chainen. + +## Deploy + +```bash +make ping_demo # Smoke-Test gegen alle Demo-Inventories +make deploy_site_demo_gymburgdorf # einzelnes Demo-Site +make deploy_site_demo # alle drei Demo-Sites +``` + +Auf macOS setzt das [Makefile](Makefile) zusätzlich +`OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` — ohne diese Env-Var +crashen Ansible-Forks beim ersten `community.hashi_vault`-Lookup. + +## Inventories + +| Inventory | Zweck | +|---|---| +| [`inventories/demo-gymburgdorf/`](inventories/demo-gymburgdorf/) | Demo-Mandant — als Vorlage für neue Mandanten empfohlen, siehe [ARCHITECTURE.md § 10](ARCHITECTURE.md#10-walkthrough-neuen-demo-mandanten-anlegen) | +| [`inventories/demo-mbazürich/`](inventories/demo-mbazürich/) | Demo-Mandant | +| [`inventories/demo-phbern/`](inventories/demo-phbern/) | Demo-Mandant | +| [`inventories/vagrant/`](inventories/vagrant/) | lokale Test-VMs; **inkompatible Gruppen-Topologie** zu den Demo-Inventories | diff --git a/inventories/demo-gymburgdorf/group_vars/traefik_servers/traefik.yml b/inventories/demo-gymburgdorf/group_vars/traefik_servers/traefik.yml index c42dc79..de50728 100644 --- a/inventories/demo-gymburgdorf/group_vars/traefik_servers/traefik.yml +++ b/inventories/demo-gymburgdorf/group_vars/traefik_servers/traefik.yml @@ -2,6 +2,7 @@ _acme_tsig: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data traefik_use_ssl: true traefik_cert_mode: "acme" +traefik_ssl_email: "hostmaster@digitalboard.ch" traefik_log_level: DEBUG traefik_network: proxy @@ -11,3 +12,9 @@ traefik_acme_tsig_algorithm: "hmac-sha256" traefik_acme_tsig_key: "{{ _acme_tsig.tsig_key }}" traefik_acme_tsig_secret: "{{ _acme_tsig.tsig_secret }}" +# UDP/53 egress from the traefik container reaches ns1.digitalboard.ch +# unreliably (i/o timeouts on lego's recursive SOA pre-check), while +# TCP/53 to the same nameserver is open. Force lego to do its DNS +# lookups over TCP so the DNS-01 challenge can proceed. +traefik_acme_tcp_only: true + diff --git a/inventories/demo-gymburgdorf/host_vars/application/authentik.yml b/inventories/demo-gymburgdorf/host_vars/application/authentik.yml index aba8775..fd94e37 100644 --- a/inventories/demo-gymburgdorf/host_vars/application/authentik.yml +++ b/inventories/demo-gymburgdorf/host_vars/application/authentik.yml @@ -5,7 +5,13 @@ # nextcloud_oidc_secret _authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/authentik', url=vault_addr) }}" -authentik_domain: "auth.gymb.souveredu.ch" +# First entry is the canonical public FQDN. Additional entries cover +# internal *.int.* names so server-to-server traffic (e.g. the LDAP +# outpost) hits authentik on a name with a valid internal cert and +# skips the DMZ hop. +authentik_domains: + - "auth.gymb.souveredu.ch" + - "auth.int.gymb.souveredu.ch" authentik_secret_key: "{{ _authentik.secret_key }}" authentik_postgres_password: "{{ _authentik.postgres_password }}" @@ -20,7 +26,9 @@ authentik_ldap_outpost: name: "ldap-outpost" token: "{{ _authentik.ldap_outpost_token }}" config: - authentik_host: "https://auth.gymb.souveredu.ch/" + # Outpost pulls config from authentik over the internal FQDN — keeps + # the round-trip in the LAN with a valid cert. + authentik_host: "https://auth.int.gymb.souveredu.ch/" log_level: "info" # OIDC clients diff --git a/inventories/demo-gymburgdorf/host_vars/application/authentik_outpost_ldap.yml b/inventories/demo-gymburgdorf/host_vars/application/authentik_outpost_ldap.yml index c02b660..562f979 100644 --- a/inventories/demo-gymburgdorf/host_vars/application/authentik_outpost_ldap.yml +++ b/inventories/demo-gymburgdorf/host_vars/application/authentik_outpost_ldap.yml @@ -3,5 +3,5 @@ # authenticate against the authentik server it talks to. _authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/authentik', url=vault_addr) }}" -authentik_outpost_ldap_host: "https://auth.gymb.souveredu.ch" +authentik_outpost_ldap_host: "https://auth.int.gymb.souveredu.ch" authentik_outpost_ldap_token: "{{ _authentik.ldap_outpost_token }}" diff --git a/inventories/demo-gymburgdorf/host_vars/application/collabora.yml b/inventories/demo-gymburgdorf/host_vars/application/collabora.yml index b02760c..da251df 100644 --- a/inventories/demo-gymburgdorf/host_vars/application/collabora.yml +++ b/inventories/demo-gymburgdorf/host_vars/application/collabora.yml @@ -1,8 +1,16 @@ --- -collabora_domain: "office.gymb.souveredu.ch" +# First entry is the canonical public FQDN. Additional entries cover +# internal *.int.* names so nextcloud's WOPI discovery hits collabora +# in the LAN with a valid internal cert. +collabora_domains: + - "office.gymb.souveredu.ch" + - "office.int.gymb.souveredu.ch" +# Hosts allowed to issue WOPI calls. Both names are listed so collabora +# accepts the callback from nextcloud regardless of which FQDN it uses. collabora_allowed_domains: - "cloud.gymb.souveredu.ch" + - "cloud.int.gymb.souveredu.ch" collabora_frame_ancestors: - "cloud.gymb.souveredu.ch" diff --git a/inventories/demo-gymburgdorf/host_vars/application/nextcloud.yml b/inventories/demo-gymburgdorf/host_vars/application/nextcloud.yml index a1719f4..06bb35a 100644 --- a/inventories/demo-gymburgdorf/host_vars/application/nextcloud.yml +++ b/inventories/demo-gymburgdorf/host_vars/application/nextcloud.yml @@ -4,16 +4,31 @@ _nextcloud: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/nextcloud', url=vault_addr) }}" _authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/authentik', url=vault_addr) }}" -nextcloud_domain: "cloud.gymb.souveredu.ch" +# First entry is the canonical public FQDN (used for OVERWRITEHOST and +# OIDC redirects). Additional entries cover internal *.int.* names so +# collabora's WOPI callbacks hit nextcloud on a name with a valid +# internal cert instead of routing through the DMZ. +nextcloud_domains: + - "cloud.gymb.souveredu.ch" + - "cloud.int.gymb.souveredu.ch" nextcloud_postgres_password: "{{ _nextcloud.postgres_password }}" nextcloud_admin_user: admin nextcloud_admin_password: "{{ _nextcloud.admin_password }}" nextcloud_enable_notify_push: true +# Use the internal FQDN for the notify_push setup check so curl from the +# nextcloud container hits the local traefik directly instead of +# hairpinning through the DMZ reverseproxy. +nextcloud_notify_push_domain: "cloud.int.gymb.souveredu.ch" # Collabora integration +# wopi_url (server-to-server: nextcloud calls collabora for discovery / +# capabilities) goes to the internal FQDN so the call stays in the LAN. +# public_wopi_url is what the browser loads the office iframe from — that +# stays on the public name reachable through the DMZ. nextcloud_enable_collabora: true -nextcloud_collabora_domain: "office.gymb.souveredu.ch" +nextcloud_collabora_domain: "office.int.gymb.souveredu.ch" +nextcloud_collabora_public_domain: "office.gymb.souveredu.ch" # Draw.io integration nextcloud_enable_drawio: true @@ -30,12 +45,14 @@ nextcloud_apps_to_install: - files_lock - notify_push -# S3 primary storage via Garage +# S3 primary storage via Garage — server-to-server, so use the internal FQDN. +# Resolves through the internal DNS to the storage host and presents a valid +# cert from the local traefik on storage. nextcloud_use_s3_storage: true nextcloud_s3_key: "{{ lookup('digitalboard.core.garage_credentials', 'nextcloud', host='storage')['key_id'] }}" nextcloud_s3_secret: "{{ lookup('digitalboard.core.garage_credentials', 'nextcloud', host='storage')['secret_key'] }}" nextcloud_s3_bucket: "nextcloud" -nextcloud_s3_host: "{{ hostvars['storage']['garage_s3_domain'] }}" +nextcloud_s3_host: "s3.int.gymb.souveredu.ch" nextcloud_s3_port: 443 nextcloud_s3_ssl: true nextcloud_s3_usepath_style: true @@ -81,6 +98,11 @@ nextcloud_oidc_providers: display_name: "Login with Authentik" client_id: nextcloud client_secret: "{{ _authentik.nextcloud_oidc_secret }}" + # Stays on the public FQDN: user_oidc validates the iss claim against + # the discovery host, and authentik returns iss based on the request + # host — using auth.int.* would break the iss match with what the + # browser sees (auth.gymb.*). Routed via the DMZ for now; revisit if + # this becomes a bottleneck. discovery_url: "https://auth.gymb.souveredu.ch/application/o/nextcloud/.well-known/openid-configuration" scope: "openid email profile" unique_uid: true diff --git a/inventories/demo-gymburgdorf/host_vars/application/traefik.yml b/inventories/demo-gymburgdorf/host_vars/application/traefik.yml new file mode 100644 index 0000000..d18cd49 --- /dev/null +++ b/inventories/demo-gymburgdorf/host_vars/application/traefik.yml @@ -0,0 +1,29 @@ +--- +# Services hosted on `application` that the DMZ reverseproxy should +# forward public traffic to. The DMZ traefik picks this up via +# hostvars[backend].traefik_dmz_exposed_services and renders a router + +# service for each entry into /config/services.yml. +traefik_dmz_exposed_services: + - name: authentik + domain: auth.gymb.souveredu.ch + backend_host: auth.int.gymb.souveredu.ch + port: 443 + protocol: https + - name: nextcloud + domain: cloud.gymb.souveredu.ch + backend_host: cloud.int.gymb.souveredu.ch + port: 443 + protocol: https + - name: collabora + domain: office.gymb.souveredu.ch + backend_host: office.int.gymb.souveredu.ch + port: 443 + protocol: https + - name: drawio + domain: draw.gymb.souveredu.ch + # No internal FQDN/cert for drawio yet — proxy by IP. Combined + # with serversTransport `insecureSkipVerify` (handled by the + # selfsigned-mode branch in the template), or accept the route's + # 500 until the cert is wired up. + port: 443 + protocol: https diff --git a/inventories/demo-gymburgdorf/host_vars/reverseproxy/traefik.yml b/inventories/demo-gymburgdorf/host_vars/reverseproxy/traefik.yml index c43e6be..48f6d08 100644 --- a/inventories/demo-gymburgdorf/host_vars/reverseproxy/traefik.yml +++ b/inventories/demo-gymburgdorf/host_vars/reverseproxy/traefik.yml @@ -1,28 +1,22 @@ --- traefik_mode: dmz -traefik_dmz_exposed_services: - - name: authentik - domain: auth.gymb.souveredu.ch - port: 443 - protocol: https - - name: nextcloud - domain: cloud.gymb.souveredu.ch - port: 443 - protocol: https - - name: collabora - domain: office.gymb.souveredu.ch - port: 443 - protocol: https - - name: drawio - domain: draw.gymb.souveredu.ch - port: 443 - protocol: https - - name: garage-webui - domain: console.s3.gymb.souveredu.ch - port: 443 - protocol: https - - name: garage-s3 - domain: s3.gymb.souveredu.ch - port: 443 - protocol: https +# The DMZ traefik discovers which services to expose by reading +# traefik_dmz_exposed_services from each backend host's host_vars +# (application/traefik.yml, storage/traefik.yml). See the role's +# tasks/main.yml — set_fact "Build service registry from backend +# servers (DMZ mode)". + +# From the DMZ network the public ns1 IP (193.43.183.169) is not +# reachable on port 53, but the internal address (172.16.9.169) is. +# Override the group-level traefik_acme_dns_nameserver from bao so +# lego's RFC2136 updates land at the internal interface. The TSIG +# key/secret are the same; only the transport target changes. +traefik_acme_dns_nameserver: "172.16.9.169" + +# Lego's propagation check normally polls the NS hostnames listed in +# the zone's SOA (ns1.digitalboard.ch.) — which resolves to the +# public IP that's unreachable from this DMZ host. Skip that check; +# lego still polls via the resolver above before asking LE to +# validate. +traefik_acme_disable_ans_checks: true diff --git a/inventories/demo-gymburgdorf/host_vars/storage/garage.yml b/inventories/demo-gymburgdorf/host_vars/storage/garage.yml index bc392c9..a6e0d79 100644 --- a/inventories/demo-gymburgdorf/host_vars/storage/garage.yml +++ b/inventories/demo-gymburgdorf/host_vars/storage/garage.yml @@ -3,7 +3,12 @@ # rpc_secret, admin_token, metrics_token, webui_password _garage: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/garage', url=vault_addr) }}" -garage_s3_domain: "s3.gymb.souveredu.ch" +# First entry is the canonical public S3 FQDN. Additional entries +# cover internal *.int.* names so server-to-server S3 traffic (e.g. +# nextcloud → garage) stays in the LAN. +garage_s3_domains: + - "s3.gymb.souveredu.ch" + - "s3.int.gymb.souveredu.ch" garage_webui_domain: "console.s3.gymb.souveredu.ch" garage_use_ssl: true garage_webui_enabled: true diff --git a/inventories/demo-gymburgdorf/host_vars/storage/traefik.yml b/inventories/demo-gymburgdorf/host_vars/storage/traefik.yml new file mode 100644 index 0000000..6cd16f5 --- /dev/null +++ b/inventories/demo-gymburgdorf/host_vars/storage/traefik.yml @@ -0,0 +1,16 @@ +--- +# Services hosted on `storage` that the DMZ reverseproxy should forward +# public traffic to. See application/traefik.yml for the mechanism. +traefik_dmz_exposed_services: + - name: garage-s3 + domain: s3.gymb.souveredu.ch + backend_host: s3.int.gymb.souveredu.ch + port: 443 + protocol: https + - name: garage-webui + domain: console.s3.gymb.souveredu.ch + # No internal FQDN/cert SAN for console.s3 yet — would need an + # extra_domain on garage-webui. Until then this route will 500 + # against the storage backend (cert mismatch on raw IP). + port: 443 + protocol: https diff --git a/playbooks/site.yml b/playbooks/site.yml index ba9f7ca..13b0a7c 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -71,17 +71,17 @@ roles: - digitalboard.core.drawio -- name: Deploy send service - hosts: send_servers - become: yes - roles: - - digitalboard.core.send +# - name: Deploy send service +# hosts: send_servers +# become: yes +# roles: +# - digitalboard.core.send -- name: Deploy openforms service - hosts: openforms_servers - become: yes - roles: - - digitalboard.core.openforms +# - name: Deploy openforms service +# hosts: openforms_servers +# become: yes +# roles: +# - digitalboard.core.openforms - name: Deploy opencloud service hosts: opencloud_servers