Domains and Authentication
Terrarium sets up a few public management endpoints for the host and lets you choose between a self-hosted identity provider or an external one.
Default Public Endpoints
By default, Terrarium exposes:
https://manage.<dashed-public-ip>.traefik.mefor Cockpithttps://proxy.<dashed-public-ip>.traefik.mefor the Traefik dashboardhttps://lxd.<dashed-public-ip>.traefik.mefor the LXD API and UIhttps://auth.<dashed-public-ip>.traefik.mefor self-hosted ZITADEL when--idp=localis used
You can override those endpoints with:
--domain--manage-domain--proxy-domain--lxd-domain--auth-domain
If you provide a custom root domain like --domain example.com, Terrarium automatically derives the subdomains:
manage.<domain>proxy.<domain>lxd.<domain>auth.<domain>(if self-hosted ZITADEL is enabled)
These domains are strictly for management and explicitly published services. A service running inside a container is not reachable from the outside until you deliberately publish it.
Published App Route Labels
Container apps become public only when you add a user.proxy label to the LXD instance. The label tells Terrarium which public domain to serve and which private port inside the container to forward to.
For normal web apps, use this format:
https://<public-host>[:container-port][/path][@auth[:group[,group...]][~callback-host]]Examples:
lxc config set my-app user.proxy "https://app.example.com:8080"
lxc config set admin-ui user.proxy "https://admin.example.com:3000@auth"
lxc config set dev-tool user.proxy "https://dev.example.com:8080@auth:admins,devops"
terrariumctl proxy syncIn the LXD UI, open the instance, go to Configuration, choose Edit YAML, and add the label under config::
config:
user.proxy: https://app.example.com:8080Save the YAML, then wait for a minute or run terrariumctl proxy sync from the host shell if you want the route applied immediately.
What each part means:
https://app.example.comis the public domain Traefik will serve.:8080is the port inside the container. If omitted, Terrarium uses container port80./pathis optional and is matched as a path prefix.@authrequires a successful OIDC login before traffic reaches the app.@auth:admins,devopsalso requires membership in one of those groups.~auth.example.comis required only for wildcard auth routes and gives Terrarium the concrete oauth2-proxy callback host.
Use https:// for normal public routes. Terrarium will request a Let's Encrypt certificate and redirect plain HTTP to HTTPS. Query strings and URL fragments are not part of route labels.
Wildcard HTTPS routes are supported after DNS-01 ACME is configured:
terrariumctl set dns provider cloudflare CF_DNS_API_TOKEN:your-token
lxc config set my-app user.proxy "https://*.example.com:8080"
terrariumctl proxy syncDNS credentials map directly to lego's provider environment variable names. See the lego DNS provider list for supported providers and required variables. To disable DNS-01 and return to HTTP-01 certificate validation, run:
terrariumctl set dns providerWildcard auth routes need a concrete callback host under the same base domain:
lxc config set admin-ui user.proxy "https://*.example.com:3000@auth:admins~auth.example.com"
terrariumctl proxy syncYou can publish more than one route from the same container by separating routes with commas or newlines:
lxc config set my-app user.proxy "https://app.example.com:8080,https://api.example.com:3000/api"
terrariumctl proxy syncRaw TCP and UDP routes are also supported:
tcp://<public-port>:<container-port>
udp://<public-port>:<container-port>Examples:
lxc config set postgres user.proxy "tcp://15432:5432"
lxc config set game user.proxy "udp://25565:25565"
terrariumctl proxy syncTCP and UDP routes do not support domains, paths, TLS termination, or @auth. Terrarium creates a dedicated Traefik entrypoint and opens the matching managed firewall port.
For every label format, the service inside the container must listen on 0.0.0.0, not only 127.0.0.1. Terrarium syncs labels automatically every minute, but terrariumctl proxy sync applies changes immediately.
Email Settings
--email: Sets the primary Terrarium contact/admin email and is used as the default ZITADEL bootstrap admin email.--acme-email: Used as the ACME account identity so Traefik and LXD can automate your SSL certificates.
If you don't provide an --acme-email, Terrarium simply falls back to using the primary --email.
Management Authentication
Terrarium enforces a strict separation between command-line SSH access and web management access.
SSH
- SSH is hardened to allow key-based authentication only.
- Password-based SSH logins are completely disabled.
Cockpit
Cockpit access requires passing two layers of authentication:
- An OIDC gate handled through Traefik
ForwardAuthand host-leveloauth2-proxy. - A standard Cockpit PAM login on the host machine.
This means users must first pass the OIDC Single Sign-On gate. Once past that, they still need a valid local host account to log into Cockpit (in practice, this means root needs a local password).
If root does not already have a password:
- The interactive installer will prompt you to create one.
- The non-interactive installer requires either the
--generate-root-pwdor--root-pwd-fileflag.
When Terrarium generates the password, it saves it securely to /etc/terrarium/secrets/cockpit_root_password with root-only permissions. It does not store the plaintext password in your /etc/terrarium/config.yaml file.
LXD
LXD handles its own native OIDC authentication and authorization.
- Terrarium automatically configures the necessary OIDC issuer and client settings.
- Access is strictly granted only to members of your configured Terrarium admin group.
Identity Provider (IDP) Modes
Terrarium supports two different ways to handle user logins.
Mode 1: Local (--idp local)
Terrarium deploys ZITADEL directly on the host and automatically provisions the clients and claims it needs.
Defaults:
- Auth domain:
auth.<domain>orauth.<dashed-public-ip>.traefik.me - Admin group:
terrarium-admins
Terrarium will automatically:
- Provision the necessary management role.
- Grant that role to the bootstrap admin user.
- Emit a flat
groupsclaim foroauth2-proxyand LXD to read. - Keep route-auth callback URLs in the managed ZITADEL app up to date when you run
terrariumctl proxy sync.
Mode 2: External OIDC (--idp oidc)
If you prefer to use an external provider (like Google Workspace, GitHub, or Auth0), Terrarium can connect to it.
You must provide the following:
--oidc--oidc-client--oidc-secret-fileor--oidc-secret--admin-group
Requirements for your external provider: You must configure your provider to allow the following callback URLs:
https://<manage-domain>/oauth2/callbackhttps://<proxy-domain>/oauth2/callbackhttps://<lxd-domain>/oidc/callback- If you plan to protect published app routes with
@auth, you must also allow each route callback. A root route useshttps://<route-host>/oauth2/callback; a path route like/adminuseshttps://<route-host>/oauth2/admin/callback.
For external providers, including ZITADEL Cloud, Terrarium does not mutate your provider app automatically. Add new published-route callback URLs manually before or when you create the protected route.
Additionally, the external provider must emit a groups claim as a JSON string array containing the configured admin group. A provider-side role assignment is not enough unless the issued ID token or userinfo response actually contains that value in groups. Check your IDP documentation to set this up properly.
WARNING
ZITADEL Cloud is probably most unintuitive and weird in this regard, and as we use it as default idp provider, here are the quirks
For ZITADEL Cloud there is no direct concept of groups, they operate with project roles. And project roles are not emitted as a flat groups claim by default, they are emited in their own claims. So do the following:
- In Project the app is added in, in settings (first screen at project) enable "Return user roles during authentication"
- In the app go to "Token settings" and enable "User roles inside ID Token" and "Include user's profile info in the ID Token"
- Copy the Project ID from the ZITADEL project that contains your Terrarium app. Go to "Actions" (in top menu), add new script named "groupsClaim", place there, and replace
replace-with-your-terrarium-project-idwith that Project ID:
function groupsClaim(ctx, api) {
var groups = [];
var terrariumProjectId = "replace-with-your-terrarium-project-id";
if (!ctx || !ctx.v1 || !ctx.v1.user || !ctx.v1.user.grants || !ctx.v1.user.grants.grants) {
api.v1.claims.setClaim("groups", groups);
return;
}
for (var i = 0; i < ctx.v1.user.grants.grants.length; i++) {
var grant = ctx.v1.user.grants.grants[i];
var grantProjectId = grant && (grant.projectId || grant.projectID || grant.project_id);
if (grantProjectId !== terrariumProjectId) {
continue;
}
if (!grant || !grant.roles) continue;
for (var j = 0; j < grant.roles.length; j++) {
var role = grant.roles[j];
if (groups.indexOf(role) === -1) groups.push(role);
}
}
api.v1.claims.setClaim("groups", groups);
}- In "Flows" below select "Complement Token" flow type, add trigger "Pre access token creation" and set action "groupsClaim". Then the same for trigger "Pre Userinfo creation"
This will add required claim to token. We hope that Zitadel team will make it easier in future, but for now it is how it is.
Terrarium reuses the exact same external OIDC client for:
- Cockpit's oauth2-proxy
- LXD
- Published HTTP(S) routes protected with the
@authlabel
If your identity provider requires you to use a separate client specifically for LXD, you can pass --lxd-oidc-client and --lxd-oidc-secret-file. For automated setups, always prefer using the secret-file flags over passing secrets directly as arguments.
The Admin Group
The management admin group controls who is allowed to access:
Cockpit (through
oauth2-proxy)LXD (through native OIDC group mapping)
Local mode default:
terrarium-adminsExternal OIDC mode: You must explicitly define this using
--admin-group
This group is intentionally kept separate from app-level route protection. You can grant a user access to a published app without giving them management access to the Terrarium host.
If you want to learn how to lock your published apps behind this same authentication system, continue to the Protecting Published Services with OIDC guide.