Which Podman test goes on which runner, and which ones cannot run on ordinary hosted CI at all. Inventory and recommendation only — no workflow is added here, and the proposed implementation is split into one small issue per lane at the end.
Everything except SELinux fits on GitHub-hosted runners, on both
architectures. That is a stronger result than #4188 assumed, and it was
measured rather than inferred: a probe workflow ran on ubuntu-latest and
ubuntu-24.04-arm and the two came back capability-identical.
| Required coverage | Lane | Runner |
|---|---|---|
| Rootless startup, exit-77, egress gate | hosted | ubuntu-latest |
| Rootful startup and egress gate | hosted | ubuntu-latest |
| Compose-provider selection | hosted | ubuntu-latest |
| Quadlet generator syntax gate | hosted, after installing the generator | ubuntu-latest |
Image build/pull + service startup, arm64 |
hosted | ubuntu-24.04-arm |
| Preflight checks (#4207–#4209) | hosted | ubuntu-latest |
| SELinux-enforcing mounts, labels, secrets | not hosted | self-hosted or release-qualification VM |
Only the last row needs infrastructure the project does not already have.
A probe workflow ran both runners and printed what Podman actually has to work with. Identical unless noted.
ubuntu-latest (amd64) |
ubuntu-24.04-arm (arm64) |
|
|---|---|---|
| Image | ubuntu-24.04 20260816.277.1, kernel 6.17.0-1022-azure |
same, aarch64 |
| Podman | 5.8.4 preinstalled at /usr/local/bin/podman |
same |
| Buildah / Skopeo | 1.33.7 / 1.13.3 | same |
| cgroups | v2 | v2 |
| Rootless stack | netavark + pasta, crun, overlay | same |
/etc/subuid, /etc/subgid |
runner:165536:65536 |
same |
max_user_namespaces |
63786 | 63642 |
ip_unprivileged_port_start |
1024 | 1024 |
Passwordless sudo |
yes | yes |
| SELinux | absent — no getenforce, no selinuxfs |
absent |
| AppArmor | enabled | enabled |
| Quadlet generator | absent | absent |
User manager DefaultTimeoutStartUSec |
90s | 90s |
Linger for the runner user |
yes |
yes |
Behavioral probes, both architectures:
rootless run rootless-run-OK
rootless + --cap-add NET_ADMIN, iptables nat NAT_CHAIN_OK
rootful (sudo podman run) rootful-run-OK
nested user namespaces inside a container 2147483647
Four of those matter more than the rest.
NAT_CHAIN_OK under --cap-add NET_ADMIN. Creating a nat chain in the
container's own network namespace is the exact prerequisite for Hive's
forced-proxy egress redirect. It works on hosted runners, on both
architectures, which means the enforcing rootless lane — the one #4188 treated
as the blocking design gate — is testable in ordinary CI. This reproduces the
local result recorded in the rootless startup spike
(src/docs/podman-rootless-startup-spike.md, #4199 / PR #4280, not yet
merged).
Passwordless sudo and a working sudo podman run. The rootful lane needs
no self-hosted machine either.
Subordinate IDs are present and are exactly 65536, the minimum Podman expects, so the subordinate-ID preflight (#4208) has something real to pass against rather than being skipped.
The Quadlet generator is absent even though Podman is present. The runner
image ships a hand-installed /usr/local/bin/podman rather than the
distribution package, so /usr/libexec/podman/quadlet does not exist. A
Quadlet syntax gate that does not install the generator first will silently
test nothing. See the lane below.
Two smaller cross-checks worth keeping: the user manager's
DefaultTimeoutStartUSec is 90s here and 45s on the Fedora host used for
the Quadlet spike, which confirms that
the startup-timeout default is host-specific and must be set explicitly rather
than assumed. And nested user namespaces are effectively unlimited, so a
rootless-Podman-inside-a-container arrangement is not blocked if a lane ever
needs one.
Built (#4334):
.github/workflows/podman-rootless-lane.ymlruns the three-case matrix per PR onubuntu-latest. It asserts the discovered Podman version floor, rootless mode, and cgroup version before probing, reports the exact version against the one recorded below, and does not swallow the probe's exit status. What it does not do is build the PR's own image — it probes a published one, so an unpublished change tosrc/deploy/entrypoint.shis not yet covered.
Everything in the rootless matrix runs here: default startup and exit 77,
--cap-add NET_ADMIN with the redirect installed, deliberate advisory mode,
bypass resistance, the preflight checks, and Compose-provider selection.
The probe script proposed in #4199 (PR #4280,
src/deploy/probe_podman_rootless_netadmin.sh) already runs this matrix and
exits non-zero when a case stops matching, so once it lands the lane is mostly
a workflow wrapper around a script that exists. If it does not land, this lane
grows that script itself.
Podman is preinstalled, so the lane needs no setup step — but it should assert the version it found rather than trusting it, because a runner-image change is silent and would otherwise turn a real regression into a skipped test.
Built (#4335):
.github/workflows/podman-rootful-lane.ymlruns the #4200 probe per PR onubuntu-latestundersudo -n. It asserts passwordless sudo, the Podman version floor, androotless=falsebefore probing — so the job cannot pass by quietly running rootless — and it does not swallow the probe's exit status. It passes no--store, leaving the probe's throwaway graphroot/runroot in place, and a post-run step checks that the runner's own rootful store came back unchanged. Like lane 1 it probes a published image, not the PR's own.
sudo -n works and sudo podman run works. The rootful egress-gate baseline
(#4200) belongs here.
The safety boundary matters more than the mechanics: a rootful lane runs
containers as real root on the runner VM. That is acceptable only because
hosted runners are ephemeral and single-use. It must never be combined with
pull_request_target, and it must not have access to secrets — see below.
Built (#4336):
.github/workflows/podman-arm64-lane.ymlrunssrc/deploy/probe_arm64_image_startup.shper PR onubuntu-24.04-arm. It asserts the runner really is arm64, prints the runtime and network backend for comparison against the amd64 lanes, then checks four things and no more: the tag advertises alinux/arm64image, that image pulls and really is arm64,/usr/local/bin/hiveexecutes, and the container answersGET /api/health. A missing arm64 manifest fails the job rather than skipping, and the lane does not build an image ad hoc — the arm64 build is already a per-push gate indocker.yml.
ubuntu-24.04-arm is already used by .github/workflows/docker.yml for the
multi-architecture image build, so the runner label is proven in this
repository. The probe shows it is capability-identical to amd64 for Podman.
#4188 asks for image build/pull plus service startup on arm64, not the whole
matrix. Keep it that way: a full duplicate matrix doubles cost for coverage
that the identical-capability result says will not diverge. If the two ever do
diverge, that is itself the signal to widen the lane.
Built (#4337): not a workflow, on purpose — see below. The release qualification documents a repeatable procedure and ships
src/deploy/qualify_podman_selinux.sh, run per release on an enforcing Fedora/CentOS Stream-class host. It covers the:z/:Zmount forms, MCS label behaviour, and secret access, and gives #4209's preflight its first coverage on a host where SELinux enforces anything. The script refuses to run outside Enforcing and records the release as UNEXECUTED rather than reporting a pass from a permissive host. The first row is recorded in the ledger there.
Hosted runners have no SELinux at all: no getenforce, no /sys/fs/selinux,
AppArmor instead. There is no way to make an Ubuntu hosted runner enforce
SELinux, so :z/:Z relabeling, mount labels, and secret access under
enforcement cannot be covered by hosted CI. This is exactly the case #4188
anticipated when it allowed "a documented, repeatable release-qualification
job" as an alternative to CI.
Two options, in order of preference:
- Release-qualification VM. A documented, repeatable job on a Fedora/CentOS Stream/RHEL image, run before a release rather than per PR. Cheapest, no persistent infrastructure, no untrusted-code exposure. The result is recorded against the release.
- Ephemeral self-hosted runner. Per-PR coverage, at the cost of running a machine. Only worth it if SELinux regressions turn out to be frequent.
Do not reach for a third option that looks tempting and is not equivalent: running a Fedora container on an Ubuntu runner does not give you SELinux enforcement, because the host kernel is what enforces, and that kernel has no SELinux at all.
Built (#4338):
.github/workflows/quadlet-gate.ymlrunssrc/deploy/test_quadlet_generator_gate.shper PR onubuntu-latest. It takes the generator from the official Podman image pinned by digest rather than fromapt— Ubuntu 24.04 packages podman 4.9.3, below the 5.0.0 that ADR-0017 derives as its binding constraint — verifies the binary is present before running anything, and dry-runs both rootful and--user. It fails on any generator diagnostic rather than on the exit status alone, because the generator warns about a short-name image while still exiting 0. No Quadlet units exist yet, so what it gates today is the generator's presence and dry-run capability; known-bad fixtures keep that from being a vacuous pass.
Small and hosted, but it needs its own issue precisely because of the
generator-absent finding. The lane must install the generator (the distribution
podman package, or the generator binary alone) and then verify it is
present before running quadlet --dryrun, so an environment change cannot
turn the gate into a no-op.
What it should gate is recorded in
the Quadlet spike: --dryrun catches
unsupported keys, dangling unit references, and missing images, but not
Notify=healthy without a healthcheck, PublishPort= on a pod member, or a
short-name image. Those three need Hive's own assertions on top.
.github/workflows/podman-contract.yml triggers only on push and
pull_request against branch v2. Mainline development is on v4, so
this check has effectively stopped running. It is a static grep gate over the
Justfile, so nothing failed loudly — it just went quiet.
That is a one-line fix and it should not be bundled into a new lane.
Applies to every lane above.
permissions: contents: read. No lane needs more. The current Podman workflow already does this and the new ones should copy it.- No secrets, and therefore no
pull_request_target. Fork PRs get no secrets, so any lane that needs a secret cannot cover contributor PRs — which is most of the value. Live tests must work against a locally built image or a public pull.pull_request_targetruns untrusted code with repository credentials and must not appear in any of these lanes, least of all the rootful one. - Never mount an engine socket into any test container. #4188 forbids it for deployment; it is equally wrong in CI, where the socket is the runner's.
- Cleanup uses the ownership contract, not pruning. Hosted runners are
ephemeral so cleanup barely matters there, but a self-hosted SELinux runner
is persistent and shares one store across jobs. A store-wide prune (
podman systemwith its prune subcommand) on that machine destroys concurrent jobs. Use the label selectors in the cleanup contract. - Isolated storage for anything that pulls. The #4199 probe builds a throwaway store with a private graphroot and runroot; a self-hosted lane should do the same so parallel jobs cannot collide.
- A self-hosted runner must be ephemeral and must not run fork PRs.
Untrusted code plus a persistent machine plus
sudois the standard self-hosted-runner compromise. Restrict it toworkflow_dispatchand pushes to protected branches. - Assert the environment, do not assume it. Every lane should print and check the Podman version, root mode, and — where relevant — SELinux state, so a runner-image change shows up as a failure rather than as silently reduced coverage.
Deliberately not opened here; a maintainer can file them as-is. Each is
Part of #4188 and none should close it.
CI: rootless Podman lane on hosted amd64.Done (#4334) —.github/workflows/podman-rootless-lane.yml. Wrapped the #4199 probe script in a workflow onubuntu-latest, asserting the discovered Podman version and root mode.- CI: rootful Podman lane on hosted amd64.
sudo podman, the #4200 baseline. AC: no secrets, nopull_request_target, rootful mode asserted. - CI:
arm64build/pull and startup lane.ubuntu-24.04-arm, scoped to image build/pull plus service startup. AC: no duplicate full matrix. - Release qualification: SELinux-enforcing Podman. A documented,
repeatable Fedora/CentOS Stream job covering
:z/:Zmounts, labels, and secret access. AC: reproducible from the doc; result recorded per release; explicitly not a hosted lane, with the reason. - CI: Quadlet generator syntax gate. Install the generator, verify it is
present, then
quadlet --dryrunfor--userand rootful. AC: the job fails if the generator is missing rather than passing vacuously. - Fix
podman-contract.ymlbranch triggers. It targetsv2and no longer runs. AC: triggers on the current mainline branch.
The capability numbers above came from a throwaway workflow on a fork, run once
on each runner label and then deleted. It printed OS and kernel, the presence
and version of podman/buildah/skopeo, cgroup version, SELinux and
AppArmor state, max_user_namespaces, subordinate IDs,
ip_unprivileged_port_start, whether sudo -n works, podman info for both
root modes, a rootless run, a rootless --cap-add NET_ADMIN run creating a nat
chain, a rootful run, and whether the Quadlet generator and user-systemd
lingering are available.
Re-run it when the runner image changes materially; the numbers in this
document are a snapshot of image 20260816.277.1, not a contract.