Skip to content

Commit 7ab2f36

Browse files
Billy99claude
andcommitted
running MCV without --privileged
Problem: MCV uses buildah internally to build/push OCI cache images. Buildah traditionally requires `--privileged` because it needs to mount overlay filesystems and run as root. This is a security concern in production Kubernetes and CI environments. What changed in the Containerfile 1. Storage driver: FUSE/overlay → VFS (redhat-et#173) driver="vfs" FUSE-overlayfs requires either --privileged or CAP_SYS_ADMIN + /dev/fuse access. VFS is a naive copy-based storage driver that needs no special kernel capabilities. It's slower (copies instead of overlays), but MCV only builds small single-layer cache images, so the performance difference is negligible. 2. Non-root user (UID/GID 1000) ``` RUN groupadd -g 1000 appgroup && \ useradd -u 1000 -g appgroup -m -s /bin/bash appuser USER appuser ``` Running as root inside the container is unnecessary and a security risk. The fixed UID/GID 1000 also makes volume mount permissions predictable. Runtime flags: Podman vs Docker Podman: `podman run -v /path/to/cache:/tests:Z,U <image> ...` - :Z — relabels the volume for SELinux (private to this container) - :U — remaps the volume ownership to match the in-container user (UID 1000). Podman runs rootless with user namespaces, so the host UID and container UID differ. :U bridges that gap so appuser can read/write the mount. You only need :Z,U on volumes the container needs to write to (the cache output directory). Read-only mounts like model files only need :Z (or :ro,Z). Docker: ``` docker run --user $(id -u):$(id -g) \ --security-opt seccomp=unconfined \ --security-opt apparmor=unconfined \ -v /path/to/cache:/tests:Z \ <image> ... ``` - `--user $(id -u):$(id -g)` — Docker doesn't have Podman's user-namespace remapping, so you explicitly run as your host UID/GID to match volume ownership. Without this, the container runs as UID 1000 (appuser) which may not own the host-side mount. - `--security-opt seccomp=unconfined` — buildah makes syscalls (like mount, unshare) that Docker's default seccomp profile blocks. Disabling seccomp allows these without granting full --privileged. - `--security-opt apparmor=unconfined` — on Ubuntu (base container), AppArmor's default Docker profile also blocks some of buildah's mount/namespace operations. Disabling it is the minimal escalation needed. - `:Z` — SELinux relabeling, same as Podman. No :U needed because --user handles ownership directly. Why the difference Podman is rootless-native — it uses user namespaces automatically, so :U remaps ownership transparently. Docker doesn't do user-namespace remapping by default, so you need --user to align UIDs, and --security-opt to relax seccomp/AppArmor enough for buildah's syscalls without going full --privileged. What you're NOT granting Neither approach uses --privileged. The container does NOT get: - Full device access - CAP_SYS_ADMIN - Host PID/network namespace - Write access to /dev, /proc, /sys It's a targeted relaxation: let buildah do its mounts and namespace operations, nothing more. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Billy McFall <22157057+Billy99@users.noreply.github.com>
1 parent 6270ecc commit 7ab2f36

5 files changed

Lines changed: 402 additions & 67 deletions

File tree

.github/workflows/mcv-build-example-images.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ jobs:
9696
archive="${out_dir}/${tag}.oci"
9797
9898
echo "==> Building ${image} using MCV container from /work/${reldir}"
99-
podman run --rm --privileged \
99+
podman run --rm \
100100
-e TAG="${tag}" \
101101
-e IMAGE="${image}" \
102102
-e RELDIR="${reldir}" \
103-
-v "${GITHUB_WORKSPACE}:/work:rw" \
103+
-v "${GITHUB_WORKSPACE}:/work:Z,U" \
104104
quay.io/gkm/mcv:latest bash -lc '
105105
set -euo pipefail
106106
/mcv -l info -c -i "$IMAGE" -d "/work/$RELDIR" &&

mcv/README.md

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,57 @@ A Model/GPU kernel cache container packaging utility inspired by
2929
### Install dependencies
3030

3131
```bash
32-
sudo dnf install gpgme-devel
33-
sudo dnf install btrfs-progs-devel
32+
sudo dnf install -y gpgme-devel btrfs-progs-devel
3433
```
34+
OR
35+
```bash
36+
sudo apt install -y libgpgme-dev libbtrfs-dev uidmap
37+
```
38+
39+
On Ubuntu 24.04, *running* `mcv` unprivileged to build cache images (its
40+
embedded buildah creates a user namespace) requires unprivileged user
41+
namespaces, which Ubuntu restricts by default via AppArmor. This is only needed
42+
at runtime — compiling with `make build` does not need it. There are two ways to
43+
allow it.
44+
45+
**Preferred — scoped AppArmor profile.** Grant the `userns` permission only to
46+
the `mcv` binary, leaving the global restriction in place for every other
47+
program. Create `/etc/apparmor.d/mcv` (adjust the path to match your installed
48+
binary):
49+
50+
```bash
51+
abi <abi/4.0>,
52+
include <tunables/global>
53+
54+
profile mcv /home/<user>/go/bin/mcv flags=(unconfined) {
55+
userns,
56+
include if exists <local/mcv>
57+
}
58+
```
59+
60+
Then load it:
61+
62+
```bash
63+
sudo apparmor_parser -r /etc/apparmor.d/mcv
64+
```
65+
66+
When running MCV inside a container instead of natively, apply the same `userns`
67+
permission to the container runtime's profile (e.g. `podman`/`rootlesskit`)
68+
rather than to `mcv`.
69+
70+
**Simpler but less secure — disable the restriction globally.** This re-enables
71+
unprivileged user namespaces for *all* programs, weakening a defense-in-depth
72+
protection against kernel exploits that abuse user namespaces. Prefer the scoped
73+
profile above; use this only on disposable/dev machines:
74+
75+
```bash
76+
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
77+
78+
# To persist across reboots:
79+
echo 'kernel.apparmor_restrict_unprivileged_userns=0' | sudo tee /etc/sysctl.d/99-userns.conf
80+
```
81+
82+
### Build and Install
3583
3684
Build the binary:
3785
@@ -110,6 +158,7 @@ Two image variants are available:
110158
**How it works:** With `--no-gpu`, MCV extracts GPU information (backend, architecture, warp size) from cache metadata rather than detecting actual hardware. The cache files created by vLLM/Triton already contain all necessary GPU information in environment variables.
111159
112160
**GPU access flags** (e.g., `--gpus all` for NVIDIA, `--device /dev/kfd --device /dev/dri` for AMD) are **ONLY** required for GPU validation/preflight checks. They are **NOT** needed when using `--no-gpu` for cache creation or extraction.
161+
When using `podman run --device`, `--group-add keep-groups` may be needed for device access. But `--group-add keep-groups` requires crun (not runc).
113162
114163
For detailed usage examples, container configuration, GPU access requirements, and CI/CD integration, see [docs/no-gpu-usage.md](./docs/no-gpu-usage.md).
115164
@@ -552,26 +601,55 @@ To use docker on the host with an MCV image, you need to mount the cache
552601
directory to the container and run the following command:
553602
554603
```bash
555-
docker run --rm -it --privileged \
556-
-v <path-to-cache>/example:/example \
604+
docker run --rm -it \
605+
--user $(id -u):$(id -g) \
606+
--security-opt seccomp=unconfined \
607+
--security-opt apparmor=unconfined \
608+
-v <path-to-cache>/example:/example:Z \
557609
quay.io/gkm/mcv bash -lc '
558610
/mcv -c -i quay.io/gkm/vector-add-cache:rocm \
559611
-d /example/vector-add-cache-rocm --no-gpu &&
560612
buildah push containers-storage:quay.io/gkm/vector-add-cache:rocm \
561613
docker-archive:/example/vector-add-cache-rocm.tar:quay.io/gkm/vector-add-cache:rocm
562614
'
615+
WARN[2025-09-11 16:46:54] running newgidmap: exit status 1: newgidmap: write to gid_map failed: Operation not permitted
616+
WARN[2025-09-11 16:46:54] /usr/bin/newgidmap should be setgid or have filecaps setgid
617+
WARN[2025-09-11 16:46:54] Falling back to single mapping
618+
WARN[2025-09-11 16:46:54] Error running newuidmap: exit status 1: newuidmap: write to uid_map failed: Operation not permitted
619+
WARN[2025-09-11 16:46:54] Falling back to single mapping
563620
INFO[2025-09-11 16:46:54] Setting log level: info
564621
INFO[2025-09-11 16:46:54] Using buildah to build the image
565622
INFO[2025-09-11 16:46:54] Detected cache components: [triton]
566623
INFO[2025-09-11 16:46:55] Image built! 8ce4bc2e98abfa8c0a5a6f6046c1c7bc8ac09805ecb029427a995dc2897828f8
567624
INFO[2025-09-11 16:46:55] OCI image created successfully.
625+
WARN[0000] running newgidmap: exit status 1: newgidmap: write to gid_map failed: Operation not permitted
626+
WARN[0000] /usr/bin/newgidmap should be setgid or have filecaps setgid
627+
WARN[0000] Falling back to single mapping
628+
WARN[0000] Error running newuidmap: exit status 1: newuidmap: write to uid_map failed: Operation not permitted
629+
WARN[0000] Falling back to single mapping
568630
Getting image source signatures
569631
Copying blob 24b82d6fef87 done
570632
Copying config 8ce4bc2e98 done
571633
Writing manifest to image destination
572634
Storing signatures
573635
```
574636
637+
> **NOTE:** The Warnings are known and everything still works fine.
638+
> An include library is making a system call that it doesn't have permission for,
639+
> so it fails and falls back to another method that succeeds.
640+
>
641+
> **Security note — `seccomp=unconfined` / `apparmor=unconfined`.** MCV runs
642+
> buildah *inside* the container to assemble the OCI image, which needs
643+
> `mount`/`unshare`/`pivot_root` and user-namespace operations that Docker's
644+
> default seccomp and AppArmor profiles block for non-privileged containers.
645+
> Disabling both is a deliberate, security-reviewed fallback — it is far narrower
646+
> than `--privileged` (no added capabilities, host devices, or host namespaces),
647+
> and the blast radius is bounded: the container runs as a non-root user
648+
> (`--user`), performs a single packaging task, and is removed on exit (`--rm`).
649+
> To harden further, replace these flags with scoped seccomp and AppArmor
650+
> profiles that allow only buildah's required syscalls/operations, or run the
651+
> same command under **rootless Podman**, which needs neither flag.
652+
575653
Then on host:
576654
577655
```bash
@@ -596,8 +674,8 @@ To use podman on the host with an MCV image, you need to mount the cache
596674
directory to the container and run the following command:
597675
598676
```bash
599-
podman run --rm -it --privileged \
600-
-v <path-to-cache>/example:/example \
677+
podman run --rm -it \
678+
-v <path-to-cache>/example:/example:Z,U \
601679
quay.io/gkm/mcv bash -lc '
602680
/mcv -c -i quay.io/gkm/vector-add-cache:rocm \
603681
-d /example/vector-add-cache-rocm --no-gpu &&

mcv/docs/no-gpu-usage.md

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ make build-image-mcv-no-gpu
4040
# or directly:
4141
podman build --target mcv-minimal -t quay.io/gkm/mcv:no-gpu -f mcv/images/Containerfile .
4242

43-
# Use (MCV pushes directly to the registry; mount auth and fuse device for buildah)
43+
# Use — --create builds the image into the container's local containers-storage;
44+
# chain `buildah push` in the same run so it reaches the registry before --rm
45+
# removes the container (and its store). Mount registry auth for buildah (the
46+
# image runs as appuser, UID 1000, so mount it under that user's home).
4447
podman run --rm \
45-
--device /dev/fuse \
46-
-v /path/to/cache:/cache \
47-
-v ${HOME}/.config/containers/auth.json:/run/containers/0/auth.json:ro \
48+
-v /path/to/cache:/cache:ro \
49+
-v ${HOME}/.config/containers/auth.json:/home/appuser/.config/containers/auth.json:ro \
50+
--entrypoint sh \
4851
quay.io/gkm/mcv:no-gpu \
49-
--create --image quay.io/myorg/cache:v1 --dir /cache --no-gpu
52+
-c "/mcv --create --image quay.io/myorg/cache:v1 --dir /cache --no-gpu \
53+
&& buildah push quay.io/myorg/cache:v1"
5054
```
5155

5256
### Unified Image (For GPU validation - NVIDIA + AMD)
@@ -61,17 +65,32 @@ make build-image-mcv
6165
# or directly:
6266
podman build --target mcv-unified -t quay.io/gkm/mcv:unified -f mcv/images/Containerfile .
6367

64-
# Use with NVIDIA GPU
68+
# Use with NVIDIA GPU (device nodes are world-accessible; no group needed)
6569
podman run --rm --device nvidia.com/gpu=all \
66-
-v /path/to/cache:/cache quay.io/gkm/mcv:unified \
70+
-v /path/to/cache:/cache:U quay.io/gkm/mcv:unified \
6771
--extract --image quay.io/myorg/cache:v1 --dir /cache
6872

6973
# Use with AMD GPU
70-
podman run --rm --device /dev/kfd --device /dev/dri \
71-
-v /path/to/cache:/cache quay.io/gkm/mcv:unified \
74+
podman run --rm --device /dev/kfd --device /dev/dri --group-add keep-groups \
75+
-v /path/to/cache:/cache:U quay.io/gkm/mcv:unified \
7276
--extract --image quay.io/myorg/cache:v1 --dir /cache
7377
```
7478

79+
On hosts with group-restricted GPU devices, the documented device mappings can make
80+
amd-smi or rocm-smi fail. Add a runtime-supported mapping, such as Podman
81+
`--group-add keep-groups` with crun or explicit host device GIDs, and run GPU preflight
82+
as appuser.
83+
84+
**Writable mounts (rootless).** Extraction writes into the mounted directory as the
85+
image's non-root user, so a plain rootless bind mount can fail with `permission
86+
denied`. Append `:U` to every writable mount (e.g. `-v /cache:/cache:U`) so Podman
87+
chowns the mount to the container user; on SELinux-enforcing hosts also add `:Z`
88+
(private to this container) or `:z` (shared) — for example `:U,Z`. Note that `:U`
89+
recursively changes ownership of the host directory; if that is undesirable, use
90+
`--userns=keep-id` instead to map your host UID into the container so existing
91+
ownership works without a chown. Docker has no `:U` — use `--user $(id -u):$(id -g)`
92+
there. Read-only mounts (`--create`) do not need `:U`.
93+
7594
## Usage Examples
7695

7796
### Creating Cache Images (No GPU Required)
@@ -82,13 +101,16 @@ mcv --create --image quay.io/myorg/vllm-cache:v1 \
82101
--dir ~/.cache/vllm/torch_compile_cache \
83102
--no-gpu
84103

85-
# In container (minimal image; MCV pushes to the registry directly)
104+
# In container (minimal image) — --create stores the image in the container's
105+
# local containers-storage; chain `buildah push` so it reaches the registry
106+
# before --rm removes the store. Mount registry auth under appuser's home.
86107
podman run --rm \
87-
--device /dev/fuse \
88108
-v ~/.cache/vllm:/cache:ro \
89-
-v ${HOME}/.config/containers/auth.json:/run/containers/0/auth.json:ro \
109+
-v ${HOME}/.config/containers/auth.json:/home/appuser/.config/containers/auth.json:ro \
110+
--entrypoint sh \
90111
quay.io/gkm/mcv:no-gpu \
91-
--create --image quay.io/myorg/vllm-cache:v1 --dir /cache --no-gpu
112+
-c "/mcv --create --image quay.io/myorg/vllm-cache:v1 --dir /cache --no-gpu \
113+
&& buildah push quay.io/myorg/vllm-cache:v1"
92114
```
93115

94116
### Extracting Cache (No GPU Required)
@@ -101,7 +123,7 @@ mcv --extract --image quay.io/myorg/vllm-cache:v1 \
101123

102124
# In container (minimal image)
103125
podman run --rm \
104-
-v ~/.cache/vllm:/cache \
126+
-v ~/.cache/vllm:/cache:U \
105127
quay.io/gkm/mcv:no-gpu \
106128
--extract --image quay.io/myorg/vllm-cache:v1 --dir /cache --no-gpu
107129
```
@@ -116,8 +138,8 @@ mcv --extract --image quay.io/myorg/vllm-cache:v1 \
116138

117139
# In container (full image)
118140
podman run --rm \
119-
--device /dev/kfd --device /dev/dri \
120-
-v ~/.cache/vllm:/cache \
141+
--device /dev/kfd --device /dev/dri --group-add keep-groups \
142+
-v ~/.cache/vllm:/cache:U \
121143
quay.io/gkm/mcv:unified \
122144
--extract --image quay.io/myorg/vllm-cache:v1 --dir /cache
123145
```
@@ -130,7 +152,7 @@ mcv --check-compat --image quay.io/myorg/vllm-cache:v1
130152

131153
# In container
132154
podman run --rm \
133-
--device /dev/kfd --device /dev/dri \
155+
--device /dev/kfd --device /dev/dri --group-add keep-groups \
134156
quay.io/gkm/mcv:unified \
135157
--check-compat --image quay.io/myorg/vllm-cache:v1
136158
```
@@ -150,31 +172,52 @@ build-cache-image:
150172
# Run vLLM to generate cache
151173
python generate_cache.py
152174
153-
- name: Build cache OCI image
175+
- name: Build and push cache OCI image
154176
run: |
155-
podman run --rm --privileged \
177+
# --create stores the image in the container's local containers-storage,
178+
# so push it in the same run — the store is gone once --rm removes the
179+
# container. Mount registry auth under appuser's home (image runs as UID 1000).
180+
podman run --rm \
156181
-v $(pwd)/.cache/vllm:/cache:ro \
182+
-v ${HOME}/.config/containers/auth.json:/home/appuser/.config/containers/auth.json:ro \
183+
--entrypoint sh \
157184
quay.io/gkm/mcv:no-gpu \
158-
--create --image quay.io/myorg/vllm-cache:${{ github.sha }} \
159-
--dir /cache --no-gpu
160-
161-
- name: Push cache image
162-
run: |
163-
podman push quay.io/myorg/vllm-cache:${{ github.sha }}
185+
-c "/mcv --create --image quay.io/myorg/vllm-cache:${{ github.sha }} \
186+
--dir /cache --no-gpu \
187+
&& buildah push quay.io/myorg/vllm-cache:${{ github.sha }}"
164188
```
165189
166190
## GPU Access Requirements
167191
168192
### When GPU Access Flags Are Required
169193
170-
**NVIDIA GPUs:**
194+
**NVIDIA GPUs:** (device nodes are world-accessible — no group needed)
171195
- Docker: `--gpus all` (or `--gpus device=0,1` for specific devices)
172196
- Podman: `--device nvidia.com/gpu=all`
173197
- **ONLY needed for**: GPU validation/preflight checks (extract without `--no-gpu`, compatibility checks)
174198
- **NOT needed for**: Creating or extracting with `--no-gpu` flag
175199

176-
**AMD GPUs:**
177-
- Docker & Podman: `--device /dev/kfd --device /dev/dri`
200+
**AMD GPUs:** (`/dev/kfd` and `/dev/dri/*` are group-owned — the non-root user must join the render/video group)
201+
- Podman (crun): `--device /dev/kfd --device /dev/dri --group-add keep-groups` (preserves the host user's render/video groups)
202+
- Docker: `--device /dev/kfd --device /dev/dri`, plus a `--group-add <gid>` for each mapped device node's owning group (`keep-groups` is Podman/crun-only). Device paths and GIDs are host-specific — `/dev/dri/renderD128` and the `video` group may be absent, and `/dev/kfd` and the various `/dev/dri/*` nodes can each be owned by a different GID — so **derive a GID for each node that actually exists** and pass only the ones you found (never a bare or empty `--group-add`). For example:
203+
```bash
204+
# Collect the distinct owning GIDs of the GPU nodes present on this host.
205+
gpu_gids=$(for d in /dev/kfd /dev/dri/renderD* /dev/dri/card*; do
206+
[ -e "$d" ] && stat -c '%g' "$d"
207+
done | sort -u)
208+
[ -n "$gpu_gids" ] || { echo "no GPU device nodes found" >&2; exit 1; }
209+
210+
docker run --rm \
211+
--user "$(id -u):$(id -g)" \
212+
--device /dev/kfd --device /dev/dri \
213+
$(printf -- '--group-add %s ' $gpu_gids) \
214+
-v /path/to/cache:/cache \
215+
quay.io/gkm/mcv:unified \
216+
--extract --image quay.io/myorg/cache:v1 --dir /cache
217+
```
218+
Docker has no `:U`; run as your host UID/GID with `--user "$(id -u):$(id -g)"`
219+
and ensure the host cache directory is writable by that UID/GID.
220+
Alternatively, require operators to supply the GIDs explicitly (e.g. `--group-add 44 --group-add 993`) when the device layout is known ahead of time.
178221
- **ONLY needed for**: GPU validation/preflight checks (extract without `--no-gpu`, compatibility checks)
179222
- **NOT needed for**: Creating or extracting with `--no-gpu` flag
180223

@@ -187,16 +230,16 @@ podman run --rm \
187230
quay.io/gkm/mcv:no-gpu \
188231
--create --image quay.io/myorg/cache:v1 --dir /cache --no-gpu
189232
190-
# NVIDIA GPU access required for validation
233+
# NVIDIA GPU access required for validation (world-accessible nodes; no group needed)
191234
podman run --rm --device nvidia.com/gpu=all \
192-
-v /path/to/cache:/cache \
235+
-v /path/to/cache:/cache:U \
193236
quay.io/gkm/mcv:unified \
194237
--extract --image quay.io/myorg/cache:v1 --dir /cache
195238
196239
# AMD GPU access required for validation
197240
podman run --rm \
198-
--device /dev/kfd --device /dev/dri \
199-
-v /path/to/cache:/cache \
241+
--device /dev/kfd --device /dev/dri --group-add keep-groups \
242+
-v /path/to/cache:/cache:U \
200243
quay.io/gkm/mcv:unified \
201244
--extract --image quay.io/myorg/cache:v1 --dir /cache
202245
```

0 commit comments

Comments
 (0)