diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f91bd7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +# Keep the build context small — the Dockerfile only needs sources + Cargo +# manifests. Everything below is either rebuilt inside the image or irrelevant. +target/ +.git/ +*.md +docs/ +deploy/ +.github/ +.vscode/ +.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 03deed0..83c68d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,14 @@ its first tagged release. ## [Unreleased] ### Added -- **Podman deployment notes** — `docs/deployment.md` documents running the eBPF - egress guard under Podman (cgroup-v2-native, daemonless): rootful/privileged - `podman run` with `--cgroupns host`, the pod-sidecar mapping (incl. `podman play - kube`), and why rootless Podman only fits the cooperative proxy layer. Marked as - untested-against-a-live-host, like the Kubernetes examples. +- **Podman deployment notes (verified)** — `docs/deployment.md` documents running + the eBPF egress guard under Podman (cgroup-v2-native, daemonless), **verified on + Lima** (Ubuntu 24.04, kernel 6.8, Podman 4.9.3): rootful/privileged self-guard + (default cgroupns) and sidecar (`--cgroupns host` + the target's dedicated cgroup + path) both enforce the allowlist in the kernel with host egress intact. Documents + the anti-pattern proven along the way — `--cgroupns host` on a `/sys/fs/cgroup` + attach cuts the *host's* egress — and why rootless Podman only fits the + cooperative proxy layer. - **`AGENTS.md` + `.github/skills/`** — a vendor-neutral orientation guide for coding agents and new contributors (build/test, crate map, working rules, deferring to CLAUDE.md as the binding authority), plus step-by-step task @@ -64,5 +67,11 @@ its first tagged release. - README: dependency-pin table, container/Helm quickstart. ### Fixed +- **Container builds now work on Podman** (surfaced verifying the Podman notes): + the `deploy/Dockerfile` and `deploy/proxy/Dockerfile` base images and the + compose `curl` image are now fully-qualified (`docker.io/library/...`), so they + resolve under Podman's stricter short-name policy (Docker is unaffected); and a + `.dockerignore` excludes `target/` etc. so the build context is no longer the + whole 6.6 GB tree. - `pasu-ebpf` was missing a `license` field; `pasu-egress` was missing the `io-util`/`sync` tokio features (surfaced by a clean build). diff --git a/deploy/Dockerfile b/deploy/Dockerfile index f4e9180..40d10b5 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -12,7 +12,7 @@ # a cgroup v2 mount, and a DEDICATED cgroup to attach to). ########## builder ########## -FROM rust:1-bookworm AS builder +FROM docker.io/library/rust:1-bookworm AS builder # eBPF build toolchain: nightly + rust-src (build-std for the bpf target). RUN rustup toolchain install nightly --profile minimal --component rust-src \ @@ -35,7 +35,7 @@ COPY . . RUN cargo build -p pasu-egress -p pasu-daemon -p pasu-cli --release ########## runtime ########## -FROM debian:bookworm-slim AS runtime +FROM docker.io/library/debian:bookworm-slim AS runtime # ca-certificates for DNS/TLS metadata; curl + iproute2 + socat make the image # useful for the demo / admin CLI. Drop them for a smaller production image. diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index a558804..55322b3 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -13,7 +13,7 @@ # name and adjust the attach path accordingly. services: agent: - image: curlimages/curl:latest + image: docker.io/curlimages/curl:latest # a stand-in agent: keeps trying an allowed and a blocked destination command: - sh diff --git a/deploy/proxy/Dockerfile b/deploy/proxy/Dockerfile index 4538982..e864dde 100644 --- a/deploy/proxy/Dockerfile +++ b/deploy/proxy/Dockerfile @@ -10,14 +10,14 @@ # docker build -f deploy/proxy/Dockerfile -t pasu-proxy:latest . ########## builder ########## -FROM rust:1-bookworm AS builder +FROM docker.io/library/rust:1-bookworm AS builder WORKDIR /src COPY . . # Portable/stable build — the proxy crate needs no eBPF toolchain. RUN cargo build --release -p pasu-proxy --bin pasu-proxy ########## runtime ########## -FROM debian:bookworm-slim +FROM docker.io/library/debian:bookworm-slim # CA roots for TLS to the real provider (https upstream). RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ diff --git a/docs/deployment.md b/docs/deployment.md index 00c816a..118fb3d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -100,13 +100,19 @@ cgroup layout. The "one rule" is runtime-agnostic: pasu-egress needs a **cgroup v2** node and the capability to attach a `cgroup_skb` program to it — not Docker specifically. Podman is **cgroup-v2-native and daemonless**, so the requirements above map -cleanly. Two paths, mirroring the Docker ones: +cleanly. Run it **rootful** (`sudo podman`); see the rootless note below. -**Rootful, privileged** (the direct analog of §2/§3) — build with `podman build` -(same [`deploy/Dockerfile`](../deploy/Dockerfile)), then: +Podman's default seccomp profile blocks `bpf()` just like Docker's, so +`--privileged` is the easy path (or grant `CAP_BPF` + `CAP_NET_ADMIN` +(+ `CAP_PERFMON`) with a profile that allows `bpf`). + +**Self-guard (one container)** — attaches to the container's own cgroup, so use +Podman's **default (private) cgroupns**; `/sys/fs/cgroup` is then the container's +own cgroup. Build with `podman build` (same [`deploy/Dockerfile`](../deploy/Dockerfile)), +then: ```bash -sudo podman run --rm --privileged --cgroupns host --entrypoint /bin/sh \ +sudo podman run --rm --privileged --entrypoint /bin/sh \ pasu-egress:latest -c ' pasu-egress --cgroup-path /sys/fs/cgroup --allow 1.1.1.1 & sleep 3 @@ -115,15 +121,28 @@ sudo podman run --rm --privileged --cgroupns host --entrypoint /bin/sh \ ' ``` -The same two gotchas from §4 apply: pass **`--cgroupns host`** (Podman also gives a -private cgroupns by default), and Podman's default seccomp profile blocks `bpf()` -too — `--privileged` clears it, or grant `CAP_BPF` + `CAP_NET_ADMIN` (+ `CAP_PERFMON`) -with a seccomp profile that allows `bpf`. +> ⚠️ **Do _not_ add `--cgroupns host` to the self-guard command.** With the host +> cgroupns, `/sys/fs/cgroup` is the **host root cgroup**, and default-deny there +> cuts the whole host's egress (verified: the host itself lost egress to +> non-allowed IPs). `--cgroupns host` is only for the sidecar case below, and +> then you attach to a **dedicated** cgroup path, never `/sys/fs/cgroup`. + +**Sidecar (guard a separate container)** — the guard needs `--cgroupns host` to +reach the target's cgroup, and attaches to that **specific** cgroup path (from +`podman inspect`), which scopes enforcement to the target and leaves the host +untouched: + +```bash +sudo podman run -d --name agent ... # your agent workload +AGCG=$(sudo podman inspect agent --format '{{.State.CgroupPath}}') +sudo podman run -d --privileged --cgroupns host --entrypoint pasu-egress \ + pasu-egress:latest --cgroup-path "/sys/fs/cgroup$AGCG" --allow 1.1.1.1 +# agent now reaches 1.1.1.1 but not 1.0.0.1; the host's own egress is unaffected. +``` -**Pod sidecar** — a Podman pod shares a cgroup across its containers, so it maps -directly onto the Kubernetes sidecar model (§4). The [k8s manifests](../deploy/k8s/) -also run under Podman via `podman play kube`: the privileged pasu-egress container -attaches to the shared pod cgroup and guards the agent container. +A Podman **pod** shares a cgroup across its containers, so `podman play kube` on +the [k8s manifests](../deploy/k8s/) maps onto the same sidecar model (§4) — the +privileged pasu-egress container attaches to the pod's cgroup slice. > ⚠️ **Rootless Podman is the hard case.** A rootless container runs in a user > namespace with a delegated cgroup subtree, and attaching a cgroup-BPF program @@ -132,9 +151,11 @@ attaches to the shared pod cgroup and guards the agent container. > userspace library/binary, runs fine rootless — `podman run` it and point the > agent's `base_url` at it.) -> These Podman commands follow the same shape as the verified Docker paths but -> have **not been run against a live Podman host** — treat them as a starting -> point, like the Kubernetes examples above. +> **Verified** on Lima (Ubuntu 24.04, kernel 6.8, cgroup v2, **Podman 4.9.3**, +> arm64): the self-guard and sidecar commands above both enforce the allowlist in +> the kernel while leaving host egress intact; `--cgroupns host` on a +> `/sys/fs/cgroup` attach cuts the host, as warned. `podman play kube` is inferred +> from the shared-cgroup model, not separately run. ### Two gotchas we hit validating this (so you don't have to)