Pocket ID Helm Chart
A singleton Pocket ID identity provider using the official pinned image, passkey authentication and native OpenID Connect. The chart initializes its first administrator privately before exposing HTTP and retains the application’s encryption key across upgrades.
Installation
Configure a dedicated HTTPS origin before enrolling any passkeys:
server:
publicUrl: https://id.example.com
bootstrap:
username: administrator
email: [email protected]
firstName: Identity
lastName: Administrator
ingress:
enabled: true
ingressClassName: nginx
hosts:
- host: id.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: identity-tls
hosts: [id.example.com]
Install with helm install identity helmforge/pocket-id -n identity --create-namespace -f values.yaml. See
onboarding before the first login.
Authentication and onboarding
The initializer copies the unchanged static application binary from the official image and starts it with native HTTP and actor listeners bound to loopback. It uses the native initial-signup API, verifies the administrator role and closed setup endpoint, then stops before the main container starts. It never changes an existing user or generates another access token during upgrades.
Generate the native single-use, one-hour login link when ready to enroll a passkey. Treat its command output as a credential. No initial password is generated because Pocket ID authenticates with passkeys.
Setting bootstrap.enabled: false skips account creation, but still checks privately that the database is initialized.
An uninitialized database fails before public exposure.
Persistence and recovery
SQLite is the default. Select database.type: postgresql and postgresql.enabled: true to install the HelmForge
PostgreSQL dependency, or supply external connection components and a password Secret. External connections default to
certificate and hostname verification. The chart prepares required extensions on a fresh bundled database and checks
prerequisites before native migrations. See database preparation before changing engines or
upgrading an existing database.
The default 5 GiB retained PVC stores SQLite and uploads. The application encryption key is generated once in a retained
Secret or supplied through encryption.existingSecret and encryption.secretKey. Back up the complete data directory
and original key together. The key protects sensitive application fields, including signing material; it does not
encrypt the entire SQLite database file. Keep a consistent, quiesced snapshot rather than copying a live database alone.
Passkeys depend on the HTTPS relying-party origin. Changing server.publicUrl can invalidate existing credentials;
treat origin changes as an identity migration. See recovery for consistent backups, fresh-volume
restoration and operator access.
Security and availability
- Exactly one application replica and Recreate upgrades, with planned downtime.
- UID/GID 1000, read-only root filesystem, dropped Linux capabilities and no mounted service account token.
- HTTP probes on
/healthz; actor listener is private and is not exposed by a Service. - NetworkPolicy enables ingress control and isolates egress to DNS plus explicitly configured destinations.
- Insecure OIDC callback URLs, query-argument logging, downgrade allowance, analytics and version checks are disabled.
- Ingress, Gateway API, dual-stack Services and External Secrets Operator use the standard HelmForge contracts.
Do not claim high availability by raising the replica count. The pinned upstream release does not expose its unfinished HA mode as an environment configuration option.
Monitoring
Enable metrics.enabled for the native Prometheus exporter on its own private Service. Optional ServiceMonitor and
PrometheusRule resources integrate with Prometheus Operator. The bootstrap process never opens that listener. See
monitoring for selectors, network access and alert boundaries.
Validation
The complete HelmForge gate passed: 23 layers, 13 runtime scenarios and 12 Helm unit tests. Coverage includes native bootstrap, browser passkey enrollment/login, OIDC code flow with PKCE, bundled and external PostgreSQL, real Prometheus scraping, External Secrets and fresh-volume identity recovery. Runtime tests use the owned local k3d cluster; production capacity and external relying-party availability remain deployment-specific.
Security Scan: pocket-id
| Framework | Score |
|---|---|
| Overall | 100.00% |
| MITRE | 100.00% |
| NSA | 100.00% |
| SOC2 | 100.00% |
Kubescape 4.0.13, default rendered manifests, 2026-09-10. No findings and no control suppressions. This configuration scan does not replace image vulnerability management or application security review.
Sources
Onboarding details
Set server.publicUrl to the final HTTPS origin and deploy its trusted TLS certificate before enrolling a passkey. The
chart closes the native first-user setup endpoint inside a loopback-only initializer. The created account has
administrator privileges but no passkey yet.
First login
Run the native operator command in a private terminal:
kubectl -n identity exec deployment/identity-pocket-id -c pocket-id -- \
/app/pocket-id one-time-access-token administrator
Use the returned URL once, within one hour. It is a bearer credential: do not store it in a ticket, screenshot, shared shell transcript or deployment log. Enroll a passkey through the application’s account settings. Add an independent recovery authenticator and verify that it can sign in before relying on the service for other applications.
The CLI looks up an existing username or email. It does not create an account and does not reset its other credentials.
Use the configured bootstrap.username, not the example name above, when they differ.
Expired or interrupted setup
If the initial link expires, run the native command again when ready. The chart deliberately does not store an expiring token in a Secret or rotate it on every restart. User creation and token issuance are separate native operations. Existing account state always wins over changed bootstrap values.
Disabling bootstrap is supported only for an already initialized database. The private initializer rejects an empty database with account creation disabled rather than exposing first-user setup publicly.
State protection
Keep the database, uploads and original encryption key as one recovery set. Restrict access to the entire database even though sensitive fields are encrypted. A restored database with a replacement encryption key cannot be assumed to retain its signing identity. Keep the original HTTPS origin so enrolled passkeys retain their relying-party identity.
Database details
SQLite is the default. Keep the database, uploads and encryption Secret together when backing up or restoring an identity provider. Selecting PostgreSQL does not migrate an existing SQLite database automatically.
Bundled PostgreSQL
database:
type: postgresql
postgresql:
enabled: true
The chart uses the HelmForge PostgreSQL subchart. Its administrator installs citext for Pocket ID and pgcrypto for
the Francis actor runtime during first initialization. Preserve this initialization script when overriding
postgresql.initdb.scripts. The application role needs USAGE and CREATE on its schema, but does not need to be a
superuser or have CREATE permission on the database. Database name, user and password references follow the subchart’s
actual authentication values.
Initialization scripts run only on a fresh database volume. Prepare an existing database with the DBA before upgrading; changing init scripts does not apply them retroactively.
External PostgreSQL
database:
type: postgresql
host: postgres.example.com
port: 5432
name: pocketid
username: pocketid
passwordSecret: pocket-id-database
passwordKey: password
sslMode: verify-full
caSecret: postgres-ca
caKey: ca.crt
postgresql:
enabled: false
Create the password and CA Secrets in the release namespace. External PostgreSQL defaults to verify-full, which checks
the certificate chain and server hostname. Omit caSecret for a server trusted by the image’s root store. Allow the
destination explicitly with networkPolicy.extraEgress. Credentials are encoded as URI components and the resulting
connection URL is written to a private memory volume, rather than a ConfigMap or command argument.
Before the first deployment, the DBA must connect to the target database and install both extensions in the application’s search path:
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
GRANT USAGE, CREATE ON SCHEMA public TO pocketid;
The initializer checks prerequisites using a read-only connection before starting the native migration runners. Pocket ID owns its application migrations; Francis separately owns its actor tables, functions, triggers and views. Do not reuse a schema managed by a different application or transfer objects away from the application role.
Failed migrations
Back up before upgrading. An interrupted or failed Pocket ID migration can leave schema_migrations.dirty set. The
chart refuses to start against that state. Installing a missing extension does not repair partially applied SQL or clear
migration state. Inspect the failed migration and restore or repair under the upstream migration procedure; the chart
never forces a migration version or clears dirty state automatically.
The chart remains a singleton with PostgreSQL because uploads and other local identity-provider files still require consistent persistent storage. A database subchart by itself does not establish an application HA deployment.
Monitoring details
Pocket ID exports OpenTelemetry metrics through its native Prometheus exporter. The chart enables that listener separately from the public identity endpoint; no exporter sidecar is required.
metrics:
enabled: true
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
serviceMonitor:
enabled: true
labels:
release: monitoring
prometheusRule:
enabled: true
labels:
release: monitoring
Install Prometheus Operator CRDs before enabling these resources. Match the labels to your Prometheus resource’s
ServiceMonitor and rule selectors, and match the network peers to the actual Prometheus pods. NetworkPolicy requires an
enforcing CNI. Without explicit ingressFrom, the chart permits metric scraping from the release namespace. Keep
serviceMonitor.scrapeTimeout no greater than serviceMonitor.interval; the operator rejects invalid timing.
The metrics Service is separate from HTTP and is omitted from the chart’s Ingress and HTTPRoute. The native metrics listener is not an authenticated public API; keep access limited to monitoring workloads. The private bootstrap process always disables metric export, even when the main application exporter is enabled.
The included PocketIDMetricsUnavailable alert detects a discovered target with up == 0 for five minutes. It does not
detect an absent ServiceMonitor, broken discovery or an unavailable Prometheus server. Monitor those conditions in the
monitoring platform. metrics.prometheusRule.additionalRules accepts application-specific recording or alert rules
based on the native series available in your version.
Health probes use the application’s HTTP health endpoint. A successful health probe does not prove a relying party can complete authentication; keep a synthetic login check for your production origin and OIDC clients.
Recovery details
An identity provider backup must preserve the database, native uploads, exact encryption key and relying-party origin. Changing any one independently can invalidate credentials, signing material or sessions. Store backups and Secrets under access controls appropriate for the identities they contain.
SQLite
- Schedule downtime and stop the application Deployment. Wait for its pod to terminate so native SQLite connections and actor processing are closed.
- Snapshot the complete persistent volume or archive every entry in
/app/data, including hidden files and any SQLite sidecars. Preserve file modes and ownership. Archive directory contents rather than overwriting the root metadata of a provisioned destination PVC. - Back up the encryption Secret through your secret-management system. An externally managed key must be restored from the same source; generating another random key does not recover the old encrypted fields.
- Restore the archive into a fresh PVC that UID/GID1000 can access. Keep the same public HTTPS origin and original
encryption Secret, then set
persistence.existingClaimto the restored claim. - Start one replica. Confirm the initializer preserves the existing account and the public setup endpoint remains closed. Verify a previously enrolled passkey, existing OIDC client, signed token and private account state before routing production traffic to the restored instance.
Keep the source backup and original volume until recovery has been accepted. Recovery testing should use an isolated environment and controlled DNS routing so two independent instances do not serve the same production identity.
PostgreSQL
Quiesce Pocket ID before taking a consistent PostgreSQL backup and matching uploads snapshot. Include application and
Francis actor schemas and preserve the same encryption Secret. Use a database backup procedure appropriate to the actual
PostgreSQL major, extension versions, ownership and recovery objective. Prepare citext and pgcrypto on the
destination before native startup.
The chart does not automatically convert SQLite into PostgreSQL, coordinate database PITR with upload snapshots or reverse database migrations during Helm rollback. A successful PostgreSQL startup/restart check is not a PostgreSQL disaster-recovery drill; validate that procedure separately against your managed database or subchart deployment.
Operator access
If no enrolled device remains usable, an authorized operator can invoke the native one-time-access-token CLI as
described in onboarding. This produces a short-lived access credential; it does not replace a
lost database or encryption key. Treat the command output as a Secret and enroll a new device promptly.
Production example
# SPDX-License-Identifier: Apache-2.0
server:
publicUrl: https://id.example.com
bootstrap:
username: administrator
email: [email protected]
encryption:
existingSecret: pocket-id-encryption
persistence:
size: 10Gi
ingress:
enabled: true
ingressClassName: nginx
hosts:
- host: id.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: pocket-id-tls
hosts: [id.example.com]
networkPolicy:
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/component: controller
Complete values
# SPDX-License-Identifier: Apache-2.0
# -- Override the chart name used in labels and generated resource names.
nameOverride: ''
# -- Override generated resource names; configure before creating persistent state.
fullnameOverride: ''
# -- commonLabels.
commonLabels: {}
# -- Exactly one instance; PostgreSQL does not enable the unfinished upstream HA mode.
replicaCount: 1
# -- image.
image:
# -- Official Pocket ID image repository.
repository: ghcr.io/pocket-id/pocket-id
# -- Pinned upstream release; validate migrations and passkey/OIDC behavior before upgrading.
tag: v2.14.0
# -- pullPolicy.
pullPolicy: IfNotPresent
# -- imagePullSecrets.
imagePullSecrets: []
# -- server.
server:
# -- Native application HTTP listener, exposed through the application Service.
port: 1411
# -- Stable HTTPS issuer and WebAuthn origin; changing it can invalidate existing credentials.
publicUrl: https://id.example.com
# -- Native TRUST_PROXY value; restrict forwarded-header trust to your actual proxy network.
trustedProxies: ''
# -- Additional native environment settings; managed security and bootstrap variables cannot be overridden.
extraEnv: []
# -- serviceAccount.
serviceAccount:
# -- create.
create: true
# -- name.
name: ''
# -- annotations.
annotations: {}
# -- automountServiceAccountToken.
automountServiceAccountToken: false
# -- service.
service:
# -- type.
type: ClusterIP
# -- port.
port: 1411
# -- annotations.
annotations: {}
# -- ipFamilyPolicy.
ipFamilyPolicy: ''
# -- ipFamilies.
ipFamilies: []
# -- ingress.
ingress:
# -- enabled.
enabled: false
# -- ingressClassName.
ingressClassName: ''
# -- annotations.
annotations: {}
# -- hosts.
hosts: []
# -- tls.
tls: []
# -- Gateway API HTTPRoute configuration.
gatewayAPI:
# -- Render canonical Gateway API HTTPRoutes.
enabled: false
# -- Route definitions with parentRefs, hostnames, rules, labels and annotations.
httpRoutes: []
# -- externalSecrets.
externalSecrets:
# -- enabled.
enabled: false
# -- refreshInterval.
refreshInterval: 1h
# -- items.
items: []
# -- networkPolicy.
networkPolicy:
# -- enabled.
enabled: true
# -- ingressFrom.
ingressFrom: []
# -- egressIsolation.
egressIsolation: true
# -- dnsEgress.
dnsEgress:
- namespaceSelector:
# -- matchLabels.
matchLabels:
kubernetes.io/metadata.name: kube-system
# -- podSelector.
podSelector:
# -- matchLabels.
matchLabels:
k8s-app: kube-dns
# -- webEgress.
webEgress: []
# -- webPorts.
webPorts:
- 443
# -- extraEgress.
extraEgress: []
# -- probes.
probes:
# -- startup.
startup:
# -- enabled.
enabled: true
# -- path.
path: /healthz
# -- failureThreshold.
failureThreshold: 60
# -- periodSeconds.
periodSeconds: 5
# -- timeoutSeconds.
timeoutSeconds: 5
# -- readiness.
readiness:
# -- enabled.
enabled: true
# -- path.
path: /healthz
# -- failureThreshold.
failureThreshold: 3
# -- periodSeconds.
periodSeconds: 10
# -- timeoutSeconds.
timeoutSeconds: 5
# -- liveness.
liveness:
# -- enabled.
enabled: true
# -- path.
path: /healthz
# -- failureThreshold.
failureThreshold: 3
# -- periodSeconds.
periodSeconds: 20
# -- timeoutSeconds.
timeoutSeconds: 5
# -- resources.
resources:
# -- requests.
requests:
# -- cpu.
cpu: 100m
# -- memory.
memory: 128Mi
# -- limits.
limits:
# -- cpu.
cpu: '1'
# -- memory.
memory: 512Mi
# -- podSecurityContext.
podSecurityContext:
# -- runAsNonRoot.
runAsNonRoot: true
# -- runAsUser.
runAsUser: 1000
# -- runAsGroup.
runAsGroup: 1000
# -- fsGroup.
fsGroup: 1000
# -- fsGroupChangePolicy.
fsGroupChangePolicy: OnRootMismatch
# -- seccompProfile.
seccompProfile:
# -- type.
type: RuntimeDefault
# -- securityContext.
securityContext:
# -- allowPrivilegeEscalation.
allowPrivilegeEscalation: false
# -- readOnlyRootFilesystem.
readOnlyRootFilesystem: true
# -- capabilities.
capabilities:
# -- drop.
drop:
- ALL
# -- podLabels.
podLabels: {}
# -- podAnnotations.
podAnnotations: {}
# -- nodeSelector.
nodeSelector: {}
# -- tolerations.
tolerations: []
# -- affinity.
affinity: {}
# -- topologySpreadConstraints.
topologySpreadConstraints: []
# -- priorityClassName.
priorityClassName: ''
# -- terminationGracePeriodSeconds.
terminationGracePeriodSeconds: 30
# -- persistence.
persistence:
# -- enabled.
enabled: true
# -- Use a preexisting or restored PVC instead of creating another claim.
existingClaim: ''
# -- storageClass.
storageClass: ''
# -- Capacity for SQLite and native uploads; PostgreSQL mode still persists local files.
size: 5Gi
# -- accessModes.
accessModes:
- ReadWriteOnce
# -- Keep the data PVC when the Helm release is uninstalled.
retain: true
# -- annotations.
annotations: {}
# -- Fixed native data path used by database, upload and recovery contracts.
mountPath: /app/data
# -- bootstrap.
bootstrap:
# -- Create the initial administrator privately when the database is empty; false refuses uninitialized databases.
enabled: true
# -- Username used only for the first native administrator.
username: admin
# -- Email used only during initial administrator creation.
email: [email protected]
# -- Initial administrator first name.
firstName: Pocket
# -- Initial administrator last name.
lastName: Administrator
# -- Official Node helper that runs the unchanged native application binary on loopback during setup.
helperImage:
# -- repository.
repository: docker.io/library/node
# -- tag.
tag: 24.21.0-alpine3.23
# -- pullPolicy.
pullPolicy: IfNotPresent
# -- runtime.
runtime:
# -- Bound for the application temporary emptyDir.
temporarySize: 256Mi
# -- encryption.
encryption:
# -- Optional 32-byte base64 key; empty generates a retained Secret. Prefer existingSecret for operator-owned keys.
key: ''
# -- Existing Secret containing the exact retained native encryption key.
existingSecret: ''
# -- Key within the encryption Secret, mounted read-only as a file.
secretKey: encryption-key
# -- database.
database:
# -- Official PostgreSQL client used only for read-only database admission.
clientImage:
# -- Official client repository.
repository: docker.io/library/postgres
# -- Pinned client release.
tag: 18.6-trixie
# -- Client pull policy.
pullPolicy: IfNotPresent
# -- Native database engine: sqlite or postgresql; engine selection does not migrate existing data.
type: sqlite
# -- External PostgreSQL hostname; must remain empty when the bundled database is enabled.
host: ''
# -- External PostgreSQL port; the bundled database uses its own Service port.
port: 5432
# -- External database name; bundled connections follow postgresql.auth.database.
name: pocketid
# -- External application role; bundled connections follow postgresql.auth.username.
username: pocketid
# -- External database password Secret in the release namespace.
passwordSecret: ''
# -- Key containing the external application password.
passwordKey: password
# -- Empty selects verify-full externally and disable for the bundled in-cluster connection.
sslMode: ''
# -- Optional CA Secret for external PostgreSQL certificate verification.
caSecret: ''
# -- Key containing the trusted PostgreSQL CA PEM.
caKey: ca.crt
# -- postgresql.
postgresql:
# -- DBA preparation on a fresh volume. Existing and external databases require equivalent preparation before upgrading.
initdb:
# -- First-volume initialization scripts; preserve required extensions when overriding.
scripts:
# -- Installs application and actor extensions using the subchart administrator connection.
20-pocket-id-extensions.sh: |
#!/bin/bash
set -euo pipefail
export PGPASSWORD="${POSTGRES_PASSWORD}"
psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${APP_DATABASE}" <<'SQL'
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
SQL
# -- securityContext.
securityContext:
# -- readOnlyRootFilesystem.
readOnlyRootFilesystem: true
# -- extraVolumes.
extraVolumes:
- name: postgres-tmp
# -- emptyDir.
emptyDir:
# -- sizeLimit.
sizeLimit: 1Gi
- name: postgres-socket
# -- emptyDir.
emptyDir:
# -- medium.
medium: Memory
# -- sizeLimit.
sizeLimit: 16Mi
# -- extraVolumeMounts.
extraVolumeMounts:
- name: postgres-tmp
# -- mountPath.
mountPath: /tmp
- name: postgres-socket
# -- mountPath.
mountPath: /var/run/postgresql
# -- networkPolicy.
networkPolicy:
# -- enabled.
enabled: true
# -- egress.
egress:
# -- enabled.
enabled: true
# -- allowDNS.
allowDNS: true
# -- allowSameNamespacePostgreSQL.
allowSameNamespacePostgreSQL: false
# -- allowHTTPS.
allowHTTPS: false
# -- Install the HelmForge PostgreSQL dependency; also select database.type=postgresql.
enabled: false
# -- architecture.
architecture: standalone
# -- auth.
auth:
# -- database.
database: pocketid
# -- username.
username: pocketid
# -- existingSecret.
existingSecret: ''
# -- existingSecretUserPasswordKey.
existingSecretUserPasswordKey: user-password
# -- metrics.
metrics:
# -- Enable the native Prometheus exporter on a dedicated private listener.
enabled: false
# -- Native metrics port; must differ from the HTTP port.
port: 9464
# -- NetworkPolicy peers allowed to scrape metrics; empty permits the release namespace.
ingressFrom: []
# -- serviceMonitor.
serviceMonitor:
# -- Create a ServiceMonitor; requires metrics.enabled and installed Prometheus Operator CRDs.
enabled: false
# -- labels.
labels: {}
# -- Prometheus scrape interval; must be at least scrapeTimeout.
interval: 30s
# -- Maximum scrape duration; cannot exceed interval.
scrapeTimeout: 10s
# -- prometheusRule.
prometheusRule:
# -- Create the native target availability rule; requires metrics.enabled and operator CRDs.
enabled: false
# -- labels.
labels: {}
# -- Additional Prometheus rules evaluated against actual native series.
additionalRules: []
Gateway API contract
Use gatewayAPI.enabled and gatewayAPI.httpRoutes[]. Set each route’s parentRefs to a shared Gateway that allows
this namespace, and configure its HTTPS listener and public hostname. Routes accept labels, annotations and rules with
matches, filters and optional backend references; omitted backends target this chart’s application Service. Ingress and
HTTPRoute resources can coexist. Verify controller conditions and public traffic before production use. See the
Gateway API documentation.