Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Simon Bärlocher
103d95740d
chore: wip on demo-gymburgdorf inventory and architecture notes 2026-05-18 15:20:01 +02:00
16 changed files with 455 additions and 8 deletions

207
ARCHITECTURE.md Normal file
View file

@ -0,0 +1,207 @@
<!-- markdownlint-disable MD013 MD060 -->
# Architekturskizze — `demo-gymburgdorf`
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.
## 1. Variablen-Hierarchie (Ansible Precedence)
```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
R["<b>role defaults/main.yml</b><br/>(niedrigste Precedence)<br/>~180 Variablen über 12 Rollen<br/>z.B. traefik_use_ssl: false<br/>keycloak_admin_password: changeme"]:::rolelayer
GA["<b>group_vars/all/</b><br/>docker.yml → docker_registry_mirrors<br/>vault.yml → vault_addr, vault_mount"]:::grouplayer
GT["<b>group_vars/traefik_servers/</b><br/>traefik.yml<br/>traefik_use_ssl, traefik_cert_mode: acme<br/>traefik_acme_dns_zone<br/>traefik_acme_tsig_* (Vault-Lookup!)"]:::grouplayer
GB["<b>group_vars/backend_servers/</b><br/>traefik.yml → traefik_mode: backend"]:::grouplayer
HR["<b>host_vars/reverseproxy/</b><br/>traefik.yml → traefik_mode: dmz"]:::hostlayer
HA["<b>host_vars/application/</b> (fehlt aktuell!)<br/>FQDNs, OIDC-Clients, DB-Passwords<br/>nextcloud_domain, authentik_domain, ..."]:::hostlayer
HS["<b>host_vars/storage/</b> (fehlt aktuell!)<br/>garage_s3_domain, garage_*_token<br/>traefik_dmz_exposed_services (für DMZ)"]:::hostlayer
V["<b>HashiCorp Vault / OpenBao</b><br/>bao.digitalboard.ch<br/>mount: demo-gymburgdorf<br/>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
```
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.
## 2. 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 (alle Hosts)"]
direction LR
subgraph DMZ["DMZ-Segment 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/>(STUN/TURN)"]:::turn
end
subgraph BE["Backend-Segment 172.16.19.0/24<br/>(group: backend_servers)"]
APP["<b>application</b><br/>172.16.19.101<br/>traefik_mode: backend<br/>+ nextcloud, opencloud,<br/>collabora, drawio,<br/>authentik, authentik_outpost_ldap"]:::app
ST["<b>storage</b><br/>172.16.19.102<br/>traefik_mode: backend<br/>+ garage (S3)"]:::stor
end
end
RP -.HTTP/HTTPS reverse proxy.-> APP
RP -.HTTP/HTTPS reverse proxy.-> ST
```
Gruppen-Mitgliedschaften (`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.
## 3. Service-Layout & 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 (key: acme_update_key_demo_gymb)<br/>dynamic 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/> → acme, tsig, ssl<br/>📍 host_vars/reverseproxy/traefik.yml<br/> → traefik_mode: dmz<br/>📍 host_vars/reverseproxy/...<br/> → traefik_dmz_exposed_services"]:::rp
end
subgraph APP["<b>application</b> — backend"]
TRA["traefik (docker provider)<br/>📍 group_vars/backend_servers<br/> → traefik_mode: backend"]:::ap
NC["nextcloud<br/>📍 host_vars/application/nextcloud.yml<br/> domain, postgres_pw, oidc, s3, ldap"]:::ap
OC["opencloud<br/>📍 host_vars/application/opencloud.yml<br/> oidc_issuer, ldap, s3"]:::ap
AK["authentik<br/>📍 host_vars/application/authentik.yml<br/> secret_key, postgres, ldap_apps, oidc_apps"]:::ap
AKO["authentik_outpost_ldap<br/>📍 host_vars/application/authentik_outpost_ldap.yml<br/> host, token"]:::ap
COL["collabora<br/>📍 host_vars/application/collabora.yml<br/> domain, allowed_domains"]:::ap
DRW["drawio<br/>📍 host_vars/application/drawio.yml"]:::ap
end
subgraph ST["<b>storage</b> — backend"]
TRS["traefik (docker provider)"]:::st
GAR["garage (S3)<br/>📍 host_vars/storage/garage.yml<br/> s3_domain, rpc_secret,<br/> admin_token, s3_keys"]:::st
end
Internet -->|HTTPS :443| TRDMZ
TRDMZ -->|HTTP backend| TRA
TRDMZ -->|HTTP backend| TRS
TRA --> NC & OC & AK & COL & DRW & AKO
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
```
## 4. Deploy-Flow
```mermaid
sequenceDiagram
participant U as User (make)
participant M as Makefile
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)
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
```
## 5. Variablen-Cheatsheet — wo gehört was hin?
| 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/<service>.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/<path>', url=vault_addr)` | Single source of truth; Pattern wie bei `_acme_tsig` |
## 6. Traefik-Modi (zentral für die Architektur)
**`traefik_mode: dmz`** (Public-facing Reverse Proxy auf `reverseproxy`):
- 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`.
**`traefik_mode: backend`** (Application/Storage Server):
- 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.
## 7. Was im aktuellen `demo-gymburgdorf` noch fehlt
Im Vergleich zur `vagrant`-Inventory (Referenz) fehlen für eine
vollständige Deployment:
- `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.
Die `vagrant`-Inventory ist das Template: dieselbe Struktur auf
`application`/`storage` mappen, FQDNs auf `*.gymb.souveredu.ch`
umstellen, Secrets durch Bao-Lookups ersetzen.

View file

@ -1,4 +1,7 @@
export BAO_ADDR=https://bao.digitalboard.ch export BAO_ADDR=https://bao.digitalboard.ch
# macOS fork-safety: Objective-C runtime is not fork-safe; Ansible forks
# per host. Without this, hashi_vault lookups crash workers.
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
install: install:
ansible-galaxy collection install -r requirements.yml -p collections ansible-galaxy collection install -r requirements.yml -p collections
@ -17,7 +20,7 @@ ping_demo:
deploy_site_demo_gymburgdorf: deploy_site_demo_gymburgdorf:
echo "deploying demo site gymburgdorf" echo "deploying demo site gymburgdorf"
ansible-playbook playbooks/site.yml -i inventories/demo-gymburgdorf/hosts.yml ansible-playbook playbooks/site.yml -i inventories/demo-gymburgdorf/hosts.yml --diff
deploy_site_demo_mbazürich: deploy_site_demo_mbazürich:
echo "deploying demo site mbazürich" echo "deploying demo site mbazürich"
@ -30,4 +33,4 @@ deploy_site_demo_phbern:
deploy_site_demo: deploy_site_demo:
make deploy_site_demo_gymburgdorf make deploy_site_demo_gymburgdorf
make deploy_site_demo_mbazürich make deploy_site_demo_mbazürich
make deploy_site_demo_phbern make deploy_site_demo_phbern

View file

@ -0,0 +1,2 @@
---
ansible_python_interpreter: /usr/bin/python3

View file

@ -5,7 +5,7 @@ traefik_cert_mode: "acme"
traefik_log_level: DEBUG traefik_log_level: DEBUG
traefik_network: proxy traefik_network: proxy
traefik_acme_dns_zone: "gymb._acme.digitalboard.ch" traefik_acme_dns_zone: "demo-gymb._acme.digitalboard.ch"
traefik_acme_dns_nameserver: "{{ _acme_tsig.server }}" traefik_acme_dns_nameserver: "{{ _acme_tsig.server }}"
traefik_acme_tsig_algorithm: "hmac-sha256" traefik_acme_tsig_algorithm: "hmac-sha256"
traefik_acme_tsig_key: "{{ _acme_tsig.tsig_key }}" traefik_acme_tsig_key: "{{ _acme_tsig.tsig_key }}"

