Commit Graph
367 Commits
Author SHA1 Message Date
jklein ed3bc2a48a Merge pull request #132 from jk-cluster/feat/rss-bridge-chart
rss-bridge: new chart
2026-08-23 21:42:31 +00:00
jklein 9259482b87 Merge pull request #134 from jk-cluster/feat/rallly-seccomp-runtimedefault
rallly: seccompProfile RuntimeDefault in the pod context
2026-08-23 21:40:11 +00:00
jklein 7e6b3beed5 rss-bridge: pin the unprivileged-port sysctl, quote env values
Follow-up to the template-chart hardening fixes in #133, which landed
while this branch was open.

The env block now quotes names, values and keyRef fields. Without it a
value like "5" renders as a YAML number and the API server rejects the
whole manifest. RSS-Bridge takes numeric settings that way
(RSSBRIDGE_http_timeout), so this was reachable with the first env entry
anyone adds.

The port-80 question is settled by measurement rather than by the
"it works in k3d" reasoning #133 warns about. Same pod spec in k3d
(k3s v1.35.5), only ip_unprivileged_port_start changed:

  1024, capabilities.drop [ALL]      nginx: bind() to 0.0.0.0:80 failed
                                     (13: Permission denied)
  1024, plus NET_BIND_SERVICE        identical failure - for a non-root
                                     process the capability lands in the
                                     permitted but not the effective
                                     set, so it buys nothing
  0                                  binds, HTTP 200

So the chart cannot buy safety with a capability the way samba does, and
port 80 is not negotiable either: nginx is configured for it inside the
image and the entrypoint's only lever is a sed against a read-only
/etc/nginx. What it can do is stop depending on the node default - the
pod securityContext now pins the sysctl to 0, so the bind no longer
relies on cluster configuration. Verified: the chart-installed pod
reports 0 and answers 200 through the Service.

Worth recording how that failure looks without probes: the entrypoint
backgrounds nginx and then execs php-fpm, so a failed bind leaves a
container reporting Running with no web server at all. Both test pods
above were Running. The probes on /?action=health are what turn it into
a pod that never becomes Ready.

trivy config still reports 0 to act on; the KSV-0117 statement is
updated to the measured reasoning.
2026-08-23 23:39:49 +02:00
jklein fe6839265c rallly: seccompProfile RuntimeDefault in the pod context
Sets seccompProfile.type: RuntimeDefault in the pod security context, so
the container runtime's default syscall filter applies to every container
in the pod.

The value lives in the pod context rather than the container context
because it covers init containers and any container added later without
being repeated; a container that needs a different filter can still
override it under securityContext.container, which wins over the pod
value.

Verified in k3d against the database path rather than a static page:
under the filter the app opens its connection to Postgres and Prisma
applies all migrations (141 rows in _prisma_migrations, 31 tables
created), and the server-rendered pages answer with HTTP 200. The kernel
reports Seccomp: 2 (filter mode) inside the container.

Major bump: runtime behavior changes.

Refs #84
2026-08-23 23:37:42 +02:00
jklein e016650f91 rss-bridge: qualify the image registry, except the port-80 finding
Two findings from the Trivy config scan that landed on main in #129.

AVD-KSV-0125 is fixed rather than excepted: writing the image as
docker.io/rssbridge/rss-bridge puts a real registry in the registry
position instead of "rssbridge", which is what ci/trivy/data/ksv0125.yaml
asks for. Verified in k3d that the qualified reference resolves to the
same digest (sha256:0eca99b2...) and still runs.

AVD-KSV-0117 (containerPort below 1024) becomes a scoped, documented
exception. nginx listens on 80 inside the image, and the entrypoint's
only way to change that is a sed against /etc/nginx/sites-enabled/default
which a read-only root filesystem rejects - so the alternative to port 80
is giving up readOnlyRootFilesystem. The premise of the check does not
hold here anyway: the container binds 80 as uid 33 with all capabilities
dropped and no NET_BIND_SERVICE.

trivy config now reports 0 to act on for this chart.
2026-08-23 23:33:23 +02:00
jklein 3fa67d55ef rss-bridge: new chart
Takes over the four loose manifests in cluster-config/rss-bridge/ as a
proper chart, on the current template-chart standard: strict values
schema, null-safe helpers, computed limits, replicas/scaleDown,
checksum/config, NOTES.txt.

Two things deviate from the repo standard on purpose:

- The Ingress models host AND TLS secret name per entry instead of the
  single conventional tls-<release>-<chart>-ingress. The installation
  this replaces has grown three separate certificates, one per host.
  Renaming them would make cert-manager issue three new ones and leave
  each host unreachable until it did - a self-inflicted outage on a
  deployment that has been up for over a year. Entries sharing a secret
  name are grouped into one tls entry, and omitting the name falls back
  to the convention, so consolidating later is a values change.

