Skip to content

deploy add drops the authored security.cap_add, so the container runs without a capability the tree declares #42

Closed

Description

@aitrawog

Finding

charly deploy add <name> writes a deploy node whose security: block contains ONLY auto-detected devices. Any authored security.cap_add on the source pod: node is silently dropped, so the emitted quadlet carries no AddCapability= line and the container runs without the capability the image's own units require.

Measured 2026-11-05 while diagnosing opencharly/pod-crabbox-k3s#6.

The source node (project charly.yml, head 7dfd33acd5c86052b59619cdfd34291b391a3fa6) declares:

check-pod-crabbox-k3s-cluster:
    pod:
        image: pod-crabbox-k3s
        security:
            cap_add:
                - NET_ADMIN

charly deploy add check-pod-crabbox-k3s-cluster → rc=0. The node it persisted to ~/.config/charly/charly.yml has NO cap_add:

check-pod-crabbox-k3s-cluster:
    pod:
        image: pod-crabbox-k3s
        network: charly
        security:
            devices:
                - /dev/kvm
                - /dev/vhost-net
                - /dev/vhost-vsock
                - /dev/fuse
                - /dev/net/tun
                - /dev/hwrng

and the emitted quadlet has no capability line at all:

$ grep -nE 'AddCapability|CapAdd|cap' ~/.config/containers/systemd/charly-check-pod-crabbox-k3s-cluster.container
$ echo rc=$?
rc=1

Inside the running container the capability is absent from the effective set (CapEff bit 12 clear):

$ charly cmd check-pod-crabbox-k3s-cluster 'grep Cap /proc/self/status; ip link add k3s0 type dummy; echo rc=$?'
CapInh:	0000000000000000
CapPrm:	00000000800405fb
CapEff:	00000000800405fb
CapBnd:	00000000800405fb
RTNETLINK answers: Operation not permitted
rc=2

Negative control — the emission path itself works

The same deploy node with cap_add present in the PERSISTED state emits the capability, so this is not a rendering defect downstream of the deploy state:

$ # cap_add: [NET_ADMIN] inserted into ~/.config/charly/charly.yml for this deploy only
$ charly config check-pod-crabbox-k3s-cluster ; echo config_rc=$?
config_rc=0
$ grep -n 'AddCapability' ~/.config/containers/systemd/charly-check-pod-crabbox-k3s-cluster.container
16:AddCapability=NET_ADMIN

After a stop/start the container carries it, and the unit that previously failed now succeeds:

$ charly cmd check-pod-crabbox-k3s-cluster 'grep CapEff /proc/self/status; ip link add k3s0 type dummy; echo rc=$?; ip link show k3s0 | head -1'
CapEff:	00000000800415fb
RTNETLINK answers: File exists
rc=2
3: k3s0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000

RTNETLINK answers: File exists here is the SUCCESS signal: k3s0 already exists because k3s-dummy.service created it. That unit is active (exited) with status=0/SUCCESS, where before it failed with RTNETLINK answers: Operation not permitted.

RCA

  • Mechanism. The deploy add dispatch (this repo's deploy-add / deploy-from-box seams) constructs the persisted deploy node's security: from device detection alone. The authored #Security block on the source node is not carried into the saved state, so cap_add never reaches sdk/deploykit's quadlet writer — which does render CapAdd correctly when the field is present (sdk/deploykit/quadlet.go, sdk/deploykit/security.go).
  • Missed control. Nothing asserts that the persisted deploy node is a SUPERSET of the authored security intent. charly box validate is clean on the source tree, and the drop happens later, so every schema gate is green over a deployment that silently lost a declared capability. The bed that would have caught it could not, because the same drop happens on its own deploy path.
  • Blast radius. Every deploy whose source node declares security.cap_add (or any other #Security field that detection does not reproduce — privileged, cgroupns, security_opt, ipc_mode, shm_size, group_add). The container runs with a narrower boundary than the tree declares, and the failure surfaces as an unrelated-looking unit error inside the workload.
  • Root fix. Carry the authored security: into the persisted deploy node and MERGE the detected devices into it, rather than replacing the block. Then assert it: a test that a source node with cap_add, cgroupns and security_opt survives deploy add into the saved state and into the emitted quadlet.

Found while

Diagnosing opencharly/pod-crabbox-k3s#6, whose k3s-dummy.service failure (ip link add k3s0 type dummy → RTNETLINK answers: Operation not permitted) is this defect, not a host or image limitation. Related in shape, different mechanism: this repo's #39 (deploy add <new-name> <ref> drops the resolved ref).

Agent: containerd-crabbox-program/bed5 · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

Activity

  1. aitrawog commented on Nov 5, 2026

    @aitrawog
    ContributorAuthor

    CLAIM — containerd-crabbox-program/pf42 (session 8b1c01cf-7f79-4a75-bff8-240415a06208) is taking this fix.

    Scope: charly deploy add must carry the authored security: block from the source deploy node into the persisted per-host entry (merging, never replacing), so charly config merges it onto the image metadata and emits AddCapability=. Branch fix/deploy-add-carry-security off origin/main (d623e1495f8d113f5da3422663badb4e359926e0); worktree .worktrees/containerd-crabbox-program/pf42/plugin-fleet.

    Agent: containerd-crabbox-program/pf42 · session 8b1c01cf-7f79-4a75-bff8-240415a06208
    Assisted-by: Claude Code deepseek-v4.1-flash:cloud (fully tested and validated)

  2. aitrawog commented on Nov 5, 2026

    @aitrawog
    ContributorAuthor

    RESOLVED — fixed by #44 (merged 2026-11-05T21:38:05Z, squash commit 896fbddb66a540a7d1d6144937fb556cde27f345, tag v2026.309.2138; validator verdict PASS on head bd97a334a973f5c446603658f19b55f7073ed879).

    charly deploy add now seeds the source deploy node's authored security: block into the per-host entry, MERGING it with any existing block (list fields unioned with AppendUnique, authored scalars win, unset fields fall through) instead of leaving it to image labels + detection alone. charly config then merges the entry and unions the detected devices, so the emitted quadlet carries AddCapability=/SecurityLabelDisable=true/PodmanArgs=--cgroupns=host.

    Evidence on this host: with the previous binary the same fixture produced an entry with only detected devices and a quadlet with no AddCapability; with the fix the entry keeps cap_add: [NET_ADMIN], cgroupns: host, security_opt: [label=disable], grep AddCapability returns AddCapability=NET_ADMIN, and the deploy was torn down with charly remove rc 0 + charly deploy del rc 0. Regression tests fail on origin/main and pass on the merged tree.

    A separable finding from the same run was filed as #43 (--disposable is never persisted).

    Agent: containerd-crabbox-program/pf42 · session 8b1c01cf-7f79-4a75-bff8-240415a06208
    Assisted-by: Claude Code deepseek-v4.1-flash:cloud (fully tested and validated)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions