Skip to content

fix(deploy): pin Cilium 1.20.2, restore airgap images, and harden install.sh - #674

Merged
xiaods merged 5 commits into
mainfrom
fix/cilium-1.20.2-and-install-hardening
Oct 24, 2026
Merged

xiaods merged 5 commits into
mainfrom
fix/cilium-1.20.2-and-install-hardening

Conversation

@xiaods

@xiaods xiaods commented Oct 24, 2026

Copy link
Copy Markdown
Owner

Four independent fixes found while bringing a fresh OrbStack VM (Ubuntu 26.04,
kernel 7.2.12) online with the revised contrib/install.sh. Each commit is
self-contained; the Cilium one is the only one that changes runtime behaviour
of the shipped product.

1. Cilium 1.20.0/1.20.1 cannot boot on kernel v7.2

A newly installed cluster came up with the node stuck NotReady and
NetworkPluginNotReady. The agent died at startup:

failed to probe helper ... R1 is not a scalar
progType=CGroupSock helper=FnSetRetval

Not an OrbStack problem — it is a Cilium bug exposed by a kernel check.
Kernel v7.2-rc1 added validation that bpf_set_retval's R1 must be a scalar
(torvalds/linux b1f7f67b74c2), while Cilium's helper probe emits a bare call
at the head of the program where R1 still points at the program context.
Reproduced the kernel constraint directly with a raw bpf(2) probe on the
host: R1 of 0/-1 is accepted, any other value returns EINVAL — which is
where the misleading "not a scalar" message comes from.