- Requests and limits are decoupled. The old manifest requested 2Gi of
  ephemeral storage, identical to its limit, reserving two gigabytes for
  a workload measured at 2.2 MB after fetching all five whitelisted
  bridges. The request drops to 64Mi; the limit stays at 2Gi.

Hardening was verified rather than assumed. The image ships no USER and
runs as root by default, but runs fully hardened once its writable paths
are provided: uid/gid 33, readOnlyRootFilesystem, all capabilities
dropped, seccompProfile RuntimeDefault. The writable paths were found by
running the image with a writable rootfs, exercising every bridge and
reading its diff layer - /app/cache, /run, /run/php, /var/lib/nginx.
/tmp did not show up that way, because PHP's opcache creates its lock
file there and unlinks it immediately; it only surfaced when the
hardened container died at startup. Stopping at the first writable path
would have shipped a chart that cannot start.

Probes use the app's own /?action=health, which renders fixed JSON
without touching the cache or any remote site, so neither a scraped site
going down nor an unwritable cache can restart the pod.

Fixes #128
2026-08-23 23:33:23 +02:00
jklein 06a5a7d517 Merge pull request #133 from jk-cluster/feat/template-chart-hardening-fixes
template-chart: seccomp profile, unprivileged port, and placeholder fixes
2026-08-23 21:31:53 +00:00
jklein 00ea301e33 template-chart: seccomp profile, unprivileged port, and two placeholder fixes
Five changes to the scaffold every future chart is copied from:

