Skip to content

Kubernetes (Helm chart)

The chart is published as an OCI artifact at oci://registry.tinyfactory.ai/tinyblox/charts/tinyguard, versioned with each release (the chart version equals the provider version). It deploys one workload — the provider — with a rendered configuration, a mounted secrets file, probes wired to the health endpoints, and a locked-down security context. The admin console is part of the same container and needs no separate deployment.

The chart creates no Secret, so secret material never lands in values.yaml. Prepare these first, in this order (a Secret can only be created in a namespace that exists, so the namespace comes first):

Terminal window
NS=tinyguard
kubectl create namespace "$NS"
# 1. The pull secret for the private registry.
kubectl -n "$NS" create secret docker-registry registry-pull \
--docker-server=registry.tinyfactory.ai \
--docker-username=<user> --docker-password=<token>
# 2. The provider's secrets file: generate it once with nothing but the image,
# and keep a copy. Losing the encryption key makes stored secrets unreadable.
IMAGE=registry.tinyfactory.ai/tinyblox/guard:0.3.2
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work \
"$IMAGE" generate-secrets --output secrets.toml
kubectl -n "$NS" create secret generic tinyguard-secrets \
--from-file=secrets.toml=secrets.toml
# 3. With storage.mode=postgres: the database login's password, as a file.
kubectl -n "$NS" create secret generic tinyguard-pg \
--from-literal=password='…'

The first administrator is seeded on the first start: set bootstrapAdmin.email, and optionally a Secret holding a password hash (see Deployment); without one, a password is generated and printed once to the pod’s log.

Terminal window
helm registry login registry.tinyfactory.ai
helm upgrade --install tinyguard oci://registry.tinyfactory.ai/tinyblox/charts/tinyguard \
--version 0.3.2 \
--namespace "$NS" \
--set publicUrl=idp.example.com \
--set image.digest=sha256:…

Always deploy by digest: a tag can be re-pushed, a digest cannot. Read a release’s digest with docker buildx imagetools inspect registry.tinyfactory.ai/tinyblox/guard:0.3.2.

Key Default Meaning
publicUrl idp.example.com The public origin; also the OIDC issuer and console redirect target.
image.repository tinyblox/guard Repository in registry.tinyfactory.ai.
image.digest / image.tag '' / latest Digest wins when set. Deploy by digest.
replicaCount 1 More than one requires storage.mode: postgres.
server.scheme http http terminates TLS at the ingress.
metrics.enabled / metrics.port true / 9090 The metrics listener, on its own port and never routed by the ingress.
metrics.vmServiceScrape.* disabled A VictoriaMetrics scrape object for it.
postgres.* — The database, below. Named as in every TinyBlox chart.
podSecurityContext, securityContext non-root, read-only filesystem The pod and container security settings; the defaults are the platform’s.
settings.* — Every other configuration setting, by its configuration key (below).
extraEnv {} An environment variable the chart does not model; never a documented setting.
server.docs.enabled false The built-in API documentation routes.
storage.mode embedded embedded or postgres.
storage.embedded.size 10Gi The PVC for embedded storage.
secrets.secretName / .key tinyguard-secrets / secrets.toml The mounted secrets file.
bootstrapAdmin.* — The first-boot administrator’s email and password Secret.
ingress.* enabled Class, cert-manager issuer (added as the cert-manager.io/cluster-issuer annotation), TLS secret.
networkPolicy.enabled true Allow the HTTP port only from the ingress namespace.

With ingress.enabled=false the chart exposes only a ClusterIP service: there is no console access from outside until you expose it yourself.

Embedded (the default) puts the database on a PersistentVolume with ReadWriteOnce. It is single-node — the chart refuses replicaCount > 1 in this mode — and the PVC carries helm.sh/resource-policy: keep, so helm uninstall does not take the tenant and credential records with it.

Postgres suits anything that scales, shares a database server, or needs managed backups. Replicas roll out without a gap (RollingUpdate, never fewer than the requested number ready), which is the other half of why replicaCount can go above one only in this mode:

replicaCount: 3
storage:
mode: postgres
postgres:
host: db.internal
dbName: tinyguard
schema: tinyguard # the schema this service owns
user: tinyguard
password:
existingSecret: tinyguard-pg # a Secret you create; key `password`
tlsMode: verify-full # the default; see below
ca:
existingSecret: pg-ca # a Secret with the CA bundle (key ca.crt)
poolSize: 10 # per replica; null keeps the server's default

The password is a mounted file, read through GUARD_STORAGE_PASSWORD_FILE: it is not in the environment, in kubectl describe pod, in the ConfigMap or in values. The chart refuses a Postgres install without it. (secrets.toml still carries the encryption key.)

Three values deserve a decision rather than a default:

  • tlsMode is verify-full. With no ca the image’s system trust store is used, which suits a server certified by a public authority. A server inside the cluster with a private authority needs ca.existingSecret, a Secret holding the bundle that signed its certificate (key ca.crt, or set ca.key). prefer and require encrypt without checking who answered; they are for development.
  • poolSize multiplies by replicaCount, and that product, plus a few connections for migrations, must fit under the server’s max_connections. See the connection budget.
  • schema lets several components share one database. Give each its own.

