Skip to content

Commit f72b54e

Browse files
committed
feat(logto): add local admin credential parity
Add configurable local Logto admin email and username, generate a dedicated Logto admin password secret, expose status/help/docs plumbing, and update smoke credentials to read the Logto secret.
1 parent 1ad63f8 commit f72b54e

21 files changed

Lines changed: 403 additions & 53 deletions

ansible/roles/idp_logto/defaults/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ terrarium_logto_instance_image: ubuntu:24.04
1010
terrarium_logto_instance_profile: terrarium
1111
terrarium_logto_core_port: 3001
1212
terrarium_logto_admin_port: 3002
13+
terrarium_logto_admin_email: "{{ terrarium_email }}"
14+
terrarium_logto_admin_username: terrarium_admin
1315
terrarium_docker_registry_config_path: /root/.docker/config.json
1416
terrarium_docker_hardened_images: auto
1517
terrarium_docker_hardened_image_mirrors: true

ansible/roles/idp_logto/tasks/main.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@
110110
'set -eu;
111111
umask 077;
112112
[ -f /etc/terrarium/secrets/logto_postgres_password ] || tr -dc "A-Za-z0-9" </dev/urandom | head -c 48 > /etc/terrarium/secrets/logto_postgres_password;
113-
[ -f /etc/terrarium/secrets/logto_secret_vault_kek ] || openssl rand -base64 32 > /etc/terrarium/secrets/logto_secret_vault_kek'
113+
[ -f /etc/terrarium/secrets/logto_secret_vault_kek ] || openssl rand -base64 32 > /etc/terrarium/secrets/logto_secret_vault_kek;
114+
if [ ! -f /etc/terrarium/secrets/logto_admin_password ]; then
115+
pw="$(tr -dc "A-Z" </dev/urandom | head -c 1)$(tr -dc "a-z" </dev/urandom | head -c 1)$(tr -dc "0-9" </dev/urandom | head -c 1)!$(tr -dc "A-Za-z0-9" </dev/urandom | head -c 28)";
116+
printf "%s" "$pw" > /etc/terrarium/secrets/logto_admin_password;
117+
fi'
114118
changed_when: false
115119
no_log: true
116120

@@ -188,6 +192,12 @@
188192
changed_when: false
189193
no_log: true
190194

195+
- name: Read Logto local admin password from system instance
196+
ansible.builtin.command: "/snap/bin/lxc exec {{ terrarium_logto_instance_name }} -- cat /etc/terrarium/secrets/logto_admin_password"
197+
register: terrarium_logto_admin_password_raw
198+
changed_when: false
199+
no_log: true
200+
191201
- name: Create local Logto staging directory
192202
ansible.builtin.file:
193203
path: "{{ terrarium_logto_stage_dir }}"
@@ -265,7 +275,7 @@
265275
- name: Run Logto reconciliation
266276
ansible.builtin.command: /usr/local/bin/terrariumctl idp sync
267277
environment:
268-
TERRARIUM_LOGTO_ADMIN_PASSWORD: "{{ terrarium_root_password_plaintext | default('', true) }}"
278+
TERRARIUM_LOGTO_ADMIN_PASSWORD: "{{ terrarium_logto_admin_password_raw.stdout }}"
269279
register: terrarium_logto_sync
270280
until: terrarium_logto_sync.rc == 0
271281
retries: 6

ansible/site.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
terrarium_logto_stage_dir: "{{ terrarium_state_dir }}/logto-container"
6666
terrarium_logto_core_port: 3001
6767
terrarium_logto_admin_port: 3002
68+
terrarium_logto_admin_email: "{{ terrarium_email }}"
69+
terrarium_logto_admin_username: terrarium_admin
6870
terrarium_local_idp_outputs_path: ""
6971
terrarium_local_idp_outputs_path_default: /etc/terrarium/idp-apps.json
7072
terrarium_zitadel_outputs_path: /etc/terrarium/zitadel-apps.json
@@ -139,6 +141,8 @@
139141
terrarium_logto_stage_dir: "{{ terrarium_logto_stage_dir }}"
140142
terrarium_logto_core_port: "{{ terrarium_logto_core_port }}"
141143
terrarium_logto_admin_port: "{{ terrarium_logto_admin_port }}"
144+
terrarium_logto_admin_email: "{{ terrarium_logto_admin_email }}"
145+
terrarium_logto_admin_username: "{{ terrarium_logto_admin_username }}"
142146
terrarium_local_idp_outputs_path: "{{ terrarium_local_idp_outputs_path_effective | default(terrarium_local_idp_outputs_path) }}"
143147
terrarium_zitadel_outputs_path: "{{ terrarium_zitadel_outputs_path_effective | default(terrarium_zitadel_outputs_path) }}"
144148
terrarium_oauth2_proxy_image: "{{ terrarium_oauth2_proxy_image_effective | default(terrarium_oauth2_proxy_image_fallback) }}"
@@ -390,6 +394,8 @@
390394
'terrarium_logto_stage_dir': terrarium_logto_stage_dir,
391395
'terrarium_logto_core_port': terrarium_logto_core_port,
392396
'terrarium_logto_admin_port': terrarium_logto_admin_port,
397+
'terrarium_logto_admin_email': terrarium_logto_admin_email,
398+
'terrarium_logto_admin_username': terrarium_logto_admin_username,
393399
'terrarium_logto_app_image': terrarium_logto_app_image_effective,
394400
'terrarium_logto_postgres_image': terrarium_logto_postgres_image_effective,
395401
'terrarium_local_idp_discovery_url': terrarium_local_idp_discovery_url_effective

