web-arm: central OneCLI credential gateway (container + host PG14, WireGuard-only exposure, borg-backed) #337

Closed
opened 2026-08-14 12:38:28 +02:00 by dominik.polakovics · 0 comments

Stand up a central OneCLI instance on web-arm, to be shared by multiple consumers (coding-lab first, a nanoclaw instance later — nanoclaw itself out of scope). This is PR 1 of 2; the lab cutover and dev-new sidecar retirement is a separate follow-up issue. Design was settled in a grill session on 2026-08-14.

Deployment shape

  • New module hosts/web-arm/modules/onecli/ imported from web-arm's configuration.
  • One podman oci-containers container, image ghcr.io/onecli/onecli pinned by tag + sha256 digest (powersync pattern, see hosts/web-arm/modules/powersync/default.nix) — no bundled postgres container.
  • Version >= 1.45.0 (lab drives the per-agent grants API added in that release; arm64 manifest confirmed on ghcr for 1.45.0).
  • Reference for env/wiring: the existing sidecar hosts/dev-new/modules/coding-lab/onecli.nix and upstream docker/docker-compose.yml.

Database: web-arm's host PG14

  • services.postgresql: ensureDatabases = [ "onecli" ], ensureUsers with ensureDBOwnership; container reaches PG over TCP via the container bridge gateway (powersync precedent).
  • Real scram password, NOT powersync's trust rule (OneCLI's DB holds grant definitions a writer could widen): password lives in web-arm sops, injected as DATABASE_URL via the container's environmentFiles (sops template). pg_hba scram-sha-256 line scoped to db+role+bridge-subnet as second layer, plus the matching firewall allowance for 5432 from that subnet only.
  • Checkpoint: upstream compose pins postgres 18; verify OneCLI's migrations actually run on PG14 before merging. Fallback if incompatible: dedicated postgres:18-alpine container on a private podman network (dev-new pattern) plus a pg_dump preHook into borg — note the deviation in the PR.

Auth: login mode

  • NEXTAUTH_SECRET set from web-arm sops (secret injected via environmentFiles — never plain environment). No local mode: the central instance must not be admin-to-whoever-connects.
  • APP_URL = https://onecli.cloonar.com (OAuth redirect_uris derive from it).
  • INTERNAL_API_URL = http://localhost:10254 (gateway→API stays in-container).
  • Checkpoint: verify the first-account bootstrap flow in login mode (how the initial user is created) and document it in the PR.

Exposure: WireGuard tunnel only, nothing public

  • Dashboard/REST (10254): container port published to loopback only; nginx vhost onecli.cloonar.com (forceSSL, cert via the existing lego DNS-01 defaults) proxying to it, with allow 10.42.96.0/24; allow 10.42.97.0/24; allow 10.42.98.0/24; deny all;.
  • Gateway (10255): published bound to the wg_cloonar address 10.42.98.10 ONLY. It is a CONNECT forward proxy and cannot ride nginx; consumers dial it directly over the tunnel. No public bind, no new WAN firewall opening on web-arm.
  • Internal DNS: add onecli.cloonar.com → 10.42.98.10 to fw's dnsmasq so internal clients and the browser resolve it over the tunnel.
  • Checkpoint: confirm fw forwards LAN/server-VLAN traffic to 10.42.98.10 over wg_cloonar (and adjust fw's forward rules if not).

Backups (all riding existing machinery)

  • services.postgresqlBackup.databases = [ "onecli" ] — nightly dump lands under /var/backup/postgresql, inside the borg job.
  • /app/data bind-mounted to host dir /var/lib/onecli (mode 0700) — deliberately NOT a named volume: web-arm's borg job excludes /var/lib/containers/, so a named volume would silently never be backed up, and upstream is silent on where the at-rest AES-256-GCM encryption key lives (most likely /app/data).
  • Checkpoint: locate the encryption key empirically (inspect /app/data and container env on the running instance) and record it in the PR.
  • Checkpoint (definition of done): one restore drill — fresh container + restored pg dump + restored /var/lib/onecli must decrypt a stored secret. A credential store with untested restore is not backed up.

Secrets for the operator to add (agent must NOT edit sops files)

In hosts/web-arm/secrets.sops.yaml via nix-shell -p sops --run 'sops hosts/web-arm/secrets.sops.yaml':

  • onecli-nextauth-secret — random 32+ byte value
  • onecli-db-password — random password for the onecli PG role

(The module should consume both via sops templates/environmentFiles; exact key names may follow the module's naming, but the PR must state them.)

Out of scope here

  • Repointing lab / retiring the dev-new sidecar and its 8081 plumbing (follow-up issue, PR 2).
  • powersync pg_hba hardening (#336) — but note this deployment makes OneCLI the "second container" ADR-0012 warned about.
  • Consider a short ADR (docs/adr/0026) recording: central OneCLI on web-arm, WireGuard-only exposure, fail-closed coupling of lab spawns to the tunnel.
Stand up a central OneCLI instance on web-arm, to be shared by multiple consumers (coding-lab first, a nanoclaw instance later — nanoclaw itself out of scope). This is PR 1 of 2; the lab cutover and dev-new sidecar retirement is a separate follow-up issue. Design was settled in a grill session on 2026-08-14. ## Deployment shape - New module `hosts/web-arm/modules/onecli/` imported from web-arm's configuration. - One podman `oci-containers` container, image `ghcr.io/onecli/onecli` pinned by **tag + sha256 digest** (powersync pattern, see hosts/web-arm/modules/powersync/default.nix) — no bundled postgres container. - Version >= 1.45.0 (lab drives the per-agent grants API added in that release; arm64 manifest confirmed on ghcr for 1.45.0). - Reference for env/wiring: the existing sidecar `hosts/dev-new/modules/coding-lab/onecli.nix` and upstream `docker/docker-compose.yml`. ## Database: web-arm's host PG14 - `services.postgresql`: `ensureDatabases = [ "onecli" ]`, `ensureUsers` with `ensureDBOwnership`; container reaches PG over TCP via the container bridge gateway (powersync precedent). - **Real scram password**, NOT powersync's trust rule (OneCLI's DB holds grant definitions a writer could widen): password lives in web-arm sops, injected as `DATABASE_URL` via the container's `environmentFiles` (sops template). `pg_hba` scram-sha-256 line scoped to db+role+bridge-subnet as second layer, plus the matching firewall allowance for 5432 from that subnet only. - **Checkpoint:** upstream compose pins postgres 18; verify OneCLI's migrations actually run on PG14 before merging. Fallback if incompatible: dedicated postgres:18-alpine container on a private podman network (dev-new pattern) plus a pg_dump preHook into borg — note the deviation in the PR. ## Auth: login mode - `NEXTAUTH_SECRET` set from web-arm sops (secret injected via environmentFiles — never plain `environment`). No local mode: the central instance must not be admin-to-whoever-connects. - `APP_URL = https://onecli.cloonar.com` (OAuth redirect_uris derive from it). - `INTERNAL_API_URL = http://localhost:10254` (gateway→API stays in-container). - **Checkpoint:** verify the first-account bootstrap flow in login mode (how the initial user is created) and document it in the PR. ## Exposure: WireGuard tunnel only, nothing public - Dashboard/REST (10254): container port published to loopback only; nginx vhost `onecli.cloonar.com` (forceSSL, cert via the existing lego DNS-01 defaults) proxying to it, with `allow 10.42.96.0/24; allow 10.42.97.0/24; allow 10.42.98.0/24; deny all;`. - Gateway (10255): published bound to the wg_cloonar address `10.42.98.10` ONLY. It is a CONNECT forward proxy and cannot ride nginx; consumers dial it directly over the tunnel. No public bind, no new WAN firewall opening on web-arm. - Internal DNS: add `onecli.cloonar.com → 10.42.98.10` to fw's dnsmasq so internal clients and the browser resolve it over the tunnel. - **Checkpoint:** confirm fw forwards LAN/server-VLAN traffic to 10.42.98.10 over wg_cloonar (and adjust fw's forward rules if not). ## Backups (all riding existing machinery) - `services.postgresqlBackup.databases = [ "onecli" ]` — nightly dump lands under /var/backup/postgresql, inside the borg job. - `/app/data` bind-mounted to host dir `/var/lib/onecli` (mode 0700) — deliberately NOT a named volume: web-arm's borg job excludes /var/lib/containers/, so a named volume would silently never be backed up, and upstream is silent on where the at-rest AES-256-GCM encryption key lives (most likely /app/data). - **Checkpoint:** locate the encryption key empirically (inspect /app/data and container env on the running instance) and record it in the PR. - **Checkpoint (definition of done):** one restore drill — fresh container + restored pg dump + restored /var/lib/onecli must decrypt a stored secret. A credential store with untested restore is not backed up. ## Secrets for the operator to add (agent must NOT edit sops files) In `hosts/web-arm/secrets.sops.yaml` via `nix-shell -p sops --run 'sops hosts/web-arm/secrets.sops.yaml'`: - `onecli-nextauth-secret` — random 32+ byte value - `onecli-db-password` — random password for the `onecli` PG role (The module should consume both via sops templates/environmentFiles; exact key names may follow the module's naming, but the PR must state them.) ## Out of scope here - Repointing lab / retiring the dev-new sidecar and its 8081 plumbing (follow-up issue, PR 2). - powersync pg_hba hardening (#336) — but note this deployment makes OneCLI the "second container" ADR-0012 warned about. - Consider a short ADR (docs/adr/0026) recording: central OneCLI on web-arm, WireGuard-only exposure, fail-closed coupling of lab spawns to the tunnel.
Sign in to join this conversation.
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#337
No description provided.