Create Pocket ID service on Milner #34
Labels
No labels
blocked
bug
duplicate
effort
high
effort
low
effort
medium
effort
minimal
enhancement
help wanted
invalid
pr
feedback
pr
reviewing
question
status
backlog
status
document
status
done
status
implement
status
pending
status
planning
status
reviewing
waiting
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tepichord/milner#34
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Naina:
Overview
Deploy Pocket ID as the cluster OIDC provider on Milner — an arm64-capable StatefulSet with SQLite on a new worker data volume, exposed to the tailnet at
pocket-id.faun-snapper.ts.netthrough the existingingress-proxiesProxyGroup (same pattern as Vault). Then wire Vault as the first OIDC consumer (JWT/OIDC auth method, manual client registration + role/policy in OPERATIONS.md) and update the docs to match.Technical Approach
worker/main.tf)Attach as
/dev/vdc(third entry inlibvirt_domain.workerdisks) and add a second TalosUserVolumeConfigin the machine-config patch.1 Today the worker has exactly two user disks (vdaworker,vdbvault — the ISO is a scsisdacdrom), so a third disk entry does land onvdc. Make both disk selectors unambiguous — vault's current bare!system_diskwould match two user disks after this lands:2 The vaultUserVolumeConfigatworker/main.tfisdiskSelector { match = "!system_disk" }with onlyminSize = 10GB— nothing that would exclude a second user disk once the pocket-id volume is added.The size split matches reality: vault's qcow2 is 20 GiB (
capacity = 21474836480), so> 15u * GiBvs< 15u * GiBkeeps the two selectors mutually exclusive.3Mount is activated only at boot →
terraform -chdir=worker apply -replace='libvirt_domain.worker'(vault precedent, OPERATIONS.md §4e).4 OPERATIONS.md §4e documents exactly this: the mount is activated "only at boot" and the recovery isterraform -chdir=worker apply -replace='libvirt_domain.worker'.pocket-id/Terraform root mirroringvault/— no new ProxyGroup (cluster-scopedingress-proxiesis owned by the vault root's state):5 Theingress-proxiesProxyGroup manifest (no namespace → cluster-scoped) is declared invault/main.tf, and OPERATIONS.md §1/§3 confirm it is Terraform-owned by the vault root.pocket-id/values.yaml— hobbit44 chart defaults need four overrides (probes default to/health, which Pocket ID doesn't serve; PVC default needs no provisioner; config-as-env; worker pinning):6 The chart'slivenessProbe/readinessProbedefault tohttpGet.path: /health; Pocket ID's canonical health endpoint is/healthz.7 (Nuance: v2.14.0 also serves/healthas an alias of/healthz, so the chart's/healthprobe is not strictly dead — but/healthzis the documented, backend-native endpoint.)Annotations on the values above: v2.14.0 is the current app release (2026-08-18) while the published hobbit44 chart 2.4.0 pins
appVersion: 2.4.0and its statefulset defaultsimage.tagto.Chart.AppVersion, so the explicit tag override is required8. "No StorageClass on Milner" is consistent with the repo: nothing installs a provisioner today and vault uses a hostPath (dataStorage.enabled: false+ volume to/var/mnt/vault) — note onlywoodpecker/values.yaml(a "coming soon" workload) referenceslocal-path9. The/app/datatarget matches the official v2.14.0 compose mount./data:/app/dataand the app's SQLite/upload defaults (data/pocket-id.db,data/uploads)1011. ⚠ Caveat: the published chart 2.4.0 unconditionally mounts the data volume at/app/backend/data(not/app/data) and does not gate that volumeMount onpersistence.enabled, so the plan's hostPath volume +/app/datavolumeMount must be reconciled with the chart's fixed/app/backend/datamount at install time.node-role: workermatches the worker machine-config nodeLabel12.Namespace gets the privileged PSA label (
hostPath), exactly like the vault namespace.13 The vault namespace carriespod-security.kubernetes.io/enforce=privileged.Imperative setup runbook →
OPERATIONS.md(§5 Pocket ID; renumber old §5 Client devices → §6, §6 Bring-up order → §7, fixing internal refs):14 Current headings: §5 "Client devices" (L201), §6 "Bring-up order (from a wiped cluster)" (L222); there is no §7 today, so the renumber is clean.https://pocket-id.faun-snapper.ts.net/setupfrom a tailnet device (passkey only, no default creds; needs HTTPS for WebAuthn).15 Pocket ID docs: HTTPS is required because the app uses the WebAuthn API; the first admin is created through the UI (/setuproute exists in the app) — there is no env-var/default-password bootstrap.Vault, confidential, callback URLshttps://vault.faun-snapper.ts.net/ui/vault/auth/oidc/oidc/callback+http://localhost:8250/oidc/callback(CLI flow).vault/README.mdthe approve step is Prereq 3, not 4 — item 4 there is client versions.)Vault OIDC auth method (imperative, OPERATIONS.md) — provider-agnostic HashiCorp tutorial flow:17 The
auth enable oidc/auth/oidc/config/ role shape and the CLI callbackhttp://localhost:8250/oidc/callbackmatch Vault's documented JWT/OIDC auth method (a localhost redirect URI is required forvault login -method=oidc; 8250 is the documented default listener).README.mdworkload list,ARCHITECTURE.mdpod topology + network diagram + service table,AGENTS.mdrepo map;tests/test_static.py(pocket-id/toterraform fmtdirs,pocket-id/values.yamlto yamllint list) andtests/test_terraform.py(pocket-id root).Implementation Details
pocket-id/main.tf,pocket-id/values.yaml,pocket-id/README.md,pocket-id/tests/plan.tftest.hcl+tests/fixtures/cp.tfstate(copy of vault's fixture).worker/main.tf,tests/test_static.py,tests/test_terraform.py,README.md,ARCHITECTURE.md,AGENTS.md,OPERATIONS.md,vault/README.md(OIDC section pointer).oci://ghcr.io/hobbit44/pocket-id-helm;18 official imageghcr.io/pocket-id/pocket-id:v2.14.0is multi-arch (arm64 ✓);19 vault-helm 0.34.1 supportsserver.hostAliasesif ever needed.20 (2.4.0 verified present on GHCR: the OCI repohobbit44/pocket-id-helm/pocket-idlists a2.4.0tag; vault-helm v0.34.1 documentsserver.hostAliasesand its statefulset template renders it.)APP_URL(https://pocket-id.faun-snapper.ts.net), and Vault is a strict go-oidc client: itsoidc_discovery_urlmust equal the issuer and be reachable+trusted from the Vault pod.21INTERNAL_APP_URLalone does not fix this (it rewrites only token/userinfo/jwks endpoints; issuer/auth endpoints stay onAPP_URL— upstream discussion pocket-id/pocket-id#1169 is unanswered).2223 (Verified inwell_known_controller.go:issuer,authorization_endpoint,end_session_endpoint,device_authorization_endpointall stay onAPP_URL; onlytoken_endpoint/userinfo_endpoint/introspection_endpoint/jwks_uri/PAR move toINTERNAL_APP_URL. Discussion #1169 is open with zero replies as of 2026-09-09, so it is indeed unanswered.) Workable topologies, in order of preference:hostAliasesmappingpocket-id.faun-snapper.ts.net→ pocket-id ClusterIP, Pocket ID serving native TLS (TLS_CERT_FILE/TLS_KEY_FILE, PR #1429) with a small private CA, Vault pinning it viaoidc_discovery_ca_pem.24 (PR #1429 "feat: add TLS support for HTTP/2 server" introducedTLS_CERT_FILE/TLS_KEY_FILE; both env vars exist in the app'senv_config.go.) Browser path unaffected (operator edge keeps its LE cert).pocket-id-operatorCRD is explicitly out of scope)25 (a communityaclerici38/pocket-id-operatorCRD project exists, but there is no official, stable client-management API to drive from Terraform — hand-run registration is the only supported path); no Postgres (SQLite on local qcow2 is the right homelab scale); no group restriction on the Vault client (leave default open, revisit later); Woodpecker CI OIDC is future work and not blocked by this design.Acceptance Criteria
worker/main.tfshows a second 10 GiBpocket-idqcow2 +UserVolumeConfig(mount/var/mnt/pocket-id) with unambiguous disk selectors;terraform -chdir=worker plan/apply+ worker-replaceactivates it.pocket-id/root passesterraform fmt,terraform -chdir=pocket-id test(mock-provider plan assertions: namespace privileged-PSA label, chart/version, ingress classtailscale+proxy-group: ingress-proxiesannotation +tls.hosts=["pocket-id"]), and updatedpytestlint lists are green.Readyon the worker (/healthzprobe), data persists under/app/dataacross pod restarts, andhttps://pocket-id.faun-snapper.ts.netserves the UI +/.well-known/openid-configurationfrom a tailnet device./setup; aVaultOIDC client exists in Pocket ID with both callback URLs.Testing Plan
python -m pytest tests/(terraform fmt incl.pocket-id/, yamllint onpocket-id/values.yaml, ruff).terraform -chdir=pocket-id test— namespace PSA label, helm chart/repo/version, image tagv2.14.0, persistence disabled + hostPath mount at/app/data,/healthzprobes, ingress pinned name + proxy-group annotation.worker/tests/plan.tftest.hcl— assert bothUserVolumeConfigs exist and selectors are mutually exclusive.kubectl -n pocket-id exec→curl :1411/healthz;curl https://pocket-id.faun-snapper.ts.net/.well-known/openid-configurationfrom a tailnet device; kill the pod and confirm SQLite survives; browservault login -method=oidccompletes.Related Issues/PRs
Research Findings
ghcr.io/hobbit44/pocket-id-helm) — the one Pocket ID docs link to; actively renovate-updated; StatefulSet + PVC default, but its default/healthprobes are wrong (must be/healthz) and its 8 GiB PVC default needs no provisioner (disable + hostPath).ingress-proxiesProxyGroup (reused across namespaces) withtailscale.com/proxy-groupannotation +tls.hosts=["pocket-id"]; don't declare a second ProxyGroup. SetTRUST_PROXY=truesince it sits behind the operator./app/data(chart'smountPath); official image is multi-arch arm64; data dir needs local block storage (docs advise against NFS), so a dedicated worker qcow2 + hostPath matches Milner's no-provisioner reality.http://localhost:8250/oidc/callback+ UIhttps://vault.faun-snapper.ts.net/ui/vault/auth/oidc/oidc/callback); Pocket ID issuesaud=client id, so no extrabound_audiencesneeded./setuppasskey on first launch; admin console service approval is required before the MagicDNS name routes.Pre-Mortem
The Vault↔Pocket ID issuer-reachability decision is left open and never converges — Vault is a strict go-oidc client that requires
oidc_discovery_urlto equal Pocket ID's issuer (always the externalAPP_URL) and be reachable and trusted from the Vault pod, yet the plan defers choosing a topology (in-cluster TLS + private CA vs Tailscale egress) to a follow-on commit. Any slip in the DNS override, private-CA pinning, or cert wiring — or the follow-on commit never landing — fails the end-to-end Vault OIDC login acceptance criterion and leaves the deployment half-done.Worker reconfiguration disrupts the cluster or strands the Pocket ID data volume — activating the boot-only mount requires
terraform apply -replaceoflibvirt_domain.worker, which reboots the same node already hosting Vault, and the new size-based disk selectors must stay mutually exclusive with the existing vault selector. A selector or qcow2 attach mistake leaves/var/mnt/pocket-idunmounted (Pocket ID CrashLoopBackOff or data loss) and risks Vault availability during the forced replace.Helm service-port and probe wiring mismatches the chart's actual defaults — the plan's ingress backend and
internalAppUrlreference port 1411 while the hobbit44 chart's ClusterIP service exposes port 80 (targeting the app's internal 1411), so the discovery URL advertised to Vault or the ingress route can point at a dead port.26 ⚠ Correction: this premise does not hold for chart 2.4.0 or main — both setservice.port: 1411and the statefulset'scontainerPort: {{ .Values.service.port }}(the servicetargetPortis the namedhttpport), so the ClusterIP service and the plan's:1411backend agree. The probe-side risk stands: default probes are/healthand the pod flaps unless overridden to/healthz. The chart's default probes also hit/healthrather than Pocket ID's real/healthz, so any missed override makes the pod flap on failed probes.