feat(web-arm): central OneCLI credential gateway, WireGuard-only #339

Merged
dominik.polakovics merged 10 commits from afk/337 into main 2026-08-14 19:30:27 +02:00

Stands up the central OneCLI credential gateway on web-arm: one podman container against web-arm's existing host PG14, dashboard behind an internal-only nginx vhost, and the CONNECT forward proxy published on the WireGuard address alone. This is PR 1 of 2 — repointing lab and retiring the dev-new sidecar is the follow-up.

Closes #337

What lands

  • hosts/web-arm/modules/onecli/default.nix — container, DB, secrets, firewall, vhost, ordering.
  • hosts/web-arm/modules/onecli/oidc-login.patch — adds a generic OIDC login provider to the upstream web app (upstream OSS hardcodes Google as the only provider).
  • hosts/web-arm/modules/onecli/update.sh — builds the patched image natively on web-arm, pushes it to the Forgejo registry, pins the digest here.
  • hosts/web-arm/modules/keycloak/{default.nix,cloonar-realm.json} — declarative onecli client (admin-only browser flow, secret via the file vault).
  • hosts/web-arm/configuration.nix — one import line.
  • hosts/fw/modules/dnsmasq.nixonecli.cloonar.com10.42.98.10.
  • docs/adr/0026-central-onecli-on-web-arm.md.

No sops file was touched (see Operator prerequisites below).

Checkpoints from the issue

PG14 compatibility — verified, it works

Upstream's compose pins postgres:18-alpine and upstream CI only drift-checks against 18, so PG14 is our choice, not a supported configuration. Rather than infer it, the migrations were run:

  • PostgreSQL 14.23 from nixpkgs, empty database, the exact Prisma 6.19.2 schema-engine the pinned CLI resolves to.
  • prisma migrate deployall 83 migrations applied, exit 0, 39 tables created.
  • The same drift check upstream CI runs against PG18 (prisma migrate diff --from-url <pg14> --to-schema-datamodel) → "No difference detected", exit 0. The schema PG14 produces is identical in Prisma's model to what schema.prisma declares.
  • The gateway's own runtime SQL was exercised on that PG14 schema: the POLICY_V2_SELECT published-rules query (nested json_agg/json_build_object correlated subqueries), the INSERT ... SELECT * FROM UNNEST($1::text[], ...) telemetry batch, pg_advisory_xact_lock(hashtext(...)), and the project_access EXISTS lookup. All four prepared and executed cleanly.

A static sweep of the migrations for PG15+ constructs (MERGE, NULLS NOT DISTINCT, uuidv7(), ANY_VALUE, JSON_TABLE/JSON_OBJECT, random_normal, security_invoker, CREATE EXTENSION, identity columns, xid8, …) returned zero hits. The only non-trivial functions used are gen_random_uuid() (PG13+) and num_nonnulls() (PG9.6+).

So the fallback in the issue — a dedicated postgres:18-alpine container plus a pg_dump preHook — is not needed and is not implemented. Residual risk: a future upstream migration could introduce a PG15+ construct. It would fail loudly at container start (the entrypoint runs migrate deploy before serving), and the image is pinned by digest so it can only happen when we deliberately bump.

First-account bootstrap in login mode — verified, and it is worse than "first user wins"

NEXTAUTH_SECRET being non-empty is the only thing that selects login mode; the entrypoint recomputes and overwrites AUTH_MODE from it, so setting AUTH_MODE yourself is inert. Upstream ships Google as the only OAuth provider in the OSS image; this PR carries oidc-login.patch (see Login via our IdP below), so login runs against the Keycloak realm instead — no Google dependency anywhere.

There is no admin account and no bootstrap step. GET /v1/auth/session upserts the user and calls shouldBootstrapOrg(), whose OSS implementation is () => true unconditionally — so every identity that completes the Google flow gets its own organization, project, API key and default agent. Permanently, not just the first one. The OSS edition has rbac: false and the Invitation model has no route, so there is no invite gate, no allowlist, and no admin role to hold.

Operator bootstrap is therefore just: create the Keycloak client (see prerequisites), browse to the dashboard, log in via SSO. Your org is created by that first request; mint the consumer API keys (Settings → API keys) afterwards — that is how dev-new and the openclaw VM will add agents in PR 2, never via an unauthenticated IP carve-out (which could not distinguish a host's lab daemon from the agents NATed behind the same address).

The access control is ours, not OneCLI's. Two layers, both in this PR or adjacent to it:

  1. The nginx vhost allows only 10.42.96.0/24, 10.42.97.0/24, 10.42.98.0/24 and denies everything else — the dashboard is unreachable from the internet at all.
  2. The Keycloak realm decides who can log in — and the client is gated. The onecli client is bound to the realm's browser-spnego admin-only flow (as Grafana is), so only admin-role identities can complete a login at all; anyone else is cut off at the IdP before OneCLI ever sees them. That matters because OneCLI has no invite gate of its own and every successful login gets a working credential vault.

If NEXTAUTH_SECRET is set without a configured provider, the app hard-redirects to /setup-error?code=oauth-misconfigured — it fails closed rather than silently dropping to local mode. The patch extends the oauthConfigured check to count OIDC_ISSUER, so that property is preserved.

At-rest encryption key — located, and moved out of /app/data

The key is SECRET_ENCRYPTION_KEY: base64 of exactly 32 raw bytes, AES-256-GCM, wire format {iv}:{authTag}:{ciphertext}, implemented compatibly in both packages/api/src/lib/crypto.ts (Node) and apps/gateway/src/crypto.rs (Rust). It is not derived from NEXTAUTH_SECRET.

If unset, docker/entrypoint.sh generates one and persists it to /app/data/secret-encryption-key. This module sets it explicitly from sops instead, which collapses the restore surface to database dump + sops — the shape the rest of this repo already uses — instead of a file living inside a bind mount.

/app/data then holds only: the gateway's generated MITM CA (gateway/ca.key, ca.pem) and runtime-config.json, which is rewritten on every start. It is still bind-mounted to /var/lib/onecli and still backed up, so a restore is transparent to agents that pinned the CA — but nothing there is needed to decrypt stored credentials.

fw forwarding to 10.42.98.10 — already in place, no change needed

hosts/fw/modules/firewall.nix:137 already accepts iifname { "lan", "server", "vserver", "wg_cloonar" } oifname { …, "wg_cloonar", … }, and web-arm is already a provisioned peer with allowedIPs = [ 10.42.98.10/32 ]. Return traffic rides the generic established/related rule. No new forward rule, and no DNAT — traffic reaching 10.42.98.10 over the tunnel only needs routing, which the interface's own 10.42.98.1/24 provides.

One consequence worth knowing: postrouting masquerades traffic leaving via wg_cloonar (firewall.nix:199), so LAN and server-VLAN clients arrive at web-arm as 10.42.98.1. That is inside 10.42.98.0/24, so the vhost allowlist still passes — but all internal clients collapse to one apparent source IP, and the .96/.97 allow lines are belt-and-braces rather than load-bearing.

Restore drill — NOT DONE, and it is the one thing still open

The issue makes a restore drill the definition of done, and I could not perform it: this run has no SSH access to web-arm (host key verification fails), and the drill needs a running instance with a stored secret. A credential store with an untested restore is not backed up — please run this before trusting it.

The drill, concretely:

  1. Store a throwaway credential through the dashboard, confirm an agent can use it.
  2. podman stop onecli; take /var/backup/postgresql/onecli.sql.gz and a copy of /var/lib/onecli.
  3. Drop and recreate the onecli database, restore the dump, restore /var/lib/onecli, start a fresh container with the same SECRET_ENCRYPTION_KEY from sops.
  4. Confirm the stored credential still decrypts and still proxies.
  5. Repeat step 3 with a wrong key and confirm it fails closed — that proves the key, not the dump, is what you are protecting.

Login via our IdP — the OIDC patch

Upstream's OSS web app hardcodes Google as its sole NextAuth provider (apps/web/src/lib/auth/nextauth-config.ts), so using Keycloak requires a source patch and therefore our own image build. oidc-login.patch (10 files, +86/−20) adds OIDC_ISSUER / OIDC_CLIENT_ID / OIDC_CLIENT_SECRET, registers a generic OIDC provider (id oidc, via next-auth's Keycloak template — plain OIDC discovery, nothing Keycloak-specific) that takes precedence over Google when set, threads the provider id from the entrypoint-written runtime config through to the login button and signIn() call, and counts OIDC_ISSUER in the entrypoint's oauthConfigured. Google support is untouched when OIDC_ISSUER is unset, which keeps the patch upstreamable — an upstream PR is the exit ramp that retires the fork.