1. seccompProfile.type: RuntimeDefault in the pod security context, in
   line with the rest of the seccomp series (#84). Pod context, because
   from there it also covers init containers and any container added
   later without being repeated.

2. containerPort 80 -> 8080, clearing AVD-KSV-0117. Note that the usual
   justification for this does not hold: a non-root container CAN bind
   port 80 on current Kubernetes, because the kubelet sets
   net.ipv4.ip_unprivileged_port_start=0 in the pod network namespace.
   Measured, not assumed - see the values.yaml comment. The reason to
   stay above 1024 is that this is a runtime default rather than a
   guarantee, and that every real chart here already does.

3. The ConfigMap placeholder key is renamed from the literal "key" to
   "example-setting", clearing AVD-KSV-01010 (secret-scanner patterns
   match any key named "key").

4. The image placeholder is fully qualified as ghcr.io/ORG/IMAGE, so the
   template stops teaching the unqualified form that produced the
   repo's unqualified Docker Hub images.

5. env values are rendered through "quote". Without it a value like
   "8080" renders as a YAML number and the API server rejects the whole
   manifest ("cannot unmarshal number into Go struct field
   EnvVar.value of type string"). Every real chart in the repo already
   quotes here; only the template did not. Found by instantiating the
   template and installing it, not by rendering it.

Verified by instantiating the scaffold into a working chart per the
README procedure and installing it in k3d: the workload becomes Ready,
the Service routes to the app on 8080 and an ephemeral container added
to the running pod inherits Seccomp: 2 from the pod context.

Refs #84
2026-08-23 23:29:51 +02:00
jklein dd9d91442a Merge pull request #129 from jk-cluster/feat/trivy-config-ci
CI: report Trivy misconfiguration findings for changed charts
2026-08-23 21:17:52 +00:00
jklein e74c25b011 Merge pull request #131 from jk-cluster/feat/apache-tika-seccomp-runtimedefault
apache-tika: seccompProfile RuntimeDefault in the pod context
2026-08-23 21:17:18 +00:00
jklein fa8c4fea45 apache-tika: seccompProfile RuntimeDefault in the pod context
Sets seccompProfile.type: RuntimeDefault in the pod security context, so
the container runtime's default syscall filter applies to every container
in the pod.

The value lives in the pod context rather than the container context
because it covers init containers and any container added later without
being repeated; a container that needs a different filter can still
override it under securityContext.container, which wins over the pod
value.

Verified in k3d against real extractions rather than a reachable port:
the -full image loads its parsers dynamically, so a blocked syscall would
surface as HTTP 200 with empty text, not as an error. The PDF parser, the
HTML parser and the detector/metadata path all return the expected
content under the filter, and the kernel reports Seccomp: 2 (filter mode)
inside the container.

Major bump: runtime behavior changes.

Refs #84
2026-08-23 23:14:54 +02:00
jklein 705ee066a4 CI: record the paperless-ngx root init container as a documented exception
The pod runs rootless as uid/gid 1000; only init-run-ownership is root, to
chown the /run emptyDir that s6-overlay requires to be owned by the runtime
uid. Justified in paperless-ngx/values.yaml, so AVD-KSV-0105/-0022/-0106 are
excepted for that chart with the reason, instead of being reported as if the
workload itself ran as root.
2026-08-23 23:14:12 +02:00
jklein 1ae5e1a7c3 CI: report Trivy misconfiguration findings for changed charts
Adds a trivy-config job to the PR pipeline that renders each changed chart
with Helm and runs `trivy config` over the rendered manifests. Reporting
only - TRIVY_CONFIG_MODE=report always exits 0, so the job never blocks a PR.

Rendered manifests rather than the chart directory: `trivy config <chart-dir>`
renders with default values, which fails on every chart here because of their
required() values - and Trivy does not fail with it, it logs "Skipping chart"
and prints an empty, green report.

Values come from the install-test fixtures (ci/<chart>/test-values.yaml), the
only committed per-chart values in the repo; none of them touches
securityContext, images or capabilities, so the scan sees the chart's own
hardening. Charts without fixtures get ci/trivy/values/<chart>.yaml.

Exceptions live in ci/trivy/trivyignore.yaml in Trivy's native ignore-file
schema, each with a statement - the script refuses to run without one and
reports suppressed findings with their reason instead of hiding them.
AVD-KSV-0125 is configured instead of excepted: ci/trivy/data/ksv0125.yaml
lists the trusted registries, so the check stays able to flag a genuinely
foreign source.

Refs #84.
2026-08-23 23:14:12 +02:00
jklein 1d73beff4c Merge pull request #130 from jk-cluster/feat/gotenberg-seccomp-runtimedefault
gotenberg: seccompProfile RuntimeDefault in the pod context
2026-08-23 21:11:07 +00:00
jklein 37b76e353c gotenberg: seccompProfile RuntimeDefault in the pod context
Sets seccompProfile.type: RuntimeDefault in the pod security context, so
the container runtime's default syscall filter applies to every container
in the pod.

The value lives in the pod context rather than the container context
because it covers init containers and any container added later without
being repeated; a container that needs a different filter can still
override it under securityContext.container, which wins over the pod
value.

Gotenberg is the chart where a syscall filter was most likely to bite: it
drives headless Chromium and LibreOffice, and browser sandboxes are the
classic casualty of a restrictive seccomp profile. Verified in k3d that
both engines still work under the filter - Chromium HTML->PDF and
LibreOffice document->PDF both return real, non-empty PDFs, and the
kernel reports Seccomp: 2 (filter mode) inside the container.

Major bump: runtime behavior changes.

Refs #84
2026-08-23 23:08:37 +02:00
jklein a039b873c9 Merge pull request #127 from jk-cluster/feat/cyberchef-seccomp-runtimedefault
cyberchef: seccompProfile RuntimeDefault in the pod context
2026-08-23 21:06:07 +00:00
jklein 4a61f763e8 cyberchef: seccompProfile RuntimeDefault in the pod context
Sets seccompProfile.type: RuntimeDefault in the pod security context, so
the container runtime's default syscall filter applies to every container
in the pod. Verified in k3d: /proc/1/status reports Seccomp: 2 (filter
mode) with one filter loaded, and nginx keeps serving the app.

The value lives in the pod context rather than the container context
because it covers init containers and any container added later without
being repeated; a container that needs a different filter can still
override it under securityContext.container, which wins over the pod
value.

Major bump: runtime behavior changes, even though it stays invisible for
this workload.

Refs #84
2026-08-23 23:00:02 +02:00
jklein 69202d5e13 Merge pull request #126 from jk-cluster/feat/gotenberg-decoupled-limits
gotenberg: small requests, explicit higher limits
2026-08-23 20:47:51 +00:00
jklein 61bdc9de90 Merge pull request #125 from jk-cluster/feat/template-chart-schema-guidelines
template-chart + README: values.schema.json, Ingress-Gate und Null-Absicherung als Referenz (Abschluss #68/#93/#99)
2026-08-23 20:37:01 +00:00
jklein 0c403b6100 gotenberg: small requests, explicit higher limits
The computed limits (limitFactor 3 x request = 1536Mi memory, 30Mi ephemeral
storage) were too small for real documents: a 12 MB HTML already gets the
container OOM-killed, and the caller sees no HTTP error at all, just a dropped
connection and a rising RESTARTS counter.

Rather than raise the requests and reserve conversion-sized capacity on the
node around the clock, requests and limits are decoupled: the requests stay as
they were, and both limits are now set explicitly. The chart already supported
this - an explicit limit wins over the limitFactor computation.

Measured in a throwaway k3d cluster with the official image (HTML via
Chromium), building on the series in the issue:

  18 MB HTML, 2Gi     -> OOMKilled
  18 MB HTML, 2560Mi  -> OOMKilled
  18 MB HTML, 3Gi     -> HTTP 200 in ~22s, 21 MB PDF
  12 MB HTML, 1536Mi  -> OOMKilled (the old computed limit)
  12 MB HTML, 2Gi     -> HTTP 200 in ~13s

Memory beyond 3Gi buys nothing: gotenberg's own request timeout is 30s and the
chart passes no flags to change it, so a document needing more memory has
already run out of time (a 25 MB HTML misses the 30s even at 4Gi).

The ephemeral-storage limit is raised in the same change, because raising
memory alone only moves the failure. With 3Gi memory and the old 30Mi limit,
/tmp peaked at 40 MB on a single conversion and at ~125 MB over a short
series, and the pod was evicted:

  Pod ephemeral local storage usage exceeds the total limit of containers 30Mi

That failure is worse than the OOM-kill: the pod does not restart, it is
replaced, and the old one is left behind as Evicted.

The defaults in the effectiveResources helper are updated in lockstep with
values.yaml, so an erased "resources:" block (#99) falls back to the new
explicit limits instead of the old computed ones.

NOTES.txt is pulled along. Its numbers came from effectiveResources already,
but the text described a threshold that has moved and advice that no longer
holds: raising the request no longer drags the limit with it now that the
limits are explicit. The warning is kept but no longer claims memory is the
first limit you hit - with these limits, the 30s timeout is.

Major, because the runtime behaviour changes and the pod may occupy more
memory during large conversions than it was allowed to before.

Fixes #106
2026-08-23 22:35:22 +02:00
jklein b218f64eac template-chart + README: values.schema.json, gate and null-safe helpers
Closes out the schema programme (#68) by moving what the 18 charts taught
into the reference implementation and the binding guidelines.

template-chart:
- values.schema.json as the reference schema: additionalProperties false
  on every chart-owned level, pod-spec passthrough left open, and the
  reasoning inline in the descriptions so a copy carries it along.
- defaultedDict and subBlock plus the per-block fallbacks for both
  security contexts, the three probes and resources, wired into both the
  Deployment and the StatefulSet.
- the reference itself violated guideline 1: its env block reads
  ingress.domain, so with the Ingress gated off it rendered an empty
  value. That read is now required(), and the comment says why - the
  template is what people copy, so it has to teach the guard, not the
  hole.

README, new section "Values that must not lie" with the five rules:
1. a value read outside its obvious template needs its own guard
   (matrix-synapse published '{"m.server": ":443"}' to federation without
   it, rendering cleanly and reporting healthy)
2. probes, contexts and resources through the null-safe helper, never
   behind an "if" - a guard removes the probe instead of showing a null
3. no liveness on an external dependency where a dependency-free path
   exists, otherwise document why on the helper
4. a fallback reproduces THIS chart's defaults, not the repo standard -
   calibre's readOnlyRootFilesystem: false is what keeps the runtime uid
   at 1000 instead of 911 and existing volume data readable
5. a probe default carries its context - port name, Host header, exec
   command - or the probe vanishes (teamspeak) or gets 403 (patchmon)

Plus the two mechanics that are easiest to lose: null reaches schema
validation only for keys WITHOUT a chart default (which is why the helper
and the ["object","null"] types each cover a case the other cannot), and
schemas must be validated against real bundle values - that step caught a
draft of mine that would have broken three live paperless releases.

CLAUDE.md: the new-chart checklist now names the schema and the
adaptation step, since the copied file is chart-specific and wrong until
it is adapted.

freesailarr is not covered: it is not on main yet (#59).
2026-08-23 22:35:03 +02:00
jklein c2f12bec70 Merge pull request #124 from jk-cluster/fix/rallly-smtp-reject-unauthorized
rallly: default smtp.rejectUnauthorized to true
2026-08-23 20:31:41 +00:00
jklein edb0837246 rallly: default smtp.rejectUnauthorized to true
The deployment renders SMTP_REJECT_UNAUTHORIZED from
rallly.smtp.rejectUnauthorized, but values.yaml never carried the key. With
SMTP enabled the variable therefore reached the app as an empty string --
a certificate check nobody had configured either way.

It now defaults to true, so the mail server's certificate is verified unless
the operator switches the check off deliberately. values.yaml explains when
that is needed (a mail server with a self-signed certificate), because the
symptom otherwise points at the mail server rather than at this value.

The env var is not rendered with "| default true": an explicit false is a
zero value and default would swallow it, silently re-enabling the check the
operator just switched off. A nil falls back to true instead, so the variable
can no longer reach the app empty. The schema now rejects strings.

Operationally inert: the rallly fleet bundle and the deployed release both
have smtp.enabled: false, so the variable is not rendered at all -- the
render with SMTP disabled is byte-identical to before.

Major, because the runtime behaviour of mail sending changes.

Fixes #109
2026-08-23 22:28:24 +02:00
jklein d6de6de263 Merge pull request #123 from jk-cluster/feat/patchmon-notes-txt
Add NOTES.txt to the patchmon chart
2026-08-23 20:15:45 +00:00
jklein 5186d711a5 Merge pull request #122 from jk-cluster/feat/values-schema-matrix
matrix-synapse: strict values.schema.json + Ingress-Gate + Null-Absicherung (Welle 3, #68/#93/#99)
2026-08-23 20:15:43 +00:00
jklein 579433c4bf Add NOTES.txt to the patchmon chart
The centrepiece is the secret inventory: the four OnePasswordItem CRs carry
only item paths, while the fields each item must contain are decided in the
deployment. The notes list them per item — including the eight OIDC fields
nobody would guess — because a missing field does not fail the install, it
starts the container with an empty variable and misbehaves later.

The warnings are the ones this chart earns:

- Sign-in is OIDC-only. The chart sets OIDC_DISABLE_LOCAL_AUTH, so a wrong
  OIDC item locks everyone out with no local account to fall back on.
- A wrong ingress.domain never becomes Ready: the server answers 403 to any
  unknown Host header, the probes send that domain, and the log shows plain
  403s rather than anything resembling a configuration error.
- The first 2.x start rebuilds database indexes and may take minutes, so the
  notes print the actual startup budget and say a slow boot is not a hang.
- The presence registry is keyed by pod hostname, so replicas do not scale.

The startup budget is read through the null-safe probe helper, and the guacd
sidecar and scaled-down states are reported only when they apply. Refs #43.
2026-08-23 22:07:45 +02:00
jklein 1ddb5f457e matrix-synapse: strict schema, ingress gate and null-safe hardening
Three changes, across both workloads (Synapse StatefulSet and delegation
nginx Deployment).

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, including the whole matrix.delegation block
   with its own contexts, probes and resources.

2. Ingress gate (#93): the Ingress template is wrapped in
   {{- if .Values.ingress.enabled }} and the key is added to values.yaml
   with default true, so behaviour is unchanged.

   The domain checkpoint found a second reader: the delegation nginx
   serves ingress.domain in its /.well-known/matrix/{server,client}
   endpoints, and read it without a guard. Gating alone would therefore
   have published '{"m.server": ":443"}' and base_url "https://" - a
   federation-breaking answer that renders and starts cleanly. Both reads
   are now required(), so that case fails loudly. Only clusterIssuer
   actually becomes unnecessary when the Ingress is off.

3. Null-safety for hardening, probes and resources (#99) for both
   workloads. The rebuilt probes keep each workload's own endpoint:
   Synapse /health, delegation /.well-known/matrix/server - the endpoint
   that workload exists to serve - including its shorter 3s timeout. The
   delegation pod context keeps having no fsGroup, because it mounts no
   volume that needs one.

The nginx checksum/config annotation from #82 is untouched.

storage stays deliberately not null-safe: an erased block yields an
invalid PVC, which is the loud class #99 excludes.

Rendering is byte-identical to 6.0.0, both with the CI values (which
enable delegation) and with the production bundle values.
2026-08-23 22:05:56 +02:00
jklein 09fddb15f5 Merge pull request #120 from jk-cluster/fix/samba-remove-globalconfig
samba: remove the unimplemented globalConfig value
2026-08-23 20:05:04 +00:00
jklein b33f5cd8b3 Merge pull request #121 from jk-cluster/feat/values-schema-paperless
paperless-ngx: strict values.schema.json + Null-Absicherung (Welle 3, #68/#99)
2026-08-23 20:05:02 +00:00
jklein 232ecdfa05 samba: remove the unimplemented globalConfig value
samba.globalConfig has been part of values.yaml since the chart's first
commit but is read by no template. Setting it never had an effect.

Rather than implement it, the key is removed and the strict schema now
rejects it: additionalProperties is already false on the samba object, so
dropping the property turns a silently ignored key into a render-time
error.

Operationally inert: neither the cluster-smb fleet bundle nor the deployed
release's user-supplied values set the key -- it only ever appeared as the
chart's own null default.

Major, because a published value disappears and is now rejected.

Fixes #117
2026-08-23 21:59:20 +02:00
jklein ac53555e44 paperless-ngx: strict values.schema.json and null-safe hardening
Largest values surface of the programme, and the first chart where the
null-safety covers two workloads.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, including files.barcodes, ocr (with enums
   for the 3.x modes), scripts, tikaGotenberg, the oidc provider block
   and the SMB CronJob.

2. Null-safety for hardening, probes and resources (#99), for the server
   AND the SMB consume-mover CronJob. It reuses the defaultedDict helper
   the barcodes block already introduced (#61) instead of adding a second
   mechanism - one merge helper, now with more callers.

   The server's pod context is consumed twice: as the pod securityContext
   and by the /run-ownership init container, which chowns /run to
   runAsUser:runAsGroup because s6-overlay refuses to start unless /run is
   owned by the runtime uid. Both reads now go through the same helper, so
   the chown cannot disagree with the context it has to match. Erasing
   that block used to abort rendering with a nil-pointer error; it now
   falls back to the defaults like every other chart.

The CronJob has no probes (documented exception), so only its two
contexts and resources are covered.

files.storage stays deliberately not null-safe: an erased value yields an
invalid PVC, which is the loud class #99 excludes.

Rendering is byte-identical to 11.0.0, both with the CI values (which
include the #61 regression guard) and with the production paperless-jk
bundle values.
2026-08-23 21:59:06 +02:00
jklein 1ef8c7d42e Merge pull request #119 from jk-cluster/feat/values-schema-mcrouter
minecraft-router: strict values.schema.json + Null-Absicherung (Welle 2, #68/#99)
2026-08-23 19:51:27 +00:00
jklein a76accf7ad minecraft-router: strict values.schema.json and null-safe hardening
No ingress gate: this chart has no Ingress at all (clients reach it via a
NodePort), and ingress.domain appears neither in its templates nor in its
values.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, including the entries of the mappings list,
   where domain and internalUrl are required.

2. Null-safety for hardening, probes and resources (#99). Two things a
   rebuilt default probe must not lose, both verified:
   - the API port is addressed by NAME ("api-port"), so the probe keeps
     working when server.apiPort is overridden.
   - mc-router still has no /health endpoint in 1.46.4, so GET /routes is
     the health signal, and the "Accept: application/json" header belongs
     to that request.

   The pod context keeps pinning uid/gid 10000 even though the image
   declares USER 65532 since 1.46.0, so the runtime identity does not
   silently change with the base image.

mappings stays deliberately not null-safe: the chart fails loudly on a
missing or empty routing table, which is the loud class #99 excludes - a
router without routes is not operable and has no sensible default.

Rendering with normal values is byte-identical to 2.0.0.
2026-08-23 21:49:18 +02:00
jklein dbabd00676 Merge pull request #118 from jk-cluster/feat/values-schema-homeassistant
homeassistant: strict values.schema.json + Null-Absicherung (Welle 2, #68/#99)
2026-08-23 19:47:48 +00:00
jklein 2cb1a05d6f homeassistant: strict values.schema.json and null-safe hardening
No ingress gate change: this chart already honours ingress.enabled, and
its ingress block keeps the upstream-style host/tls lists that the README
documents as an exception. The schema models that shape as it is instead
of pushing the repo's domain/clusterIssuer pair onto it, and the list
entries stay open because they go verbatim into the Ingress spec.

The domain checkpoint ran over values.yaml and templates and came back
empty: ingress.domain does not exist in this chart at all.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level. Pod-spec passthrough (volumes, volumeMounts,
   nodeSelector, tolerations, affinity, ingress hosts/tls) stays open.

2. Null-safety for hardening, probes and resources (#99).

   This chart's hardening is a documented deviation and the fallbacks
   reproduce it exactly: the pod context is deliberately EMPTY, because
   the official image must start as root (s6-overlay does its setup as
   uid 0 and the image has no non-root user), and capabilities.add keeps
   NET_RAW, without which device discovery (DHCP watcher, ping/arp
   integrations) logs "Operation not permitted".

core.storage stays deliberately not null-safe: an erased block yields an
invalid PVC, which is the loud class #99 excludes.

The #82 limitation around the 1Password-backed config Secret is
untouched: Helm never sees the content, so it cannot be hashed, and the
values.yaml note about the manual rollout restart stays as it is.

Rendering with normal values is byte-identical to 5.0.0.
2026-08-23 21:44:49 +02:00
jklein f0faf8c222 Merge pull request #116 from jk-cluster/feat/values-schema-samba
samba: strict values.schema.json + Null-Absicherung (Welle 2, #68/#99)
2026-08-23 19:42:30 +00:00
jklein 24d3366e8b samba: strict values.schema.json and null-safe hardening
No ingress gate: this chart has no Ingress at all (SMB and NetBIOS are
exposed via NodePort), and ingress.domain appears neither in its
templates nor in its values.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, including the entries of the samba.shares
   list.

2. Null-safety for hardening, probes and resources (#99).

   This chart's hardening is a documented deviation forced by the
   ghcr.io/dockur/samba image, and the fallbacks reproduce it exactly
   instead of restoring the repo standard: no runAs* values (the
   entrypoint creates Unix users and smbd impersonates the share user per
   session, both requiring a root start), readOnlyRootFilesystem stays
   false (the entrypoint writes /etc/passwd, /etc/group, the healthcheck
   symlink and the Samba TDBs), and capabilities.add keeps the six
   entries a multi-user root file server needs. Restoring the standard
   here would stop the file server from working.

   The probes target the SMB TCP port directly, because SMB is not HTTP.

The users.conf / checksum-config limitation from #82 is untouched: Helm
never sees the Secret content, so it cannot be hashed, and the comment in
values.yaml documenting the manual rollout restart stays as it is.

Rendering with normal values is byte-identical to 5.0.0.
2026-08-23 21:39:49 +02:00
jklein 1b8087d77c Merge pull request #115 from jk-cluster/feat/values-schema-calibre
calibre-web-automated: strict values.schema.json + Ingress-Gate + Null-Absicherung (Welle 2, #68/#93/#99)
2026-08-23 19:37:24 +00:00
jklein 6c04a68763 calibre-web-automated: strict schema, ingress gate and null-safe hardening
Three changes, all about values that do not do what they promise:

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level.

2. Ingress gate (#93): the Ingress template is wrapped in
   {{- if .Values.ingress.enabled }} and the key is added to values.yaml
   with default true, so behaviour is unchanged.

   The domain checkpoint from rallly ran over values.yaml and templates
   and came back empty here: ingress.domain is read nowhere but in the
   Ingress template. Unlike rallly, zipline, voucher-vault and outline,
   this gate therefore frees both domain and clusterIssuer, and no extra
   required() is needed.

3. Null-safety for hardening, probes and resources (#99).

   This chart's hardening is a documented deviation from the repo
   standard, and the fallbacks reproduce it exactly instead of restoring
   the standard: no runAs* values (s6-overlay boots as root and drops to
   PUID/PGID 1000), readOnlyRootFilesystem stays false (read-only mode
   disables the PUID/PGID remapping, silently moving the runtime uid from
   1000 to 911 and breaking existing volume data), and capabilities.add
   keeps its six entries that the s6 init and supervision need. A
   "cleaned up" standard default here would not be tidier, it would be an
   outage.

cwa.storage stays deliberately not null-safe: an erased value yields an
invalid PVC, which is the loud class #99 excludes.

Rendering with normal values is byte-identical to 4.0.0.
2026-08-23 21:34:32 +02:00
jklein 4e63e8c164 Merge pull request #114 from jk-cluster/feat/values-schema-satisfactory
satisfactory: strict values.schema.json + Null-Absicherung (Welle 2, #68/#99)
2026-08-23 19:31:07 +00:00
jklein 290c69cc7d satisfactory: strict values.schema.json and null-safe hardening
No ingress gate: this chart has no Ingress at all (game and messaging
ports are exposed via NodePort), and ingress.domain appears neither in
its templates nor in its values.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level. Like teamspeak-server, this chart keeps env,
   probes and securityContext at the TOP level, and the schema mirrors
   that shape.

2. Null-safety for hardening, probes and resources (#99). What a rebuilt
   default must not lose here:
   - liveness and startup run the image's own healthcheck script via
     exec. It queries the server's HTTPS API on SERVERGAMEPORT, so it
     survives a port override, which a tcpSocket default could not.
   - readiness keeps the named "api" port.
   - the resources default keeps limitFactor 2 rather than the repo-wide
     3, which is what holds the memory limit at the published 8Gi.
   - the pod context keeps fsGroupChangePolicy OnRootMismatch, which is
     what avoids a recursive chown of multi-GB game files on every start.

server.storage stays deliberately not null-safe: an erased value yields
an invalid PVC, which is the loud class #99 excludes.

This chart is excluded from the CI install-test, so the local proof is
the only one. Rendering is byte-identical to 4.0.0 with default values
and with the production bundle values, and the rendered manifests were
additionally validated by the cluster's API server via a server-side
dry-run.
2026-08-23 21:28:52 +02:00
jklein 5b3524a354 Merge pull request #113 from jk-cluster/feat/values-schema-teamspeak
teamspeak-server: strict values.schema.json + Null-Absicherung (Welle 2, #68/#99)
2026-08-23 19:26:03 +00:00
jklein e54776e6c1 teamspeak-server: strict values.schema.json and null-safe hardening
No ingress gate: this chart has no Ingress at all (voice/file-transfer
are exposed via NodePort), and ingress.domain appears neither in its
templates nor in its values.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level. Note that this chart keeps replicas,
   resources, probes and securityContext at the TOP level rather than
   under a chart-named key, so the schema mirrors that shape.

2. Null-safety for hardening, probes and resources (#99). The rebuilt
   probes keep the "file-transfer" port name: the voice service is UDP
   and cannot be probed, so that TCP port is the only thing a probe can
   target - a default probe without it would leave the workload with no
   working probe at all.

   The container defaults keep capabilities.add as an empty list, exactly
   as values.yaml has it, so an erased block restores today's rendering
   byte for byte rather than a cleaned-up variant.

storage stays deliberately not null-safe: an erased block yields an
invalid PVC, which is the loud class #99 excludes.

Rendering with normal values is byte-identical to 4.0.0.
2026-08-23 21:23:30 +02:00
jklein 24fb0d2d2b Merge pull request #112 from jk-cluster/feat/values-schema-outline
outline: strict values.schema.json + Null-Absicherung (Welle 2, #68/#99)
2026-08-23 19:21:58 +00:00
jklein 24ca3513fc outline: strict values.schema.json and null-safe hardening
No ingress gate: this chart already gates its Ingress correctly. Two
changes plus one guard the existing gate turned out to need.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level.

2. Null-safety for hardening, probes and resources (#99). All three
   probes keep /_health when rebuilt: it is Outline's only health
   endpoint, and "/" returns 500 as well when the database is down, so
   there is no dependency-free path to fall back to.

3. ingress.domain is required() where the StatefulSet reads it. It feeds
   the URL env var - Outline's public URL for generated links and the
   OIDC redirect URI - and did so without a guard. With the existing gate
   and no domain it rendered "https://", breaking logins silently. That
   hole predates this change; it now fails loudly.

outline.storage stays deliberately not null-safe: an erased block yields
an invalid PVC, which is the loud class #99 excludes.

Apart from an added comment, the rendered manifest is unchanged.
2026-08-23 21:18:58 +02:00
jklein 81549d6aab Merge pull request #111 from jk-cluster/feat/values-schema-voucher-vault
voucher-vault: strict values.schema.json + Null-Absicherung (Welle 2, #68/#99)
2026-08-23 19:17:14 +00:00
jklein f2fc18c6ae voucher-vault: strict values.schema.json and null-safe hardening
No ingress gate: this chart already gates its Ingress correctly. Two
changes plus one guard that the existing gate turned out to need.

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level. Lists that go verbatim into the pod spec
   (ports, storage.volumes, storage.volumeMounts) stay open.

2. Null-safety for hardening, probes and resources (#99). Two specifics
   of this chart:

   - The probes were wrapped in "if" guards, so an erased probe block did
     not render "livenessProbe: null" - it removed the probe entirely and
     silently. The guards are gone; an empty block now means chart
     defaults, so the workload keeps its probes.
   - The rebuilt probes keep the Host: 127.0.0.1 header. Django's
     ALLOWED_HOSTS only accepts 127.0.0.1 and the configured DOMAIN, so a
     probe without it would get 400 and the pod would never be ready.

3. ingress.domain is required() where it is read. The dependency lives in
   values.yaml, not in a template: the DOMAIN env entry is a tpl string
   feeding Django's ALLOWED_HOSTS. With the existing gate and no domain
   the app rendered DOMAIN: "" and answered 400 to every real request -
   silently, and already before this change. It now fails loudly.

Apart from an added comment, the rendered manifest is unchanged.
2026-08-23 21:14:01 +02:00
jklein e22e1235e6 Merge pull request #110 from jk-cluster/feat/values-schema-zipline
zipline: strict values.schema.json + Ingress-Gate + Null-Absicherung (Welle 2, #68/#93/#99)
2026-08-23 19:10:12 +00:00
jklein 40289a2e67 zipline: strict values.schema.json, ingress gate and null-safe hardening
Three changes, all about values that do not do what they promise:

1. values.schema.json (#68): additionalProperties: false on every
   chart-owned object level, so a key the chart does not read is rejected
   at render time instead of being silently ignored.

2. Ingress gate (#93): the Ingress template is wrapped in
   {{- if .Values.ingress.enabled }} and the key is added to values.yaml
   with default true, so behaviour is unchanged.

   As in rallly, ingress.domain is not a pure Ingress value: the
   StatefulSet also feeds it to CORE_DEFAULT_DOMAIN, the app's default
   domain for generated share links, and did so without required(). The
   guarantee only came from the ungated Ingress template, so the gate
   would have handed out unusable links silently. That read is now
   required() as well; only clusterIssuer becomes unnecessary.

3. Null-safety for hardening, probes and resources (#99): an erased block
   fell back to "securityContext: null" (unhardened), a vanished probe or
   a container without resource accounting. The rebuilt liveness probe
   keeps "/" rather than /api/healthcheck, so a database outage still does
   not restart the pod.

config stays deliberately not null-safe: storage and storageClass are
required(), which puts an erased block in the loud class #99 excludes.

Apart from an added comment, the rendered manifest is unchanged.
2026-08-23 21:07:11 +02:00
jklein 4f5870db20 Merge pull request #108 from jk-cluster/feat/values-schema-rallly
rallly: strict values.schema.json + Ingress-Gate + Null-Absicherung (Welle 2, #68/#93/#99)
2026-08-23 19:02:37 +00:00