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 the self-hosted identity provider 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 a self-hosted identity provider 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 a self-hosted identity provider and automatically provisions the clients and claims it needs. The local provider is selected with --idp-provider zitadel|logto during install:
terrariumctl install --idp local --idp-provider zitadel
terrariumctl install --idp local --idp-provider logtoIf you omit --idp-provider in local mode, Terrarium keeps the compatibility default of local ZITADEL.
Defaults:
- Auth domain:
auth.<domain>orauth.<dashed-public-ip>.traefik.me - Admin group:
terrarium-admins - Local Logto admin email:
terrarium_email - Local Logto admin username:
terrarium_admin
With local ZITADEL, 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.
Choose local ZITADEL when the lower-resource local IDP is the priority. Choose local Logto when richer identity functionality and a more polished admin/user experience are worth the heavier local footprint.
With local Logto, Terrarium also uses the same auth.<domain> public endpoint and runs Logto plus Postgres in the managed IDP system instance. Bootstrap is unattended: Terrarium seeds the Logto database, creates the Terrarium OAuth clients, creates the admin role, assigns it to the bootstrap admin email when that user exists, and writes the local client outputs consumed by oauth2-proxy, LXD, and route auth. The bootstrap email defaults to your Terrarium contact email and the username defaults to terrarium_admin; override them with --logto-admin-email and --logto-admin-username. Logto defaults to the roles claim and openid profile email roles scopes for both management and LXD unless you override the claim/scope flags.
Mode 2: External OIDC (--idp oidc)
If you prefer to use an external provider (like Google Workspace, GitHub, Auth0, ZITADEL Cloud, or Logto Cloud), Terrarium can connect to it.
You must provide the following:
--oidc--oidc-client--oidc-secret-fileor--oidc-secret--admin-group
Provider-specific defaults are optional for external OIDC. If you omit --idp-provider, Terrarium keeps the compatibility default of generic OIDC with a groups claim and openid profile email scopes. If your external issuer is Logto or Logto Cloud, pass --idp-provider logto; Terrarium then defaults management, LXD, and route auth to the Logto roles claim and openid profile email roles scopes. You can still override those values with the claim/scope flags documented in the terrariumctl reference.
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. The same is true for Logto Cloud. Add new published-route callback URLs manually before or when you create the protected route.
Additionally, the external provider must emit the configured groups/roles claim as a JSON string array containing the configured admin group. Generic OIDC and ZITADEL default to groups; Logto defaults to roles. A provider-side role assignment is not enough unless the issued ID token or userinfo response actually contains that value in the claim Terrarium is configured to read. 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.
For external Logto/Logto Cloud, configure an application that allows the management callbacks above and requests/permits the roles scope. If you use a separate LXD application, allow https://<lxd-domain>/oidc/callback there and keep the same roles claim and openid profile email roles scopes unless you have explicitly overridden Terrarium's LXD claim/scope settings.
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.