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
+55
-5
@@ -235,7 +235,8 @@ class Worker:
|
||||
repos_runner=None, heartbeat_file=None,
|
||||
worker_id=None, capacity=1, fallback_models=None,
|
||||
tenant_isolation=False, tenants_root=None,
|
||||
tenant_template_home=None, tenant_template_repo=None):
|
||||
tenant_template_home=None, tenant_template_repo=None,
|
||||
egress_proxy_port=0):
|
||||
self.db_path = str(db_path)
|
||||
self.host_py_path = host_py_path
|
||||
self.config_path = config_path
|
||||
@@ -316,6 +317,11 @@ class Worker:
|
||||
"/var/lib/codex-app-server/codex-home"
|
||||
self.tenant_template_repo = tenant_template_repo or \
|
||||
"/var/lib/codex-app-server/test-repo"
|
||||
# egress 代理端口(0 = 不启用):启用后 Docker 任务容器
|
||||
# 的 HTTP(S) 出口全部走宿主上的过滤代理(网关 IP + 该
|
||||
# 端口),代理只放行白名单域名;网关查不到时任务直接
|
||||
# failed,绝不无代理跑(无代理等于绕过白名单)。
|
||||
self.egress_proxy_port = egress_proxy_port or 0
|
||||
# 运维心跳:路径优先显式参数,其次环境变量,默认当前目录下
|
||||
# 的 worker-heartbeat.json;主机路径不在代码里硬编码,主机
|
||||
# 部署时用 --heartbeat-file 或环境变量传入(见 deploy/README)。
|
||||
@@ -1101,6 +1107,7 @@ class Worker:
|
||||
if is_docker:
|
||||
tenant_mounts = None
|
||||
tenant_network = None
|
||||
proxy_env = None
|
||||
if self.tenant_isolation:
|
||||
try:
|
||||
tenant_mounts = self._tenant_mounts(
|
||||
@@ -1131,10 +1138,34 @@ class Worker:
|
||||
"stderr": ""},
|
||||
"租户 Docker 网络创建失败: %s"
|
||||
% tenant_network)
|
||||
if self.egress_proxy_port:
|
||||
# 代理在宿主上,容器经所在网络的网关访问;
|
||||
# 租户模式用项目网络,非租户用默认 bridge。
|
||||
gw = self.executor.network_gateway(tenant_network)
|
||||
if not gw:
|
||||
self._running_clear(task_id)
|
||||
return ({"executor": "docker",
|
||||
"task_id": task_id,
|
||||
"status": "failed", "stdout": "",
|
||||
"returncode": None, "host_status": None,
|
||||
"stderr": ""},
|
||||
"egress 代理网关解析失败,拒绝无代理"
|
||||
"运行(避免绕过出口白名单)")
|
||||
proxy_url = "http://%s:%s" % (
|
||||
gw, self.egress_proxy_port)
|
||||
proxy_env = {
|
||||
"HTTP_PROXY": proxy_url,
|
||||
"HTTPS_PROXY": proxy_url,
|
||||
"http_proxy": proxy_url,
|
||||
"https_proxy": proxy_url,
|
||||
"NO_PROXY": "localhost,127.0.0.1",
|
||||
"no_proxy": "localhost,127.0.0.1",
|
||||
}
|
||||
result = self.executor.dispatch_task(
|
||||
task, timeout=effective_docker_timeout,
|
||||
extra_mounts=tenant_mounts,
|
||||
network=tenant_network)
|
||||
network=tenant_network,
|
||||
extra_env=proxy_env)
|
||||
else:
|
||||
result = self.executor.dispatch_task(task)
|
||||
combined = (result.get("stdout", "") or "") + \
|
||||
@@ -1147,7 +1178,9 @@ class Worker:
|
||||
extra_mounts=tenant_mounts
|
||||
if self.tenant_isolation else None,
|
||||
network=tenant_network
|
||||
if self.tenant_isolation else None)
|
||||
if self.tenant_isolation else None,
|
||||
extra_env=proxy_env
|
||||
if self.egress_proxy_port else None)
|
||||
else:
|
||||
result = self.executor.run_task(task)
|
||||
except Exception as exc: # runner 异常也如实写回 failed
|
||||
@@ -1737,7 +1770,9 @@ def _build_cli_executor(args, host_py):
|
||||
user=args.docker_user, worktrees_root=args.docker_worktrees_root,
|
||||
env_key=resolve_env_key(args.env_key, args.codex_config),
|
||||
timeout=args.docker_timeout,
|
||||
extra_mounts=args.docker_extra_mounts)
|
||||
extra_mounts=args.docker_extra_mounts,
|
||||
security_opts=args.docker_security_opt,
|
||||
pids_limit=args.docker_pids_limit)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
@@ -1814,6 +1849,20 @@ def main(argv=None):
|
||||
"用于容器内 host.py 写 work/、host-state.json,"
|
||||
"以及挂载原仓库和 codex-home(同路径挂载,"
|
||||
"容器内配置无需改路径)")
|
||||
parser.add_argument("--docker-security-opt", action="append",
|
||||
default=None,
|
||||
help="docker --security-opt,可重复(如"
|
||||
" apparmor=docker-codex;no-new-privileges"
|
||||
" 已硬编码,重复传也不重复)")
|
||||
parser.add_argument("--docker-pids-limit", type=int, default=512,
|
||||
help="docker --pids-limit(默认 512,防 fork 炸弹;"
|
||||
"codex 工具链正常用量远低于此)")
|
||||
parser.add_argument("--egress-proxy-port", type=int, default=0,
|
||||
help="egress 过滤代理端口(0 = 不启用;生产配"
|
||||
" 8888):启用后 Docker 任务容器的 HTTP(S)"
|
||||
" 出口全部走宿主该端口的代理,代理只放行"
|
||||
"白名单域名;网关查不到时任务直接 failed,"
|
||||
"绝不无代理运行")
|
||||
parser.add_argument("--task-timeout", type=float, default=1800,
|
||||
help="每任务截止时间(秒,默认 1800);超时后真杀"
|
||||
"容器/进程组、任务置 failed;与 --docker-timeout"
|
||||
@@ -1862,7 +1911,8 @@ def main(argv=None):
|
||||
tenant_isolation=args.tenant_isolation,
|
||||
tenants_root=args.tenants_root,
|
||||
tenant_template_home=args.tenant_template_home,
|
||||
tenant_template_repo=args.tenant_template_repo)
|
||||
tenant_template_repo=args.tenant_template_repo,
|
||||
egress_proxy_port=args.egress_proxy_port)
|
||||
# CLI 启动清理兜底(常驻与单次都做):清掉上一轮残留容器。
|
||||
worker.cleanup_stale_containers()
|
||||
# CLI 起 worker 前先建表/迁移:全新库直接起循环不应因缺表而炸。
|
||||
|
||||
Reference in new issue
Block a user