Both experiments are in, and together they answer the question the scope of
#5 rests on - with an answer that is not the one the plan assumed.
MEASURED, run 888 on this branch:
Install-test cyberchef SUCCESS, 1m9s probe port 8080 restricted to
192.0.2.11/32, which cannot be the
k3d node's address
Install-test outline FAILURE, 5m59s DNS egress rule removed, so
"postgres" and "redis" do not
resolve and /_health cannot pass
So: POLICIES ARE ENFORCED in the install-test - outline's pod never became
Ready and rollout status timed out after six minutes, which is the net this
rollout needs. But THE KUBELET'S PROBE TRAFFIC IS EXEMPT from policy in k3s:
cyberchef became Ready in 69 seconds with its probe port restricted to an
address no probe can come from.
What that changes, and what it does not:
- The plan's claim was "a policy that blocks the probe path makes the
install-test red". That is NOT true here, and it is now measured rather
than assumed. The install-test's net is for EGRESS and for anything the
application itself needs, not for the probe path.
- It does not undermine the rollout, because this convention never restricts
a probe port by default - the rule is rendered without a "from" until an
operator sets probeSources. The trap CI cannot see is therefore not
triggered by this PR; it is triggered by a deliberate narrowing, which is
what the NOTES.txt sentence and ci/run-networkpolicy-test.sh's fourth
assertion exist for.
- It raises the stakes for the render assertions. A wrong ingress port - the
NodePort mistake above all - would leave the pod Ready in CI (probes exempt)
and no ingress controller runs there either, so nothing in the pipeline
would notice. That failure is caught mechanically by assertion 2 or not at
all.
- And it says nothing about the production cluster, whose CNI is Calico, not
k3s'. There, probe traffic is subject to policy, which is exactly why the
default stays open.
Part of #5
The first experiment did not settle it. Restricting cyberchef's probe port 8080
to 192.0.2.11/32 - an address that cannot be the k3d node's - left the
install-test GREEN, which has two very different causes: either k3s' policy
controller exempts the kubelet's probe traffic (good news for this convention,
whose default leaves the probe port open anyway), or it enforces nothing at all
(no net under this rollout).
This separates them without touching a probe. Outline's egress rule for DNS is
removed, so the pod cannot resolve "postgres" or "redis" - and all three of
its probes hit /_health, which checks both stores.
Expected: RED, which proves egress is enforced. Green means nothing is enforced
in CI and the scope has to be renegotiated.
Reverted in the next commit either way.
Part of #5
Major, as every chart in this rollout is.
Inbound 3000 only; outbound DNS plus 5432, 6379 and 443. Those three egress
ports are the STANDARD ones and not the configured ones, which is this chart's
real limitation and is written into values.yaml, the template header and
NOTES.txt rather than left to be discovered: Outline's database, Redis and OIDC
provider all arrive as connection URLs inside 1Password items that the chart
never parses, so it cannot know where they listen and cannot validate a guess.
A dependency on another port is blocked, and because all three probes hit
/_health - which checks Postgres and Redis - the symptom is a pod that never
becomes Ready rather than an error anyone reads. networkPolicy.egress.extraPorts
is the escape hatch.
That same property is what makes this chart the right place for the enforcement
experiment in the next commit.
Also reverts the cyberchef fixture from the first experiment: with the probe
port restricted to an address that is not the node's, the install-test went
GREEN, so the claim that it catches a blocked probe path does not hold as
written. The next commit establishes whether policies are enforced in CI at
all.
Part of #5
Group 2 of the #114 sweep: the charts whose behaviour hangs off an nginx
annotation - calibre-web-automated, outline, paperless-ngx, zipline
(proxy-body-size) and freesailarr (rewrite-target).
Each gets the form the pilot (cyberchef, PR #115) fixed and group 1 (PR #116)
repeated: an httpRoute block off by default, a template rendering ONLY an
HTTPRoute with parentRefs - no Gateway, no gatewayClassName, no Certificate, no
implementation-specific policy object - render aborts on both modes at once
(naming both value paths), on an empty parentRefs and on a missing hostname,
empty hostnames meaning [ingress.domain], a schema entry closed against unknown
keys with an open parentRef definition, and a NOTES.txt that names the active
mode and what the mode costs.
Minor, additive, defaults unchanged: `helm template` with each chart's CI values
is byte-identical to origin/main for all five.
The upload limit is DOCUMENTED, not rebuilt (decision 2 of the issue). The
Gateway API knows no body size; a policy object would bind the chart to one
implementation. So the Gateway branch of NOTES.txt says, per chart and in the
chart's own terms, that the value is NOT ENFORCED and names where to set it: an
e-book library (calibre), an upload service (zipline), the web-UI half of the
document intake while the consume directory keeps no limit at all (paperless),
and the open #12 pair (outline).
Four findings that were not in the plan, each argued in the file it changes:
freesailarr gated freesailarr.sharedIngressApps on ingress.enabled, and that
list is read by the <APP>__SERVER__URLBASE variables as well as by the routing
objects. In Gateway mode it would have come out EMPTY while the route served
radarr, sonarr and prowlarr under their prefixes: UrlBase unset, assets and API
calls linked at the host root, broken UI behind a Ready pod. The helper's two
guards (seerr/mediathekarr only at "/", no duplicate paths) would have gone
unenforced there too. Fixed with freesailarr.published (ingress.enabled OR
httpRoute.enabled) - the same shape as sure.publishedOverTls in group 1, and
identical to ingress.enabled while httpRoute.enabled is false. Measured: on
origin/main with ingress.enabled: false the render emits ZERO URLBASE
variables; with this change in Gateway mode it emits three.
paperless-ngx was the last chart in the repository rendering its Ingress
UNCONDITIONALLY. Without an ingress.enabled switch there is no second value
path for the mutual-exclusion abort to name, and the Ingress plus its
unconditional required() on clusterIssuer would have kept rendering in Gateway
mode - a value demanded for nothing, which is issue #114's "Folge 1" and
README guideline 12. The switch was added, defaulting to true, so the render is
unchanged.
paperless-ngx PAPERLESS_URL had no required() of its own: it relied on the
Ingress template's, which stops firing the moment the Ingress is switchable.
With httpRoute.hostnames set and ingress.domain empty it would have rendered
"https://" and refused every login POST as a CSRF failure. It now carries its
own required(), naming what the value is for.
calibre-web-automated claimed, in values.yaml and in its schema, that
ingress.domain is read nowhere but in the Ingress template. The HTTPRoute reads
it, so the claim now holds only while both modes are off. Corrected rather than
left standing - the same correction garage needed in group 1.
freesailarr is where the Gateway API is LESS machinery than what it replaces,
not a translation of it. rewrite-target applies per Ingress OBJECT, so
qBittorrent (no base path, prefix must be stripped) needed a second Ingress on
the shared host, which then had to repeat the first object's TLS secret name,
omit the cert-manager annotation conditionally so only one Certificate is
requested, and match an ImplementationSpecific regex path for the rewrite's
capture group. A URLRewrite filter applies per RULE: one route, one rule per
app, the filter on qBittorrent's rule, a plain PathPrefix. All four crutches
gone. It is also the only chart without httpRoute.hostnames - it has no
ingress.domain either, and a chart-global hostname list could not say which of
the ten apps it belonged to, so the hostnames stay in ingress.host and
<app>.ingress.host.
CI: one render overlay per chart under ci/httproute/, so the assertion is not
vacuum-green - ci/run-httproute-match-test.sh goes from 20 held assertions over
10 charts to 36 over 15. freesailarr's overlay moves bazarr onto a host of its
own, so the shared-host route AND the per-app route are both rendered; without
that the per-app branch would be exercised by nothing. The install-test fixtures
stay in Ingress mode: the install runs one configuration per chart, cyberchef
already pays that price against the real Gateway-API CRDs, and for paperless-ngx
that fixture is the only install of the valkey-subchart path anywhere.
README also carries a stale figure fixed here: the HTTPRoute test was still
documented at the pilot's 4 assertions over 2 charts, although group 1 had taken
it to 20 over 10.
Not proven, by construction: that traffic flows. No Gateway controller runs in
CI. Not run locally either: k3d, Docker, Trivy. The Gateway branch of NOTES.txt
is EXECUTED by the render test (a template error in it fails the job, verified)
but its text is printed by no pipeline stage - helm template does not emit
notes, and the install-test installs in Ingress mode.
Part of #114
The doc comments on the resources and defaultedDict helpers justified the
copy-per-chart with "charts in this repo deliberately have no dependencies".
That rule fell with #214: a dependency is admissible now, just expensive, and
it sits in the README as guideline 14, not 13. The justification still holds,
but its reason is a different one - sharing the helper would mean a library
chart, a second artefact to publish, version and pull for something that
changes about once a year. searxng got that wording in PR #38; this carries it
to 21 further charts, 40 occurrences in all.
rss-bridge is deliberately left out. Its install-test fails on the Gitea
runner for a reason that predates this branch, and touching the chart would
keep it in the install-test matrix and the check red. Its copy of the sentence
is carried by #51, which has to open the chart anyway.
Comments only: every touched chart renders byte-identical against its
ci/<chart>/test-values.yaml. Each published chart takes a patch bump,
appVersion and +up suffix untouched; template-chart is never published and
takes none, but is corrected because every new chart inherits from it.
Two repo files carried the same claim at no version cost:
validate-charts.yml explained the all-charts quantity render test with it, and
change-app-version.yml still counted nine images for freesailarr - MediathekArr
made it ten.
Part of #157: multiplyQuantity turned a bare integer resource value into
exponential notation and aborted the render. The null-safe helpers pass
values through fromYaml, which makes every number a float64, and Go's "%v"
switches float64 to exponential form from about 1e6 upwards - so
"1048576" arrived as "1.048576e+06" and the suffix check failed. A new
outline.formatNumber helper renders such a value as a plain decimal
string; it is applied to the input and to the non-integer product. This is
the canonical fix agreed for the issue, taken over unchanged so the copies
of the helper in all charts stay identical.
Part of #43: the chart gets a NOTES.txt. It renders the access URL (or the
port-forward command when the Ingress is off), lists the fields each of the
three 1Password items has to carry plus the OIDC redirect URI, and states
the traps that are otherwise found the hard way: editing a value inside a
1Password item restarts nothing, Postgres and Redis are external and
unencrypted by default, migrations run inside the startup probe's budget,
uploads sit on a single RWO volume that helm uninstall takes with it, the
Ingress body-size limit is ten times smaller than the app's, and the chart
configures no mail at all.
Minor bump: the fix changes no rendered output for any value the schema
accepted before, and a NOTES.txt renders no object.
Sets seccompProfile.type: RuntimeDefault in the pod security context of
minecraft-router, outline, urlaubsverwaltung, zipline, voucher-vault,
satisfactory, patchmon and calibre-web-automated, 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.
Five of the eight also get the docker.io/ prefix on their previously
unqualified image references, so the pull no longer depends on the
runtime's default-registry setting: itzg/mc-router,
outlinewiki/outline, urlaubsverwaltung/urlaubsverwaltung,
l4rm4nd/vouchervault (twice, app and init container) and
guacamole/guacd:1.6.0. zipline, satisfactory and calibre-web-automated
were already qualified with ghcr.io.
Verified in a throwaway k3d cluster against real application work, not
just a Running pod, and with Seccomp: 2 (filter mode) read from
/proc/<pid>/status inside each container:
- minecraft-router: a Minecraft handshake for a mapped domain travels
through the router to a backend and the backend's status payload comes
back to the client.
- outline: all migrations apply (45 tables), OIDC discovery succeeds and
the server-rendered page answers 200 with <title>Outline</title>.
- urlaubsverwaltung: Liquibase applies 79 changesets (35 tables), the app
seeds its settings and vacation types, /login answers 200.
- zipline: 27 migrations applied, then a full round trip - first user
created, login, token, upload, and the file downloaded back byte-exact
from the PVC.
- voucher-vault: the init container's collectstatic output is served
(200, 22 KB), Django has its 22 tables and uWSGI answers 200 with
<title>VoucherVault</title>.
- satisfactory: application behavior is NOT proven - the image is
multiple GB and pulls 10 GB+ of game files on first start, which is why
the chart sits in ci/excluded-charts.txt. What is proven is that the
rendered pod securityContext is accepted server-side and yields
Seccomp: 2 with uid/gid 1000 on a substitute image.
- patchmon: both containers run under the pod profile and both report
Seccomp: 2. The server applies 47 migrations (44 tables), connects to
Postgres and Redis and serves its dashboard; guacd answers a real
Guacamole "select rdp" instruction with its full args list, over the
same 127.0.0.1:4822 path the chart wires up.
- calibre-web-automated: the readOnlyRootFilesystem: false deviation is
unaffected - id abc is still uid 1000 (not the 911 the image falls back
to in read-only mode), /config and the Calibre metadata.db are owned by
1000, and both the root s6 init and the uid-1000 application report
Seccomp: 2. calibredb 9.1 runs and adds a book. Its IPC worker prints a
traceback while doing so, but an unconfined control pod on the same
image prints the same class of traceback, so it is not the filter.
trivy config now reports "0 to act on" for all eight; the untouched
samba still shows KSV-0104 and KSV-0030, which confirms these charts are
what closed the findings.
No values.schema.json needed changing: in all eight schemas
securityContext.pod is type ["object", "null"] with no property
restrictions, so the new key validates as it stands. The #99 null paths
still hold - nulling securityContext or securityContext.pod falls back to
the defaults including the new one.
Renovate resolves the prefixed guacd reference: a local dry-run lookup
(renovate 44.40.1, --platform=local --dry-run=lookup) reports depName
docker.io/guacamole/guacd against registryUrl https://index.docker.io
with lookupName guacamole/guacd, currentVersion 1.6.0 and its real
release timestamp, with no warnings. The other four prefixed references
live in templates, which Renovate never reads - they are tracked by the
customManagers via appVersion in Chart.yaml with a depName hardcoded in
renovate.json5, so they are unaffected.
Major bump for all eight: runtime behavior changes.
Refs #84
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.
- apache-tika, gotenberg, cyberchef, homeassistant, patchmon: additive env
value blocks rendered through tpl (value, secretKeyRef and configMapKeyRef
forms), following the template-chart/outline pattern; patchmon gets separate
blocks for the server container and the guacd sidecar.
- homeassistant: ingress.className now defaults to nginx; an empty className
omits ingressClassName from the rendered Ingress, so the flexible upstream
ingress style stays intact.
- outline, paperless-ngx: remove the ingress.clusterIssuer defaults
(letsencrypt / letsencrypt-issuer) - the value is now strictly required,
consistent with the repo-wide ingress standard; CI test-values set it
explicitly.
Chart bumps: apache-tika 7.2.0, gotenberg 4.2.0, cyberchef 5.2.0,
patchmon 5.2.0, homeassistant 4.2.0 (minor); outline 5.0.0,
paperless-ngx 10.0.0 (major).
- Pod and container securityContext are now values-configurable
(outline.securityContext.pod/container) with hardened defaults:
runAsNonRoot (uid/gid 1001, the upstream image's nodejs user),
readOnlyRootFilesystem, allowPrivilegeEscalation false, all
capabilities dropped. /tmp is mounted as an emptyDir for Outline's
import/export scratch files.
- Probes moved to values (outline.livenessProbe/readinessProbe/
startupProbe); defaults are exactly the previously hardcoded ones.
- Ingress raised to the repo standard: ingress.className (default
nginx), ingress.annotations merged with the always-set annotations,
ingress.enabled flag; domain/clusterIssuer stay required and the
TLS secret name was already conform.
- Additive outline.env block rendered through tpl with value/
secretKeyRef/configMapKeyRef forms; chart-managed env stays in the
template.
Major bump 3.1.1+up1.9.2 -> 4.0.0+up1.9.2 (runtime behavior changes
through hardening).
Part of #32
- Add icon: to all 17 Chart.yaml (official project icons); removes the
last helm lint --strict INFO finding across the repo
- Rename template files to the <app>.<kind>.yaml scheme (apache-tika,
gotenberg, teamspeak-server PVC, paperless-ngx configmap) - pure
git mv, no content changes
- template-chart: fix missing space in {{ .Chart.Name}} and rename the
ConfigMap resource -config-map -> -configmap (chart is never published)
- rallly: fix typos in required messages (rallySecret -> ralllySecret,
databseSecret -> databaseSecret)
- README: document the deliberate PVC-name exception from the naming
scheme (renames would provision new empty volumes)
- Patch-bump all 16 published charts
Rendered resources are byte-identical for all published charts (only
helm's '# Source:' comments change for the renamed files); the only
render change is the template-chart ConfigMap name.
Part of #32
Every Deployment/StatefulSet now takes its replica count from a replicas
value (default 1, explicit 0 allowed) via a new per-chart <chart>.replicas
helper; the helper uses a kindIs "invalid" nil check so an explicit 0 is
not swallowed by Helm's default function. A chart-global scaleDown flag
(default false) takes precedence: it renders every workload with
replicas: 0 and suspends the paperless-ngx consume CronJob, stopping the
app without uninstalling it (PVCs, Secrets, Services and Ingress remain).
Defaults leave the rendered manifests byte-identical to main, so all
published charts get a minor version bump; the template-chart carries the
reference helper implementation and stays unversioned.
Fixes#25
Every chart now renders its container resources through a duplicated
_helpers.tpl helper (reference implementation in template-chart): requests
ship as chart defaults, missing memory and ephemeral-storage limits are
computed as resources.limitFactor x the request (default 3, satisfactory
keeps 2 to preserve its previously published 8Gi limit), an explicitly set
limit always wins, and a CPU limit is only rendered when set explicitly.
Fixed limit defaults are removed from all values.yaml files; teamspeak-server
gains a values-driven resources block instead of hardcoded template values.
Major version bump for all published charts.
Every chart was installed twice in a throwaway k3d cluster (origin/main
vs this branch) with real Postgres/Redis/dex instances and observed for
readiness ordering, stability and dependency-outage behavior. Findings:
- voucher-vault: Django only accepts Hosts 127.0.0.1 and the configured
DOMAIN; the default probe Host (pod IP) always got 400. Probes now
send 'Host: 127.0.0.1'.
- zipline: /api/healthcheck includes the database, so liveness now uses
'/' (readiness/startup keep /api/healthcheck). Verified live: on a
Postgres outage the pod turns NotReady within 30s and is never
restarted by the kubelet.
- outline: '/' also returns 500 when the database is down, so there is
no dependency-free endpoint; all three probes use /_health.
Every container in every Deployment/StatefulSet now defines all three
probes. The paperless-ngx consume CronJob intentionally has none (batch
workload).
- Add probes: apache-tika, calibre-web-automated, cyberchef, gotenberg,
homeassistant, patchmon-backend, samba (tcp/445), teamspeak-server
(tcp/30033), zipline (/api/healthcheck)
- Fix voucher-vault null probe values with real defaults
- Fix startup-probe semantics in template-chart and rallly (no
initialDelaySeconds, generous failureThreshold)
- Switch outline probes to its /_health endpoint
- Add readiness/startup probes to satisfactory
- Add startup probes to matrix-synapse, matrix-delegation-nginx,
paperless-server, patchmon-frontend, urlaubsverwaltung
- Major version bump for all published charts