Supersedes #446, which removed the apk add but left the two real problems in place. Closing that in favour of this.
The actual problems
The installer fetched the containernetworking/plugins release tarball from GitHub on the node and extracted it into /opt/cni/bin with no integrity verification of any kind — wget, then tar. That directory is executed as root in the CNI path on every node, so a tampered or MITM'd artifact would have obtained privileged execution cluster-wide with nothing to detect it.
Secondly it put github.com on the critical path of every node rebuild.
Binaries are baked into ci/images/cni-plugins, where the download happens once and the sha256 is verified. The installer becomes a local file copy with zero network calls — verified: no wget/curl/apk/https in the rendered script.
The pinned digest was cross-checked against upstream's own published .sha256 asset rather than merely recorded from my download — a download can be tampered with, so comparing against what upstream states is the entire point:
The gate demonstrably gates. With a deliberately wrong digest, sha256sum -c FAILS and the extract step is never reached. And in the real build (pipeline 3091):
/tmp/cni-plugins-linux-amd64-v1.5.1.tgz: OK
INFO Pushing image to gitea.renner.uno/gitea_admin/cni-plugins:2026-07-30
Install script changes beyond dropping the fetch
Copies unconditionally, replacing the "already installed?" guard. That guard existed to avoid re-downloading; a local copy is cheap, so copying every time means a node always matches the version pinned in the manifest instead of keeping whatever it first installed.
Temp name + rename(2) rather than cp over the target. cp onto a plugin the kubelet is currently executing can fail with ETXTBSY; rename never does and is atomic, so a concurrent CNI invocation sees the old or the new binary, never a partial one.
LICENSE/README.md no longer land in /opt/cni/bin.
chown 0:0 kept, but re-framed as a repair
Its original cause is gone: extraction applied the archive's ./ uid/gid (1001/127, the github-actions runner) to the destination, and cp into an existing directory doesn't touch its ownership. It still runs because nodes provisioned by the old installer may already carry the wrong ownership, which breaks Cilium's apply-sysctl-overwrites init container. Safe to drop once every node has been through this version.
Rendered install script, including the repair path:
pre-seeded owner 1001:127 -> 0:0 after run
fresh install -> host-local/macvlan/static present, macvlan reports v1.5.1, exit 0
re-run -> idempotent, exit 0, 18 plugins, no .tmp files left
Plus woodpecker lint, kustomize build, server-side dry run.
Things to weigh
~80MB image (18 binaries), matching what the old installer already placed on each node. I kept all of them rather than trimming to the few in use, so this change doesn't also remove plugins already present on nodes. Trimming would be a reasonable follow-up, deliberately scoped separately.
Image already exists and pulls (cni-plugins:2026-07-30, verified above), published by this branch's push build — so merging can't strand the DaemonSet.
Benign failure mode if it ever couldn't pull: the init container blocks, no new install happens, and plugins already on nodes are untouched. Unlike the zigbee2mqtt case this isn't immediately service-affecting.
**Supersedes #446**, which removed the `apk add` but left the two real problems in place. Closing that in favour of this.
## The actual problems
The installer fetched the containernetworking/plugins release tarball from GitHub **on the node** and extracted it into `/opt/cni/bin` with **no integrity verification of any kind** — `wget`, then `tar`. That directory is executed as root in the CNI path on every node, so a tampered or MITM'd artifact would have obtained privileged execution cluster-wide with nothing to detect it.
Secondly it put `github.com` on the critical path of every node rebuild.
Swapping `curl`→`wget` (as #446 did) addressed neither.
## Fix
Binaries are baked into `ci/images/cni-plugins`, where the download happens once and the sha256 **is** verified. The installer becomes a local file copy with **zero network calls** — verified: no `wget`/`curl`/`apk`/`https` in the rendered script.
The pinned digest was cross-checked against upstream's own published `.sha256` asset rather than merely recorded from my download — a download can be tampered with, so comparing against what upstream *states* is the entire point:
```
computed: 77baa2f669980a82255ffa2f2717de823992480271ee778aa51a9c60ae89ff9b
published: 77baa2f669980a82255ffa2f2717de823992480271ee778aa51a9c60ae89ff9b
```
**The gate demonstrably gates.** With a deliberately wrong digest, `sha256sum -c` FAILS and the extract step is never reached. And in the real build (pipeline 3091):
```
/tmp/cni-plugins-linux-amd64-v1.5.1.tgz: OK
INFO Pushing image to gitea.renner.uno/gitea_admin/cni-plugins:2026-07-30
```
## Install script changes beyond dropping the fetch
- **Copies unconditionally**, replacing the "already installed?" guard. That guard existed to avoid re-downloading; a local copy is cheap, so copying every time means a node always matches the version pinned in the manifest instead of keeping whatever it first installed.
- **Temp name + `rename(2)`** rather than `cp` over the target. `cp` onto a plugin the kubelet is currently executing can fail with `ETXTBSY`; rename never does and is atomic, so a concurrent CNI invocation sees the old or the new binary, never a partial one.
- `LICENSE`/`README.md` no longer land in `/opt/cni/bin`.
## `chown 0:0` kept, but re-framed as a repair
Its original cause is **gone**: extraction applied the archive's `./` uid/gid (1001/127, the github-actions runner) to the destination, and `cp` into an existing directory doesn't touch its ownership. It still runs because nodes provisioned by the old installer may **already** carry the wrong ownership, which breaks Cilium's `apply-sysctl-overwrites` init container. Safe to drop once every node has been through this version.
## Verification
Published image:
```
count=18 license/readme present? 0 macvlan: CNI macvlan plugin v1.5.1 size=79.6M
```
Rendered install script, including the repair path:
```
pre-seeded owner 1001:127 -> 0:0 after run
fresh install -> host-local/macvlan/static present, macvlan reports v1.5.1, exit 0
re-run -> idempotent, exit 0, 18 plugins, no .tmp files left
```
Plus `woodpecker lint`, `kustomize build`, server-side dry run.
## Things to weigh
- **~80MB image** (18 binaries), matching what the old installer already placed on each node. I kept all of them rather than trimming to the few in use, so this change doesn't also remove plugins already present on nodes. Trimming would be a reasonable follow-up, deliberately scoped separately.
- **Image already exists and pulls** (`cni-plugins:2026-07-30`, verified above), published by this branch's push build — so merging can't strand the DaemonSet.
- **Benign failure mode** if it ever couldn't pull: the init container blocks, no new install happens, and plugins already on nodes are untouched. Unlike the zigbee2mqtt case this isn't immediately service-affecting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Supersedes the wget-instead-of-curl approach in #446, which removed the
`apk add` but left the real problems in place.
The installer fetched the containernetworking/plugins release tarball from
GitHub on the node and extracted it into /opt/cni/bin with NO integrity
verification of any kind. That directory is executed as root in the CNI
path on every node, so a tampered or MITM'd artifact would have obtained
privileged execution cluster-wide with nothing to detect it. It also put
github.com on the critical path of every node rebuild.
Now the binaries are baked into ci/images/cni-plugins, where the download
happens once and the sha256 IS verified, and the installer is a local file
copy with zero network calls (verified: no wget/curl/apk/https in the
rendered script).
The pinned digest was cross-checked against upstream's own published
cni-plugins-linux-amd64-v1.5.1.tgz.sha256 asset, not merely recorded from a
local download — a download can be tampered with, so comparing against
what upstream states is the point:
77baa2f669980a82255ffa2f2717de823992480271ee778aa51a9c60ae89ff9b
Verified the gate actually gates: with the correct digest the layer logic
downloads, verifies, extracts and asserts macvlan/static/host-local are
executable; with a deliberately wrong digest `sha256sum -c` FAILS and the
extract step is never reached.
Install script changes beyond dropping the fetch:
- Copies unconditionally instead of the old "already installed?" guard.
The guard existed to avoid re-downloading; a local copy is cheap, so
copying every time means a node always matches the version pinned in the
manifest rather than keeping whatever it first installed.
- Copies to a temp name in the same directory then rename(2)s over the
target. `cp` onto a plugin the kubelet is currently executing can fail
with ETXTBSY; rename never does and is atomic, so a concurrent CNI
invocation sees either the old or the new binary, never a partial one.
- LICENSE and README.md are no longer installed into /opt/cni/bin; the old
tar extraction put them there.
The chown 0:0 is kept but re-framed as a REPAIR. Its original cause is
gone — extraction applied the archive's `./` uid/gid (1001/127, the
github-actions runner) to the destination, and `cp` into an existing
directory does not touch its ownership. It still runs because nodes
provisioned by the old installer may already carry the wrong ownership,
which breaks Cilium's apply-sysctl-overwrites init container.
Verified end-to-end with the rendered script, including that repair path:
pre-seeded owner 1001:127 -> 0:0 after run
fresh install -> host-local/macvlan/static present, macvlan executes
and reports "CNI macvlan plugin v1.5.1", exit 0
re-run -> idempotent, exit 0, 18 plugins, no .tmp files left
Note the image carries ~80MB of extracted plugins (18 binaries), matching
what the old installer put on each node. All are kept rather than trimming
to the few in use, to avoid removing plugins already present on nodes as a
side effect of this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🤖Automated review — model CLI default (complexity: complex), commit 8ac30b0ef767d68e14c22b78247086d6f4752fb5.
Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.
Summary
This PR replaces the multus cni-plugins-installer DaemonSet's node-side, unverified wget+tar fetch of the containernetworking/plugins release tarball with binaries baked into a new ci/images/cni-plugins image, whose build verifies the sha256 checksum. The installer becomes a checksum-verified-at-build-time local file copy with an atomic temp-name-then-rename pattern, and the chown 0:0 is correctly re-scoped from "fix extraction fallout" to "repair pre-existing nodes." The change is well-reasoned, matches the repo's existing ci/images/* conventions (pinned base digest, sha256sum -c gate, dated immutable tags), and the PR body documents concrete verification (build log, rendered-script dry run, deliberate-bad-digest failure test). I found no blocking issues.
🟡 Suggestions
ci/images/cni-plugins/Containerfile:52-54 — the build-time sanity check only verifies macvlan, static, and host-local exist and are executable. But kubernetes/infrastructure/multus/cni-dhcp-daemon.yaml:39-41 documents that the dhcp plugin binary is also a real runtime dependency, shipped from this same installer DaemonSet ("whole tarball extracted... keep the version pins in step"). If upstream ever restructures the archive such that dhcp is dropped while the three checked plugins remain, this build would pass silently and the dhcp DaemonSet would start failing only later, on the affected node. Since the whole /plugins directory is still copied through regardless of what's checked, this doesn't block anything today — but adding dhcp to the check list would make the "fail loudly if the archive changes shape" guarantee actually cover every binary this repo depends on, not just three of them.
🟢 Nits
ci/images/cni-plugins/Containerfile:56 reuses the exact same alpine:3.21@sha256:... digest as the fetch stage, consistent with ci/images/alpine-jq/Containerfile and ci/images/tofu/Containerfile. No action needed, just noting it's intentionally consistent rather than an oversight.
The PR body's claim "no wget/curl/apk/https in the rendered script" is accurate for the DaemonSet's command script (kubernetes/infrastructure/multus/cni-plugins-installer.yaml), though the final image itself (being Alpine-based, not scratch) still contains busybox wget. That's a reasonable tradeoff for keeping a shell available for cp/mv/chown, and doesn't contradict the claim, which is specifically about the installer's own commands.
Tests
No automated tests are added or applicable here (this is IaC: a Dockerfile and a DaemonSet manifest, matching the pattern of every other ci/images/* entry in this repo, none of which carry unit tests). The PR body documents substantial manual verification instead: a real kaniko build with the checksum gate observed to both pass on the correct digest and fail on a deliberately wrong one, a rendered install-script dry run covering fresh install / re-run idempotency / the ownership-repair path, and kustomize build / woodpecker lint / server-side dry-run checks. That's proportionate for this kind of change and no more is needed for this PR to be safely merged.
Recommendation
Looks reasonable — no blocking issues; comments above are optional.
🤖 **Automated review** — model `CLI default` (complexity: complex), commit `8ac30b0ef767d68e14c22b78247086d6f4752fb5`.
> Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review.
---
## Summary
This PR replaces the multus `cni-plugins-installer` DaemonSet's node-side, unverified `wget`+`tar` fetch of the `containernetworking/plugins` release tarball with binaries baked into a new `ci/images/cni-plugins` image, whose build verifies the sha256 checksum. The installer becomes a checksum-verified-at-build-time local file copy with an atomic temp-name-then-rename pattern, and the `chown 0:0` is correctly re-scoped from "fix extraction fallout" to "repair pre-existing nodes." The change is well-reasoned, matches the repo's existing `ci/images/*` conventions (pinned base digest, `sha256sum -c` gate, dated immutable tags), and the PR body documents concrete verification (build log, rendered-script dry run, deliberate-bad-digest failure test). I found no blocking issues.
## 🟡 Suggestions
- `ci/images/cni-plugins/Containerfile:52-54` — the build-time sanity check only verifies `macvlan`, `static`, and `host-local` exist and are executable. But `kubernetes/infrastructure/multus/cni-dhcp-daemon.yaml:39-41` documents that the **`dhcp`** plugin binary is also a real runtime dependency, shipped from this same installer DaemonSet ("whole tarball extracted... keep the version pins in step"). If upstream ever restructures the archive such that `dhcp` is dropped while the three checked plugins remain, this build would pass silently and the dhcp DaemonSet would start failing only later, on the affected node. Since the whole `/plugins` directory is still copied through regardless of what's checked, this doesn't block anything today — but adding `dhcp` to the check list would make the "fail loudly if the archive changes shape" guarantee actually cover every binary this repo depends on, not just three of them.
## 🟢 Nits
- `ci/images/cni-plugins/Containerfile:56` reuses the exact same `alpine:3.21@sha256:...` digest as the `fetch` stage, consistent with `ci/images/alpine-jq/Containerfile` and `ci/images/tofu/Containerfile`. No action needed, just noting it's intentionally consistent rather than an oversight.
- The PR body's claim "no wget/curl/apk/https in the rendered script" is accurate for the DaemonSet's `command` script (`kubernetes/infrastructure/multus/cni-plugins-installer.yaml`), though the final image itself (being Alpine-based, not `scratch`) still *contains* busybox `wget`. That's a reasonable tradeoff for keeping a shell available for `cp`/`mv`/`chown`, and doesn't contradict the claim, which is specifically about the installer's own commands.
## Tests
No automated tests are added or applicable here (this is IaC: a Dockerfile and a DaemonSet manifest, matching the pattern of every other `ci/images/*` entry in this repo, none of which carry unit tests). The PR body documents substantial manual verification instead: a real kaniko build with the checksum gate observed to both pass on the correct digest and fail on a deliberately wrong one, a rendered install-script dry run covering fresh install / re-run idempotency / the ownership-repair path, and `kustomize build` / `woodpecker lint` / server-side dry-run checks. That's proportionate for this kind of change and no more is needed for this PR to be safely merged.
## Recommendation
Looks reasonable — no blocking issues; comments above are optional.
<!-- pr-review:sha=8ac30b0ef767d68e14c22b78247086d6f4752fb5 -->
Review point on #447. The build-time check asserted macvlan, static and
host-local were executable but omitted dhcp, which the sibling
cni-dhcp-daemon DaemonSet runs and which its own comment says ships from
this installer ("the dhcp binary ships from the sibling
cni-plugins-installer DaemonSet ... whole tarball extracted").
Verified that dependency in kubernetes/infrastructure/multus/
cni-dhcp-daemon.yaml before changing anything, rather than taking the
review's word for it.
Consequence of the omission: since the whole /plugins directory is copied
regardless of what the loop checks, nothing is broken today — but the
"fail loudly if upstream restructures the archive" guarantee only covered
three of the four binaries this repo depends on. Upstream dropping dhcp
while keeping the others would have built green and surfaced later as a
broken DHCP daemon on whichever node rolled first.
Confirmed all four are executable in the already-published image, so this
tightens the assertion without changing what ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Good catch, and I verified the dependency in the repo before acting on it rather than taking it on trust — kubernetes/infrastructure/multus/cni-dhcp-daemon.yaml says so explicitly:
The dhcp binary ships from the sibling cni-plugins-installer DaemonSet (containernetworking/plugins v1.5.1, whole tarball extracted). Keep the version pins in step.
So dhcp is a real runtime dependency of this image and was absent from the check. You are right that nothing breaks today, since the whole /plugins directory is copied regardless of what the loop asserts — but that means the "fail loudly if upstream restructures the archive" guarantee covered three of the four binaries this repo actually needs. Upstream dropping dhcp while keeping the others would have built green and surfaced later as a broken DHCP daemon on whichever node rolled first, which is exactly the delayed, node-specific failure the assertion exists to prevent.
Now for p in macvlan static host-local dhcp, with a comment recording why the list is what it is so a future edit does not quietly narrow it again. Confirmed all four are executable in the already-published image, so this tightens the assertion without changing what ships:
The reused base digest is intentional, matching alpine-jq and tofu.
Fair distinction on the "no wget" claim — it is about the installer's own commands, not the image contents. The image is Alpine rather than scratch precisely because the install step needs a shell for cp/mv/chown, so busybox wget is present but unused. Worth being precise about, since "no network" is the security argument here: the guarantee is that the install path makes no network calls, not that the image is incapable of it. If that distinction ever needs to be airtight, a scratch image plus a static busybox would be the way, and that is a bigger change than this PR.
Addressed in `d62c5da5`.
### 🟡 `dhcp` missing from the build assertion
Good catch, and I verified the dependency in the repo before acting on it rather than taking it on trust — `kubernetes/infrastructure/multus/cni-dhcp-daemon.yaml` says so explicitly:
> The dhcp binary ships from the sibling cni-plugins-installer DaemonSet (containernetworking/plugins v1.5.1, whole tarball extracted). Keep the version pins in step.
So `dhcp` is a real runtime dependency of this image and was absent from the check. You are right that nothing breaks today, since the whole `/plugins` directory is copied regardless of what the loop asserts — but that means the "fail loudly if upstream restructures the archive" guarantee covered three of the four binaries this repo actually needs. Upstream dropping `dhcp` while keeping the others would have built green and surfaced later as a broken DHCP daemon on whichever node rolled first, which is exactly the delayed, node-specific failure the assertion exists to prevent.
Now `for p in macvlan static host-local dhcp`, with a comment recording *why* the list is what it is so a future edit does not quietly narrow it again. Confirmed all four are executable in the already-published image, so this tightens the assertion without changing what ships:
```
macvlan executable
static executable
host-local executable
dhcp executable
```
### 🟢 Nits
Both accurate, no action taken:
- The reused base digest is intentional, matching `alpine-jq` and `tofu`.
- Fair distinction on the "no wget" claim — it is about the installer's own commands, not the image contents. The image is Alpine rather than `scratch` precisely because the install step needs a shell for `cp`/`mv`/`chown`, so busybox `wget` is present but unused. Worth being precise about, since "no network" is the security argument here: the guarantee is that the install path makes no network calls, not that the image is incapable of it. If that distinction ever needs to be airtight, a `scratch` image plus a static busybox would be the way, and that is a bigger change than this PR.
🤖Automated review — model CLI default (complexity: complex), commit d62c5da5a0fafc4e3bcc0872e5bac6541cb9339a.
Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.
Summary
This PR replaces the multus cni-plugins-installer DaemonSet's node-side wget/tar fetch of the containernetworking/plugins release tarball (no checksum verification, GitHub on the node-rebuild critical path) with a checksum-verified build-time bake into a new ci/images/cni-plugins image, consumed by the installer as a local file copy. It also switches the install from cp-over-target to a temp-file-plus-rename(2) pattern and drops the "already installed?" skip guard in favor of unconditional copy. I checked the diff against the actual checkout (installer manifest, sibling cni-dhcp-daemon.yaml, the other ci/images/*/Containerfile conventions, and the .woodpecker/build-ci-images.yml pipeline) and did not find any blocking issues.
🟡 Suggestions
kubernetes/infrastructure/multus/cni-plugins-installer.yaml:74 — echo "Installed from $(basename "$SRC") image:" with SRC=/opt/cni-plugins always prints the literal string cni-plugins, not anything image-tag-specific (e.g. the 2026-07-30 tag). It's a harmless log line, not a correctness issue, but if the intent was to record which image version ran, basename on a fixed path can't provide that — consider dropping it or logging the image tag another way if this log is ever used for debugging drift.
The sha256sum -c digest pin (ci/images/cni-plugins/Containerfile:77) is claimed in the PR description to be cross-checked against upstream's published .sha256 asset and validated by a real pipeline run (pipeline 3091, per the PR body). I have no network access in this review environment to independently re-verify the hash against https://github.com/containernetworking/plugins/releases/download/v1.5.1/cni-plugins-linux-amd64-v1.5.1.tgz.sha256, so I can't confirm it myself — but the mechanism is sound (build fails closed on mismatch) and the base-image digest pin (alpine:3.21@sha256:48b03...) matches the one already used in ci/images/alpine-jq/Containerfile, which is a good consistency signal.
kubernetes/infrastructure/multus/cni-plugins-installer.yaml:47 pins the runtime image to gitea.renner.uno/gitea_admin/cni-plugins:2026-07-30, a dated tag matching the ci-ansible/alpine-jq convention used elsewhere in this repo (confirmed against zigbee2mqtt-deployment.yaml:67 and build-ci-images.yml). No imagePullSecrets is set on this DaemonSet, but that matches the existing pattern for other gitea_admin/* images pulled the same way elsewhere in the cluster, so this isn't a regression introduced by this PR.
🟢 Nits
ci/images/cni-plugins/Containerfile:103 — the required-plugin list (macvlan static host-local dhcp) is a reasonable "fail loud if upstream restructures" gate, and correctly includes dhcp (verified: cni-dhcp-daemon.yaml:88 execs /opt/cni/bin/dhcp directly, so a silently-missing dhcp binary would indeed only surface later as a broken DHCP daemon, exactly as the comment says).
Tests
There's no automated test harness for these shell-script installers/Containerfiles in this repo (consistent with the other ci/images/* entries, none of which carry tests). The PR body documents manual verification: a deliberately-wrong-digest build failing closed, a real kaniko build/push (pipeline 3091), and a rendered install-script dry run (fresh install, idempotent re-run, pre-seeded-ownership repair path). That's reasonable coverage for infra scripts of this kind given the project's existing conventions; I could not independently re-run any of it in this read-only review environment (no cluster/registry access).
Recommendation
Looks reasonable — no blocking issues; comments above are optional.
🤖 **Automated review** — model `CLI default` (complexity: complex), commit `d62c5da5a0fafc4e3bcc0872e5bac6541cb9339a`.
> Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review.
---
## Summary
This PR replaces the multus `cni-plugins-installer` DaemonSet's node-side `wget`/`tar` fetch of the containernetworking/plugins release tarball (no checksum verification, GitHub on the node-rebuild critical path) with a checksum-verified build-time bake into a new `ci/images/cni-plugins` image, consumed by the installer as a local file copy. It also switches the install from `cp`-over-target to a temp-file-plus-`rename(2)` pattern and drops the "already installed?" skip guard in favor of unconditional copy. I checked the diff against the actual checkout (installer manifest, sibling `cni-dhcp-daemon.yaml`, the other `ci/images/*/Containerfile` conventions, and the `.woodpecker/build-ci-images.yml` pipeline) and did not find any blocking issues.
## 🟡 Suggestions
- `kubernetes/infrastructure/multus/cni-plugins-installer.yaml:74` — `echo "Installed from $(basename "$SRC") image:"` with `SRC=/opt/cni-plugins` always prints the literal string `cni-plugins`, not anything image-tag-specific (e.g. the `2026-07-30` tag). It's a harmless log line, not a correctness issue, but if the intent was to record which image version ran, `basename` on a fixed path can't provide that — consider dropping it or logging the image tag another way if this log is ever used for debugging drift.
- The `sha256sum -c` digest pin (`ci/images/cni-plugins/Containerfile:77`) is claimed in the PR description to be cross-checked against upstream's published `.sha256` asset and validated by a real pipeline run (pipeline 3091, per the PR body). I have no network access in this review environment to independently re-verify the hash against `https://github.com/containernetworking/plugins/releases/download/v1.5.1/cni-plugins-linux-amd64-v1.5.1.tgz.sha256`, so I can't confirm it myself — but the mechanism is sound (build fails closed on mismatch) and the base-image digest pin (`alpine:3.21@sha256:48b03...`) matches the one already used in `ci/images/alpine-jq/Containerfile`, which is a good consistency signal.
- `kubernetes/infrastructure/multus/cni-plugins-installer.yaml:47` pins the runtime image to `gitea.renner.uno/gitea_admin/cni-plugins:2026-07-30`, a dated tag matching the `ci-ansible`/`alpine-jq` convention used elsewhere in this repo (confirmed against `zigbee2mqtt-deployment.yaml:67` and `build-ci-images.yml`). No `imagePullSecrets` is set on this DaemonSet, but that matches the existing pattern for other `gitea_admin/*` images pulled the same way elsewhere in the cluster, so this isn't a regression introduced by this PR.
## 🟢 Nits
- `ci/images/cni-plugins/Containerfile:103` — the required-plugin list (`macvlan static host-local dhcp`) is a reasonable "fail loud if upstream restructures" gate, and correctly includes `dhcp` (verified: `cni-dhcp-daemon.yaml:88` execs `/opt/cni/bin/dhcp` directly, so a silently-missing `dhcp` binary would indeed only surface later as a broken DHCP daemon, exactly as the comment says).
## Tests
There's no automated test harness for these shell-script installers/Containerfiles in this repo (consistent with the other `ci/images/*` entries, none of which carry tests). The PR body documents manual verification: a deliberately-wrong-digest build failing closed, a real kaniko build/push (pipeline 3091), and a rendered install-script dry run (fresh install, idempotent re-run, pre-seeded-ownership repair path). That's reasonable coverage for infra scripts of this kind given the project's existing conventions; I could not independently re-run any of it in this read-only review environment (no cluster/registry access).
## Recommendation
Looks reasonable — no blocking issues; comments above are optional.
<!-- pr-review:sha=d62c5da5a0fafc4e3bcc0872e5bac6541cb9339a -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Supersedes #446, which removed the
apk addbut left the two real problems in place. Closing that in favour of this.The actual problems
The installer fetched the containernetworking/plugins release tarball from GitHub on the node and extracted it into
/opt/cni/binwith no integrity verification of any kind —wget, thentar. That directory is executed as root in the CNI path on every node, so a tampered or MITM'd artifact would have obtained privileged execution cluster-wide with nothing to detect it.Secondly it put
github.comon the critical path of every node rebuild.Swapping
curl→wget(as #446 did) addressed neither.Fix
Binaries are baked into
ci/images/cni-plugins, where the download happens once and the sha256 is verified. The installer becomes a local file copy with zero network calls — verified: nowget/curl/apk/httpsin the rendered script.The pinned digest was cross-checked against upstream's own published
.sha256asset rather than merely recorded from my download — a download can be tampered with, so comparing against what upstream states is the entire point:The gate demonstrably gates. With a deliberately wrong digest,
sha256sum -cFAILS and the extract step is never reached. And in the real build (pipeline 3091):Install script changes beyond dropping the fetch
rename(2)rather thancpover the target.cponto a plugin the kubelet is currently executing can fail withETXTBSY; rename never does and is atomic, so a concurrent CNI invocation sees the old or the new binary, never a partial one.LICENSE/README.mdno longer land in/opt/cni/bin.chown 0:0kept, but re-framed as a repairIts original cause is gone: extraction applied the archive's
./uid/gid (1001/127, the github-actions runner) to the destination, andcpinto an existing directory doesn't touch its ownership. It still runs because nodes provisioned by the old installer may already carry the wrong ownership, which breaks Cilium'sapply-sysctl-overwritesinit container. Safe to drop once every node has been through this version.Verification
Published image:
Rendered install script, including the repair path:
Plus
woodpecker lint,kustomize build, server-side dry run.Things to weigh
cni-plugins:2026-07-30, verified above), published by this branch's push build — so merging can't strand the DaemonSet.🤖 Generated with Claude Code
🤖 Automated review — model
CLI default(complexity: complex), commit8ac30b0ef767d68e14c22b78247086d6f4752fb5.Summary
This PR replaces the multus
cni-plugins-installerDaemonSet's node-side, unverifiedwget+tarfetch of thecontainernetworking/pluginsrelease tarball with binaries baked into a newci/images/cni-pluginsimage, whose build verifies the sha256 checksum. The installer becomes a checksum-verified-at-build-time local file copy with an atomic temp-name-then-rename pattern, and thechown 0:0is correctly re-scoped from "fix extraction fallout" to "repair pre-existing nodes." The change is well-reasoned, matches the repo's existingci/images/*conventions (pinned base digest,sha256sum -cgate, dated immutable tags), and the PR body documents concrete verification (build log, rendered-script dry run, deliberate-bad-digest failure test). I found no blocking issues.🟡 Suggestions
ci/images/cni-plugins/Containerfile:52-54— the build-time sanity check only verifiesmacvlan,static, andhost-localexist and are executable. Butkubernetes/infrastructure/multus/cni-dhcp-daemon.yaml:39-41documents that thedhcpplugin binary is also a real runtime dependency, shipped from this same installer DaemonSet ("whole tarball extracted... keep the version pins in step"). If upstream ever restructures the archive such thatdhcpis dropped while the three checked plugins remain, this build would pass silently and the dhcp DaemonSet would start failing only later, on the affected node. Since the whole/pluginsdirectory is still copied through regardless of what's checked, this doesn't block anything today — but addingdhcpto the check list would make the "fail loudly if the archive changes shape" guarantee actually cover every binary this repo depends on, not just three of them.🟢 Nits
ci/images/cni-plugins/Containerfile:56reuses the exact samealpine:3.21@sha256:...digest as thefetchstage, consistent withci/images/alpine-jq/Containerfileandci/images/tofu/Containerfile. No action needed, just noting it's intentionally consistent rather than an oversight.commandscript (kubernetes/infrastructure/multus/cni-plugins-installer.yaml), though the final image itself (being Alpine-based, notscratch) still contains busyboxwget. That's a reasonable tradeoff for keeping a shell available forcp/mv/chown, and doesn't contradict the claim, which is specifically about the installer's own commands.Tests
No automated tests are added or applicable here (this is IaC: a Dockerfile and a DaemonSet manifest, matching the pattern of every other
ci/images/*entry in this repo, none of which carry unit tests). The PR body documents substantial manual verification instead: a real kaniko build with the checksum gate observed to both pass on the correct digest and fail on a deliberately wrong one, a rendered install-script dry run covering fresh install / re-run idempotency / the ownership-repair path, andkustomize build/woodpecker lint/ server-side dry-run checks. That's proportionate for this kind of change and no more is needed for this PR to be safely merged.Recommendation
Looks reasonable — no blocking issues; comments above are optional.
Addressed in
d62c5da5.🟡
dhcpmissing from the build assertionGood catch, and I verified the dependency in the repo before acting on it rather than taking it on trust —
kubernetes/infrastructure/multus/cni-dhcp-daemon.yamlsays so explicitly:So
dhcpis a real runtime dependency of this image and was absent from the check. You are right that nothing breaks today, since the whole/pluginsdirectory is copied regardless of what the loop asserts — but that means the "fail loudly if upstream restructures the archive" guarantee covered three of the four binaries this repo actually needs. Upstream droppingdhcpwhile keeping the others would have built green and surfaced later as a broken DHCP daemon on whichever node rolled first, which is exactly the delayed, node-specific failure the assertion exists to prevent.Now
for p in macvlan static host-local dhcp, with a comment recording why the list is what it is so a future edit does not quietly narrow it again. Confirmed all four are executable in the already-published image, so this tightens the assertion without changing what ships:🟢 Nits
Both accurate, no action taken:
alpine-jqandtofu.scratchprecisely because the install step needs a shell forcp/mv/chown, so busyboxwgetis present but unused. Worth being precise about, since "no network" is the security argument here: the guarantee is that the install path makes no network calls, not that the image is incapable of it. If that distinction ever needs to be airtight, ascratchimage plus a static busybox would be the way, and that is a bigger change than this PR.🤖 Automated review — model
CLI default(complexity: complex), commitd62c5da5a0fafc4e3bcc0872e5bac6541cb9339a.Summary
This PR replaces the multus
cni-plugins-installerDaemonSet's node-sidewget/tarfetch of the containernetworking/plugins release tarball (no checksum verification, GitHub on the node-rebuild critical path) with a checksum-verified build-time bake into a newci/images/cni-pluginsimage, consumed by the installer as a local file copy. It also switches the install fromcp-over-target to a temp-file-plus-rename(2)pattern and drops the "already installed?" skip guard in favor of unconditional copy. I checked the diff against the actual checkout (installer manifest, siblingcni-dhcp-daemon.yaml, the otherci/images/*/Containerfileconventions, and the.woodpecker/build-ci-images.ymlpipeline) and did not find any blocking issues.🟡 Suggestions
kubernetes/infrastructure/multus/cni-plugins-installer.yaml:74—echo "Installed from $(basename "$SRC") image:"withSRC=/opt/cni-pluginsalways prints the literal stringcni-plugins, not anything image-tag-specific (e.g. the2026-07-30tag). It's a harmless log line, not a correctness issue, but if the intent was to record which image version ran,basenameon a fixed path can't provide that — consider dropping it or logging the image tag another way if this log is ever used for debugging drift.sha256sum -cdigest pin (ci/images/cni-plugins/Containerfile:77) is claimed in the PR description to be cross-checked against upstream's published.sha256asset and validated by a real pipeline run (pipeline 3091, per the PR body). I have no network access in this review environment to independently re-verify the hash againsthttps://github.com/containernetworking/plugins/releases/download/v1.5.1/cni-plugins-linux-amd64-v1.5.1.tgz.sha256, so I can't confirm it myself — but the mechanism is sound (build fails closed on mismatch) and the base-image digest pin (alpine:3.21@sha256:48b03...) matches the one already used inci/images/alpine-jq/Containerfile, which is a good consistency signal.kubernetes/infrastructure/multus/cni-plugins-installer.yaml:47pins the runtime image togitea.renner.uno/gitea_admin/cni-plugins:2026-07-30, a dated tag matching theci-ansible/alpine-jqconvention used elsewhere in this repo (confirmed againstzigbee2mqtt-deployment.yaml:67andbuild-ci-images.yml). NoimagePullSecretsis set on this DaemonSet, but that matches the existing pattern for othergitea_admin/*images pulled the same way elsewhere in the cluster, so this isn't a regression introduced by this PR.🟢 Nits
ci/images/cni-plugins/Containerfile:103— the required-plugin list (macvlan static host-local dhcp) is a reasonable "fail loud if upstream restructures" gate, and correctly includesdhcp(verified:cni-dhcp-daemon.yaml:88execs/opt/cni/bin/dhcpdirectly, so a silently-missingdhcpbinary would indeed only surface later as a broken DHCP daemon, exactly as the comment says).Tests
There's no automated test harness for these shell-script installers/Containerfiles in this repo (consistent with the other
ci/images/*entries, none of which carry tests). The PR body documents manual verification: a deliberately-wrong-digest build failing closed, a real kaniko build/push (pipeline 3091), and a rendered install-script dry run (fresh install, idempotent re-run, pre-seeded-ownership repair path). That's reasonable coverage for infra scripts of this kind given the project's existing conventions; I could not independently re-run any of it in this read-only review environment (no cluster/registry access).Recommendation
Looks reasonable — no blocking issues; comments above are optional.