Skip to main content

Container builds in Woodpecker CI: new Podman plugin, private agent, and privilege filter

Table of Contents

Three recent releases together cover the container build workflow in Woodpecker CI: a Podman-based build plugin for any Woodpecker agent, a private agent on Hetzner Cloud as a home for pipelines with secrets or large builds, and a filter proxy that decides which plugin images may run privileged on your own agent.

Woodpecker CI Plugin Podman 0.1.0
#

A direct wrapper around podman build, podman push, and podman manifest push --all. The plugin keeps the same interface that developers already know from running podman locally.

Codeberg’s public CI: the plugin runs out of the box. It needs nothing beyond CLONE_NEWUSER, which the default seccomp profile permits. Storage falls back to vfs (no /dev/fuse on the shared agents), and multi-arch builds work as far as the runner hosts ship the necessary binfmt_misc configuration.

Self-hosted agents (Chilly Willy Agent v0.3.0 as the reference setup): overlay plus fuse-overlayfs (much faster for layer-heavy builds), real cross-arch RUN steps for arbitrary manifest list entries, and privilege force-escalation through Agent Guard.

steps:
  - name: publish
    image: head1328/woodpecker-ci-plugin-podman
    settings:
      repo: head1328/myapp
      tags: latest
      platforms: linux/amd64,linux/arm64
      username:
        from_secret: docker_hub_user
      password:
        from_secret: docker_hub_token

A deliberate design choice: the plugin never registers binfmt_misc inside its own container. Since Linux 6.7 binfmt_misc is scoped per user namespace, and in nested rootless Podman qemu handlers cannot be set reliably inside the build sub-runtime. The plugin defers to the host instead: if the host registers qemu handlers with the F (fix-binary) flag, cross-arch RUN steps just work; if not, the build fails cleanly.

Chilly Willy Agent v0.3.0
#

The agent provisions a private Woodpecker agent on Hetzner Cloud and registers it with a Woodpecker instance (e.g. Codeberg’s). A full introduction is in the initial-release post ; 0.3.0 brings three larger changes:

  • Native multi-arch in the host kernel: on first boot a systemd oneshot unit layers qemu-user-static via rpm-ostree install and reboots. On second boot systemd-binfmt registers the qemu interpreters from /usr/lib/binfmt.d/qemu-*-static.conf with the F flag in the host kernel. That flag opens the interpreter file descriptor at registration time and exposes it to every nested rootless user namespace - including Podman’s build sub-runtime.

    [Unit]
    Description=Layer qemu-user-static via rpm-ostree for cross-arch builds
    After=hetzner-ipv6.service network-online.target nss-lookup.target
    Before=zincati.service boot-complete.target
    ConditionPathExists=!/var/lib/cwa-qemu-layered.stamp
    
    [Service]
    Type=oneshot
    RemainAfterExit=yes
    ExecStart=/usr/bin/rpm-ostree install --allow-inactive qemu-user-static
    ExecStart=/usr/bin/touch /var/lib/cwa-qemu-layered.stamp
    ExecStart=/usr/bin/systemctl reboot
  • Two presets: secure (high-security baseline, vfs+chroot, no privileged plugins) and privileged (overlay+FUSE, privilege escalation via allowlist). Both inherit the qemu binfmt handlers from the host kernel and can therefore build multi-arch manifests.

  • Wally Walrus Agent Guard v0.2.0 integration plus a deterministic update window: the agent talks to Podman through the guard socket, and Zincati reboots are bounded to a daily 03:00 to 05:00 UTC window instead of the uncontrolled immediate strategy.

Wally Walrus Agent Guard v0.2.0
#

Agent Guard is a filtering reverse proxy that sits between the Woodpecker agent and the Podman socket. Every POST /containers/create is inspected and matched against the agent operator’s privileged_images allowlist.

With 0.2.0 the passive filter turns into an active policy. Three cases:

  1. Image on the allowlist, server sends Privileged=true: passed through.
  2. Image on the allowlist, server sends Privileged=false: Agent Guard force-escalates to Privileged=true (new in 0.2.0). The agent operator independently decides which plugin images run privileged on this agent - regardless of what the upstream server allowed.
  3. Image not on the allowlist, server sends Privileged=true: HTTP 403, the container-create is rejected.

This matters in the Codeberg world: Codeberg maintains WOODPECKER_PLUGINS_PRIVILEGED as a central list. Which plugins Codeberg lets run privileged is the Codeberg admins’ call, not the repository owner’s. If you want a different plugin (or a different version) to run privileged on your own agent without colliding with the Codeberg list, Agent Guard can force-escalate it on the agent side, scoped strictly to your own allowlist.

How the three play together in the privileged preset
#

The demo pipeline .woodpecker/privileged.yml in the agent repo exercises the setup end-to-end with four podman-plugin steps: each against the rolling :next tag and against the digest-pinned release tag :0.1.0@sha256:c3f066..., once single-arch (linux/amd64) and once multi-arch (linux/amd64,linux/arm64).

The multi-arch run actually does this:

  1. Woodpecker sends the container-create to the agent API.
  2. Codeberg does not have head1328/woodpecker-ci-plugin-podman on its server allowlist, so the request arrives with Privileged=false.
  3. Agent Guard finds the image in WALLY_GUARD_PRIVILEGED_IMAGES, rewrites the request to Privileged=true, and forwards it to Podman.
  4. The plugin runs inside the privileged pipeline container and invokes podman build --platform=linux/amd64,linux/arm64 --manifest=....
  5. For the linux/arm64 build, podman build opens a fresh rootless userns. The first exec of an arm64 binary (/bin/sh from the Alpine image) triggers the binfmt_misc lookup in the kernel; the kernel finds qemu-aarch64 with the F flag and reuses the already-open interpreter fd directly.
  6. RUN apk add jq ca-certificates runs inside the arm64 container through qemu-aarch64-static. uname -m reports aarch64.

Licenses
#

All three projects are Apache 2.0.

Acknowledgments
#

  • Woodpecker CI - container-native CI agent
  • Codeberg - Forgejo-based Git forge with Woodpecker CI integration
  • Fedora CoreOS - minimal, transactionally updated container host OS
  • Podman - daemonless container runtime
  • QEMU - machine emulator (qemu-user-static for userspace emulation)
  • Hetzner Cloud - cloud provider with IPv6-only servers

Found this helpful?
Consider supporting via: