A JMAP webmail client for the stalwart chart (Gitea #65): StatefulSet, one PVC at /app/data, Service plus headless Service, Ingress at a host root, two OnePasswordItems, three probes, computed limits, replicas plus scaleDown, strict values.schema.json. Everything below was measured against the source at tag 1.12.0 rather than read out of the documentation, which was wrong about three of them. THE DEFINING PROPERTY: THE VOLUME BEATS THE CHART, FOR EVERY VALUE, SILENTLY. configManager.get resolves "admin override > env var > default", the admin dashboard may write any key of CONFIG_ENV_MAP, and nothing reports that it just disconnected the chart - so a helm upgrade can succeed and change nothing. Upstream documents this for the server URL only; it is true of everything. Exactly three values escape it (SESSION_SECRET(_FILE), ADMIN_PASSWORD, BULWARK_TELEMETRY). The value that closes it, bulwark.adminConfigReadOnly, defaults to false because its price is threefold: it freezes the settings policy (and with it the only way to move Web Push off the vendor relay), it freezes an override that ALREADY exists instead of removing it, and on a fresh install it stops admin.json from being written at all - so the admin dashboard never comes up and its login answers HTTP 500 while the pod stays Ready. OIDC-only sign-in, with the real way back out of a lockout: not helm upgrade with OAUTH_ONLY=false (oauthOnly goes through the same precedence chain) but removing the override in the admin dashboard. That is why ADMIN_PASSWORD is set - the dashboard is the door. It generates no random password and logs nothing, so the #65 analogy does not apply. SESSION_SECRET_FILE and OAUTH_CLIENT_SECRET_FILE are both used - the better path garage (#81) and keycloak (#83) did not have - in one projected volume mounted WITHOUT subPath, because both are re-read on every use and a subPath mount is never refreshed, and at mode 0440 because secret files are owned by root and fsGroup only supplies the group. Two backup statements, both at the value: without the session secret the synchronized settings are unreadable out of an intact volume, and if the dashboard's OAuth setup was ever used the client secret sits in config.json on that volume in cleartext. Probes: all three a plain GET on /api/health, with the limit written at the readiness value - it carries process, HTTP acceptance, a free event loop and Next.js routing, and nothing about the reported state or about Stalwart. No ConfigMap, so the checksum/config rule (#82) has nothing to attach to, and values.yaml says so explicitly; the op:// paths are covered by checksum/secret-ref. Renovate: no versioningTemplate, measured rather than assumed. 315 tags in eight shapes (releases, a second v-prefixed family of the same images, an -always flavour, floating minors and names, commit SHAs, betas, attestations) and plain docker versioning excludes all six unwanted ones. Dry runs at 1.12.0, 1.11.0 and 1.10.0 are recorded in the block. Part of #89 - deliberately not "Fixes", because one binding decision of the issue was changed out of necessity and wants ratification rather than an automatic close: the plan specifies HEAD /api/health for all three probes, and a Kubernetes probe cannot send HEAD at all (HTTPGetAction has no method field; the kubelet always issues GET). The alternative, an exec probe, would mean starting a Node process every few seconds in an image that carries no curl and no wget. GET is used instead and is marginally more capable than HEAD, so every limit the issue argues is unchanged - but the decision is the maintainer's to confirm.
110 lines
6.4 KiB
YAML
110 lines
6.4 KiB
YAML
{{- /*
|
|
The 1Password items this chart materializes. TWO of them, split BY AUDIENCE
|
|
rather than by count:
|
|
|
|
<release>-bulwark-app-secret this release's own secrets - the session
|
|
secret and the admin password. Nobody else
|
|
knows or needs them.
|
|
<release>-bulwark-oauth-secret the OIDC client secret. NOT ours: the
|
|
identity provider (keycloak, #83) creates it
|
|
when the client is registered and rotates it
|
|
there, so it is the one of the three that a
|
|
second system also holds. A separate item is
|
|
what lets it be repointed and rotated without
|
|
touching the other two.
|
|
|
|
Same reasoning as garage's split between its RPC secret and its S3 key.
|
|
|
|
WHAT THE FIELD NAMES ARE AND WHY THEY ARE NOT FREE-FORM: the operator
|
|
materializes one Secret key per FIELD of the item, and this chart references
|
|
those keys by name - two as file paths in the StatefulSet's volumeMounts, one by
|
|
secretKeyRef. An item without the expected field leaves the container in
|
|
CreateContainerConfigError, or - worse for the file form - produces a container
|
|
that starts with a MISSING file, which readFileEnv answers with null rather than
|
|
an error. NOTES.txt lists every field for the installer.
|
|
|
|
TWO SECRETS, TWO DIFFERENT ROTATION STORIES, and the difference is a property of
|
|
the application rather than of this chart:
|
|
|
|
session-secret, oauth-client-secret FILE form. Re-read on every use with no
|
|
cache, so a rotation is live WITHOUT a
|
|
restart - see the whole-directory mount
|
|
in bulwark.sfs.yaml, which is what makes
|
|
that true.
|
|
admin-password CONSUMED ONCE. initAdminPassword hashes
|
|
it on the first start, writes admin.json
|
|
to the PVC, and never looks at the
|
|
variable again. Rotating the field after
|
|
that has NO effect, restart or not.
|
|
|
|
AND THE LIMIT THAT CANNOT BE CLOSED FOR EITHER: a change to the CONTENT of an
|
|
item restarts nothing, because Helm never sees a secret the 1Password operator
|
|
materializes and there is nothing to hash. What this chart does cover is the
|
|
op:// PATH, through checksum/secret-ref on the pod template. The operator's
|
|
auto-restart annotation is NOT set here and must not be: verified against
|
|
operator 1.12.0 it enumerates Deployments only, so on a StatefulSet it is a
|
|
silent no-op advertising a guarantee it does not provide (README, guideline 9).
|
|
*/ -}}
|
|
{{- $settings := fromYaml (include "bulwark.settings" .) -}}
|
|
apiVersion: onepassword.com/v1
|
|
kind: OnePasswordItem
|
|
metadata:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-app-secret
|
|
labels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
spec:
|
|
# Must carry the fields: session-secret, admin-password
|
|
#
|
|
# session-secret: 32 bytes of randomness, base64 - `openssl rand -base64 32`.
|
|
# MANDATORY. Persistent "remember me" sessions and the encryption of the
|
|
# synchronized user settings both hang on it, and the second one fails
|
|
# SILENTLY without it: app/api/config/route.ts computes
|
|
# `settingsSyncEnabled: configManager.get(...) && hasSessionSecret()`, so
|
|
# bulwark.settingsSync: true plus a missing secret means synchronization is
|
|
# simply off, with no error anywhere.
|
|
#
|
|
# LOSING IT LOSES DATA THAT AN INTACT VOLUME STILL HOLDS: the settings on the
|
|
# PVC are encrypted with it. This item belongs in the backup next to the
|
|
# volume - a backup of the volume alone is not a backup.
|
|
#
|
|
# admin-password: the initial password of the ADMIN DASHBOARD.
|
|
# It is set because the dashboard is the way back out of an OIDC lockout
|
|
# (see bulwark.oauth.only in values.yaml), not because of any secret leak -
|
|
# measured, there is none: without ADMIN_PASSWORD Bulwark generates no
|
|
# random password and logs nothing, it just disables the dashboard.
|
|
#
|
|
# CONSUMED ON THE FIRST START ONLY. After admin.json exists on the PVC this
|
|
# field is never read again, so rotating it here changes nothing; the reset
|
|
# goes through the dashboard or by deleting admin.json.
|
|
itemPath: {{ required "You must provide an app secret reference (secrets.appSecretRef) - the 1Password item holding the fields 'session-secret' (openssl rand -base64 32; mandatory, the settings sync is silently disabled without it) and 'admin-password' (the admin dashboard, which is the only way back out of an OIDC lockout)" .Values.secrets.appSecretRef }}
|
|
{{- if $settings.oauth.enabled }}
|
|
---
|
|
apiVersion: onepassword.com/v1
|
|
kind: OnePasswordItem
|
|
metadata:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-oauth-secret
|
|
labels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
spec:
|
|
# Must carry the field: oauth-client-secret
|
|
#
|
|
# The confidential OIDC client's secret, as issued by the identity provider
|
|
# when the client is registered. The registration is a PRE-INSTALL step, not a
|
|
# follow-up: with bulwark.oauth.only (the default) and no working client, the
|
|
# first sign-in is impossible. NOTES.txt prints the redirect URI to register.
|
|
#
|
|
# It arrives as a FILE, through OAUTH_CLIENT_SECRET_FILE, so it is not in the
|
|
# container's process environment - which garage (#81) and keycloak (#83) could
|
|
# not achieve because neither application offered a file form. #81's other half
|
|
# was checked here and does not apply: lib/read-file-env.ts is readFileSync
|
|
# plus try/catch with no mode check, so the group-read bit fsGroup adds is
|
|
# harmless.
|
|
#
|
|
# AND THE ONE WAY A ROTATION SILENTLY DOES NOTHING: getGlobalClientSecret
|
|
# checks the ADMIN OVERRIDE FIRST. If the dashboard's OAuth setup has ever
|
|
# run, config.json on the PVC carries oauthClientSecret in PLAINTEXT and that
|
|
# value beats this file forever. That is also why a backup of the volume can
|
|
# contain this secret in the clear.
|
|
itemPath: {{ required "You must provide an OAuth client secret reference (secrets.oauthSecretRef) - the 1Password item holding the field 'oauth-client-secret', as issued by the identity provider when the OIDC client was registered. Required while bulwark.oauth.enabled is true; set it to false for a password-login install" .Values.secrets.oauthSecretRef }}
|
|
{{- end }}
|