Skip to content

Commit eca429a

Browse files
committed
security(container-host-isolation): add docker security config
1 parent f230516 commit eca429a

File tree

7 files changed

+142
-1
lines changed

7 files changed

+142
-1
lines changed

server/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ cp example.config.toml ~/.sandbox.toml
9393
network_mode = "bridge" # Isolated container networking
9494
```
9595

96+
**Security hardening (applies to all Docker modes)**
97+
```toml
98+
[docker]
99+
# Drop dangerous capabilities and block privilege escalation by default
100+
drop_capabilities = ["AUDIT_WRITE", "MKNOD", "NET_ADMIN", "NET_RAW", "SYS_ADMIN", "SYS_MODULE", "SYS_PTRACE", "SYS_TIME", "SYS_TTY_CONFIG"]
101+
no_new_privileges = true
102+
apparmor_profile = "" # e.g. "docker-default" when AppArmor is available
103+
# Limit fork bombs and optionally enforce seccomp / read-only rootfs
104+
pids_limit = 512 # set to null to disable
105+
seccomp_profile = "" # path or profile name; empty uses Docker default
106+
read_only_rootfs = false # enable for stricter isolation if your image allows it
107+
```
108+
Further reading on Docker container security: https://docs.docker.com/engine/security/
109+
96110
### Run the server
97111

98112
Start the server using `uv`:
@@ -121,6 +135,8 @@ Once the server is running, interactive API documentation is available:
121135
- **Swagger UI**: [http://localhost:8080/docs](http://localhost:8080/docs)
122136
- **ReDoc**: [http://localhost:8080/redoc](http://localhost:8080/redoc)
123137

138+
Further reading on Docker container security: https://docs.docker.com/engine/security/
139+
124140
### API authentication
125141

126142
Authentication is enforced only when `server.api_key` is set. If the value is empty or missing, the middleware skips API Key checks (intended for local/dev). For production, always set a non-empty `server.api_key` and send it via the `OPEN-SANDBOX-API-KEY` header.

server/README_zh.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,21 @@ execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd
9797
network_mode = "bridge" # 容器隔离网络
9898
```
9999

100+
**安全加固(适用于所有 Docker 模式)**
101+
```toml
102+
[docker]
103+
# 默认关闭危险能力、防止提权
104+
drop_capabilities = ["AUDIT_WRITE", "MKNOD", "NET_ADMIN", "NET_RAW", "SYS_ADMIN", "SYS_MODULE", "SYS_PTRACE", "SYS_TIME", "SYS_TTY_CONFIG"]
105+
no_new_privileges = true
106+
# 当宿主机启用了 AppArmor 时,可指定策略名称(如 "docker-default");否则留空
107+
apparmor_profile = ""
108+
# 限制进程数量,可选的 seccomp/只读根文件系统
109+
pids_limit = 512 # 设为 null 可关闭
110+
seccomp_profile = "" # 配置文件路径或名称;为空使用 Docker 默认
111+
read_only_rootfs = false # 如果镜像允许写 /,可开启以进一步隔离
112+
```
113+
更多 Docker 安全参考:https://docs.docker.com/engine/security/
114+
100115
### 启动服务
101116

102117
使用 `uv` 启动服务:

server/example.config.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,19 @@ execd_image = "opensandbox/execd:latest"
3737
[docker]
3838
# Docker-specific knobs
3939
# -----------------------------------------------------------------
40-
# Supported values for network_mode: "host", "bridge"
40+
# Use bridge for network isolation
4141
network_mode = "bridge"
42+
# Drop dangerous capabilities and block privilege escalation
43+
drop_capabilities = ["AUDIT_WRITE", "MKNOD", "NET_ADMIN", "NET_RAW", "SYS_ADMIN", "SYS_MODULE", "SYS_PTRACE", "SYS_TIME", "SYS_TTY_CONFIG"]
44+
no_new_privileges = true
45+
# Optional: set an AppArmor profile name (e.g., "docker-default") when AppArmor is enabled
46+
apparmor_profile = ""
47+
# Limit process count to reduce host impact from fork bombs; set to null to disable
48+
pids_limit = 512
49+
# Seccomp profile: empty string uses Docker default; set to an absolute path for a custom profile
50+
seccomp_profile = ""
51+
# Make root filesystem read-only to avoid host writes via mounted volumes; disable if your image needs write access to /
52+
read_only_rootfs = true
4253

4354

4455
# -----------------------------------------------------------------

server/example.config.zh.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd
3939
# -----------------------------------------------------------------
4040
# Supported values for network_mode: "host", "bridge"
4141
network_mode = "bridge"
42+
# Drop dangerous capabilities and block privilege escalation
43+
drop_capabilities = ["AUDIT_WRITE", "MKNOD", "NET_ADMIN", "NET_RAW", "SYS_ADMIN", "SYS_MODULE", "SYS_PTRACE", "SYS_TIME", "SYS_TTY_CONFIG"]
44+
no_new_privileges = true
45+
# Optional: set an AppArmor profile name (e.g., "docker-default") when AppArmor is enabled
46+
apparmor_profile = ""
47+
# Limit process count to reduce host impact from fork bombs; set to null to disable
48+
pids_limit = 512
49+
# Seccomp profile: empty string uses Docker default; set to an absolute path for a custom profile
50+
seccomp_profile = ""
51+
# Make root filesystem read-only to avoid host writes via mounted volumes; disable if your image needs write access to /
52+
read_only_rootfs = true
4253