docs/getting-started/domains-and-auth.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,16 @@ If you omit `--idp-provider` in local mode, Terrarium keeps the compatibility de
166166
Defaults:
167167
- Auth domain: `auth.<domain>` or `auth.<dashed-public-ip>.traefik.me`
168168
- Admin group: `terrarium-admins`
169+
- Local Logto admin email: `terrarium_email`
170+
- Local Logto admin username: `terrarium_admin`
169171

170172
With local ZITADEL, Terrarium will automatically:
171173
- Provision the necessary management role.
172174
- Grant that role to the bootstrap admin user.
173175
- Emit a flat `groups` claim for `oauth2-proxy` and LXD to read.
174176
- Keep route-auth callback URLs in the managed ZITADEL app up to date when you run `terrariumctl proxy sync`.
175177

176-
With local Logto, Terrarium also uses the same `auth.<domain>` public endpoint, but it 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. Logto defaults to the `roles` claim and `openid profile email roles` scopes for both management and LXD unless you override the claim/scope flags.
178+
With local Logto, Terrarium also uses the same `auth.<domain>` public endpoint, but it 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.
177179

178180
### Mode 2: External OIDC (`--idp oidc`)
179181

docs/getting-started/installation.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Use local Logto instead:
7373
terrariumctl install --idp local --idp-provider logto
7474
```
7575

76-
Local Logto is heavier than local ZITADEL: Terrarium runs Logto and Postgres in the managed IDP system instance, seeds Logto unattended, and provisions the Terrarium OIDC clients during `terrariumctl idp sync`.
76+
Local Logto is heavier than local ZITADEL: Terrarium runs Logto and Postgres in the managed IDP system instance, seeds Logto unattended, and provisions the Terrarium OIDC clients during `terrariumctl idp sync`. The bootstrap admin email defaults to your Terrarium contact email and the username defaults to `terrarium_admin`; override them with `--logto-admin-email` and `--logto-admin-username`.
7777

7878
Use external generic OIDC, preserving the existing `groups` claim and `openid profile email` scope defaults:
7979

@@ -126,6 +126,7 @@ The installer will guide you through:
126126
- IDP provider:
127127
- `zitadel` is the compatibility default for local installs
128128
- `logto` selects local Logto or external Logto/Logto Cloud defaults
129+
- Local Logto bootstrap email and username when `--idp-provider logto` is selected.
129130
- Storage mode and storage source.
130131
- Optional S3 archive backups.
131132
- Optional syncoid replication.

docs/reference/terrariumctl.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ trm image delete golden-web
163163
| `--non-interactive` | none | no | interactive mode if omitted | Disables prompts and requires all needed config through flags. |
164164
| `--yes` | none | no | prompt before destructive actions | Auto-confirms destructive or confirmation prompts. |
165165
| `--ref` | git branch or tag | no | `main` when invoking `terrariumctl` directly; release-selected tag when run through `install.sh` | Installs a specific Terrarium release tag, or builds from a branch-like source ref such as `main`. |
166-
| `--email` | email address | yes in non-interactive mode; no in interactive mode | prompted in interactive mode | Sets the Terrarium contact/admin email and default ZITADEL bootstrap admin email. |
166+
| `--email` | email address | yes in non-interactive mode; no in interactive mode | prompted in interactive mode | Sets the Terrarium contact/admin email and the default local-provider bootstrap admin email. |
167167
| `--acme-email` | email address | no | falls back to `--email` | Sets the ACME account identity for Traefik and LXD certificate automation. |
168168
| `--domain` | root domain | no | service domains default to `<service>.<dashed-public-ip>.traefik.me` when omitted | Sets the root domain used to derive service subdomains. |
169169
| `--manage-domain` | domain | no | `manage.<domain>` when `--domain` is set, otherwise `manage.<dashed-public-ip>.traefik.me` | Overrides the Cockpit domain. |
@@ -185,6 +185,8 @@ trm image delete golden-web
185185
| `--lxd-oidc-groups-claim` | claim name | no | follows provider default unless set | Overrides the LXD OIDC groups/roles claim. |
186186
| `--lxd-oidc-scopes` | space-separated scopes | no | follows provider default unless set | Overrides the LXD OIDC scopes. |
187187
| `--zitadel-admin-email` | email address | no | falls back to `--email` | Sets the initial admin email for self-hosted ZITADEL. |
188+
| `--logto-admin-email` | email address | no | falls back to `--email` for local Logto | Sets the bootstrap admin email for self-hosted Logto. |
189+
| `--logto-admin-username` | username | no | `terrarium_admin` for local Logto | Sets the bootstrap admin username for self-hosted Logto. Whitespace and control characters are rejected. |
188190
| `--generate-root-pwd` | none | yes in non-interactive mode when root has no usable local password unless `--root-pwd-file` is passed; no otherwise | none | Generates a strong Cockpit root password and saves it to `/etc/terrarium/secrets/cockpit_root_password` with root-only permissions. |
189191
| `--root-pwd-file` | path | yes in non-interactive mode when root has no usable local password unless `--generate-root-pwd` is passed; no otherwise | none | Reads the Cockpit root password from a local file. |
190192
| `--storage-mode` | `disk`, `partition`, or `file` | yes in non-interactive mode; no in interactive mode | prompted or auto-selected in interactive mode | Selects how the LXD ZFS pool is created. |
@@ -675,6 +677,7 @@ Behavior:
675677
| `--email` | email address | no | existing configured value | Updates the Terrarium contact/admin email. |
676678
| `--acme-email` | email address | no | existing configured value or falls back to `--email` | Updates the ACME account email. |
677679
| `--zitadel-admin-email` | email address | no | existing configured value or falls back to `--email` | Updates the self-hosted ZITADEL bootstrap admin email. |
680+
| `--logto-admin-email` | email address | no | existing configured value or falls back to `--email` when local Logto is selected | Updates the self-hosted Logto bootstrap admin email. |
678681

679682
## set idp
680683

@@ -697,6 +700,8 @@ Behavior:
697700
| `--lxd-oidc-scopes` | space-separated scopes | no | follows provider default unless set | Overrides the LXD OIDC scopes. |
698701
| `--local-idp-outputs-path` | path | no | `/etc/terrarium/zitadel-apps.json` compatibility path | Overrides where local provider client outputs are read/written. |
699702
| `--zitadel-admin-email` | email address | no | existing configured value or `--email` | Updates the ZITADEL bootstrap admin email when mode is `local`. |
703+
| `--logto-admin-email` | email address | no | existing configured value or `--email` when provider is `logto` and mode is `local` | Updates the Logto bootstrap admin email when mode is `local`. |
704+
| `--logto-admin-username` | username | no | existing configured value or `terrarium_admin` when provider is `logto` and mode is `local` | Updates the Logto bootstrap admin username when mode is `local`. Whitespace and control characters are rejected. |
700705

701706
External OIDC notes:
702707

@@ -715,6 +720,7 @@ Local provider notes:
715720
- Local ZITADEL remains the default when mode is `local` and no provider is set. External OIDC remains generic when no provider is set.
716721
- Local ZITADEL runs in the `terrarium-idp` LXD system instance, so its data is part of the LXD/ZFS backup set instead of host Docker state.
717722
- Local Logto also runs in the managed `terrarium-idp` system instance. It starts Logto plus Postgres through `terrarium-logto.service`, seeds the database unattended, and provisions Terrarium clients during `terrariumctl idp sync`.
723+
- Local Logto bootstrap defaults use `terrarium_email` for `terrarium_logto_admin_email` and `terrarium_admin` for `terrarium_logto_admin_username`; override them with `--logto-admin-email` and `--logto-admin-username` during install or `terrariumctl set idp local --provider logto`.
718724
- Terrarium auto-provisions a management role named after `terrarium_admin_group`, defaulting to `terrarium-admins`.
719725
- The bootstrap admin is granted that role automatically when the selected provider can resolve that user.
720726
- ZITADEL flattens Terrarium role assignments into a `groups` claim. Logto uses its `roles` claim and `openid profile email roles` scopes by default.

scripts/ctl/completion.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ describe("terrariumctl completion", () => {
5050
expect(script).toContain("--profile --disk --memory --cpu --requirements --playbook --role --docker-compose --cloud-init --proxy");
5151
expect(script).toContain("--ref --skip-reconfigure --non-interactive");
5252
expect(script).toContain("--skip-reconfigure");
53+
expect(script).toContain("--logto-admin-email");
54+
expect(script).toContain("--logto-admin-username");
5355
expect(script).toContain("--storage-source");
5456
expect(script).toContain("local oidc");
5557
expect(script).toContain("provider");

scripts/ctl/completion.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const optionGroups: Record<string, string[]> = {
5959
"--proxy-domain",
6060
"--lxd-domain",
6161
"--idp",
62+
"--idp-provider",
6263
"--admin-group",
6364
"--oidc",
6465
"--oidc-client",
@@ -69,6 +70,8 @@ const optionGroups: Record<string, string[]> = {
6970
"--lxd-oidc-secret-file",
7071
"--auth-domain",
7172
"--zitadel-admin-email",
73+
"--logto-admin-email",
74+
"--logto-admin-username",
7275
"--generate-root-pwd",
7376
"--root-pwd-file",
7477
"--storage-mode",
@@ -147,6 +150,10 @@ const optionGroups: Record<string, string[]> = {
147150
"--email",
148151
"--acme-email",
149152
"--zitadel-admin-email",
153+
"--logto-admin-email",
154+
"--logto-admin-username",
155+
"--provider",
156+
"--idp-provider",
150157
"--admin-group",
151158
"--oidc",
152159
"--oidc-client",

scripts/ctl/config.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ describe("terrariumctl config reconciliation", () => {
5858

5959
expect(ctlSource).toContain('.option("--provider <provider>"');
6060
expect(ctlSource).toContain('.option("--idp-provider <provider>"');
61+
expect(ctlSource).toContain('.option("--logto-admin-email <email>"');
62+
expect(ctlSource).toContain('.option("--logto-admin-username <username>"');
6163
expect(configSource).toContain('provider: cliOption(rawOptions, "provider", ["idpProvider", "idp-provider"])');
6264
});
6365

@@ -68,7 +70,9 @@ describe("terrariumctl config reconciliation", () => {
6870
"oidc-scopes": "openid profile email roles",
6971
"lxd-oidc-groups-claim": "organization_roles",
7072
"lxd-oidc-scopes": "openid email organizations",
71-
"local-idp-outputs-path": "/run/terrarium/idp-apps.json"
73+
"local-idp-outputs-path": "/run/terrarium/idp-apps.json",
74+
"logto-admin-email": "owner@example.test",
75+
"logto-admin-username": "owner_admin"
7276
});
7377

7478
expect(parsed.idp.provider).toBe("logto");
@@ -77,6 +81,16 @@ describe("terrariumctl config reconciliation", () => {
7781
expect(parsed.idp.lxdOidcGroupsClaim).toBe("organization_roles");
7882
expect(parsed.idp.lxdOidcScopes).toBe("openid email organizations");
7983
expect(parsed.idp.localIdpOutputsPath).toBe("/run/terrarium/idp-apps.json");
84+
expect(parsed.idp.logtoAdminEmail).toBe("owner@example.test");
85+
expect(parsed.idp.logtoAdminUsername).toBe("owner_admin");
86+
});
87+
88+
test("parses Logto admin email set email flag", () => {
89+
const parsed = parseSetCommandOptions({
90+
"logto-admin-email": "owner@example.test"
91+
});
92+
93+
expect(parsed.emails.logtoAdminEmail).toBe("owner@example.test");
8094
});
8195

8296
test("syncs local IDP outputs before proxy config convergence finishes", async () => {
@@ -231,6 +245,37 @@ describe("terrariumctl config reconciliation", () => {
231245
expect(lxdOidcScopes(config)).toBe("openid lxd");
232246
});
233247

248+
test("defaults local Logto admin config when switching to local Logto", () => {
249+
const config: Record<string, unknown> = {
250+
terrarium_public_ip: "203.0.113.10",
251+
terrarium_root_domain: "example.test",
252+
terrarium_email: "admin@example.test"
253+
};
254+
255+
applySetIdpConfig(config, { mode: "local", provider: "logto" });
256+
257+
expect(config.terrarium_logto_admin_email).toBe("admin@example.test");
258+
expect(config.terrarium_logto_admin_username).toBe("terrarium_admin");
259+
});
260+
261+
test("persists explicit local Logto admin config", () => {
262+
const config: Record<string, unknown> = {
263+
terrarium_public_ip: "203.0.113.10",
264+
terrarium_root_domain: "example.test",
265+
terrarium_email: "admin@example.test"
266+
};
267+
268+
applySetIdpConfig(config, {
269+
mode: "local",
270+
provider: "logto",
271+
logtoAdminEmail: " owner@example.test ",
272+
logtoAdminUsername: " owner_admin "
273+
});
274+
275+
expect(config.terrarium_logto_admin_email).toBe("owner@example.test");
276+
expect(config.terrarium_logto_admin_username).toBe("owner_admin");
277+
});
278+
234279
test("skips ZITADEL output stabilization for local Logto provider", async () => {
235280
const calls: string[] = [];
236281

0 commit comments

Comments
 (0)