From a44dc25e00930c1fbd1179036b3fe82b789283fa Mon Sep 17 00:00:00 2001 From: Mauricio Perera Date: Thu, 30 Jul 2026 14:14:37 -0600 Subject: [PATCH 1/3] Harden sandbox seccomp: drop ptrace and tighten socket domain allowlist Two fixes from a security audit of this repo: - Remove process_vm_readv/process_vm_writev/ptrace from the allowlist. All sandboxed processes share the same uid, so ptrace let one process inspect/modify another's memory (e.g. read a secret held by a sibling process) inside the sandbox -- inter-process isolation was effectively void despite the seccomp profile's intent. - Replace the 3 socket-domain rules with an explicit allowlist of AF_UNIX(1)/AF_INET(2)/AF_INET6(10). The previous rules (domain<38, domain==39, domain>40) only actually blocked AF_ALG(38) and AF_VSOCK(40) -- AF_NETLINK(16) and AF_PACKET(17) were allowed, along with everything else outside that narrow gap, making the egress control close to a no-op. Verified with a real Docker build + run on a separate host, under the exact production security contract (--cap-drop ALL --security-opt no-new-privileges + this seccomp profile): - DNS resolution (getaddrinfo) and `git clone` over HTTPS still work (AF_INET/AF_INET6 unaffected) -- confirms this doesn't break the scanner's own legitimate network use. - AF_NETLINK and AF_PACKET socket() calls now fail with EPERM inside the container (previously succeeded) -- confirms the fix is real, not cosmetic. Local commit only -- this is an audit clone of openai/codex-security, not a fork with push access. --- docker/codex-security-seccomp.json | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/docker/codex-security-seccomp.json b/docker/codex-security-seccomp.json index 9b281782..ed582e3e 100644 --- a/docker/codex-security-seccomp.json +++ b/docker/codex-security-seccomp.json @@ -377,25 +377,21 @@ ], "action": "SCMP_ACT_ALLOW" }, - { - "names": ["process_vm_readv", "process_vm_writev", "ptrace"], - "action": "SCMP_ACT_ALLOW", - "includes": { "minKernel": "4.8" } - }, { "names": ["socket"], "action": "SCMP_ACT_ALLOW", - "args": [{ "index": 0, "value": 38, "op": "SCMP_CMP_LT" }] + "comment": "AF_UNIX(1)/AF_INET(2)/AF_INET6(10) only: local IPC + HTTPS for git clone and the OpenAI API. Everything else (notably AF_NETLINK=16, AF_PACKET=17) is denied by defaultAction; the previous 3-rule form here only blocked AF_ALG=38/AF_VSOCK=40 and left NETLINK/PACKET/everything-else allowed.", + "args": [{ "index": 0, "value": 1, "op": "SCMP_CMP_EQ" }] }, { "names": ["socket"], "action": "SCMP_ACT_ALLOW", - "args": [{ "index": 0, "value": 39, "op": "SCMP_CMP_EQ" }] + "args": [{ "index": 0, "value": 2, "op": "SCMP_CMP_EQ" }] }, { "names": ["socket"], "action": "SCMP_ACT_ALLOW", - "args": [{ "index": 0, "value": 40, "op": "SCMP_CMP_GT" }] + "args": [{ "index": 0, "value": 10, "op": "SCMP_CMP_EQ" }] }, { "names": ["personality"], From d88aec51659864e472bc2f4df23ad91480f4133b Mon Sep 17 00:00:00 2001 From: Mauricio Perera Date: Thu, 30 Jul 2026 14:24:28 -0600 Subject: [PATCH 2/3] Fail closed if not launched with --cap-drop ALL --security-opt no-new-privileges docker/codex-security-seccomp.json unconditionally allows mount/pivot_root/ unshare/setns/clone for Codex's own unprivileged Bubblewrap sandboxing -- those syscalls can't be removed without breaking that feature entirely. Their safety previously depended entirely on a comment in the seccomp profile asserting the caller runs with --cap-drop ALL and --security-opt no-new-privileges (true for compose.yaml, not enforced for a bare `docker run`). Adds a runtime self-check at the top of the entrypoint: read /proc/self/status, refuse to start (exit 3, clear message) unless CapEff is all-zero and NoNewPrivs is 1. Converts an undocumented-until-you-read- the-seccomp-comment assumption into a loud, immediate failure. Verified with a real Docker build + run on a separate host, 4 launch scenarios: - --cap-drop ALL --security-opt no-new-privileges (contract met): starts normally. - No flags, default non-root user (10001 from the Dockerfile): blocked by the NoNewPrivs check. - --security-opt no-new-privileges only, no --cap-drop: starts -- correct, not a false block, since a non-root process already has an empty effective capability set regardless of --cap-drop. - --user root, no flags: blocked by the CapEff check, message shows the actual non-empty capability bitmask (00000000a80425fb). Local commit only -- this is an audit clone of openai/codex-security, not a fork with push access. --- docker/entrypoint.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 4eb56299..a4e79ff8 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -2,6 +2,24 @@ set -eu +# Fail closed instead of silently degrading: docker/codex-security-seccomp.json +# allows mount/pivot_root/unshare/setns/clone unconditionally, on the assumption +# (previously only a comment) that the caller runs this image with +# --cap-drop ALL --security-opt no-new-privileges (as compose.yaml does). If +# someone runs the raw image without that contract, refuse to start rather than +# let the seccomp profile's namespace/mount allowlist apply with capabilities +# or new-privilege escalation still available. +caps=$(awk '/^CapEff:/{print $2}' /proc/self/status 2>/dev/null || true) +if [ "$caps" != "0000000000000000" ]; then + printf '%s\n' "codex-security: refusing to start without --cap-drop ALL (effective capabilities are not empty: ${caps:-unknown})." >&2 + exit 3 +fi +nnp=$(awk '/^NoNewPrivs:/{print $2}' /proc/self/status 2>/dev/null || true) +if [ "$nnp" != "1" ]; then + printf '%s\n' "codex-security: refusing to start without --security-opt no-new-privileges." >&2 + exit 3 +fi + if [ "${1:-}" = bulk-scan ]; then case "${2:-}" in --help|-h) From f09a34eafa39c41ff3be35b0f4f4535b4370f09b Mon Sep 17 00:00:00 2001 From: Mauricio Perera Date: Thu, 30 Jul 2026 22:01:39 -0600 Subject: [PATCH 3/3] Address Codex review: CI hardening flags, AF_NETLINK for bwrap, drop socketcall Three fixes for the automated review findings on this PR: 1. .github/workflows/container-ci.yml: the bare `docker run` smoke-test invocations (Verify bundled scanner, Reject interactive repository discovery) never passed --cap-drop ALL --security-opt no-new-privileges, so entrypoint.sh's new fail-closed check aborted them with exit 3 before --version/help/discovery validation could run. Added the flags to match the production contract these tests are supposed to exercise. 2. docker/codex-security-seccomp.json: the AF_UNIX/AF_INET/AF_INET6-only socket() allowlist rejected AF_NETLINK, which Bubblewrap needs (a NETLINK_ROUTE socket) to run RTM_NEWLINK/RTM_NEWADDR when configuring the loopback interface inside its own isolated network namespace during codex sandbox startup. The CI workflow already silently tolerated this by retrying with --enable use_legacy_landlock; added an explicit AF_NETLINK(16) rule restricted to protocol NETLINK_ROUTE(0) so the normal Bubblewrap path succeeds without needing that fallback. 3. docker/codex-security-seccomp.json: "socketcall" was unconditionally allowed in the generic syscall list with no argument filtering. On the supported SCMP_ARCH_X86 subarchitecture, this legacy multiplexer lets a 32-bit executable open sockets (including AF_NETLINK/AF_PACKET) via socketcall's own SYS_SOCKET dispatch, bypassing the domain-restricted socket() rules entirely -- seccomp can't filter socketcall by socket domain since that lives behind a pointer argument, not a plain integer, so the safe fix is to stop allowing it unconditionally. socketpair (only ever AF_UNIX by kernel definition) stays allowed. Verified with a real Docker build + container runs on a separate host (not the CI runner, not the openai org): - Bare `docker run` (no flags): still fails closed with exit 3, unchanged. - With the added flags: --version, bulk-scan --help, info --json all exit 0; the discovery-rejection test still returns exit 2 with the expected message. - A/B on the sandbox seccomp: the pre-fix profile reproduces the exact error from the review ("bwrap: loopback: Failed to create NETLINK_ROUTE socket: Operation not permitted"); the fixed profile runs the same `codex sandbox /usr/bin/true` command to completion (exit 0) without the use_legacy_landlock fallback. - Normal HTTPS egress (AF_INET, via Node's https module) still works under the fixed profile, confirming the socketcall removal doesn't regress git-clone/API access on x86_64. --- .github/workflows/container-ci.yml | 8 ++++---- docker/codex-security-seccomp.json | 10 +++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/container-ci.yml b/.github/workflows/container-ci.yml index 8d4dc10f..accf9a23 100644 --- a/.github/workflows/container-ci.yml +++ b/.github/workflows/container-ci.yml @@ -89,9 +89,9 @@ jobs: - name: Verify bundled scanner run: | - docker run --rm codex-security:ci --version - docker run --rm codex-security:ci bulk-scan --help - docker run --rm codex-security:ci info --json + docker run --rm --cap-drop ALL --security-opt no-new-privileges codex-security:ci --version + docker run --rm --cap-drop ALL --security-opt no-new-privileges codex-security:ci bulk-scan --help + docker run --rm --cap-drop ALL --security-opt no-new-privileges codex-security:ci info --json - name: Validate hardened customer Compose configuration env: @@ -133,7 +133,7 @@ jobs: shell: bash run: | set -euo pipefail - if output="$(docker run --rm -t codex-security:ci bulk-scan 2>&1)"; then + if output="$(docker run --rm -t --cap-drop ALL --security-opt no-new-privileges codex-security:ci bulk-scan 2>&1)"; then echo "The customer container must require a repository CSV." >&2 exit 1 else diff --git a/docker/codex-security-seccomp.json b/docker/codex-security-seccomp.json index ed582e3e..dd5f7a73 100644 --- a/docker/codex-security-seccomp.json +++ b/docker/codex-security-seccomp.json @@ -323,7 +323,6 @@ "signalfd4", "sigprocmask", "sigreturn", - "socketcall", "socketpair", "splice", "stat", @@ -393,6 +392,15 @@ "action": "SCMP_ACT_ALLOW", "args": [{ "index": 0, "value": 10, "op": "SCMP_CMP_EQ" }] }, + { + "names": ["socket"], + "action": "SCMP_ACT_ALLOW", + "comment": "AF_NETLINK(16) restricted to NETLINK_ROUTE(0): Bubblewrap's Linux sandbox needs a NETLINK_ROUTE socket to run RTM_NEWLINK/RTM_NEWADDR when it configures the loopback interface inside its own (already-isolated) network namespace during codex sandbox startup.", + "args": [ + { "index": 0, "value": 16, "op": "SCMP_CMP_EQ" }, + { "index": 2, "value": 0, "op": "SCMP_CMP_EQ" } + ] + }, { "names": ["personality"], "action": "SCMP_ACT_ALLOW",