# AppArmor profile for the HOST-LEVEL sandbox worker (codex-host-worker). # # This is docker-codex (outer hardening) PLUS what bwrap needs: # - userns: allow creating user namespaces (bwrap's inner sandbox) # - mount, umount, pivot_root: allow (bwrap mounts proc/tmpfs/binds # and pivot_roots into the new root) # It KEEPS docker-codex's denies: raw network, ptrace, dangerous # capabilities, writes to /proc/sys, /sys, /proc/keys. # # Why a separate profile: docker-codex deliberately denies mount # (containers must never mount; inner sandbox stays off there by # choice). The host worker NEEDS mount+userns because its tasks # run codex workspace-write (bwrap) directly on the host. The two # profiles must not be confused: a task container under codex-host # could bwrap (defeats the Docker-only design), and a host worker # under docker-codex cannot bwrap at all (mount denied). # # Security note (honest): allowing mount+userns for the host worker # means the WORKER process (trusted, codexsvc) can mount. The AGENT # inside a task cannot mount on the host: its mounts happen inside # bwrap's new mount namespace (isolated), and bwrap's pid namespace # hides other tasks' processes. The agent's writable surface is its # own worktree (+ a private tmpfs /tmp that never reaches host /tmp). # # Install: cp to /etc/apparmor.d/codex-host, then # apparmor_parser -r /etc/apparmor.d/codex-host # Use: aa-exec -p codex-host -- python3 -m cloud.worker ... # (see deploy/host-worker/codex-host-worker.service) # Verify: cat /proc//attr/current shows # "codex-host (enforce)". #include profile codex-host flags=(attach_disconnected,mediate_deleted) { #include network inet stream, network inet dgram, network inet6 stream, network inet6 dgram, # bwrap brings up loopback in its new network namespace via a # NETLINK_ROUTE socket (verified 2026-10-26: without this, # codex workspace-write fails with "bwrap: loopback: Failed # to bind NETLINK_ROUTE socket: Permission denied" and no tool # runs at all). CAREFUL: a bare "deny network raw" would # override this allow (deny wins over allow in AppArmor) and # break netlink too -- verified by isolated netlink bind test. # So the raw deny must name inet/inet6 explicitly, leaving # netlink raw allowed. network netlink raw, deny network inet raw, deny network inet6 raw, # Capabilities: bwrap NEEDS userns-scoped capabilities (verified # 2026-10-26: codex-host with deny capability sys_admin etc. # made bwrap fail at userns creation, while the same profile # with capability, allowed it). This is safe for an unprivileged # user: codexsvc holds NO host capabilities, so the only caps # this allows are fake-root caps inside bwrap's own user # namespace (scoped to that namespace, harmless to the host). # The profile's other denies still hold independently: raw # network is gated by network rules, ptrace by ptrace rules, # /proc/sys writes by file rules -- allowing caps does not # void any of them. capability, # bwrap needs these (docker-codex denies them). userns, mount, umount, pivot_root, deny ptrace, file, deny @{PROC}/sys/** w, deny @{PROC}/sysrq-trigger rw, deny @{PROC}/keys/** rw, deny @{PROC}/kcore rw, deny /sys/** w, deny /etc/shadow rw, deny /etc/gshadow rw, signal, unix, dbus, ptrace (readby), }