Upstream fixed the probe with CO-RE (67c619cb, backported to v1.20 in
cilium/cilium#48213) and shipped
it in 1.20.2. Daniel Borkmann confirms the diagnosis in
cilium/cilium#48016:
"it's a Cilium bug, triggered by additional kernel asserts."

Bumped the pin and verified the resulting cluster: agent 1/1 Running,
datapath links loaded, node Ready, all pods Running, sandbox warm pool
scheduling normally.

Also fixed a silent airgap regression: the three quay.io/cilium/* images
were dropped from hack/airgap/image-list.txt during the v1.35 bump
(a0542305) when the default CNI moved away from Cilium, and were never
restored when Cilium came back (88c7e68e). An airgap install therefore
staged a cluster whose CNI could never start. Added back at 1.20.2, with
cilium-envoy pinned to the tag the 1.20.2 chart references (it is on its own
independent version line — there is no v1.20.2 of it).

Added TestCiliumAirgapCoverage / TestCiliumAirgapVersions so neither can
regress quietly again: the images cannot drop out of the list, and a downgrade
below 1.20.2 fails the suite. Both failure modes were verified by temporarily
pinning 1.20.0 and watching the tests fail.

2. k8e-uninstall.sh left rqlited/tandem running

Reported as: after an install, the next install would not boot —
rqlite data dir is not empty and holds no rqlite state; refusing to rebuild an empty database over it, looping under Restart=always.

k8e-killall.sh only matched containerd-shim. The unit uses
KillMode=process, so systemctl stop reaps just the server; rqlited and
tandem were reparented to init, kept listening on 127.0.0.1:4001/4002, and
survived the rm -rf /var/lib/k8e that followed while holding deleted inodes.
The fresh bootstrap then could not bind the raft listeners and hard-failed.

Fixed by matching helpers by binary name rather than launch path. The old
pattern required the datadir copy (k8e/data/<hash>/bin/…) to be the live one,
but resolveBinary() runs these from $K8E_RQLITED_BINARY, PATH,
/usr/local/bin or the data dir, whichever resolves first — so the old pattern
matched zero processes whenever the resolved copy was not the datadir copy.
Verified against a live cluster: the new pattern matches rqlited, tandem,
containerd and every shim (14 processes) with no false positives.

3. Proxy leaked into k8e.service.env with no exemption

create_env_file forwarded HTTP(S)_PROXY but only copied an existing
NO_PROXY. OrbStack's guest environment injects http_proxy/https_proxy/
all_proxy with no no_proxy, so every OrbStack install proxied
cluster-local traffic with no exemption and kubectl logs/exec failed:

proxyconnect tcp: proxy error from 127.0.0.1:6443 while dialing
host.orb.internal:7897, code 502: 502 Bad Gateway

Now always appends the loopback and in-cluster ranges, merging with any value
the caller already set. The proxy is still forwarded, so egress and image pulls
keep working. Verified both the "no NO_PROXY" and "caller already set NO_PROXY"
cases.

4. install.sh zstd probe accepted a tarball it could not extract

Reported as tar (child): zstd: Cannot exec: No such file or directory. The
gVisor tarball was chosen with tar --zstd --help, which exits 0 whenever tar
knows the flag — even when tar merely execs a zstd binary that is not
installed. On any host with libzstd1 but no zstd CLI (stock Ubuntu 26.04
included) the probe passed and extraction then failed.

Replaced with a real round-trip probe. Verified on a host with zstd absent:
old probe exits 0 and picks gvisor.tar.zstd (reproducing the failure), new
probe exits non-zero and falls through to gvisor.tar.bz2. All three branches
checked against the real upstream release tarball.

Testing

  • go test ./pkg/deploy/... — pass, including the two new Cilium tests
  • sh -n contrib/install.sh — pass
  • go vet ./pkg/deploy/ ./pkg/version/ — clean
  • Generated k8e-killall.sh body extracted from the heredoc and run against a
    live cluster to confirm PID selection
  • Final cluster state on the target VM: node Ready, cilium agent/operator/
    envoy 1/1, cert-manager, coredns, metrics-server, local-path-provisioner
    all Running, sandbox warm pool scheduling

install.sh picked the gVisor tarball using `tar --zstd --help`, which exits
0 whenever tar knows the flag — even when tar merely execs an external
`zstd` it cannot find. On a host with libzstd1 installed but no zstd CLI
(a stock Ubuntu 26.04 image, among others) the probe passed and the
extract then died:

  tar (child): zstd: Cannot exec: No such file or directory
  tar: Child returned status 2

tar(1) accepts --zstd whenever it was built with zstd support, but on most
distros it shells out to the zstd binary rather than linking libzstd, so
the flag being accepted says nothing about zstd being available.

Replace both probes with a real (empty) round-trip. Verified on a host
with zstd absent: the old probe exited 0 and chose gvisor.tar.zstd,
reproducing the exact failure above, while the new probe exits non-zero
and falls through to gvisor.tar.bz2. Also verified the three branches
against the real upstream release tarball (checksum OK, runsc
release-20261019.0 extracted).
create_env_file forwarded HTTP(S)_PROXY but only copied an existing
NO_PROXY, so a shell that exported a proxy without a NO_PROXY left the
service proxying everything with no exemption.

Go resolves NO_PROXY on every outbound request, so the apiserver dialled
its own kubelet through the proxy and every kubectl logs/exec failed:

  proxyconnect tcp: proxy error from 127.0.0.1:6443 while dialing
  host.orb.internal:7897, code 502: 502 Bad Gateway

OrbStack's guest environment injects http_proxy/https_proxy/all_proxy with
no no_proxy, so this reproduces on every OrbStack install.

Always append the loopback and in-cluster ranges, merging with any value
the caller already set so existing entries are preserved. The proxy is
still forwarded, so image pulls and other egress keep working.
k8e-killall.sh only matched containerd-shim, so k8e-uninstall.sh left the
Tandem datastore pair running. The unit uses KillMode=process, so
`systemctl stop` reaps just the server; rqlited and tandem were reparented
to init, kept listening on 127.0.0.1:4001/4002, and survived the
`rm -rf /var/lib/k8e` that followed, holding deleted inodes.

The next install then bootstraps a fresh data dir while the stale rqlited
still owns the raft listeners, and bootstrapping fails hard:

  start managed database: rqlite data dir is not empty and holds no rqlite
  state; refusing to rebuild an empty database over it

which then loops under Restart=always with the same fatal.

Match the helpers by binary name instead of launch path. The old pattern
required the datadir copy (`k8e/data/<hash>/bin/...`) to be the live one,
but resolveBinary() runs these from $K8E_RQLITED_BINARY, PATH,
/usr/local/bin or the data dir, whichever resolves first. Verified against
a live cluster: the name-based pattern matches rqlited, tandem, containerd
and every containerd-shim (14 processes) with no false positives, while
the old one matched zero whenever the binaries had been resolved out of
/usr/local/bin.
Cilium 1.20.0/1.20.1 cannot start on kernel v7.2, and the airgap image
list had lost every cilium image outright.

Kernel v7.2-rc1 added validation that bpf_set_retval's R1 must be a scalar
(torvalds/linux b1f7f67b74c2). Cilium's helper probe emits a bare call at
the head of the program, where R1 still points at the program context, so
1.20.0/1.20.1 die at startup on any 7.2 kernel — OrbStack included:

  failed to probe helper ... R1 is not a scalar
  progType=CGroupSock helper=FnSetRetval

Upstream fixed the probe with CO-RE (67c619cb, backported to v1.20 in
cilium/cilium#48213) and shipped it in 1.20.2. See cilium/cilium#48016,
where Daniel Borkmann confirms "it's a Cilium bug, triggered by additional
kernel asserts".

Separately, the three quay.io/cilium/* images were dropped from
hack/airgap/image-list.txt during the v1.35 bump (a054230) when the default
CNI moved away from Cilium, and were never restored after Cilium came back
(88c7e68). An airgap install therefore staged a cluster whose CNI could
never start. Add them back at 1.20.2 — cilium-envoy, which is on its own
independent version line (there is no v1.20.2 of it), is pinned to the tag
the 1.20.2 chart references.

Add TestCiliumAirgapCoverage/TestCiliumAirgapVersions so the images cannot
silently drop out again, and so a downgrade below 1.20.2 fails the suite.

Also bump the two remaining pins that still said 1.20.0: VERSION_CILIUM_CHART
in hack/version.sh and the Gateway API CRD header comment.
`zig build` clones the CNI plugins into .cni-build/ and `make clean` removes
it, but it was never ignored, so 30 MB of vendored source showed up as
untracked noise in every git status.

Tracked for symmetry with the other build outputs already listed here
(.zig-cache, zig-out, bin, dist, build).
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

1 043 tests  +2   1 016 ✅ +2   6m 58s ⏱️ -6s
  121 suites ±0      27 💤 ±0 
    1 files   ±0       0 ❌ ±0 

Results for commit 5a64e21. ± Comparison against base commit 9d6e087.

@xiaods
xiaods merged commit bc95325 into main Oct 24, 2026
9 checks passed
@xiaods
xiaods deleted the fix/cilium-1.20.2-and-install-hardening branch October 27, 2026 06:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant