Protecting Published Services
Terrarium lets you put OIDC sign-in in front of web apps running inside LXC containers.
Use it when a service:
- has no auth at all
- only has a weak shared password or static token
- is meant for a small team, not the public internet
- should reuse the same login system as the rest of your Terrarium host
You add protection directly to the container's user.proxy label, and Terrarium handles the proxying, callback routing, and auth checks for you.
The Simple Version
Use @auth on an HTTP(S) route when any signed-in user should be allowed in.
lxc config set my-app user.proxy "https://app.example.com:3000@auth"Use @auth:group1,group2 when only specific groups should be allowed in.
lxc config set admin-tool user.proxy "https://admin.example.com:8080@auth:admins,devops"Then run:
terrariumctl proxy syncOr just wait for the built-in sync timer.
When you add @auth to a route, Terrarium automatically:
- keeps Traefik in front of the container
- adds OIDC sign-in through the host's configured identity provider
- reuses the Terrarium auth stack instead of asking you to deploy your own
- handles callback routing on the management domain
- applies group checks if you listed groups
So the workflow is just:
- expose the app
- add
@author@auth:group1,group2 - run
terrariumctl proxy sync
Syntax
Supported HTTP(S) forms:
https://host[:container_port][/path]@auth
http://host[:container_port][/path]@auth
https://host[:container_port][/path]@auth:group1,group2
http://host[:container_port][/path]@auth:group1,group2Meaning:
@authmeans any authenticated user@auth:group1,group2means the user must be in at least one listed group
Examples:
lxc config set codebox user.proxy "https://code.example.com:3000@auth"
lxc config set hermes user.proxy "https://hermes.example.com:8642@auth:agents,admins"
lxc config set dashboard user.proxy "https://dash.example.com/app@auth"Good Fits
This works well when:
- you run
VSCodium serve-weband do not want to rely only on a static token - you expose agent UIs like Hermes or internal dashboards
- you want a quick login gate in front of a developer tool
- you want different groups for management tools and published apps
Do not use it for raw tcp:// or udp:// routes. This feature is for HTTP and HTTPS only.
Identity Provider Setup
If you use --idp=local
This is the easy path. Terrarium already manages the local ZITADEL side for you.
In practice, you usually just:
lxc config set my-app user.proxy "https://app.example.com:3000@auth"
terrariumctl proxy syncIf you use --idp=oidc
Terrarium can still protect published routes, but your external OIDC client must allow this callback:
https://manage.<your-domain>/oauth2/app/callbackTerrarium reuses the same external OIDC client for Cockpit, LXD, and route protection, so this callback is an addition to the normal management callbacks rather than a separate app.
If you use group-restricted routes, your provider must also include a groups claim.
One-time checklist for external OIDC:
- set the issuer, client ID, and client secret in Terrarium
- allow
https://manage.<your-domain>/oauth2/app/callbackin your OIDC client - make sure your provider sends a
groupsclaim if you use@auth:group1,group2
Group-Based Access
Examples:
https://code.example.com:3000@authAnyone who can sign in is allowed through.https://hermes.example.com:8642@auth:agentsOnly members ofagentscan get in.https://admin.example.com:8080@auth:admins,devopsMembers of eitheradminsordevopscan get in.
Use groups when:
- Cockpit and LXD should stay admin-only
- internal apps should be visible to a broader team
- some apps should be limited to a specific role or project group
Important Limitation
Published-route auth currently works only for hosts on your Terrarium root domain or its subdomains, because Terrarium uses the shared callback:
https://manage.<domain>/oauth2/app/callbackSo routes like these are fine:
https://code.example.com@authhttps://hermes.dev.example.com@auth:agents
This is not a good fit:
https://totally-other-domain.net@auth
One important default to keep in mind: if you installed Terrarium without a custom root domain and stayed on the default IP-based management domains, route auth is effectively limited to the manage hostname. For arbitrary protected app hostnames, use a real --domain.
How the Callback Works
The callback does not need to live on the same hostname as the protected app.
With Terrarium, the sign-in callback for published apps lives on:
https://manage.<domain>/oauth2/app/callbackThat does not mean users stay on manage.<domain>.
The flow is:
- a user opens
https://app.example.com - Terrarium redirects them to sign in
- the identity provider sends them back to
https://manage.example.com/oauth2/app/callback - Terrarium finishes the login and then sends them back to the original app URL
So a user can start on https://app.example.com, briefly pass through the shared callback on https://manage.example.com, and then land back on https://app.example.com.
This shared callback is exactly why route protection works well for *.your-domain apps, but is not meant for unrelated domains.
Quick Example
Expose a service on port 8080 inside the container and require login:
lxc exec my-app -- sh
# start your app on 0.0.0.0:8080 inside the container
exit
lxc config set my-app user.proxy "https://app.example.com:8080@auth"
terrariumctl proxy syncRequire a group on a second route:
lxc config set my-app user.proxy "https://app.example.com:8080@auth,https://admin.example.com:9090@auth:admins"
terrariumctl proxy syncTroubleshooting
If a protected route does not work:
- make sure the app inside the container listens on
0.0.0.0:<port> - make sure the route host is on your Terrarium root domain
- run
terrariumctl proxy sync - check
terrariumctl status - if you use external OIDC, verify that
https://manage.<domain>/oauth2/app/callbackis allowed in the client config - if you use group restrictions, verify your token includes a
groupsclaim with the expected group names
Practical Advice
Start with @auth. Add groups only when you actually need different access levels.