Authentication & Identity Flow¶
Identity Stack Overview¶
The homelab runs two independent Keycloak deployments with completely separate scopes:
| Deployment | Hostname | Namespace | Scope | User base |
|---|---|---|---|---|
| MDAPI Keycloak | idp.mdapi.ch |
keycloak |
MDAPI organisation SSO — guards every *.mdapi.ch app via oauth2-proxy |
tillo, family and friends |
| Envuassu Keycloak | login.envuassu.ch |
envuassu |
"En Vuassu" neighbourhood community — guards Nextcloud + Zammad for the families sharing the joint property | The villas in the En Vuassu block |
They share no realms, users, or trust. Each has its own Postgres and its own letsencrypt-prod Issuer.
flowchart TD
subgraph ldap_stack["OpenLDAP — 192.168.1.52"]
ldap["OpenLDAP\ndc=mdapi,dc=ch"]
lam["LAM admin UI\nlam.mdapi.ch"]
end
subgraph kc_mdapi["Keycloak — idp.mdapi.ch (MDAPI)"]
kc1["keycloak ns"]
kc1_pg["Postgres 5Gi"]
end
subgraph kc_env["Keycloak — login.envuassu.ch (En Vuassu)"]
kc2["envuassu ns"]
kc2_pg["Postgres"]
end
mail_auth["docker-mailserver\nPostfix + Dovecot\nLDAP auth"]
sssd_vpn["sssd on mbptillo\nOpenVPN PAM auth"]
oauth2_proxy["oauth2-proxy\nauth.mdapi.ch"]
apps_mdapi["MDAPI apps\n*.mdapi.ch"]
apps_env["Envuassu apps\nNextcloud, Zammad"]
ldap --> mail_auth & sssd_vpn
lam --> ldap
kc1 --> kc1_pg
kc2 --> kc2_pg
ldap <-->|"two-way sync\n(users, groups,\neverything LDAP-supported)"| kc1
kc1 -->|"OIDC + LDAP-derived\nroles/groups claims"| oauth2_proxy --> apps_mdapi
kc2 -->|"OIDC"| apps_env
MDAPI Keycloak — idp.mdapi.ch¶
Keycloak is the OIDC/SSO provider for the MDAPI organisation. It runs in the keycloak namespace with a dedicated Postgres backend.
Realms:
| Realm | Purpose |
|---|---|
master |
Admin console |
tilloch |
Main user realm (tillo.ch domain) |
The tilloch realm uses default-roles-tilloch to assign baseline permissions automatically: view-applications, view-consent, manage-account-links. These are required for the account console tabs to appear.
Image tracked on quay.io/keycloak/keycloak 26.x tag. A dedicated CI pipeline polls for new upstream releases and updates the Dockerfile automatically.
LDAP federation — two-way sync with OpenLDAP¶
The tilloch realm is federated against the on-cluster OpenLDAP at 192.168.1.52 (dc=mdapi,dc=ch). Sync is bidirectional for everything LDAP can represent — username, full name, email, phone, postal address, group membership — so user maintenance can happen on either side and converges within the next sync window.
flowchart LR
ldap["OpenLDAP\ndc=mdapi,dc=ch"]
kc["Keycloak — tilloch realm\n(idp.mdapi.ch)"]
apps_ldap["LDAP-bound apps\n(Postfix, Dovecot, sssd/OpenVPN)"]
apps_oidc["OIDC-bound apps\n(everything behind oauth2-proxy)"]
ldap <-->|"two-way sync\n(user attrs + groups)"| kc
ldap --> apps_ldap
kc --> apps_oidc
What does not round-trip:
- Preferred language — no clean LDAP attribute, kept Keycloak-only.
- MFA / WebAuthn factors, recovery codes, session policies — Keycloak-native concepts; LDAP has no equivalent.
These attributes are written and read only on the Keycloak side and don't get clobbered by sync.
Group → role mapping¶
Keycloak's role/group claims for the tilloch realm are derived from LDAP groups (ou=Groups,dc=mdapi,dc=ch via the role-ldap-mapper). This means adding a user to an LDAP group is enough — they pick up the matching realm role on the next login, and any OIDC client that asserts roles / groups claims sees it. Authorisation policy stays in LDAP; Keycloak just publishes it as OIDC claims.
Envuassu Keycloak — login.envuassu.ch¶
A second Keycloak instance lives in the envuassu namespace alongside the neighbourhood's Nextcloud AIO and Zammad. It is the IdP for those community apps only — there is no federation or trust between the two Keycloaks. Manifests at https://gitlab.mdapi.ch/mdapi/fleet/-/tree/main/envuassu/keycloak (public mirror).
OpenLDAP¶
OpenLDAP is the authoritative directory for dc=mdapi,dc=ch. Two primary consumers:
- Mail stack — Postfix and Dovecot authenticate via LDAP. Users are
objectClass=roomentries underou=People. - OpenVPN — sssd on mbptillo authenticates VPN users via PAM → LDAP, with
cache_credentials=truefor resilience when the cluster is unreachable.
LAM provides a web UI at lam.mdapi.ch.
Secret Management¶
flowchart LR
akeyless["Akeyless SaaS"]
cm_vm["CipherTrust Manager\ncm.home.tillo.ch\ncustomer fragment\n(on-premise)"]
eso["External Secrets Operator\ncm-akeyless ClusterSecretStore"]
k8s_sec["K8s Secret"]
pod["Pod"]
akeyless <-->|"customer fragment\nnever leaves LAN"| cm_vm
eso -->|"fetch /mdapi/* secrets"| cm_vm
eso --> k8s_sec --> pod
All /mdapi/ paths use with_customer_fragment: true. The customer fragment is stored exclusively on the CipherTrust Manager VM — Akeyless SaaS cannot decrypt these secrets without it.
If CipherTrust Manager is unreachable, ExternalSecret refreshes fail cluster-wide. Restore the VM from its Longhorn snapshot to recover.
TLS — cert-manager + Let's Encrypt¶
flowchart LR
annotation["Ingress annotation\ncert-manager.io/issuer:\nletsencrypt-prod"]
cm_op["cert-manager"]
bind9["BIND9\n31.3.128.59:53\n(ns.mdapi.ch external)"]
le["Let's Encrypt ACME v2"]
tls_secret["TLS Secret"]
annotation --> cm_op
cm_op -->|"nsupdate TSIG\nkey: mdapi"| bind9
bind9 --> le -->|"certificate"| cm_op --> tls_secret
DNS-01 is used for the public mdapi.ch / tillo.ch domains — it supports wildcards and works for services not reachable from the internet; names under the internal-only home.tillo.ch zone validate via HTTP-01 instead. The TSIG key never leaves the cluster — it lives in a K8s Secret referenced by cert-manager.
cert-manager targets 31.3.128.59:53 (external IP of ns.mdapi.ch, DNAT'd to 192.168.1.53 by BPI-R4). Each namespace has its own letsencrypt-prod Issuer configured with this nameserver, including the bootstrap namespace — its issuer now lives at https://gitlab.mdapi.ch/mdapi/fleet/-/tree/main/docs (public mirror) and is reconciled by Fleet.