makerfloss/src/containers/podman/DESIGN.md
Lars Rossen 04b4437c95 Merge portainer into podman: engine plus GUI in one module
Podman is the container foundation, Portainer CE is the web interface onto it,
running as a container on the very engine it manages. This is what the separate
portainer module already did, so it is retired rather than duplicated.

Two sockets on purpose: the rootful one is the Docker-compatible API Portainer
drives (it does not support rootless), the rootless user socket is for people
who ssh in and run containers by hand — which was the original podman module's
point and is worth keeping.

An explicit proxyDomain lets the module publish at the environment's own domain
instead of <module>.<environment-domain>, making it that environment's gateway.

DESIGN.md keeps the Cockpit identity analysis: it is the reason for the change,
and anyone proposing 'just put Cockpit behind forward-auth' should read it.
2026-08-25 09:30:11 +02:00

5.9 KiB

Podman — design notes

Primary audience: whoever changes this module next.

Why Debian and not NixOS

The default TAPPaaS module VM is NixOS. Podman ships in Debian 13 (trixie) main, as do cockpit and cockpit-podman, so a Debian cloud image gets the whole module from apt with no packaging work and no flake to maintain. The trade-off is that the VM's state lives in update.sh rather than in a declarative .nix file — acceptable for a container host, whose real content is the containers the operator runs on it.

Identity integration — the analysis

The module currently authenticates against Linux (PAM) accounts on the VM: the cloud-init tappaas user has no password until an operator sets one by hand. That is a per-VM credential nobody manages, and it is the one part of this module that is not platform-integrated.

What TAPPaaS offers

The identity module (Authentik) provides two services, and USERS.md is explicit that they must never be stacked on one app — double login, no benefit:

Mode dependsOn Mechanism In-app account
OIDC identity:identity the app is an OIDC client of Authentik; groups arrive in the groups claim created just-in-time on first login
Forward-auth identity:accessControl Caddy asks the Authentik outpost, then injects X-Authentik-* headers none — the URL is simply gated

identity:identity writes OIDC_CLIENT_ID / OIDC_CLIENT_SECRET / OIDC_DISCOVERY_URI into the VM's secrets env file and restarts a configure service, so the app can register the provider. identity:accessControl creates a Proxy Provider in Authentik, attaches it to the embedded outpost, and flips the module's existing Caddy handler to ForwardAuth=1. Both bind the application to an allow-list of groups at install time — mandatory, because Authentik fails open.

What Cockpit can actually consume

Cockpit is not an OIDC client, and cannot become one by configuration. Its maintainers are direct about the alternatives (cockpit#20814):

  • launching cockpit-ws --local-session=cockpit-bridge behind an authenticating proxy — which means "there is zero authentication on that port", so :9090 must be made unreachable by anything but the proxy;
  • writing a custom authentication command in the shape of cockpit-session — "this is difficult -- this isn't configuration, this is real development".

And in every approach, including the proxy one, a local Unix account must still exist for the authenticated person: cockpit-bridge runs inside a PAM session as that user. Cockpit does not abstract that away.

So identity:identity is the wrong dependency for this module — the deck was optimistic. There is no configuration that makes Cockpit take an OIDC login.

The three real options

1. identity:accessControl — gate the URL (recommended first step). Authentik guards https://podman.<domain> at Caddy and only the bound groups get through; behind it Cockpit still shows its own PAM form. Two logins, but the exposure is centrally managed and group-driven, and it is one line in dependsOn plus whatever zone0 / proxyDomain the environment resolves. No new machinery.

2. Authentik LDAP outpost + SSSD on the VM — one credential. Authentik can serve LDAP; with SSSD on the Debian VM the Linux accounts are the Authentik accounts, so Cockpit's own form takes the same username and password as everything else, and podadmin-style hand-made users disappear. This is the honest fix for the local-account requirement. It is not something this module should carry: TAPPaaS's identity module runs only the embedded outpost, so an LDAP outpost plus a posix-accounts service belongs upstream in identity (a third service alongside identity and accessControl).

3. True single sign-on — real development. Option 1 or 2 plus a custom Cockpit auth command that trusts X-Authentik-Username, with :9090 firewalled to the proxy only. Highest value, highest risk: header-trust auth is a full bypass if the port is reachable any other way, and this module currently advertises direct https://<vm-ip>:9090 access as a feature.

What was actually done (2026-08-25)

None of the three. All of them are workarounds for a console that cannot take an OIDC login, and the requirement had grown: registered people managing containers across the hosts in the zone, which Cockpit's deprecated host switcher cannot do securely either.

So Cockpit was dropped and Portainer CE became this module's GUI, with Podman still the engine underneath it. Portainer is an OIDC client, so identity:identity — the native TAPPaaS OIDC path — works exactly as designed, and multi-host is an agent per host rather than a browser loading remote JavaScript. The analysis above is kept because it is the reason for the change, and because anyone proposing "just put Cockpit behind forward-auth" should read it first.

One thing to verify on a live system first

identity:accessControl's install-service hard-requires vmname, zone0, proxyDomain and proxyPort in the installed config and — unlike identity:identity — does not derive proxyDomain when it is absent. This module deliberately sets neither zone0 nor proxyDomain, so that both come from the environment (ADR-007 P5). copy-update-json.sh documents that install-module.sh "computes vmname/zone0/proxyDomain from the environment file and passes them as explicit overrides", but no such computation is visible in install-module.sh for proxyDomain. Check the resolved /home/tappaas/config/podman-<env>.json after an install; if proxyDomain is empty, accessControl will die, and the fix is upstream — mirror the derivation that identity/install-service.sh already does.