Files
jklein e1adcef71f
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 11s
Install-test / Install-test patchmon (pull_request) Successful in 1m41s
Validate Helm Charts / Resource quantities (pull_request) Successful in 11s
Validate Helm Charts / Selector match (pull_request) Successful in 16s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / NetworkPolicy (pull_request) Failing after 11s
Validate Helm Charts / YAML scalars (pull_request) Successful in 10s
Validate Helm Charts / Build apache-tika (pull_request) Successful in 6s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 17s
Validate Helm Charts / Build bulwark (pull_request) Successful in 16s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 7s
Validate Helm Charts / Trivy config apache-tika (pull_request) Successful in 13s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 4s
Validate Helm Charts / Build homeassistant (pull_request) Successful in 5s
Validate Helm Charts / Build gotenberg (pull_request) Successful in 6s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 5s
Validate Helm Charts / Build searxng (pull_request) Successful in 5s
Validate Helm Charts / Build sure (pull_request) Successful in 5s
Validate Helm Charts / Build template-chart (pull_request) Successful in 5s
Validate Helm Charts / Build outline (pull_request) Successful in 5s
Validate Helm Charts / Build patchmon (pull_request) Successful in 5s
Validate Helm Charts / Build rallly (pull_request) Successful in 5s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 15s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 7s
Validate Helm Charts / Build ytdlp-web-player (pull_request) Successful in 7s
Install-test / Install-test rallly (pull_request) Successful in 1m41s
Validate Helm Charts / Build zipline (pull_request) Successful in 7s
Validate Helm Charts / Trivy config bulwark (pull_request) Successful in 13s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 18s
Validate Helm Charts / Trivy config gotenberg (pull_request) Successful in 13s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 12s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 16s
Validate Helm Charts / Trivy config homeassistant (pull_request) Successful in 12s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 14s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 12s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 13s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 20s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 24s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 15s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 14s
Validate Helm Charts / Trivy config ytdlp-web-player (pull_request) Successful in 17s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 15s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 11s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 24s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m9s
Install-test / Install-test searxng (pull_request) Successful in 1m9s
Install-test / Install-test sure (pull_request) Successful in 2m24s
Install-test / Install-test apache-tika (pull_request) Successful in 1m23s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m44s
Install-test / Install-test bentopdf (pull_request) Successful in 59s
Install-test / Install-test bulwark (pull_request) Successful in 1m23s
Install-test / Install-test voucher-vault (pull_request) Successful in 1m58s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 1m55s
Install-test / Install-test cyberchef (pull_request) Successful in 1m6s
Install-test / Install-test gotenberg (pull_request) Successful in 1m19s
Install-test / Install-test ytdlp-web-player (pull_request) Successful in 59s
Install-test / Install-test zipline (pull_request) Successful in 1m52s
Install-test / Install-test homeassistant (pull_request) Successful in 2m26s
Validate Helm Charts / Install-test (pull_request) Successful in 27m21s
Install-test / Install-test outline (pull_request) Successful in 2m7s
feat!: NetworkPolicy in nine more charts, on by default
bulwark, calibre-web-automated, patchmon, rallly, urlaubsverwaltung,
voucher-vault, zipline, sure, homeassistant. One major each.

These are the charts with real outbound dependencies, and they all share one
limitation that is now written into every one of them: THE EGRESS PORTS ARE THE
STANDARD ONES, NOT THE CONFIGURED ONES. Databases, caches and OIDC issuers
arrive as connection URLs or as fields inside 1Password items that the charts
never parse, so a chart cannot know where its dependency listens and cannot
validate a guess. A dependency somewhere else is blocked, and
networkPolicy.egress.extraPorts is the escape hatch. That is the upgrade risk
of this rollout, stated per chart instead of discovered per release.

Four of them differ in ways that mattered:

HOMEASSISTANT gets policyTypes: [Ingress] and NO egress section at all, and
that is the decision in the file rather than an omission. Its outbound traffic
is the union of every integration an operator enables - mDNS and SSDP
discovery, MQTT, a Zigbee or Z-Wave bridge on whatever port its firmware chose,
ESPHome, printers, speakers, inverters, cloud APIs, NTP - which no chart can
know. A cap would protect little, since the devices sit on the node's own LAN,
and would break integrations one at a time, months apart, each looking like a
flaky device rather than like a policy. So valkey's rule applies in the other
direction: policyTypes names only what there is something to restrict.

URLAUBSVERWALTUNG allows BOTH 5432 and 3306, deliberately. It is handed a full
JDBC URL inside a 1Password item and never parses it, so it cannot tell
PostgreSQL from MariaDB - and this cluster runs both. Allowing one would break
half the possible deployments on upgrade, with the symptom buried in a Java
stack trace. A small widening against a large and silent failure.

RALLLY derives its SMTP rule from rallly.smtp.enabled rather than always
rendering it: with mail switched off there is nothing to permit, and a rule
that permits nothing makes the object look stricter than it is.

BULWARK is the chart where the cap can fail most quietly. Its probes test its
own port and say nothing about the JMAP server, so a jmapServerUrl on a port
the policy does not allow yields a Ready pod serving an empty webmail. 443 is
allowed, Stalwart's in-cluster 8080 is not, and both halves of that are in
values.yaml and NOTES.txt.

PATCHMON and HOMEASSISTANT select their pods by the BARE release-and-chart
label, with no -deployment or -sfs suffix. Copying a selector from a
neighbouring chart would have matched nothing and left a policy that filters
nothing - which is precisely the first assertion of the render test.

Five CI fixtures gain networkPolicy.egress.extraPorts, and they are the only
place in the repository where the cap's cost is actually exercised: the CI dex
runs on 5556 rather than 443 (patchmon, rallly, urlaubsverwaltung,
voucher-vault) and the CI JMAP server on plain 8080 (bulwark).

Part of #5
2026-10-14 14:26:25 +00:00

368 lines
25 KiB
JSON

{
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "bulwark values",
"description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key this chart does not read is rejected at render time instead of being silently ignored; the objects that go verbatim into the Kubernetes pod spec (probes, security contexts, global, free-form annotation maps) stay open, because the chart does not interpret their contents. The blocks the chart dereferences additionally accept null, because an erased block means 'chart defaults apply' (issue #99) rather than a vanished setting. Note what this schema CANNOT enforce: secrets.appSecretRef, bulwark.jmapServerUrl and the two OIDC values all ship as null and JSON Schema's 'required' tests presence, not null-ness, so a 'required' array would never fire - the enforcement is required() in bulwark.secret.yaml and fail() in bulwark.sfs.yaml (issue #18). And note what no schema can express at all for this chart: an admin override in config.json on the PVC beats every value below, silently (values.yaml, point 1).",
"type": "object",
"additionalProperties": false,
"properties": {
"global": {
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"type": "object"
},
"networkPolicy": {
"description": "The chart's NetworkPolicy (Gitea #5), on by default. A ceiling on reachability and not an authentication layer: only the declared container ports are reachable at all, egress is capped by PORT (a NetworkPolicy matches CIDRs and selectors, never hostnames, so a destination host cannot be expressed), and ingress is capped by source for every port no network probe touches. Only a boundary where the cluster's CNI enforces policies - an unenforced policy is accepted by the API server and filters nothing. The template's header carries the full list of what it does not do.",
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"allowedNamespaces": {
"description": "Namespaces whose pods may reach this workload, in addition to the release's own namespace, which is always allowed. Matched on the kubernetes.io/metadata.name label Kubernetes sets on every namespace itself, so no labelling work is needed - and no typo protection either: a name that does not exist matches nothing and is silently ineffective. An entry opens the port to EVERY pod in that namespace; for the ingress controller use ingressController instead.",
"type": ["array", "null"],
"items": {
"type": "string"
}
},
"probeSources": {
"description": "Source CIDRs allowed to reach the port the network probes address. EMPTY MEANS OPEN, not blocked: with no entry that rule is rendered without a \"from\" selector. The kubelet's probes come from the IP of the worker node running the pod, an address the chart cannot know and must not guess - a guessed CIDR that excludes the real node blocks every probe and the pod never becomes Ready, which reads as a broken application rather than as a policy. Setting this restricts the rule, which is why the chart adds the own namespace, ingressController and allowedNamespaces to the same rule.",
"type": ["array", "null"],
"items": {
"type": "string"
}
},
"ingressController": {
"description": "The ingress controller allowed to reach the routed port once probeSources restricts it; rendered only while ingress.enabled or httpRoute.enabled is true. The two keys become ONE \"from\" entry and are therefore an AND - pods carrying podLabels IN namespace - which is the whole point: the controller's namespace in allowedNamespaces would open the port to every pod in kube-system. The default is measured on this cluster (DaemonSet rke2-traefik in kube-system, which also serves the Gateway API here), not assumed; a policy written against the wrong controller is accepted, looks strict and silently blocks all web traffic.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"namespace": {
"type": ["string", "null"]
},
"podLabels": {
"description": "Label set the controller's pods carry, rendered verbatim as a podSelector's matchLabels. Kept open because the chart does not interpret the keys.",
"type": ["object", "null"]
}
}
},
"egress": {
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"extraPorts": {
"description": "Additional outbound TCP ports, to ANY destination - ports and not destinations, because a NetworkPolicy cannot express a hostname. DNS is always allowed and has no switch. Without an entry here an outbound port the chart does not already derive is blocked, which is the cap's whole purpose and also its upgrade risk.",
"type": ["array", "null"],
"items": {
"type": "integer"
}
}
}
}
}
},
"scaleDown": {
"description": "true renders the StatefulSet with replicas: 0 and takes precedence over bulwark.replicas. The PVC, Secrets, Service and Ingress stay in place.",
"type": "boolean"
},
"secrets": {
"type": "object",
"additionalProperties": false,
"properties": {
"appSecretRef": {
"description": "op:// item path. The item must carry the fields 'session-secret' and 'admin-password'. Required always. The session secret is mandatory rather than recommended: without it the settings sync is switched off SILENTLY, and the encrypted settings already on the volume become unreadable. The admin password is consumed exactly once, on the first start.",
"type": ["string", "null"]
},
"oauthSecretRef": {
"description": "op:// item path. The item must carry the field 'oauth-client-secret', as issued by the identity provider when the OIDC client was registered. Required while bulwark.oauth.enabled is true.",
"type": ["string", "null"]
}
}
},
"bulwark": {
"type": "object",
"additionalProperties": false,
"properties": {
"replicas": {
"description": "0 or 1. Capped at 1 because of the single ReadWriteOnce PVC, not because of the application: two pods cannot both mount it, so a second replica would sit in ContainerCreating forever. An RWX volume would not help either - nothing coordinates two processes over that directory, and the admin config is read into memory once at start.",
"type": "integer",
"minimum": 0,
"maximum": 1
},
"port": {
"$ref": "#/definitions/port"
},
"jmapServerUrl": {
"description": "The Stalwart JMAP endpoint. Required - and it does two things: it is the mail server's address AND the switch that disables the first-run setup wizard. Left empty, Bulwark starts in bootstrap mode and whatever is clicked through its wizard lands in config.json on the PVC, which then overrides every value here. Enforced by fail() in the StatefulSet, not by this schema (see the title).",
"type": ["string", "null"]
},
"appName": {
"description": "APP_NAME - the product name in the UI and the PWA. null leaves upstream's 'Webmail'.",
"type": ["string", "null"]
},
"stalwartFeatures": {
"description": "STALWART_FEATURES - the Stalwart-specific UI parts (password change, Sieve rules). Does not gate the server-side credential passthrough, which has its own upstream switch this chart does not render.",
"type": "boolean"
},
"stalwartAdminAccess": {
"description": "STALWART_ADMIN_ACCESS - who may open the admin dashboard, which after bulwark.oauth.only is the only way back into a broken login path. 'auto' (upstream's default, kept here) signs a Stalwart admin in without the Bulwark admin password; 'password' requires it; 'off' ignores Stalwart admin status entirely. The consequence of 'auto' is at the value in values.yaml: a Stalwart admin can write the config overrides that disconnect this chart from its own values.",
"type": "string",
"enum": ["auto", "password", "off"]
},
"settingsSync": {
"description": "SETTINGS_SYNC_ENABLED - per-account preferences follow the user instead of living in one browser. true means /app/data/settings on the PVC carries USER DATA, encrypted with the session secret. Without that secret this switch is ignored, silently.",
"type": "boolean"
},
"adminConfigReadOnly": {
"description": "ADMIN_CONFIG_READONLY - refuse every write to the admin config directory at the application layer, which makes this file authoritative and closes the precedence trap for good. FALSE by default because the price is threefold and measured: it also 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 rather than removing it, because removeAdminOverride is guarded by the same check; and on a FRESH install it prevents admin.json from being written at all, so the admin dashboard - the rescue door - never comes up and its login endpoint answers HTTP 500 while the pod stays Ready. values.yaml has the only safe sequence.",
"type": "boolean"
},
"telemetry": {
"description": "BULWARK_TELEMETRY. false. Upstream's default is already off (telemetry is opt-in), so what this buys is the LOCK: the environment beats the state file and upstream blocks the admin UI from changing it while the override is active.",
"type": "boolean"
},
"updateCheck": {
"description": "BULWARK_UPDATE_CHECK - the hourly version check. Unlike telemetry this is ON unless switched off, so false does change behaviour. Off because Renovate already watches the image tag inbound.",
"type": "boolean"
},
"logLevel": {
"type": "string",
"enum": ["error", "warn", "info", "debug"]
},
"logFormat": {
"description": "text or json. json here, against upstream's default of text, because the logs go to a cluster log collector rather than to a terminal.",
"type": "string",
"enum": ["text", "json"]
},
"oauth": {
"description": "May be null / empty: the chart then falls back to its defaults (OIDC on, password form hidden). That fallback is deliberate and is the safe direction - an erased block must not silently turn the whole sign-in path off on a release whose login is OIDC.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"enabled": {
"description": "OAUTH_ENABLED. Gates the OIDC values, the OAuth OnePasswordItem and the secret file mount.",
"type": "boolean"
},
"only": {
"description": "OAUTH_ONLY - hide the password form, so the identity provider is the only way in for a user. This chart's riskiest default, taken deliberately. The way back out of a lockout is NOT a helm upgrade with this false: oauthOnly is read through configManager.get, so an override in config.json on the PVC wins. The repair is removing that override in the admin dashboard.",
"type": "boolean"
},
"clientId": {
"description": "OAUTH_CLIENT_ID. Required while enabled - enforced by fail() in the StatefulSet.",
"type": ["string", "null"]
},
"issuerUrl": {
"description": "OAUTH_ISSUER_URL, the issuer Bulwark fetches the discovery document from (keycloak: https://<host>/realms/<realm>). Required while enabled - enforced by fail() in the StatefulSet. The endpoints it advertises must share the issuer's origin or be public, or Bulwark's discovery validator rejects them.",
"type": ["string", "null"]
},
"scopes": {
"description": "OAUTH_SCOPES - REPLACES the scope list rather than adding to it. null leaves upstream's default, which is the right starting point: an incomplete list here is a login that succeeds and then has no identity to work with. Use OAUTH_EXTRA_SCOPES through bulwark.env to add.",
"type": ["string", "null"]
},
"endSession": {
"description": "OAUTH_END_SESSION - signing out also ends the provider's session. true (upstream's default): on a shared browser a sign-out that leaves the provider signed in means the next click is straight back in.",
"type": "boolean"
}
}
},
"writableCache": {
"description": "The sizeLimit of the two emptyDirs that readOnlyRootFilesystem: true makes necessary - /tmp and Next.js's own cache at /app/.next/cache. Both are CACHES, not state: losing them on a restart costs one round of image re-optimization. They belong here rather than on the PVC, because a cache on a ReadWriteOnce network volume is slower than regenerating it and would grow the only volume that has to be backed up. These limits and the pod's ephemeral-storage request under bulwark.resources are two halves of one accounting and should move together.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"tmpSizeLimit": {
"type": ["string", "null"]
},
"nextCacheSizeLimit": {
"type": ["string", "null"]
}
}
},
"resources": {
"$ref": "#/definitions/resources"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"securityContext": {
"$ref": "#/definitions/securityContext"
},
"env": {
"$ref": "#/definitions/env"
}
}
},
"storage": {
"description": "The single PVC at /app/data, carrying all four of Bulwark's directories as subdirectories - one volume, because a snapshot is atomic per volume and never across volumes (the lesson of Gitea #81). Deliberately NOT null-safe: an erased size yields an invalid PVC, which fails loudly at apply time. Issue #99 covers only the silent class, and on an existing release a chart-side fallback could not help anyway, because a PVC cannot shrink.",
"type": "object",
"additionalProperties": false,
"properties": {
"amount": {
"description": "What actually grows here is audit.log, and it grows with admin activity rather than with mail volume - the application does not rotate it.",
"type": ["string", "null"]
},
"storageClass": {
"description": "null uses the cluster's default StorageClass. ReadWriteOnce is enough and is what the single replica needs.",
"type": ["string", "null"]
}
}
},
"ingress": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"description": "Gates the whole Ingress template (issue #93). Mutually exclusive with httpRoute.enabled - the render aborts when both are true. Checked in both directions for this chart, and CORRECTED with Gitea #114: it used to say that nothing outside that template functionally reads ingress.domain, which the HTTPRoute now does. Switching the INGRESS off frees clusterIssuer; it frees domain only if httpRoute is off as well. What still holds is the reason - Bulwark derives its own public URL from the request origin and takes no base-URL variable - so a release with no routing object at all is complete without a domain. NOTES.txt renders a visible placeholder rather than carrying a required() that helm template could never reach.",
"type": "boolean"
},
"clusterIssuer": {
"type": ["string", "null"]
},
"domain": {
"description": "The public hostname. Bulwark is served at the ROOT of it and cannot be served under a path: NEXT_PUBLIC_BASE_PATH is compiled into the bundle, and a rewrite-target annotation does not help because the bundle's own asset URLs are built for the root - which is also why the HTTPRoute renders no URLRewrite filter. Read by the Ingress, by the HTTPRoute (when httpRoute.hostnames is empty) and by NOTES.txt for the OIDC redirect URI.",
"type": ["string", "null"]
},
"className": {
"type": ["string", "null"]
},
"annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open. The one worth adding for this application is nginx.ingress.kubernetes.io/proxy-body-size: attachment uploads go through this Ingress and nginx's default of 1m rejects anything larger with a 413 that reads like an application fault - and that is what this installation's live Ingress actually carries (25m, measured 2026-10-12). INERT IN GATEWAY MODE: the Gateway API has no annotation channel and no body-size counterpart at all.",
"type": "object"
}
}
},
"httpRoute": {
"description": "Gateway-API alternative to the Ingress (Gitea #114). Mutually exclusive with ingress.enabled - the render aborts when both are true, naming both value paths. The chart renders only an HTTPRoute with parentRefs; class and certificate sit on the Gateway and belong to the platform. ONE rule, at the root, with no URLRewrite filter - Bulwark cannot be served under a sub-path, so there is nothing to strip and the #118 prefix/redirect assurance has no precondition to meet here. NOT COVERED IN THIS MODE: proxy-body-size, which this installation's live Ingress carries at 25m and which has no counterpart in the specification - an oversized attachment is then rejected by the Gateway with a 413 that reads like a webmail fault. See values.yaml, httpRoute block. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"parentRefs": {
"description": "The Gateway(s) the route attaches to. Required while httpRoute.enabled is true, enforced by required()-equivalent in the template (an empty list aborts the render) - a JSON Schema \"required\" cannot do it, because the key ships as an empty list and is therefore present. Entries are the Gateway API's ParentReference and are passed through unchanged.",
"type": "array",
"items": { "$ref": "#/definitions/parentRef" }
},
"hostnames": {
"description": "Empty means [ingress.domain], so the hostname has a single source of truth. Setting it means a second origin, which has to be registered as an OIDC redirect URI with the identity provider before a login can succeed on it.",
"type": "array",
"items": { "type": "string" }
}
}
}
},
"definitions": {
"parentRef": {
"description": "A Gateway API ParentReference, passed verbatim into HTTPRoute.spec.parentRefs. Deliberately left OPEN (no additionalProperties: false), for the same reason probes and security contexts are: Kubernetes owns this schema, the Gateway API adds fields to it across releases, and a closed list here would reject a field only upstream knows about. What the chart can assert is that \"name\" is there - it is required by the API and a reference without it attaches to nothing, which is the silent failure this chart aborts the render over.",
"type": "object",
"required": ["name"]
},
"port": {
"type": ["integer", "null"],
"minimum": 1,
"maximum": 65535
},
"resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting. One thing specific to this chart: the memory LIMIT is what V8's heap limit is derived from, and /api/health reports 'degraded' once heap usage crosses its threshold - a report the probes cannot see, so a limit set too low produces a pod that is permanently degraded and permanently Ready.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"limitFactor": {
"type": "number",
"exclusiveMinimum": 0
},
"requests": {
"$ref": "#/definitions/resourceList"
},
"limits": {
"$ref": "#/definitions/resourceList"
}
}
},
"resourceList": {
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"cpu": {
"type": ["string", "number", "null"]
},
"memory": {
"type": ["string", "number", "null"]
},
"ephemeral-storage": {
"type": ["string", "number", "null"]
},
"ephemeralStorage": {
"type": ["string", "number", "null"]
}
}
},
"env": {
"description": "Additive container env entries. An entry repeating a name the chart already sets does not replace it - on an existing release the pair collapses to the chart's own value while helm upgrade reports success. And a second caveat specific to this application: an environment variable is only the MIDDLE of three layers, so anything set here is a default that an admin override in config.json on the PVC may already be beating, silently.",
"type": ["array", "null"],
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
},
"valueFrom": {
"type": "object",
"additionalProperties": false,
"properties": {
"secretKeyRef": {
"$ref": "#/definitions/keyRef"
},
"configMapKeyRef": {
"$ref": "#/definitions/keyRef"
}
}
}
}
}
},
"keyRef": {
"type": "object",
"additionalProperties": false,
"required": ["name", "key"],
"properties": {
"name": {
"type": "string"
},
"key": {
"type": "string"
}
}
},
"securityContext": {
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99). This chart's fallback reproduces ITS defaults, deviations included - uid/gid/fsGroup 1001 rather than this repo's 20000, because that is the user the image creates (addgroup --gid 1001 nodejs / adduser --uid 1001 nextjs) and the owner of /app/data in the image layer. fsGroupChangePolicy is the repo's 'Always' and is correct here rather than merely default: /app/data holds a few JSON files and an append-only log, so the recursive chown is cheap, and it is what makes a freshly provisioned volume writable on the first start.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"pod": {
"description": "Merged onto the chart's pod hardening defaults and rendered as the pod's securityContext; kept open because the chart does not interpret the individual fields.",
"type": ["object", "null"]
},
"container": {
"description": "Merged onto the chart's container hardening defaults and rendered as the container's securityContext; kept open because the chart does not interpret the individual fields.",
"type": ["object", "null"]
}
}
},
"probe": {
"description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. All three defaults carry the same CONTEXT: an HTTP GET on /api/health, addressed at the port NAME 'http'. Not HEAD, although Bulwark offers one and it would be cheaper - a Kubernetes HTTPGetAction has no method field and the kubelet always issues a GET, and the only alternative (an exec probe) would mean a Node process every few seconds in an image with no curl or wget. What that proves and what it does not is measured rather than read off the path - it carries the process, HTTP acceptance, a free event loop and Next.js routing, and it carries NOTHING about the reported application state (the handler returns a hard 200 and hard-codes the body to 'healthy' for an unauthenticated caller) and NOTHING about Stalwart (with the mail server gone the probe is green and the pod stays Ready). There is no better endpoint: /api/setup/test-jmap would really test the connection but sits behind the bootstrap setup token, which this chart disables.",
"type": ["object", "null"]
}
}
}