View file

@ -0,0 +1,53 @@
---
# Bao secret expected at <mount>/data/authentik with keys:
# secret_key, postgres_password, admin_password,
# ldap_outpost_token,
# nextcloud_oidc_secret
_authentik: "{{ lookup('community.hashi_vault.hashi_vault', vault_mount + '/data/authentik', url=vault_addr) }}"
authentik_domain: "auth.gymb.souveredu.ch"
authentik_secret_key: "{{ _authentik.secret_key }}"
authentik_postgres_password: "{{ _authentik.postgres_password }}"
# LDAP outpost (provider for nextcloud)
authentik_ldap_apps:
- slug: ldap
name: LDAP
base_dn: "dc=gymb,dc=souveredu,dc=ch"
search_group: admins
authentik_ldap_outpost:
name: "ldap-outpost"
token: "{{ _authentik.ldap_outpost_token }}"
config:
authentik_host: "https://auth.gymb.souveredu.ch/"
log_level: "info"
# OIDC clients
authentik_oidc_apps:
- slug: nextcloud
name: Nextcloud
client_id: nextcloud
client_secret: "{{ _authentik.nextcloud_oidc_secret }}"
redirect_uris:
- url: "https://cloud.gymb.souveredu.ch/apps/user_oidc/code"
matching_mode: strict
signing_key_name: "authentik Self-signed Certificate"
flows:
authorization_slug: default-provider-authorization-implicit-consent
invalidation_slug: default-provider-invalidation-flow
scopes: [openid, email, profile, offline_access]
authentik_groups:
- name: admins
- name: users
authentik_local_users:
- username: akadmin
name: "Authentik Admin"
email: "admin@gymb.souveredu.ch"
password: "{{ _authentik.admin_password }}"
is_active: true
groups:
- authentik Admins
- admins

View file

@ -0,0 +1,7 @@
---
# Same token as authentik_ldap_outpost.token above — outpost uses it to
# 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_token: "{{ _authentik.ldap_outpost_token }}"

View file

@ -0,0 +1,8 @@
---
collabora_domain: "office.gymb.souveredu.ch"
collabora_allowed_domains:
- "cloud.gymb.souveredu.ch"
collabora_frame_ancestors:
- "cloud.gymb.souveredu.ch"

View file

@ -0,0 +1,2 @@
---
drawio_domain: "draw.gymb.souveredu.ch"

View file

@ -0,0 +1,3 @@
---
# application host runs: authentik, authentik-ldap-outpost,
# nextcloud, collabora, drawio

View file

