Folds the good parts of #1412 into this PR and addresses both review-bot reviews (#1412 review 1789, #1414 review 1791): - Everything after the OpenBao store is best-effort. Decode, reconnect, listing and revokes run in reap_superseded(); reap_summary() turns any error into "reap skipped" plus a warning, so rotate_ha_token returns Ok. A failed run would start a second pod (backoffLimit: 1) that keeps the token just stored and its successor and reaps the token consumers still hold until ESO syncs, i.e. the #1081 outage. Two unit tests cover it. - Warn when the replaced token is not named "<client_name> [<6 hex>]": no later run reaps it, so it needs a manual revoke (from #1412). - Staged image rollout (from #1412): only the three HA CronJobs carry the flux-system:secret-rotator marker; the other 17 rotators stay on 2026-07-30 until a manual HA run validates the new binary, then a follow-up adds the marker to them. rotation/kustomization.yaml records the exception. - Merged current main (a6f37e2b,1691a0ac): the Reloader annotations for zone-configurator and speech-to-phrase are already there via #1409.
10 KiB
Flux Image Automation
Tracks upstream container images and commits tag bumps to the homelab
repo when new versions are published. No bootstrap step — reuses the
existing flux-system GitRepository whose Secret was created by
flux bootstrap gitea --token-auth and already has write access.
Components
update-automation.yaml— singleImageUpdateAutomationCR. Walks manifests under./kubernetes, appliesSettersstrategy from# {"$imagepolicy": "..."}markers, commits direct tomain. Push uses the bootstrap token's identity; commit author is set toFlux Image Automation <security-bot@renner.uno>for visibility.paperless-ngx.yaml, etc. — oneImageRepository+ImagePolicyper managed image.
Adding a new managed image
- Create
kubernetes/infrastructure/image-automation/<image-name>.yamlwith oneImageRepository(the upstream URL) and oneImagePolicy(the version range to track). - Add the file to this directory's
kustomization.yaml. - In the consumer manifest (
kubernetes/apps/<app>/...), append the marker comment to the image line:image: ghcr.io/upstream/foo:1.2.3 # {"$imagepolicy": "flux-system:<image-name>"} - Commit. Flux populates available tags within
interval, thenImageUpdateAutomationrewrites the tag to the highest in-policy match on its next pass (~30 min).
Currently enrolled
| Image | Range | Notes |
|---|---|---|
| paperless-ngx | 2.x |
|
| audiobookshelf | 2.x |
|
| music-assistant | 2.x |
|
| immich-server / immich-machine-learning | 2.x |
v-prefix filter; bump in lockstep |
| home-assistant | 2026.x |
Calver — bump range each year |
| matter-server | 8.x |
Pre-release filter |
| zwave-js-ui | 11.x |
|
| netbird-ingress | 0.x |
-rootless suffix filter; pre-1.0, bounded to the 0.x major |
| wyoming-{whisper,openwakeword,speech-to-phrase} | 3.x / 2.x / 1.x |
|
| authentik-gate-sync | YYYY-MM-DD-HHMM |
In-house (build-ci-images.yml); newest tag wins |
| kuma-sync | YYYY-MM-DD-HHMM |
In-house (build-ci-images.yml); newest tag wins |
| secret-rotator | YYYY-MM-DD |
In-house (build-ci-images.yml); newest tag wins; staged: only the Home Assistant rotation CronJobs carry the marker so far |
| gatus | 5.x |
v-prefix filter |
| velero-plugin-for-aws | 1.13.x |
Tight: must match velero server major.minor |
| gotenberg | 8.x |
Plain-variant filter (rejects -libreoffice/-chromium/-cloudrun/-aws-lambda) |
| redis-immich | 7.4.x |
-alpine suffix extract; tight to match LXC 225 compose |
| redis-paperless | 8.x |
-alpine suffix extract |
| ceph | 19.x |
v-prefix filter; rejects date-suffixed builds |
| tika | 3.x |
4-component tags; .0 image-revision pinned in the filter so the extracted key is unique |
| lidarr | Lidarr 3.x |
LinuxServer -ls<n> rebuilds; synthetic <build>.<ls>.0 semver key — major bounded in the filter, not the range |
| protonmail-bridge | 3.x |
-1 build-suffix extract |
| firefox | 1.x |
-rolling-daily suffix extract; rejects arch-prefixed duplicates |
| mmwave-configurator | 2.x |
Plain 3-component filter (rejects addon-/-armv7/-arm64/-amd64 variants) |
| cnpg-barman-plugin | 0.x |
v-prefix filter; marker lives in upstream-imported manifest (see below) |
Not enrolled (structurally impossible)
-
woodpeckerci/woodpecker-cli— the lint image in.woodpecker/ci-manifests-test.yml. It publishes a perfectly good version stream (v3.16.0-alpine), so unlike the three below this is not a tag-format problem. It cannot be enrolled because image-automation can only patch YAML that is a Kubernetes manifest — a maintainer states it plainly in fluxcd/flux2#2826: "Flux can only patch Kubernetes manifests. This means the YAML manifest must haveapiVersionandkindfields." A Woodpecker pipeline file is plain YAML with neither, so the marker would be silently ignored.Note this is not fixable by widening
update.path. The path only controls which directory is walked; objects withoutapiVersion/kindare skipped regardless. Widening the path to./buys nothing here and additionally pulls the wholeansible/andesphome/trees into the walk, so don't.Consequence: the CLI is a manual pin while the server and agent auto-bump, so it will drift. That is a known, accepted gap, covered by the update-audit agent (item 5) rather than by automation. When bumping the server major/minor, bump this too — it must stay on the latest stable like everything else, and a CLI behind the server can pass a config the server then rejects.
Not enrolled (deliberate)
These three images publish no usable version stream — there is nothing for image-policy to sort against:
fauria/vsftpd— only:latestis published.openthread/otbr— only Ubuntu codenames (focal,jammy,hirsute) and:latest/:test/:reference-device; no semver release stream.gitea.renner.uno/konairius/ical-proxy— onlymain,latest, andmain-<sha>commit builds; no release tags.
The security-scan agent will continue to surface vuln findings on these as "Skipped / Flux image-automation owns this" even without a policy.
Only a tag-format problem belongs on that list. The policy is to run the
latest version and let image-automation hold us there wherever the tags
allow it, so an image with a usable semver stream gets enrolled even when
a bump carries risk. Where a bad bump is hard to detect, the answer is a
note about what to check afterwards — see apps/trmnl/README.md for the
panel, whose failure mode is a blank screen rather than a failed rollout
— not an exclusion.
Re-import hazard: cnpg-barman-plugin
kubernetes/infrastructure/cnpg-barman-plugin/manifest.yaml is an
upstream-generated YAML imported wholesale. The $imagepolicy marker
on its image line will get clobbered any time the manifest is
re-imported from upstream. If you re-import: re-add the marker on the
plugin-barman-cloud image line. The ImagePolicy itself
(cnpg-barman-plugin.yaml in this directory) is repo-native and
unaffected.
Important
- Strategy is direct push to main, not a PR branch. Bad upstream
releases land in main immediately. Mitigation: ImagePolicy ranges
must be tight (
semver: range: "2.x"not"*") so a major bump requires manual policy widening. extractmust produce a total order over the tagspatternmatches. Any part of the tag that varies has to be either in the extracted key or excluded by the filter. A component that is matched but not extracted makes two different tags share one key, and the winner is then an arbitrary tie-break that can flap and downgrade a running app with no human in the loop. This is not hypothetical: it auto-committed a lidarr downgrade (ls42 → ls11) to master inb858b4aaand rolled the pod. Seelidarr.yamlfor the two-field case (synthetic semver key) andtika.yaml/protonmail-bridge.yamlfor the narrow-the-filter case.- Image findings still surface in the security-scan agent's report (Skipped / "Flux image-automation owns this") so they remain visible even when an image isn't yet enrolled in automation here.
What watches this
Worth stating explicitly, because for a while nothing did and each
plausible reporter deferred to another: the update-audit agent skips
$imagepolicy-marked images (ci/agents/update-audit/prompt.md), the
security scan records them as "Flux image-automation owns this" (above),
and both are correct to — an enrolled image is supposed to be this
directory's problem.
What exists today:
flux-notifications/alerts.yamlemails error-severity events from every image kind. Immediate, but edge-triggered: one mail at the moment of transition, then silence. Nothing says "still broken twelve hours later" and nothing answers "is anything not-Ready right now".apps/monitoring-rules/flux-podmonitor.yamlscrapes the controllers, so reconcile latency and controller-level error counts are queryable. Readiness is not on that endpoint: at the controller versions pinned inclusters/homelab/flux-system/gotk-components.yaml,gotk_reconcile_conditionandgotk_suspend_statusare declared by the shared runtime library and recorded by nothing, so they export no series however the scrape is written.apps/monitoring/helmrelease-prometheus.yamltherefore turns on kube-state-metricscustomResourceStateover eight Flux kinds, which is where per-object readiness now comes from:gotk_resource_infowithreadyandsuspendedlabels. For ImagePolicy it also carries the resolvedtag, and each ImageRepository getsgotk_image_repository_tag_count.apps/monitoring-rules/prometheusrule-flux.yamlbuilds the standing alerts on those series.FluxImageAutomationNotReadyfires after an ImageRepository, ImagePolicy or ImageUpdateAutomation has been not-Ready for 2h (two scan intervals for the 31 of 34 repositories atinterval: 1h);FluxResourceNotReadycovers every other Flux kind at 1h;FluxImagePolicyMetricsMissingis the dead man's switch that says so if the metrics themselves stop. This is the "still broken two hours later" signal the edge-triggered mail above cannot give.
What still does not exist: an alert for a policy that is perfectly
Ready while its own filter has frozen it — tika if upstream ships a
3.x. The series for it are now recorded
(flux:image_policy_tag_frozen:14d and the two rules under it), but no
threshold is attached yet, because none is derivable from the history
available today. Measured 2026-09-26 against the 191 automated bump
commits and the upstream registries: a flat duration cannot work (gatus
went 127 days between bumps while healthy), a 10% tag-count growth
comparator fires on three variant-churning repositories and misses
tika at 3.0%, relaxing it to one new tag adds velero-plugin-for-aws,
whose pin is deliberate, and .status.lastScanResult.latestTags is
sorted alphabetically rather than newest-first so it cannot substitute.
The recording rules exist to accumulate the per-repository tagCount
history that a defensible threshold has to be derived from; the alert is
tracked separately.