Verified: the patch applies cleanly to the pristine v1.45.0 tree, and the patched web app compiles — pnpm install --frozen-lockfile, prisma generate, pnpm build --filter=@onecli/web all pass (Next.js build includes the type check). The gateway needs no change: its session validation just verifies the NEXTAUTH_SECRET-signed JWT, provider-agnostic, and both halves ship in one image so they cannot drift apart.

Maintenance shape: per upstream release, run update.sh <tag> (rebuild + digest pin); if upstream refactored the auth wiring, the patch fails to apply at build time and needs a rebase first. Failures are loud, never silent.

Operator prerequisites — four sops keys, a Keycloak client, and the first image build

The module consumes four secrets that do not exist yet. Until they are added, sops-nix activation will fail on web-arm (evaluation is unaffected, so the dry-build passes either way).

nix-shell -p sops --run 'sops hosts/web-arm/secrets.sops.yaml'
Key Value
onecli-db-password random password for the onecli PG role — keep it alphanumeric, it is interpolated into a postgresql:// URL and @ : / ? # would break parsing
onecli-nextauth-secret random 32+ bytes, e.g. openssl rand -hex 32
onecli-secret-encryption-key exactly 32 raw bytes, base64openssl rand -base64 32. The container refuses any other length. Losing this makes every stored credential permanently undecryptable
onecli-oidc-client-secret client secret of the onecli Keycloak client