@ -0,0 +1,90 @@
---
# Bao secret <mount>/data/nextcloud expected to contain:
# postgres_password, admin_password
_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"
nextcloud_postgres_password: "{{ _nextcloud.postgres_password }}"
nextcloud_admin_user: admin
nextcloud_admin_password: "{{ _nextcloud.admin_password }}"
nextcloud_enable_notify_push: true
# Collabora integration
nextcloud_enable_collabora: true
nextcloud_collabora_domain: "office.gymb.souveredu.ch"
# Draw.io integration
nextcloud_enable_drawio: true
nextcloud_drawio_url: "https://draw.gymb.souveredu.ch"
nextcloud_apps_to_install:
- groupfolders
- richdocuments
- spreed
- user_ldap
- user_oidc
- whiteboard
- drawio
- files_lock
- notify_push
# S3 primary storage via Garage
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_port: 443
nextcloud_s3_ssl: true
nextcloud_s3_usepath_style: true
# Share the LDAP docker network with the authentik LDAP outpost
nextcloud_extra_networks:
- ldap
# LDAP backend (Authentik LDAP outpost)
nextcloud_ldap_enabled: true
nextcloud_ldap_config:
ldapHost: "ldap://authentik-outpost-ldap-ldap-1"
ldapPort: "3389"
ldapAgentName: "cn=akadmin,ou=users,dc=gymb,dc=souveredu,dc=ch"
ldapAgentPassword: "{{ _authentik.admin_password }}"
ldapBase: "dc=gymb,dc=souveredu,dc=ch"
ldapBaseUsers: "ou=users,dc=gymb,dc=souveredu,dc=ch"
ldapTLS: "0"
turnOffCertCheck: "1"
ldapUserFilter: "(&(objectClass=user)(cn=*))"
ldapUserFilterObjectclass: "user"
ldapLoginFilter: "(&(objectClass=user)(cn=%uid))"
ldapLoginFilterUsername: "1"
ldapUserDisplayName: "cn"
ldapEmailAttribute: "mail"
ldapExpertUsernameAttr: "cn"
ldapExpertUUIDUserAttr: "uid"
ldapExpertUUIDGroupAttr: "uid"
ldapBaseGroups: "ou=groups,dc=gymb,dc=souveredu,dc=ch"
ldapGroupFilter: "(&(objectClass=group))"
ldapGroupFilterObjectclass: "group"
ldapGroupDisplayName: "cn"
ldapGroupMemberAssocAttr: "member"
ldapAdminGroup: "admins"
ldapCacheTTL: "600"
ldapPagingSize: "500"
ldapExperiencedAdmin: "1"
ldapConfigurationActive: "1"
# OIDC providers for login (Authentik)
nextcloud_oidc_providers:
- identifier: authentik
display_name: "Login with Authentik"
client_id: nextcloud
client_secret: "{{ _authentik.nextcloud_oidc_secret }}"
discovery_url: "https://auth.gymb.souveredu.ch/application/o/nextcloud/.well-known/openid-configuration"
scope: "openid email profile"
unique_uid: true
mapping:
uid: preferred_username
display_name: name
email: email

View file

@ -1 +1,28 @@
traefik_mode: dmz ---
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

View file

@ -0,0 +1,27 @@
---
# Bao secret <mount>/data/garage expected to contain:
# 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"
garage_webui_domain: "console.s3.gymb.souveredu.ch"
garage_use_ssl: true
garage_webui_enabled: true
garage_webui_username: "admin"
garage_webui_password: "{{ _garage.webui_password }}"
garage_rpc_secret: "{{ _garage.rpc_secret }}"
garage_admin_token: "{{ _garage.admin_token }}"
garage_metrics_token: "{{ _garage.metrics_token }}"
# Initial cluster bootstrap (single-node)
garage_bootstrap_enabled: true
garage_bootstrap_zone: "burgdorf1"
garage_bootstrap_capacity: "100G"
# Buckets and keys consumed by nextcloud
garage_s3_keys:
- name: nextcloud
buckets:
- name: nextcloud
permissions: ["read", "write"]

View file

@ -0,0 +1,2 @@
---
# storage host runs: garage (S3 + WebUI)

View file

@ -32,10 +32,6 @@ all:
hosts: hosts:
application: application:
opencloud_servers:
hosts:
application:
collabora_servers: collabora_servers:
hosts: hosts:
application: application:
@ -45,5 +41,9 @@ all:
application: application:
authentik_servers: authentik_servers:
hosts:
application:
authentik_outpost_ldap_servers:
hosts: hosts:
application: application:

View file

@ -78,5 +78,9 @@ all:
backend: backend:
opencloud_servers: opencloud_servers:
hosts:
backend:
openforms_servers:
hosts: hosts:
backend: backend:

View file

@ -65,6 +65,18 @@
roles: roles:
- digitalboard.core.drawio - digitalboard.core.drawio
- 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 opencloud service - name: Deploy opencloud service
hosts: opencloud_servers hosts: opencloud_servers
become: yes become: yes