Outer hardening: AppArmor profile, pids limit, egress allowlist proxy
Inner codex sandbox stays danger-full-access by choice (bwrap path is a separate decision); this hardens everything around it: - DockerExecutor: --security-opt plumbing (repeatable, dedups no-new-privileges), --pids-limit (CLI default 512, fork-bomb guard), per-call extra_env (-e NAME=VALUE, non-secrets only, e.g. proxy URLs; the model key still goes by -e NAME only). - Worker: --egress-proxy-port (0 = off). When on, Docker task containers get HTTP(S)_PROXY pointing at the host filtering proxy via their network gateway IP (tenant network gateway or default bridge gateway). Gateway unresolvable => the attempt fails instead of running unproxied (fail closed, same rule as tenant network creation failure). - AppArmor profile deploy/apparmor/docker-codex: docker-default base + deny network raw/packet, deny ptrace, deny mount/ umount/pivot_root, deny dangerous capabilities, deny writes to /proc/sys, /sys, /proc/keys. Deliberately NO userns rule (that would enable the bwrap path, not this one). File rules mirror docker-default (broad + sensitive denies); a strict file allowlist would need profiling codex's file access set and is not claimed here. - Egress proxy deploy/egress/: Tinyproxy with FilterDefaultDeny + anchored domain allowlist (relay, github, pypi, npm). Anchoring matters: an unanchored 'github.com' regex would also match 'evilgithub.com'. - Seccomp honesty (README): Docker default seccomp already enforces on all task containers; no new stricter allowlist is claimed in this change (building one from memory risks silently breaking the toolchain; it needs syscall profiling first). Tests: 3 outer-hardening tests (security-opt dedup, pids limit, proxy env without leaking the key value, gateway parse/fail, proxy fail-closed). Full suites pass.
This commit is contained in:
1 parent
4f7c7eb13e
commit
1e4623c61c
8 files changed
+399
-18
No files matched your search
@@ -0,0 +1,78 @@
|
||||
# AppArmor profile for codex task containers (outer hardening).
|
||||
#
|
||||
# Based on Docker's default container profile, with stricter denies:
|
||||
# - deny network raw: no raw sockets (ping/scan/sniff tricks)
|
||||
# - deny ptrace: no process injection / debugging other processes
|
||||
# - deny mount/umount: containers must never mount (bwrap path is
|
||||
# NOT enabled here; inner sandbox stays danger-full-access by
|
||||
# choice, so mount inside the container is pure attack surface)
|
||||
# - deny dangerous capabilities (defense in depth; containers also
|
||||
# run with --cap-drop=ALL, so these are a second lock, not the
|
||||
# only one)
|
||||
# - deny writes to /proc/sys, /sys, /proc/keys (kernel tunables)
|
||||
#
|
||||
# Deliberately NOT included: `userns` (that would enable the bwrap
|
||||
# inner-sandbox path, which is a separate decision, not this one).
|
||||
#
|
||||
# Install: cp to /etc/apparmor.d/docker-codex, then
|
||||
# apparmor_parser -r /etc/apparmor.d/docker-codex
|
||||
# Use: docker --security-opt apparmor=docker-codex
|
||||
# (worker flag: --docker-security-opt apparmor=docker-codex)
|
||||
# Verify: docker inspect <container> shows
|
||||
# "AppArmorProfile": "docker-codex" (not "docker-default").
|
||||
|
||||
#include <tunables/global>
|
||||
|
||||
profile docker-codex flags=(attach_disconnected,mediate_deleted) {
|
||||
#include <abstractions/base>
|
||||
|
||||
network inet tcp,
|
||||
network inet udp,
|
||||
network inet icmp,
|
||||
network inet6 tcp,
|
||||
network inet6 udp,
|
||||
network inet6 icmp,
|
||||
deny network raw,
|
||||
deny network packet,
|
||||
|
||||
capability chown,
|
||||
capability dac_override,
|
||||
capability fsetid,
|
||||
capability fowner,
|
||||
capability mknod,
|
||||
capability net_bind_service,
|
||||
capability setfcap,
|
||||
capability setgid,
|
||||
capability setpcap,
|
||||
capability setuid,
|
||||
capability sys_chroot,
|
||||
deny capability sys_admin,
|
||||
deny capability sys_ptrace,
|
||||
deny capability net_admin,
|
||||
deny capability net_raw,
|
||||
deny capability sys_module,
|
||||
deny capability sys_rawio,
|
||||
deny capability sys_boot,
|
||||
deny capability sys_time,
|
||||
deny capability mac_admin,
|
||||
deny capability mac_override,
|
||||
|
||||
deny mount,
|
||||
deny umount,
|
||||
deny 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),
|
||||
}
|
||||
@@ -14,7 +14,7 @@ EnvironmentFile=/etc/codex-app-server/env
|
||||
# scaler 会按实例强制追加;--db 只给 scaler,scaler 会补给 worker。
|
||||
# 注意:启用 autoscaler 后必须停掉静态 codex-cloud-worker,
|
||||
# 否则两边同时派任务(scaler 只管理 autoscale-* 实例)。
|
||||
ExecStart=/usr/bin/python3 -m cloud.autoscaler --db /var/lib/codex-app-server/control.db --interval 30 --min-workers 1 --max-workers 2 --per-worker-capacity 2 --scale-down-after 3 --drain-timeout 60 --state-file /var/lib/codex-app-server/autoscaler-state.json --log-dir /var/lib/codex-app-server/autoscaler-logs -- --interval 10 --config /opt/codex-app-server/host-config-docker.json --executor docker --docker-image codex-app-server:latest --docker-user 1003:1004 --docker-cpus 2 --docker-memory 2g --docker-extra-mounts /opt/codex-app-server/work:/app/work,/var/lib/codex-app-server/codex-home:/var/lib/codex-app-server/codex-home,/var/lib/codex-app-server/repos:/var/lib/codex-app-server/repos,/var/lib/codex-app-server/test-repo:/var/lib/codex-app-server/test-repo --repos-dir /var/lib/codex-app-server/repos --tenant-isolation --tenants-root /var/lib/codex-app-server/tenants --tenant-template-home /var/lib/codex-app-server/codex-home --tenant-template-repo /var/lib/codex-app-server/test-repo
|
||||
ExecStart=/usr/bin/python3 -m cloud.autoscaler --db /var/lib/codex-app-server/control.db --interval 30 --min-workers 1 --max-workers 2 --per-worker-capacity 2 --scale-down-after 3 --drain-timeout 60 --state-file /var/lib/codex-app-server/autoscaler-state.json --log-dir /var/lib/codex-app-server/autoscaler-logs -- --interval 10 --config /opt/codex-app-server/host-config-docker.json --executor docker --docker-image codex-app-server:latest --docker-user 1003:1004 --docker-cpus 2 --docker-memory 2g --docker-extra-mounts /opt/codex-app-server/work:/app/work,/var/lib/codex-app-server/codex-home:/var/lib/codex-app-server/codex-home,/var/lib/codex-app-server/repos:/var/lib/codex-app-server/repos,/var/lib/codex-app-server/test-repo:/var/lib/codex-app-server/test-repo --repos-dir /var/lib/codex-app-server/repos --tenant-isolation --tenants-root /var/lib/codex-app-server/tenants --tenant-template-home /var/lib/codex-app-server/codex-home --tenant-template-repo /var/lib/codex-app-server/test-repo --docker-security-opt apparmor=docker-codex --docker-pids-limit 512 --egress-proxy-port 8888
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
# 停 scaler 时要等它把 worker 优雅排空(drain 60 秒 + 缓冲),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Tinyproxy Filter lines are extended regexes, matched as substrings
|
||||
# unless anchored. A bare "github.com" would also match
|
||||
# "evilgithub.com", so every entry is anchored to the domain and
|
||||
# its subdomains only.
|
||||
^(.+\.)?xcmapi\.org$
|
||||
^(.+\.)?github\.com$
|
||||
^(.+\.)?githubusercontent\.com$
|
||||
^(.+\.)?pypi\.org$
|
||||
^(.+\.)?pythonhosted\.org$
|
||||
^(.+\.)?npmjs\.org$
|
||||
@@ -0,0 +1,52 @@
|
||||
# Tinyproxy egress allowlist for codex task containers.
|
||||
#
|
||||
# Only domains in the Filter file are reachable. Everything else
|
||||
# gets 403 from the proxy (fail closed: a task that needs a new
|
||||
# domain fails loudly instead of silently bypassing the list).
|
||||
#
|
||||
# Domains (one per line in tinyproxy-filter):
|
||||
# - xcmapi.org: model relay (the only model endpoint tasks need)
|
||||
# - github.com, *.githubusercontent.com: git clone/fetch
|
||||
# - pypi.org, files.pythonhosted.org: pip install
|
||||
# - registry.npmjs.org: npm install
|
||||
# Add a domain here + reload tinyproxy when a legitimate task need
|
||||
# appears; never add a wildcard like * (that voids the list).
|
||||
#
|
||||
# Install on host:
|
||||
# apt-get install -y tinyproxy
|
||||
# cp tinyproxy.conf /etc/tinyproxy/tinyproxy.conf
|
||||
# cp tinyproxy-filter /etc/tinyproxy/tinyproxy-filter
|
||||
# systemctl enable tinyproxy && systemctl restart tinyproxy
|
||||
# Worker: --egress-proxy-port 8888 (containers reach the proxy via
|
||||
# their network gateway IP, e.g. http://172.18.0.1:8888).
|
||||
# Firewall (host, restricts WHO can use the proxy to containers):
|
||||
# iptables -A INPUT -p tcp --dport 8888 -s 172.16.0.0/12 -j ACCEPT
|
||||
# iptables -A INPUT -p tcp --dport 8888 -s 127.0.0.1 -j ACCEPT
|
||||
# iptables -A INPUT -p tcp --dport 8888 -j DROP
|
||||
# (Docker bridge subnets live in 172.16.0.0/12; the host LAN is
|
||||
# NOT in that range, so LAN hosts cannot use the proxy.)
|
||||
|
||||
User tinyproxy
|
||||
Group tinyproxy
|
||||
Port 8888
|
||||
Listen 0.0.0.0
|
||||
Timeout 600
|
||||
LogLevel Info
|
||||
PidFile "/run/tinyproxy/tinyproxy.pid"
|
||||
MaxClients 100
|
||||
MinSpareServers 5
|
||||
MaxSpareServers 20
|
||||
StartServers 10
|
||||
DefaultErrorFile "/usr/share/tinyproxy/default.html"
|
||||
StatFile "/usr/share/tinyproxy/stats.html"
|
||||
# CONNECT is required for HTTPS (relay + git are HTTPS).
|
||||
ConnectPort 443
|
||||
ConnectPort 80
|
||||
# Allowlist: deny everything not in the filter file.
|
||||
FilterDefaultDeny Yes
|
||||
Filter "/etc/tinyproxy/tinyproxy-filter"
|
||||
# Upstream: direct (no parent proxy). The host's own egress is
|
||||
# unrestricted; only container traffic goes through this proxy.
|
||||
Upstream none
|
||||
# Do not forward proxy headers that leak container info.
|
||||
DisableViaHeader Yes
|
||||
Reference in new issue
Block a user