With replicas sharing a store, the singleton duties (signing-key rotation, cleanup, the email and event pipelines) run on one replica at a time, chosen by a lease in the database, and move to another within about thirty seconds if that replica goes away.

By default every pod applies pending schema steps when it starts, taking turns on a database lock. To keep the runtime login to data access only, have the chart run them as a hook before each install and upgrade, with a login that may change the schema:

postgres:
autoMigrate: false # pods only check the schema is current
migrateJob:
enabled: true
owner:
user: tinyguard_owner
password:
existingSecret: tinyguard-pg-owner # key `password`

The Job runs idp-server migrate with the owner login in place of the runtime one (the chart refuses migrateJob.enabled with autoMigrate: true, which would apply the schema twice). A failed Job fails the release, visibly, instead of a crash loop in the pods; a pod that starts against a schema that is behind refuses to serve and names the command to run.

The chart ships a values.schema.json that rejects any key it does not know, at every level: a misspelt networkPolcy is an error, not a silent no-op. It also checks enums (postgres.tlsMode), ports, the shape of image.digest and that publicUrl carries no scheme. helm lint and helm install both run it.

Each documented configuration setting is a named value under settings, spelled as the configuration key, and the chart turns it into the right environment variable (settings.hashing.argon2_m_cost becomes GUARD_HASHING_ARGON2_M_COST). Nothing is passed as free text, so the schema checks the name, the type and the shape of every one, and helm show values lists them all with their documentation.

settings:
tenants:
enabled: true
observability:
enabled: true
endpoint: http://otel-collector.observability:4318
service_name: tinyguard
security:
blacklist: [203.0.113.7]
events:
notify:
slack:
webhook_url: # a secret: a reference, never the value
existingSecret: tinyguard-notify
key: slack-webhook
  • null, or leaving a value out, keeps the server’s default.
  • A list is an array. The chart joins it with commas.
  • A secret setting (a password, token or webhook) is never a literal: it names a Secret and a key, the chart mounts it as a file, and the server reads it through the _FILE form of the variable.
  • The settings the chart names itself (publicUrl, server.scheme, metrics.*, postgres.*) are not repeated under settings.
  • extraEnv remains for an environment variable the chart does not model. The schema refuses a documented setting there and tells you to use settings.

The complete list, with defaults and the variable each becomes, is in the environment reference.

Probes, shutdown, and the security context

Section titled “Probes, shutdown, and the security context”
  • Liveness hits /livez and readiness hits /readyz, which answers only when storage answers; a startup probe on /readyz (150 tries, two seconds apart: five minutes, startupProbe.failureThreshold) covers a slow first start.
  • The pod runs as 65532:65532 with a read-only root filesystem, all capabilities dropped, no privilege escalation, and RuntimeDefault seccomp. Writable paths are the data volume and a tmpfs at /tmp.
  • The service account token is not mounted (automountServiceAccountToken: false) — the pod needs no Kubernetes API access.
  • A pod stops in two steps: a preStop sleep (preStopSleepSeconds, 5 by default, the native sleep action, so Kubernetes 1.30 or newer) lets endpoint removal propagate before SIGTERM, then the server drains for up to server.graceful_shutdown_seconds. terminationGracePeriodSeconds must cover both, and the chart refuses a sleep that does not leave room.
  • A NetworkPolicy allows the HTTP port only from networkPolicy.ingressNamespace and the metrics port only from networkPolicy.metrics.scraperNamespace (default observability). Egress is limited to DNS, the database (networkPolicy.egress.databasePort, optionally narrowed with databaseTo), HTTPS and the OTLP collector. Mail or another port needs a rule under networkPolicy.egress.extra. Your network plugin must enforce NetworkPolicies for this to matter.
  • Metrics scraping: set metrics.vmServiceScrape.enabled=true to render a VictoriaMetrics VMServiceScrape for the metrics port, and put your VMAgent’s selector labels under metrics.vmServiceScrape.labels.
  • Every object carries the standard labels, including app.kubernetes.io/part-of: tinyblox.

Because the root filesystem is read-only and there is no shell in the image, anything that expects to kubectl exec a shell will not work. Use the endpoints and logs instead.

The console is already gated: administrators authenticate through the provider the pod itself hosts, so there is nothing extra to deploy. If you want a second, infrastructure-level gate in front of the whole host (for example limiting the console to a corporate domain during a rollout), the chart’s ingress is the place to add it — deploy an authenticating proxy and route the ingress through it, the same pattern the design-system catalogue uses. That proxy can use TinyGuard itself as its OIDC provider.

helm upgrade with a new digest is the whole procedure; the ConfigMap checksum in the pod template rolls the pods when configuration changes. The server applies storage migrations on boot and refuses to start on a failed migration, so a bad upgrade fails visibly rather than serving half-migrated data. helm rollback returns to the previous revision and digest.

Keep the secrets Secret out of the chart’s lifecycle: it is created out of band and survives upgrades, rollbacks, and uninstalls.