4354

4455
# -----------------------------------------------------------------

server/src/config.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,47 @@ class DockerConfig(BaseModel):
127127
default="host",
128128
description="Docker network mode for sandbox containers (host, bridge, ...).",
129129
)
130+
drop_capabilities: list[str] = Field(
131+
default_factory=lambda: [
132+
"AUDIT_WRITE",
133+
"MKNOD",
134+
"NET_ADMIN",
135+
"NET_RAW",
136+
"SYS_ADMIN",
137+
"SYS_MODULE",
138+
"SYS_PTRACE",
139+
"SYS_TIME",
140+
"SYS_TTY_CONFIG",
141+
],
142+
description=(
143+
"Linux capabilities to drop from sandbox containers. Defaults to a conservative set to reduce host impact."
144+
),
145+
)
146+
apparmor_profile: Optional[str] = Field(
147+
default=None,
148+
description=(
149+
"Optional AppArmor profile name applied to sandbox containers. Leave unset to let Docker choose the default."
150+
),
151+
)
152+
no_new_privileges: bool = Field(
153+
default=True,
154+
description="Enable the kernel no_new_privileges flag to block privilege escalation inside the container.",
155+
)
156+
seccomp_profile: Optional[str] = Field(
157+
default=None,
158+
description=(
159+
"Optional seccomp profile name or path applied to sandbox containers. Leave unset to use Docker's default profile."
160+
),
161+
)
162+
pids_limit: Optional[int] = Field(
163+
default=512,
164+
ge=1,
165+
description="Maximum number of processes allowed per sandbox container. Set to null to disable the limit.",
166+
)
167+
read_only_rootfs: bool = Field(
168+
default=False,
169+
description="Mount the container root filesystem as read-only. Disable if images need write access to /.",
170+
)
130171

131172

132173
class AppConfig(BaseModel):

server/src/services/docker.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,22 @@ def _provision_sandbox(
785785
nano_cpus = parse_nano_cpus(resource_limits.get("cpu"))
786786

787787
host_config_kwargs: Dict[str, Any] = {"network_mode": self.network_mode}
788+
security_opts: list[str] = []
789+
docker_cfg = self.app_config.docker
790+
if docker_cfg.no_new_privileges:
791+
security_opts.append("no-new-privileges:true")
792+
if docker_cfg.apparmor_profile:
793+
security_opts.append(f"apparmor={docker_cfg.apparmor_profile}")
794+
if docker_cfg.seccomp_profile:
795+
security_opts.append(f"seccomp={docker_cfg.seccomp_profile}")
796+
if security_opts:
797+
host_config_kwargs["security_opt"] = security_opts
798+
if docker_cfg.drop_capabilities:
799+
host_config_kwargs["cap_drop"] = docker_cfg.drop_capabilities
800+
if docker_cfg.pids_limit is not None:
801+
host_config_kwargs["pids_limit"] = docker_cfg.pids_limit
802+
if docker_cfg.read_only_rootfs:
803+
host_config_kwargs["read_only"] = True
788804
if mem_limit:
789805
host_config_kwargs["mem_limit"] = mem_limit
790806
if nano_cpus:

server/tests/test_docker_service.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,37 @@ def test_env_allows_empty_string_and_skips_none():
8888
assert all(not item.startswith("NONE=") for item in environment)
8989

9090

91+
@patch("src.services.docker.docker")
92+
def test_create_sandbox_applies_security_defaults(mock_docker):
93+
mock_client = MagicMock()
94+
mock_client.containers.list.return_value = []
95+
mock_client.api.create_host_config.return_value = {"host": "cfg"}
96+
mock_client.api.create_container.return_value = {"Id": "cid"}
97+
mock_client.containers.get.return_value = MagicMock()
98+
mock_docker.from_env.return_value = mock_client
99+
100+
service = DockerSandboxService(config=_app_config())
101+
request = CreateSandboxRequest(
102+
image=ImageSpec(uri="python:3.11"),
103+
timeout=120,
104+
resourceLimits=ResourceLimits(root={}),
105+
env={},
106+
metadata={},
107+
entrypoint=["python"],
108+
)
109+
110+
with patch.object(service, "_ensure_image_available"), patch.object(
111+
service, "_prepare_sandbox_runtime"
112+
):
113+
service.create_sandbox(request)
114+
115+
host_kwargs = mock_client.api.create_host_config.call_args.kwargs
116+
assert "no-new-privileges:true" in host_kwargs["security_opt"]
117+
assert host_kwargs["cap_drop"] == service.app_config.docker.drop_capabilities
118+
assert host_kwargs["pids_limit"] == service.app_config.docker.pids_limit
119+
assert mock_client.api.create_container.call_args.kwargs["host_config"] == {"host": "cfg"}
120+
121+
91122
@patch("src.services.docker.docker")
92123
def test_create_sandbox_rejects_invalid_metadata(mock_docker):
93124
mock_client = MagicMock()

0 commit comments

Comments
 (0)