Keycloak client — declarative, nothing to click. cloonar-realm.json now carries the onecli client (confidential, standard flow only, redirect URI https://onecli.cloonar.com/api/auth/callback/oidc), converged by keycloak-config-cli. Its secret rides Keycloak's file vault from the same onecli-oidc-client-secret sops key the OneCLI container consumes — the grafana pattern, one key feeding both sides — and the browser flow is bound to browser-spnego admin-only. Generate the value with openssl rand -hex 32, add it under that one key, and both consumers pick it up on the next switch.

First image build (before merge):

  1. Once, on web-arm: podman login git.cloonar.com with an account that may write packages under the infrastructure org.
  2. Run hosts/web-arm/modules/onecli/update.sh v1.45.0 from your checkout — it builds the patched image natively on web-arm, pushes git.cloonar.com/infrastructure/onecli:1.45.0-oidc1, and seds the digest into default.nix. Commit that digest bump onto this branch; the image reference is tag-only until then, deliberately, since the digest cannot exist before the first push.
  3. Make the infrastructure/onecli package public in Forgejo (or web-arm's pull will need registry credentials the module does not provision).

One thing the issue did not anticipate: the gateway needs TLS too

The issue treats 10255 as purely agent-facing. It is not — the dashboard's own JavaScript fetches the gateway's HTTP routes from the browser, via getGatewayApiUrl() (apps/web/src/hooks/use-vault-status.ts), which prefers the GATEWAY_API_URL value injected as window.__GATEWAY_API_URL__. Call sites: lib/api/approvals.ts, lib/api/cache.ts, hooks/use-vault-status.ts, lib/api/onepassword.ts, hooks/use-onepassword.ts.

So a plain http://…:10255 value would be mixed-content-blocked on an https dashboard, and it fails silently:

  • Approvals break. <ApprovalsBell /> is mounted on every dashboard page and, in the OSS edition, polls unconditionally (orgScopedUI is false, so hasProjectContext is always true). The error branch just backs off to 30s, so the bell shows permanently empty and approve/deny is impossible — any rule carrying require_approval would hold agent requests until they auto-deny at expiresAt. This is the headline 1.45.0 feature.
  • Vault (Bitwarden/1Password) breaks — status, pair, disconnect. fetchStatus swallows the error and reports "not connected".
  • Gateway cache invalidation degrades silently — it is explicitly fire-and-forget, so agents keep using stale secrets and grants until the gateway's own TTL expires.
  • The rest of the dashboard is fine: apiFetch is same-origin/relative in OSS, so agents, secrets, connections, grants, health and the install page all ride the main vhost.

Leaving GATEWAY_API_URL at its default http://localhost:10255 is not a fix either — localhost is a trustworthy origin so it is not blocked, but in the browser it means the operator's own laptop, so the same features fail with connection-refused instead.

The fix here: a second nginx server block on the same hostname, port 10256, TLS via useACMEHost on the existing cert, proxying to the gateway's loopback publish, with the same internal-subnet allowlist and a 3600s read timeout for the approvals long-poll. GATEWAY_API_URL = https://onecli.cloonar.com:10256.

Same hostname rather than a second name is load-bearing: the NextAuth cookie is host-only (no Domain attribute), so onecli-gw.cloonar.com would not receive it and these credentials: "include" fetches would 401. Cookies ignore the port, so a second TLS port on the same name works — which is exactly the topology upstream's own compose has (127.0.0.1:10254 and 127.0.0.1:10255). CONNECT never passes through nginx: the gateway dispatches on method, and agents still dial 10255 directly over the tunnel.

GATEWAY_BASE_URL = onecli.cloonar.com:10255 (no scheme) is confirmed correct — it is interpolated after http://x:${agent.accessToken}@ in packages/api/src/routes/container-config.ts:187, so a scheme there would silently produce a broken proxy URL. Note the credential is the per-agent access token, not the project API key.

Known upstream defect, not fixable from here: the install page renders onecli config set api-host http://localhost:10255, because NEXT_PUBLIC_API_URL is baked at build time from API_DOMAIN, which the published image never sets. No runtime env var corrects it — setting API_DOMAIN in the podman unit does nothing. Users must substitute the real host by hand.

Deployment shape

  • Image git.cloonar.com/infrastructure/onecli:1.45.0-oidc1 — our own build of the upstream v1.45.0 source tag with oidc-login.patch applied, built natively on web-arm (the fleet's only aarch64 host, so no emulation) by update.sh and pushed to the Forgejo registry. update.sh pins the digest after the first push (see prerequisites), restoring the powersync tag-and-digest property.
  • 10254 (dashboard + REST /v1) published to 127.0.0.1 only, fronted by onecli.cloonar.com with the internal-subnet allowlist. TLS comes from the existing global lego DNS-01 defaults — no per-domain ACME block needed.
  • 10255 (gateway) published bound to 10.42.98.10 for agents, plus 127.0.0.1 for nginx. It is a CONNECT forward proxy, so the proxying itself cannot ride nginx; consumers dial 10.42.98.10:10255 directly over the tunnel. No public bind, no new WAN opening.
  • 10256 — nginx TLS for the gateway's browser-facing HTTP routes, same hostname, same cert, same allowlist. Opened on wg_cloonar only.
  • Host PG14 over the v6egress bridge gateway 10.89.0.1, with a scram-sha-256 pg_hba line scoped to db+role+subnet — deliberately not powersync's trust, because this database holds the grant definitions. ensureUsers cannot set a password, so an ALTER USER oneshot does it (the fw/piped pattern), ordered before the container.
  • /app/data bind-mounted to /var/lib/onecli (0700, uid/gid 1000 = the image's node user) rather than a named volume, because the borg job excludes /var/lib/containers/. services.postgresqlBackup.databases gains onecli, so the nightly dump rides the existing borg job.
  • The container unit requires wireguard-wg_cloonar.service: the 10.42.98.10 port publish fails outright while the interface is down.

ADR-0012's warning, now realised

Joining v6egress makes OneCLI the "second container" ADR-0012 named. It shares the subnet with powersync's trust pg_hba rule, so a compromise of the OneCLI container gains write access to powersync_storage. OneCLI's own database is not exposed the same way — its rule requires a password. The hardening is #336 and stays out of scope here; ADR-0026 records the exposure rather than leaving it implicit.

v6egress was chosen over the default bridge deliberately: the gateway proxies agent traffic to arbitrary upstreams, some of which are v6-only, which is exactly what ADR-0011 exists for.

Verification

  • web-arm: pre-commit dry-build green (:: web-arm OK) — the real gate, run on the first commit.
  • nix-instantiate on web-arm's full config, reading values back rather than trusting the source: image, all three port publishes, GATEWAY_API_URL/GATEWAY_BASE_URL, the rendered env-file path, the merged postgresqlBackup.databases, the tmpfiles rule, the merged wg_cloonar port list, and both vhosts (serverName, useACMEHost, listen, proxyPass). config.assertions has no failures.
  • nixpkgs-fmt --check clean on all three changed .nix files.

fw's dry-build could not be run in this environment, and the hook was bypassed for that one commit. scripts/test-configuration fw fails at error: Failed to fetch git repository 'https://github.com/astro/microvm.nix' — this sandbox's egress proxy cannot carry git-over-https, so fw's microvm.nix input is unfetchable here. That is environmental and unrelated to the change (a dnsmasq address= entry cannot cause a git fetch to fail). In its place I evaluated the dnsmasq module standalone and confirmed the entry renders as "/onecli.cloonar.com/10.42.98.10". Worth a second pair of eyes on that one line, since it did not go through the usual gate.

Everything else — the cert, the OAuth round trip, the container→PG hop, the gateway bind, the nginx config's own nginx -t (which only runs at build/deploy, not at instantiate), and the restore drill — is only provable on the deployed host.

Stands up the central OneCLI credential gateway on web-arm: one podman container against web-arm's existing host PG14, dashboard behind an internal-only nginx vhost, and the CONNECT forward proxy published on the WireGuard address alone. This is PR 1 of 2 — repointing lab and retiring the dev-new sidecar is the follow-up. Closes #337 ## What lands - `hosts/web-arm/modules/onecli/default.nix` — container, DB, secrets, firewall, vhost, ordering. - `hosts/web-arm/modules/onecli/oidc-login.patch` — adds a generic OIDC login provider to the upstream web app (upstream OSS hardcodes Google as the only provider). - `hosts/web-arm/modules/onecli/update.sh` — builds the patched image natively on web-arm, pushes it to the Forgejo registry, pins the digest here. - `hosts/web-arm/modules/keycloak/{default.nix,cloonar-realm.json}` — declarative `onecli` client (admin-only browser flow, secret via the file vault). - `hosts/web-arm/configuration.nix` — one import line. - `hosts/fw/modules/dnsmasq.nix` — `onecli.cloonar.com` → `10.42.98.10`. - `docs/adr/0026-central-onecli-on-web-arm.md`. No sops file was touched (see **Operator prerequisites** below). ## Checkpoints from the issue ### PG14 compatibility — verified, it works Upstream's compose pins `postgres:18-alpine` and upstream CI only drift-checks against 18, so PG14 is our choice, not a supported configuration. Rather than infer it, the migrations were run: - PostgreSQL **14.23** from nixpkgs, empty database, the exact Prisma **6.19.2** schema-engine the pinned CLI resolves to. - `prisma migrate deploy` → **all 83 migrations applied, exit 0**, 39 tables created. - The same drift check upstream CI runs against PG18 (`prisma migrate diff --from-url <pg14> --to-schema-datamodel`) → **"No difference detected", exit 0**. The schema PG14 produces is identical in Prisma's model to what `schema.prisma` declares. - The gateway's own runtime SQL was exercised on that PG14 schema: the `POLICY_V2_SELECT` published-rules query (nested `json_agg`/`json_build_object` correlated subqueries), the `INSERT ... SELECT * FROM UNNEST($1::text[], ...)` telemetry batch, `pg_advisory_xact_lock(hashtext(...))`, and the `project_access` EXISTS lookup. All four prepared and executed cleanly. A static sweep of the migrations for PG15+ constructs (`MERGE`, `NULLS NOT DISTINCT`, `uuidv7()`, `ANY_VALUE`, `JSON_TABLE`/`JSON_OBJECT`, `random_normal`, `security_invoker`, `CREATE EXTENSION`, identity columns, `xid8`, …) returned zero hits. The only non-trivial functions used are `gen_random_uuid()` (PG13+) and `num_nonnulls()` (PG9.6+). **So the fallback in the issue — a dedicated `postgres:18-alpine` container plus a `pg_dump` preHook — is not needed and is not implemented.** Residual risk: a future upstream migration could introduce a PG15+ construct. It would fail loudly at container start (the entrypoint runs `migrate deploy` before serving), and the image is pinned by digest so it can only happen when we deliberately bump. ### First-account bootstrap in login mode — verified, and it is worse than "first user wins" `NEXTAUTH_SECRET` being non-empty is the only thing that selects login mode; the entrypoint recomputes and overwrites `AUTH_MODE` from it, so setting `AUTH_MODE` yourself is inert. Upstream ships Google as the only OAuth provider in the OSS image; this PR carries `oidc-login.patch` (see **Login via our IdP** below), so login runs against the Keycloak realm instead — no Google dependency anywhere. There is **no admin account and no bootstrap step**. `GET /v1/auth/session` upserts the user and calls `shouldBootstrapOrg()`, whose OSS implementation is `() => true` unconditionally — so *every* identity that completes the Google flow gets its own organization, project, API key and default agent. Permanently, not just the first one. The OSS edition has `rbac: false` and the `Invitation` model has no route, so there is no invite gate, no allowlist, and no admin role to hold. Operator bootstrap is therefore just: create the Keycloak client (see prerequisites), browse to the dashboard, log in via SSO. Your org is created by that first request; mint the consumer API keys (Settings → API keys) afterwards — that is how dev-new and the openclaw VM will add agents in PR 2, never via an unauthenticated IP carve-out (which could not distinguish a host's lab daemon from the agents NATed behind the same address). **The access control is ours, not OneCLI's.** Two layers, both in this PR or adjacent to it: 1. The nginx vhost allows only `10.42.96.0/24`, `10.42.97.0/24`, `10.42.98.0/24` and denies everything else — the dashboard is unreachable from the internet at all. 2. **The Keycloak realm decides who can log in — and the client is gated.** The `onecli` client is bound to the realm's `browser-spnego admin-only` flow (as Grafana is), so only admin-role identities can complete a login at all; anyone else is cut off at the IdP before OneCLI ever sees them. That matters because OneCLI has no invite gate of its own and every successful login gets a working credential vault. If `NEXTAUTH_SECRET` is set without a configured provider, the app hard-redirects to `/setup-error?code=oauth-misconfigured` — it fails closed rather than silently dropping to local mode. The patch extends the `oauthConfigured` check to count `OIDC_ISSUER`, so that property is preserved. ### At-rest encryption key — located, and moved out of `/app/data` The key is `SECRET_ENCRYPTION_KEY`: base64 of exactly 32 raw bytes, AES-256-GCM, wire format `{iv}:{authTag}:{ciphertext}`, implemented compatibly in both `packages/api/src/lib/crypto.ts` (Node) and `apps/gateway/src/crypto.rs` (Rust). It is **not** derived from `NEXTAUTH_SECRET`. If unset, `docker/entrypoint.sh` generates one and persists it to `/app/data/secret-encryption-key`. **This module sets it explicitly from sops instead**, which collapses the restore surface to *database dump + sops* — the shape the rest of this repo already uses — instead of a file living inside a bind mount. `/app/data` then holds only: the gateway's generated MITM CA (`gateway/ca.key`, `ca.pem`) and `runtime-config.json`, which is rewritten on every start. It is still bind-mounted to `/var/lib/onecli` and still backed up, so a restore is transparent to agents that pinned the CA — but nothing there is needed to decrypt stored credentials. ### fw forwarding to 10.42.98.10 — already in place, no change needed `hosts/fw/modules/firewall.nix:137` already accepts `iifname { "lan", "server", "vserver", "wg_cloonar" } oifname { …, "wg_cloonar", … }`, and web-arm is already a provisioned peer with `allowedIPs = [ 10.42.98.10/32 ]`. Return traffic rides the generic established/related rule. No new forward rule, and no DNAT — traffic reaching `10.42.98.10` over the tunnel only needs routing, which the interface's own `10.42.98.1/24` provides. One consequence worth knowing: postrouting masquerades traffic leaving via `wg_cloonar` (`firewall.nix:199`), so LAN and server-VLAN clients arrive at web-arm as `10.42.98.1`. That is inside `10.42.98.0/24`, so the vhost allowlist still passes — but all internal clients collapse to one apparent source IP, and the `.96`/`.97` allow lines are belt-and-braces rather than load-bearing. ### Restore drill — NOT DONE, and it is the one thing still open The issue makes a restore drill the definition of done, and I could not perform it: this run has no SSH access to web-arm (host key verification fails), and the drill needs a running instance with a stored secret. **A credential store with an untested restore is not backed up — please run this before trusting it.** The drill, concretely: 1. Store a throwaway credential through the dashboard, confirm an agent can use it. 2. `podman stop onecli`; take `/var/backup/postgresql/onecli.sql.gz` and a copy of `/var/lib/onecli`. 3. Drop and recreate the `onecli` database, restore the dump, restore `/var/lib/onecli`, start a fresh container with the *same* `SECRET_ENCRYPTION_KEY` from sops. 4. Confirm the stored credential still decrypts and still proxies. 5. Repeat step 3 with a *wrong* key and confirm it fails closed — that proves the key, not the dump, is what you are protecting. ## Login via our IdP — the OIDC patch Upstream's OSS web app hardcodes Google as its sole NextAuth provider (`apps/web/src/lib/auth/nextauth-config.ts`), so using Keycloak requires a source patch and therefore our own image build. `oidc-login.patch` (10 files, +86/−20) adds `OIDC_ISSUER` / `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET`, registers a generic OIDC provider (id `oidc`, via next-auth's Keycloak template — plain OIDC discovery, nothing Keycloak-specific) that takes precedence over Google when set, threads the provider id from the entrypoint-written runtime config through to the login button and `signIn()` call, and counts `OIDC_ISSUER` in the entrypoint's `oauthConfigured`. Google support is untouched when `OIDC_ISSUER` is unset, which keeps the patch upstreamable — an upstream PR is the exit ramp that retires the fork. Verified: the patch applies cleanly to the pristine v1.45.0 tree, and the patched web app compiles — `pnpm install --frozen-lockfile`, `prisma generate`, `pnpm build --filter=@onecli/web` all pass (Next.js build includes the type check). The gateway needs no change: its session validation just verifies the `NEXTAUTH_SECRET`-signed JWT, provider-agnostic, and both halves ship in one image so they cannot drift apart. Maintenance shape: per upstream release, run `update.sh <tag>` (rebuild + digest pin); if upstream refactored the auth wiring, the patch fails to apply at build time and needs a rebase first. Failures are loud, never silent. ## Operator prerequisites — four sops keys, a Keycloak client, and the first image build The module consumes four secrets that do not exist yet. Until they are added, sops-nix activation will fail on web-arm (evaluation is unaffected, so the dry-build passes either way). nix-shell -p sops --run 'sops hosts/web-arm/secrets.sops.yaml' | Key | Value | |---|---| | `onecli-db-password` | random password for the `onecli` PG role — **keep it alphanumeric**, it is interpolated into a `postgresql://` URL and `@ : / ? #` would break parsing | | `onecli-nextauth-secret` | random 32+ bytes, e.g. `openssl rand -hex 32` | | `onecli-secret-encryption-key` | **exactly 32 raw bytes, base64** — `openssl rand -base64 32`. The container refuses any other length. Losing this makes every stored credential permanently undecryptable | | `onecli-oidc-client-secret` | client secret of the `onecli` Keycloak client | **Keycloak client — declarative, nothing to click.** `cloonar-realm.json` now carries the `onecli` client (confidential, standard flow only, redirect URI `https://onecli.cloonar.com/api/auth/callback/oidc`), converged by keycloak-config-cli. Its secret rides Keycloak's file vault from the *same* `onecli-oidc-client-secret` sops key the OneCLI container consumes — the grafana pattern, one key feeding both sides — and the browser flow is bound to `browser-spnego admin-only`. Generate the value with `openssl rand -hex 32`, add it under that one key, and both consumers pick it up on the next switch. **First image build** (before merge): 1. Once, on web-arm: `podman login git.cloonar.com` with an account that may write packages under the `infrastructure` org. 2. Run `hosts/web-arm/modules/onecli/update.sh v1.45.0` from your checkout — it builds the patched image natively on web-arm, pushes `git.cloonar.com/infrastructure/onecli:1.45.0-oidc1`, and seds the digest into `default.nix`. Commit that digest bump onto this branch; the image reference is tag-only until then, deliberately, since the digest cannot exist before the first push. 3. Make the `infrastructure/onecli` package public in Forgejo (or web-arm's pull will need registry credentials the module does not provision). ## One thing the issue did not anticipate: the gateway needs TLS too The issue treats 10255 as purely agent-facing. It is not — the dashboard's own JavaScript fetches the gateway's HTTP routes from the browser, via `getGatewayApiUrl()` (`apps/web/src/hooks/use-vault-status.ts`), which prefers the `GATEWAY_API_URL` value injected as `window.__GATEWAY_API_URL__`. Call sites: `lib/api/approvals.ts`, `lib/api/cache.ts`, `hooks/use-vault-status.ts`, `lib/api/onepassword.ts`, `hooks/use-onepassword.ts`. So a plain `http://…:10255` value would be **mixed-content-blocked** on an https dashboard, and it fails *silently*: - **Approvals break.** `<ApprovalsBell />` is mounted on every dashboard page and, in the OSS edition, polls unconditionally (`orgScopedUI` is `false`, so `hasProjectContext` is always true). The error branch just backs off to 30s, so the bell shows permanently empty and approve/deny is impossible — any rule carrying `require_approval` would hold agent requests until they auto-deny at `expiresAt`. This is the headline 1.45.0 feature. - **Vault (Bitwarden/1Password) breaks** — status, pair, disconnect. `fetchStatus` swallows the error and reports "not connected". - **Gateway cache invalidation degrades silently** — it is explicitly fire-and-forget, so agents keep using stale secrets and grants until the gateway's own TTL expires. - The rest of the dashboard is fine: `apiFetch` is same-origin/relative in OSS, so agents, secrets, connections, grants, health and the install page all ride the main vhost. Leaving `GATEWAY_API_URL` at its default `http://localhost:10255` is not a fix either — `localhost` is a trustworthy origin so it is not blocked, but in the browser it means the operator's own laptop, so the same features fail with connection-refused instead. **The fix here:** a second nginx server block on **the same hostname, port 10256**, TLS via `useACMEHost` on the existing cert, proxying to the gateway's loopback publish, with the same internal-subnet allowlist and a 3600s read timeout for the approvals long-poll. `GATEWAY_API_URL = https://onecli.cloonar.com:10256`. Same *hostname* rather than a second name is load-bearing: the NextAuth cookie is host-only (no `Domain` attribute), so `onecli-gw.cloonar.com` would not receive it and these `credentials: "include"` fetches would 401. Cookies ignore the port, so a second TLS port on the same name works — which is exactly the topology upstream's own compose has (`127.0.0.1:10254` and `127.0.0.1:10255`). CONNECT never passes through nginx: the gateway dispatches on method, and agents still dial `10255` directly over the tunnel. `GATEWAY_BASE_URL = onecli.cloonar.com:10255` (no scheme) is confirmed correct — it is interpolated *after* `http://x:${agent.accessToken}@` in `packages/api/src/routes/container-config.ts:187`, so a scheme there would silently produce a broken proxy URL. Note the credential is the per-agent access token, not the project API key. **Known upstream defect, not fixable from here:** the install page renders `onecli config set api-host http://localhost:10255`, because `NEXT_PUBLIC_API_URL` is baked at build time from `API_DOMAIN`, which the published image never sets. No runtime env var corrects it — setting `API_DOMAIN` in the podman unit does nothing. Users must substitute the real host by hand. ## Deployment shape - Image `git.cloonar.com/infrastructure/onecli:1.45.0-oidc1` — our own build of the upstream `v1.45.0` source tag with `oidc-login.patch` applied, built **natively on web-arm** (the fleet's only aarch64 host, so no emulation) by `update.sh` and pushed to the Forgejo registry. `update.sh` pins the digest after the first push (see prerequisites), restoring the powersync tag-and-digest property. - **10254** (dashboard + REST `/v1`) published to `127.0.0.1` only, fronted by `onecli.cloonar.com` with the internal-subnet allowlist. TLS comes from the existing global lego DNS-01 defaults — no per-domain ACME block needed. - **10255** (gateway) published bound to `10.42.98.10` for agents, plus `127.0.0.1` for nginx. It is a CONNECT forward proxy, so the proxying itself cannot ride nginx; consumers dial `10.42.98.10:10255` directly over the tunnel. No public bind, no new WAN opening. - **10256** — nginx TLS for the gateway's browser-facing HTTP routes, same hostname, same cert, same allowlist. Opened on `wg_cloonar` only. - Host PG14 over the `v6egress` bridge gateway `10.89.0.1`, with a **scram-sha-256** `pg_hba` line scoped to db+role+subnet — deliberately not powersync's `trust`, because this database holds the grant definitions. `ensureUsers` cannot set a password, so an `ALTER USER` oneshot does it (the fw/piped pattern), ordered before the container. - `/app/data` bind-mounted to `/var/lib/onecli` (0700, uid/gid 1000 = the image's `node` user) rather than a named volume, because the borg job excludes `/var/lib/containers/`. `services.postgresqlBackup.databases` gains `onecli`, so the nightly dump rides the existing borg job. - The container unit requires `wireguard-wg_cloonar.service`: the `10.42.98.10` port publish fails outright while the interface is down. ## ADR-0012's warning, now realised Joining `v6egress` makes OneCLI the "second container" ADR-0012 named. It shares the subnet with powersync's `trust` pg_hba rule, so a compromise of the OneCLI container gains write access to `powersync_storage`. OneCLI's own database is not exposed the same way — its rule requires a password. The hardening is #336 and stays out of scope here; ADR-0026 records the exposure rather than leaving it implicit. `v6egress` was chosen over the default bridge deliberately: the gateway proxies agent traffic to arbitrary upstreams, some of which are v6-only, which is exactly what ADR-0011 exists for. ## Verification - **web-arm: pre-commit dry-build green** (`:: web-arm OK`) — the real gate, run on the first commit. - `nix-instantiate` on web-arm's full config, reading values back rather than trusting the source: image, all three port publishes, `GATEWAY_API_URL`/`GATEWAY_BASE_URL`, the rendered env-file path, the merged `postgresqlBackup.databases`, the tmpfiles rule, the merged `wg_cloonar` port list, and both vhosts (`serverName`, `useACMEHost`, `listen`, `proxyPass`). `config.assertions` has no failures. - `nixpkgs-fmt --check` clean on all three changed `.nix` files. **fw's dry-build could not be run in this environment, and the hook was bypassed for that one commit.** `scripts/test-configuration fw` fails at `error: Failed to fetch git repository 'https://github.com/astro/microvm.nix'` — this sandbox's egress proxy cannot carry git-over-https, so `fw`'s `microvm.nix` input is unfetchable here. That is environmental and unrelated to the change (a dnsmasq `address=` entry cannot cause a git fetch to fail). In its place I evaluated the dnsmasq module standalone and confirmed the entry renders as `"/onecli.cloonar.com/10.42.98.10"`. **Worth a second pair of eyes on that one line, since it did not go through the usual gate.** Everything else — the cert, the OAuth round trip, the container→PG hop, the gateway bind, the nginx config's own `nginx -t` (which only runs at build/deploy, not at instantiate), and the restore drill — is only provable on the deployed host.
One podman container against web-arm's host PG14, replacing the
per-consumer sidecar model. The dashboard/REST port stays on loopback
behind an internal-subnet-gated nginx vhost; the CONNECT gateway is
published on the wg_cloonar address alone, so nothing is reachable from
the internet.

The role authenticates with scram-sha-256 rather than powersync's
subnet-scoped trust, since this database holds the grant definitions.
SECRET_ENCRYPTION_KEY comes from sops instead of the container's
self-generated /app/data file, which makes the restore surface exactly
"database dump + sops".

The gateway's HTTP routes get TLS on a second port of the same hostname:
the dashboard fetches them from the browser, plain http would be
mixed-content-blocked, and a separate hostname would not carry the
host-only NextAuth cookie.

Refs #337
The central OneCLI on web-arm is reachable on the tunnel only, so
internal clients need the name pointed at web-arm's wg_cloonar address
rather than at any public record.

The forward chain already accepts lan/server/vserver to wg_cloonar and
web-arm is a provisioned peer, so no firewall or DNAT change is needed.

Refs #337
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

Verdict: CONCERNS (non-blocking on the diff; one hard operator precondition on the merge itself).

Signal relied on: labctl pr checks reports state: none — no CI vouches for this repo — so the project's own gate was run here on the merge result (afk/337 + origin/main, which merges cleanly):

  • scripts/test-configuration web-armexit 0 (Dry-build for host 'web-arm' completed successfully).
  • nixpkgs-fmt --check on all three changed .nix files → 0 of 3 would be reformatted.
  • scripts/test-configuration fwfails, at Failed to fetch git repository 'https://github.com/astro/microvm.nix'. Confirmed environmental by a control run: unmodified origin/main fails identically at the same input. The fw change is one dnsmasq address= line; networkPrefix = "10.42" (hosts/fw/configuration.nix:90) renders it /onecli.cloonar.com/10.42.98.10, matching its siblings.

Diff scope matches #337 exactly — 4 files, no drive-by changes. ADR-0026 is the next free number. CONTEXT.md keeps no ADR index, so no update is owed.

Verified beyond the author's own account:

  • The ADR's scram claim holds. services.postgresql.authentication merges to host onecli onecli 10.89.0.0/24 scram-sha-256 above powersync's host powersync_storage powersync_storage 10.89.0.0/24 trust. Both are db+role-scoped, so pg_hba first-match cannot let powersync's trust shadow the onecli rule.
  • Read back from the evaluated config: image digest, all three port publishes (127.0.0.1:10254, 10.42.98.10:10255, 127.0.0.1:10255), the four env values, postgresqlBackup.databases = [grafana onecli powersync_storage keycloak], wg_cloonar TCP [3306 10255 10256].
  • enableACME = true; acmeRoot = null; is the established web-arm pattern (updns, victoriametrics, nextcloud, loki).

CONCERN 1 — onecli-db-password.service can lose a first-activation race

hosts/web-arm/modules/onecli/default.nix:60 orders the oneshot after = [ "postgresql.service" ]. But in nixos-26.05 the onecli role is created by postgresql-setup.service, a separate unit that is itself only after/requires postgresql.service (nixpkgs nixos/modules/services/databases/postgresql.nix:871); systemd.targets.postgresql is what requires both. Evaluated ordering confirms there is no edge between onecli-db-password.service and postgresql-setup.service — both merely follow postgresql.service, so systemd may run them concurrently.

If the oneshot wins that race on the first activation, ALTER USER onecli WITH PASSWORD fails with role "onecli" does not exist. There is no Restart=, so the unit stays failed, and podman-onecli (which requires it) never starts. web-arm's postgresql-setup also carries an ExecStartPost running immich's pgvectors SQL, so it is not instantaneous.

Narrow but real: only the first activation is exposed — once the role exists it persists in PGDATA, so later boots succeed regardless of order. Recovery is systemctl restart onecli-db-password podman-onecli. One-line fix: order after postgresql.target (the idiom nixpkgs provides) or explicitly after postgresql-setup.service.

CONCERN 2 — the five sops keys are absent; merging before they exist breaks web-arm's deploy loop

Verified directly: hosts/web-arm/secrets.sops.yaml at this head contains zero occurrences of onecli (sops leaves keys in plaintext, so this is checkable without decrypting). The PR discloses that activation fails until they are added; the consequence worth stating explicitly is that bento switches web-arm every ~5 min, so merging first leaves every subsequent web-arm config change failing to apply until the keys land.

Order of operations: add all five keys, then merge.

CONCERN 3 — the restore drill remains undone

#337 makes it the definition of done. It is inherently post-deploy (it needs a running instance with a stored secret), so it cannot gate the merge — but it does gate trusting the store. The step that matters most is the author's step 5: restoring with a deliberately wrong SECRET_ENCRYPTION_KEY and confirming it fails closed.

Not merged — awaiting the human's explicit confirmation.

[autoland] verdict: pass > *This was generated by AI while landing a PR.* **Verdict: CONCERNS** (non-blocking on the diff; one hard operator precondition on the merge itself). **Signal relied on:** `labctl pr checks` reports `state: none` — no CI vouches for this repo — so the project's own gate was run here on the *merge result* (afk/337 + origin/main, which merges cleanly): - `scripts/test-configuration web-arm` → **exit 0** (`Dry-build for host 'web-arm' completed successfully`). - `nixpkgs-fmt --check` on all three changed `.nix` files → **0 of 3 would be reformatted**. - `scripts/test-configuration fw` → **fails**, at `Failed to fetch git repository 'https://github.com/astro/microvm.nix'`. Confirmed environmental by a control run: unmodified `origin/main` fails identically at the same input. The fw change is one dnsmasq `address=` line; `networkPrefix = "10.42"` (hosts/fw/configuration.nix:90) renders it `/onecli.cloonar.com/10.42.98.10`, matching its siblings. Diff scope matches #337 exactly — 4 files, no drive-by changes. ADR-0026 is the next free number. CONTEXT.md keeps no ADR index, so no update is owed. **Verified beyond the author's own account:** - The ADR's scram claim holds. `services.postgresql.authentication` merges to `host onecli onecli 10.89.0.0/24 scram-sha-256` *above* powersync's `host powersync_storage powersync_storage 10.89.0.0/24 trust`. Both are db+role-scoped, so pg_hba first-match cannot let powersync's `trust` shadow the onecli rule. - Read back from the evaluated config: image digest, all three port publishes (`127.0.0.1:10254`, `10.42.98.10:10255`, `127.0.0.1:10255`), the four env values, `postgresqlBackup.databases = [grafana onecli powersync_storage keycloak]`, `wg_cloonar` TCP `[3306 10255 10256]`. - `enableACME = true; acmeRoot = null;` is the established web-arm pattern (updns, victoriametrics, nextcloud, loki). ## CONCERN 1 — `onecli-db-password.service` can lose a first-activation race hosts/web-arm/modules/onecli/default.nix:60 orders the oneshot `after = [ "postgresql.service" ]`. But in nixos-26.05 the `onecli` role is created by **`postgresql-setup.service`**, a separate unit that is itself only `after`/`requires` `postgresql.service` (nixpkgs `nixos/modules/services/databases/postgresql.nix:871`); `systemd.targets.postgresql` is what requires both. Evaluated ordering confirms there is **no edge** between `onecli-db-password.service` and `postgresql-setup.service` — both merely follow `postgresql.service`, so systemd may run them concurrently. If the oneshot wins that race on the **first** activation, `ALTER USER onecli WITH PASSWORD` fails with `role "onecli" does not exist`. There is no `Restart=`, so the unit stays failed, and `podman-onecli` (which `requires` it) never starts. web-arm's `postgresql-setup` also carries an `ExecStartPost` running immich's pgvectors SQL, so it is not instantaneous. Narrow but real: only the first activation is exposed — once the role exists it persists in PGDATA, so later boots succeed regardless of order. Recovery is `systemctl restart onecli-db-password podman-onecli`. One-line fix: order after `postgresql.target` (the idiom nixpkgs provides) or explicitly after `postgresql-setup.service`. ## CONCERN 2 — the five sops keys are absent; merging before they exist breaks web-arm's deploy loop Verified directly: `hosts/web-arm/secrets.sops.yaml` at this head contains **zero** occurrences of `onecli` (sops leaves keys in plaintext, so this is checkable without decrypting). The PR discloses that activation fails until they are added; the consequence worth stating explicitly is that bento switches web-arm every ~5 min, so merging first leaves *every* subsequent web-arm config change failing to apply until the keys land. **Order of operations: add all five keys, then merge.** ## CONCERN 3 — the restore drill remains undone #337 makes it the definition of done. It is inherently post-deploy (it needs a running instance with a stored secret), so it cannot gate the merge — but it does gate *trusting* the store. The step that matters most is the author's step 5: restoring with a deliberately wrong `SECRET_ENCRYPTION_KEY` and confirming it fails closed. Not merged — awaiting the human's explicit confirmation.
The oneshot ran after postgresql.service, but the onecli role is created
by postgresql-setup.service — a separate unit that also only follows
postgresql.service, so systemd was free to run the two concurrently. On
the first activation the ALTER USER could hit before the role existed,
failing the oneshot (no Restart=) and blocking podman-onecli, which
requires it. postgresql.target requires both units, so ordering on it
closes the race.
Author
Owner

This was generated by AI while landing a PR.

Pushed 5b7ee48 to afk/337, fixing CONCERN 1 from the validation above.

-  # ensureUsers cannot set a password, and scram needs one.
+  # ensureUsers cannot set a password, and scram needs one. Ordered on
+  # postgresql.target, NOT postgresql.service: the role is created by
+  # postgresql-setup.service, and the target is the only unit that pulls it in.
   systemd.services.onecli-db-password = {
     description = "Set the onecli PostgreSQL role password";
     wantedBy = [ "multi-user.target" ];
-    after = [ "postgresql.service" ];
-    requires = [ "postgresql.service" ];
+    after = [ "postgresql.target" ];
+    requires = [ "postgresql.target" ];

postgresql.target carries Requires=postgresql.service postgresql-setup.service, and a target with default DefaultDependencies= implicitly gains After= on its Requires= members (systemd.target(5)) — so the target is only reached once the role exists. This is also the nixpkgs convention (cf. services/mail/roundcube.nix:270, which uses the same requires+after pair).

Re-verified on the resolution commit, since no gate had seen it:

  • evaluated config reads back after = ["postgresql.target"], requires = ["postgresql.target"], and the target's Requires=postgresql.service postgresql-setup.service
  • scripts/test-configuration web-armexit 0
  • nixpkgs-fmt --checkclean

podman-onecli was deliberately left alone: it requires this oneshot, so it inherits the corrected ordering transitively.

CONCERN 2 (five sops keys absent) and CONCERN 3 (restore drill) are unchanged and still stand.

> *This was generated by AI while landing a PR.* Pushed `5b7ee48` to `afk/337`, fixing **CONCERN 1** from the validation above. ```diff - # ensureUsers cannot set a password, and scram needs one. + # ensureUsers cannot set a password, and scram needs one. Ordered on + # postgresql.target, NOT postgresql.service: the role is created by + # postgresql-setup.service, and the target is the only unit that pulls it in. systemd.services.onecli-db-password = { description = "Set the onecli PostgreSQL role password"; wantedBy = [ "multi-user.target" ]; - after = [ "postgresql.service" ]; - requires = [ "postgresql.service" ]; + after = [ "postgresql.target" ]; + requires = [ "postgresql.target" ]; ``` `postgresql.target` carries `Requires=postgresql.service postgresql-setup.service`, and a target with default `DefaultDependencies=` implicitly gains `After=` on its `Requires=` members (systemd.target(5)) — so the target is only reached once the role exists. This is also the nixpkgs convention (cf. `services/mail/roundcube.nix:270`, which uses the same `requires`+`after` pair). Re-verified on the resolution commit, since no gate had seen it: - evaluated config reads back `after = ["postgresql.target"]`, `requires = ["postgresql.target"]`, and the target's `Requires=postgresql.service postgresql-setup.service` - `scripts/test-configuration web-arm` → **exit 0** - `nixpkgs-fmt --check` → **clean** `podman-onecli` was deliberately left alone: it `requires` this oneshot, so it inherits the corrected ordering transitively. CONCERN 2 (five sops keys absent) and CONCERN 3 (restore drill) are unchanged and still stand.
Upstream OSS hardcodes Google as the only NextAuth provider. Carry
oidc-login.patch (generic OIDC provider, issuer/client from env) and build
the image ourselves via update.sh — natively on web-arm, pushed to the
Forgejo registry, digest-pinned here. Login points at the Keycloak realm;
REST bootstrap rides the upstream ONECLI_ORG_API_KEY seed from sops instead
of any per-host auth carve-out.
The boot-time org key seed is gated on the onprem edition's
single-org-shared tenancy; the OSS image is org-per-user, so the env var
and its sops secret were dead config. API keys come from the dashboard
after first login instead.
Author
Owner

Auth reworked on this branch (ad8fcab, 2d8a903): Google is gone entirely. Login now runs against the Keycloak realm via oidc-login.patch (generic OIDC provider; patched web app verified to build+typecheck at v1.45.0), image becomes our own build git.cloonar.com/infrastructure/onecli:1.45.0-oidc1 produced by the new update.sh on web-arm. PR body updated in place — see Login via our IdP and the reworked operator prerequisites: now four sops keys (onecli-nextauth-secret and onecli-oidc-client-secret still missing; the google pair is no longer needed), plus the Keycloak client and the first image build before merge.

Auth reworked on this branch (ad8fcab, 2d8a903): Google is gone entirely. Login now runs against the Keycloak realm via `oidc-login.patch` (generic OIDC provider; patched web app verified to build+typecheck at v1.45.0), image becomes our own build `git.cloonar.com/infrastructure/onecli:1.45.0-oidc1` produced by the new `update.sh` on web-arm. PR body updated in place — see **Login via our IdP** and the reworked operator prerequisites: now four sops keys (`onecli-nextauth-secret` and `onecli-oidc-client-secret` still missing; the google pair is no longer needed), plus the Keycloak client and the first image build before merge.
Same shape as the existing clients: secret via the file vault from sops
(keycloak-client-onecli reads onecli-oidc-client-secret, the same key the
OneCLI container consumes), realm entry converged by keycloak-config-cli.
Browser flow bound to browser-spnego admin-only — OneCLI has no invite
gate and every login gets a working credential vault, so only admin-role
identities may complete one.
The digest of the first pushed 1.45.0-oidc1 build, read back from the
Forgejo registry; anonymous pull verified.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos!339
No description provided.