Skip to content

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=room entries under ou=People.
  • OpenVPN — sssd on mbptillo authenticates VPN users via PAM → LDAP, with cache_credentials=true for 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.