opensandbox-server 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. opensandbox_server-0.2.1/PKG-INFO +43 -0
  2. opensandbox_server-0.2.1/README.md +4 -0
  3. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/api/schema.py +13 -4
  4. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/examples/example.config.k8s.toml +2 -2
  5. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/examples/example.config.k8s.zh.toml +2 -2
  6. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/examples/example.config.toml +2 -2
  7. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/examples/example.config.zh.toml +2 -2
  8. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/extensions/__init__.py +4 -0
  9. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/extensions/keys.py +9 -0
  10. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/constants.py +16 -0
  11. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/docker_service.py +106 -23
  12. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/networking.py +10 -2
  13. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/runtime.py +36 -0
  14. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/helpers.py +35 -1
  15. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/agent_sandbox_provider.py +32 -7
  16. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/batchsandbox_provider.py +29 -7
  17. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/client.py +54 -0
  18. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/create_helpers.py +32 -0
  19. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/egress_helper.py +12 -7
  20. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/kubernetes_service.py +506 -58
  21. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/provider_common.py +34 -2
  22. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/security_context.py +14 -0
  23. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/windows_profile.py +16 -4
  24. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/workload_provider.py +3 -0
  25. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/runtime_resolver.py +17 -1
  26. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/validators.py +38 -1
  27. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/pyproject.toml +6 -4
  28. opensandbox_server-0.2.0/PKG-INFO +0 -327
  29. opensandbox_server-0.2.0/README.md +0 -290
  30. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/.gitignore +0 -0
  31. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/LICENSE +0 -0
  32. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/__init__.py +0 -0
  33. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/api/__init__.py +0 -0
  34. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/api/devops.py +0 -0
  35. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/api/lifecycle.py +0 -0
  36. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/api/pool.py +0 -0
  37. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/api/proxy.py +0 -0
  38. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/cli.py +0 -0
  39. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/config.py +0 -0
  40. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/examples/.gitkeep +0 -0
  41. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/examples/__init__.py +0 -0
  42. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/examples/e2e.batchsandbox-template.yaml +0 -0
  43. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/examples/example.batchsandbox-template.yaml +0 -0
  44. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/extensions/codec.py +0 -0
  45. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/extensions/validation.py +0 -0
  46. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/__init__.py +0 -0
  47. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/renew_intent/__init__.py +0 -0
  48. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/renew_intent/constants.py +0 -0
  49. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/renew_intent/consumer.py +0 -0
  50. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/renew_intent/controller.py +0 -0
  51. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/renew_intent/intent.py +0 -0
  52. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/renew_intent/logutil.py +0 -0
  53. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/renew_intent/proxy_renew.py +0 -0
  54. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/renew_intent/redis_client.py +0 -0
  55. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/integrations/renew_intent/runner.py +0 -0
  56. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/logging_config.py +0 -0
  57. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/main.py +0 -0
  58. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/middleware/__init__.py +0 -0
  59. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/middleware/auth.py +0 -0
  60. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/middleware/request_id.py +0 -0
  61. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/py.typed +0 -0
  62. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/repositories/__init__.py +0 -0
  63. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/repositories/snapshots/__init__.py +0 -0
  64. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/repositories/snapshots/factory.py +0 -0
  65. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/repositories/snapshots/sqlite.py +0 -0
  66. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/__init__.py +0 -0
  67. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/__init__.py +0 -0
  68. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/container_ops.py +0 -0
  69. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/docker_diagnostics.py +0 -0
  70. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/metadata.py +0 -0
  71. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/ossfs_mixin.py +0 -0
  72. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/port_allocator.py +0 -0
  73. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/snapshot_runtime.py +0 -0
  74. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/volumes.py +0 -0
  75. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/docker/windows_profile.py +0 -0
  76. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/endpoint_auth.py +0 -0
  77. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/extension_service.py +0 -0
  78. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/factory.py +0 -0
  79. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/__init__.py +0 -0
  80. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/agent_sandbox_template.py +0 -0
  81. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/batchsandbox_template.py +0 -0
  82. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/endpoint_resolver.py +0 -0
  83. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/error_helpers.py +0 -0
  84. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/image_pull_secret_helper.py +0 -0
  85. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/informer.py +0 -0
  86. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/k8s_diagnostics.py +0 -0
  87. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/label_selector.py +0 -0
  88. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/list_helpers.py +0 -0
  89. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/pool_service.py +0 -0
  90. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/provider_factory.py +0 -0
  91. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/rate_limiter.py +0 -0
  92. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/snapshot_runtime.py +0 -0
  93. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/status_helpers.py +0 -0
  94. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/template_manager.py +0 -0
  95. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/volume_helper.py +0 -0
  96. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/workload_access.py +0 -0
  97. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/k8s/workload_mapper.py +0 -0
  98. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/sandbox_service.py +0 -0
  99. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/signing.py +0 -0
  100. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/snapshot_models.py +0 -0
  101. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/snapshot_repository.py +0 -0
  102. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/snapshot_restore.py +0 -0
  103. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/snapshot_runtime.py +0 -0
  104. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/snapshot_runtime_factory.py +0 -0
  105. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/snapshot_service.py +0 -0
  106. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/services/windows_common.py +0 -0
  107. {opensandbox_server-0.2.0 → opensandbox_server-0.2.1}/opensandbox_server/startup_guard.py +0 -0
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: opensandbox-server
3
+ Version: 0.2.1
4
+ Summary: FastAPI control plane for OpenSandbox that manages sandbox lifecycle on Docker (ready) and Kubernetes (planned) runtimes.
5
+ Project-URL: Homepage, https://github.com/opensandbox-group/OpenSandbox
6
+ Project-URL: Repository, https://github.com/opensandbox-group/OpenSandbox
7
+ Project-URL: Issues, https://github.com/opensandbox-group/OpenSandbox/issues
8
+ Author-email: OpenSandbox Team <pangjiping.pjp@alibaba-inc.com>
9
+ License: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: control-plane,fastapi,opensandbox,sandbox,server
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: docker
26
+ Requires-Dist: fastapi>=0.137.0
27
+ Requires-Dist: httpx[socks]
28
+ Requires-Dist: kubernetes
29
+ Requires-Dist: pydantic
30
+ Requires-Dist: pydantic-settings
31
+ Requires-Dist: python-multipart>=0.0.30
32
+ Requires-Dist: pyyaml
33
+ Requires-Dist: redis>=5
34
+ Requires-Dist: starlette>=1.3.1
35
+ Requires-Dist: tomli; python_version < '3.11'
36
+ Requires-Dist: uvicorn[standard]
37
+ Requires-Dist: websockets>=14.0
38
+ Description-Content-Type: text/markdown
39
+
40
+ # OpenSandbox Server
41
+
42
+ Documentation: [docs/components/server.md](../docs/components/server.md)
43
+
@@ -0,0 +1,4 @@
1
+ # OpenSandbox Server
2
+
3
+ Documentation: [docs/components/server.md](../docs/components/server.md)
4
+
@@ -198,9 +198,9 @@ class PVC(BaseModel):
198
198
  description=(
199
199
  "When true, the volume is automatically removed when the sandbox is "
200
200
  "deleted. Only applies to volumes that were auto-created by the server "
201
- "(Docker only). Pre-existing volumes are never removed. Has no effect "
202
- "on Kubernetes PVCs, whose lifecycle is managed by the StorageClass "
203
- "reclaim policy."
201
+ "on this request; pre-existing volumes are never removed. For "
202
+ "Kubernetes, the resulting PVC delete then triggers the bound PV's "
203
+ "StorageClass reclaim policy (Retain/Delete)."
204
204
  ),
205
205
  )
206
206
 
@@ -424,7 +424,16 @@ class CreateSandboxRequest(BaseModel):
424
424
  resource_limits: Optional[ResourceLimits] = Field(
425
425
  None,
426
426
  alias="resourceLimits",
427
- description="Runtime resource constraints for the sandbox instance. Optional when poolRef is provided.",
427
+ description="Runtime resource constraints (hard caps) for the sandbox instance. Optional when poolRef is provided.",
428
+ )
429
+ resource_requests: Optional[ResourceLimits] = Field(
430
+ None,
431
+ alias="resourceRequests",
432
+ description=(
433
+ "Resource reservations (guaranteed minimums) for the sandbox instance. "
434
+ "When provided, used as Kubernetes resource requests, enabling Burstable QoS. "
435
+ "When omitted, resourceLimits values are used for both limits and requests."
436
+ ),
428
437
  )
429
438
  env: Optional[Dict[str, Optional[str]]] = Field(
430
439
  None,
@@ -32,7 +32,7 @@ level = "INFO"
32
32
 
33
33
  [runtime]
34
34
  type = "kubernetes"
35
- execd_image = "opensandbox/execd:v1.0.19"
35
+ execd_image = "opensandbox/execd:v1.0.20"
36
36
 
37
37
  [storage]
38
38
  # Allowlist of host path prefixes permitted for bind mounts.
@@ -75,7 +75,7 @@ batchsandbox_template_file = "~/batchsandbox-template.yaml"
75
75
  mode = "direct"
76
76
 
77
77
  [egress]
78
- image = "opensandbox/egress:v1.1.0"
78
+ image = "opensandbox/egress:v1.1.3"
79
79
  mode = "dns"
80
80
  # Default is true (recommended for dual-stack CNI). Set false only if you need IPv6 in the netns (see server/configuration.md).
81
81
  # disable_ipv6 = false
@@ -32,7 +32,7 @@ level = "INFO"
32
32
 
33
33
  [runtime]
34
34
  type = "kubernetes"
35
- execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:v1.0.19"
35
+ execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:v1.0.20"
36
36
 
37
37
  [storage]
38
38
  # 允许进行 bind mount 的宿主机路径前缀白名单。
@@ -76,7 +76,7 @@ batchsandbox_template_file = "~/batchsandbox-template.yaml"
76
76
  mode = "direct"
77
77
 
78
78
  [egress]
79
- image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:v1.1.0"
79
+ image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:v1.1.3"
80
80
  mode = "dns"
81
81
  # Default is true (recommended for dual-stack CNI). Set false only if you need IPv6 in the netns (see server/configuration.md).
82
82
  # disable_ipv6 = false
@@ -32,7 +32,7 @@ level = "INFO"
32
32
 
33
33
  [runtime]
34
34
  type = "docker"
35
- execd_image = "opensandbox/execd:v1.0.19"
35
+ execd_image = "opensandbox/execd:v1.0.20"
36
36
 
37
37
  [storage]
38
38
  # Allowlist of host path prefixes permitted for bind mounts.
@@ -63,7 +63,7 @@ seccomp_profile = ""
63
63
  mode = "direct"
64
64
 
65
65
  [egress]
66
- image = "opensandbox/egress:v1.1.0"
66
+ image = "opensandbox/egress:v1.1.3"
67
67
  mode = "dns"
68
68
 
69
69
  # 🧪 [EXPERIMENTAL] Renew-on-access. Off by default — see server/README.md.
@@ -32,7 +32,7 @@ level = "INFO"
32
32
 
33
33
  [runtime]
34
34
  type = "docker"
35
- execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:v1.0.19"
35
+ execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:v1.0.20"
36
36
 
37
37
  [storage]
38
38
  allowed_host_paths = []
@@ -61,7 +61,7 @@ seccomp_profile = ""
61
61
  mode = "direct"
62
62
 
63
63
  [egress]
64
- image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:v1.1.0"
64
+ image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:v1.1.3"
65
65
  mode = "dns"
66
66
 
67
67
  # 🧪 [EXPERIMENTAL] 按访问续期。默认关闭 — 见 server/README_zh.md。
@@ -23,8 +23,10 @@ from opensandbox_server.extensions.codec import (
23
23
  from opensandbox_server.extensions.keys import (
24
24
  ACCESS_RENEW_EXTEND_SECONDS_KEY,
25
25
  ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY,
26
+ BOOTSTRAP_EXECD_ISOLATION_KEY,
26
27
  EXTENSIONS_ANNOTATION_PREFIX,
27
28
  ANNOTATION_METADATA_PREFIX,
29
+ ISOLATION_UPPER_MOUNT_PATH,
28
30
  )
29
31
  from opensandbox_server.extensions.validation import (
30
32
  ACCESS_RENEW_EXTEND_SECONDS_MAX,
@@ -42,4 +44,6 @@ __all__ = [
42
44
  "apply_extensions_to_annotations",
43
45
  "EXTENSIONS_ANNOTATION_PREFIX",
44
46
  "ANNOTATION_METADATA_PREFIX",
47
+ "BOOTSTRAP_EXECD_ISOLATION_KEY",
48
+ "ISOLATION_UPPER_MOUNT_PATH",
45
49
  ]
@@ -18,6 +18,15 @@
18
18
  ACCESS_RENEW_EXTEND_SECONDS_KEY = "access.renew.extend.seconds"
19
19
  ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY = "opensandbox.io/access-renew-extend-seconds"
20
20
 
21
+ # bootstrap.execd.isolation extension key
22
+ # When set to "enable", the sandbox container receives CAP_SYS_ADMIN so that
23
+ # execd can use bwrap namespace isolation (OSEP-0013).
24
+ BOOTSTRAP_EXECD_ISOLATION_KEY = "bootstrap.execd.isolation"
25
+
26
+ # Default mount path for the isolation upper directory.
27
+ # Must match the execd DefaultConfig().UpperRoot value.
28
+ ISOLATION_UPPER_MOUNT_PATH = "/var/lib/execd/isolation"
29
+
21
30
  # Extensions to annotations transformation prefix
22
31
  # User-specified extension keys starting with EXTENSIONS_ANNOTATION_PREFIX
23
32
  # are automatically propagated to Pod annotations with ANNOTATION_METADATA_PREFIX
@@ -42,6 +42,19 @@ EGRESS_MODE_ENV = "OPENSANDBOX_EGRESS_MODE"
42
42
  # Must match components/egress/pkg/constants/configuration.go EnvEgressToken
43
43
  OPENSANDBOX_EGRESS_TOKEN = "OPENSANDBOX_EGRESS_TOKEN"
44
44
  OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT = "OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT"
45
+
46
+ EGRESS_ENV_PREFIX = "OPENSANDBOX_EGRESS_"
47
+ OPENSANDBOX_EGRESS_MITMPROXY_SSL_INSECURE = "OPENSANDBOX_EGRESS_MITMPROXY_SSL_INSECURE"
48
+ ALLOWED_EGRESS_ENV_VARS = frozenset({
49
+ "OPENSANDBOX_EGRESS_LOG_LEVEL",
50
+ "OPENSANDBOX_EGRESS_DNS_UPSTREAM_TIMEOUT",
51
+ OPENSANDBOX_EGRESS_MITMPROXY_SSL_INSECURE,
52
+ OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT,
53
+ "OPENSANDBOX_EGRESS_MITMPROXY_SCRIPT",
54
+ "OPENSANDBOX_EGRESS_DENY_WEBHOOK",
55
+ "OPENSANDBOX_EGRESS_METRICS_EXTRA_ATTRS",
56
+ "OPENSANDBOX_EGRESS_CREDENTIAL_VAULT_REQUIRE_TLS",
57
+ })
45
58
  OPENSANDBOX_RUNTIME_VOLUME_NAME = "opensandbox-bin"
46
59
  OPENSANDBOX_RUNTIME_MOUNT_PATH = "/opt/opensandbox"
47
60
 
@@ -146,6 +159,9 @@ __all__ = [
146
159
  "EGRESS_MODE_ENV",
147
160
  "OPENSANDBOX_EGRESS_TOKEN",
148
161
  "OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT",
162
+ "EGRESS_ENV_PREFIX",
163
+ "OPENSANDBOX_EGRESS_MITMPROXY_SSL_INSECURE",
164
+ "ALLOWED_EGRESS_ENV_VARS",
149
165
  "OPENSANDBOX_RUNTIME_VOLUME_NAME",
150
166
  "OPENSANDBOX_RUNTIME_MOUNT_PATH",
151
167
  "SandboxErrorCodes",
@@ -39,6 +39,8 @@ from fastapi import HTTPException, status
39
39
 
40
40
  from opensandbox_server.extensions import (
41
41
  ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY,
42
+ BOOTSTRAP_EXECD_ISOLATION_KEY,
43
+ ISOLATION_UPPER_MOUNT_PATH,
42
44
  )
43
45
  from opensandbox_server.api.schema import (
44
46
  CreateSandboxRequest,
@@ -83,6 +85,7 @@ from opensandbox_server.services.docker.windows_profile import (
83
85
  )
84
86
  from opensandbox_server.services.extension_service import ExtensionService
85
87
  from opensandbox_server.services.constants import (
88
+ OPENSANDBOX_EGRESS_MITMPROXY_SSL_INSECURE,
86
89
  OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT,
87
90
  OPENSANDBOX_RUNTIME_MOUNT_PATH,
88
91
  SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY,
@@ -102,6 +105,7 @@ from opensandbox_server.services.endpoint_auth import (
102
105
  from opensandbox_server.services.helpers import (
103
106
  matches_filter,
104
107
  parse_timestamp,
108
+ split_egress_env,
105
109
  )
106
110
  from opensandbox_server.services.docker.ossfs_mixin import OSSFSMixin
107
111
  from opensandbox_server.services.sandbox_service import SandboxService
@@ -157,6 +161,7 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
157
161
  self.network_mode = (self.app_config.docker.network_mode or HOST_NETWORK_MODE).lower()
158
162
  self._execd_archive_cache: Dict[str, bytes] = {}
159
163
  self._bootstrap_script_cache: Dict[str, bytes] = {}
164
+ self._bwrap_archive_cache: Dict[str, bytes] = {}
160
165
  self._windows_profile_cache: Dict[str, bytes] = {}
161
166
  self._daemon_platform: Optional[PlatformSpec] = None
162
167
  self._metadata_store = DockerMetadataStore()
@@ -402,11 +407,18 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
402
407
  except DockerException as exc:
403
408
  logger.warning("Failed to remove expired sandbox %s: %s", sandbox_id, exc)
404
409
 
410
+ managed_volumes_raw = labels.get(SANDBOX_MANAGED_VOLUMES_LABEL, "[]")
411
+ try:
412
+ managed_volumes: list[str] = json.loads(managed_volumes_raw)
413
+ except (TypeError, json.JSONDecodeError):
414
+ managed_volumes = []
415
+
405
416
  self._remove_expiration_tracking(sandbox_id)
406
417
  # Ensure sidecar is also cleaned up on expiration
407
418
  self._cleanup_egress_sidecar(sandbox_id)
408
419
  self._cleanup_windows_oem_volume(sandbox_id, labels)
409
420
  self._release_ossfs_mounts(mount_keys)
421
+ self._cleanup_managed_volumes(sandbox_id, managed_volumes)
410
422
  self._metadata_store.delete(sandbox_id)
411
423
 
412
424
  def _restore_existing_sandboxes(self) -> None:
@@ -630,11 +642,33 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
630
642
  self._ensure_secure_access_support(request)
631
643
  self._ensure_network_policy_support(request)
632
644
  self._validate_network_exists()
645
+
646
+ try:
647
+ sandbox_env, egress_env = split_egress_env(request.env)
648
+ except ValueError as e:
649
+ raise HTTPException(
650
+ status_code=status.HTTP_400_BAD_REQUEST,
651
+ detail={
652
+ "code": SandboxErrorCodes.INVALID_PARAMETER,
653
+ "message": str(e),
654
+ },
655
+ ) from e
656
+
633
657
  pvc_inspect_cache, auto_created_volumes = self._validate_volumes(request)
634
658
  sandbox_id, created_at, expires_at = self._prepare_creation_context(request)
635
- return self._provision_sandbox(
636
- sandbox_id, request, created_at, expires_at, pvc_inspect_cache, auto_created_volumes,
637
- )
659
+ try:
660
+ return self._provision_sandbox(
661
+ sandbox_id, request, created_at, expires_at, pvc_inspect_cache, auto_created_volumes,
662
+ sandbox_env=sandbox_env, egress_env=egress_env,
663
+ )
664
+ except ValueError as e:
665
+ raise HTTPException(
666
+ status_code=status.HTTP_400_BAD_REQUEST,
667
+ detail={
668
+ "code": SandboxErrorCodes.INVALID_PARAMETER,
669
+ "message": str(e),
670
+ },
671
+ ) from e
638
672
 
639
673
  def _async_provision_worker(
640
674
  self,
@@ -761,8 +795,13 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
761
795
  expires_at: Optional[datetime],
762
796
  pvc_inspect_cache: Optional[dict[str, dict]] = None,
763
797
  auto_created_volumes: Optional[list[str]] = None,
798
+ sandbox_env: Optional[Dict[str, Optional[str]]] = None,
799
+ egress_env: Optional[Dict[str, Optional[str]]] = None,
764
800
  ) -> CreateSandboxResponse:
765
- labels, environment = self._build_labels_and_env(sandbox_id, request, expires_at)
801
+ if sandbox_env is None and egress_env is None:
802
+ sandbox_env, egress_env = split_egress_env(request.env)
803
+ patched_request = request.model_copy(update={"env": sandbox_env or None})
804
+ labels, environment = self._build_labels_and_env(sandbox_id, patched_request, expires_at)
766
805
  if auto_created_volumes:
767
806
  labels[SANDBOX_MANAGED_VOLUMES_LABEL] = json.dumps(
768
807
  auto_created_volumes, separators=(",", ":"),
@@ -772,6 +811,25 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
772
811
  egress_token: Optional[str] = None
773
812
  requested_windows_profile = is_windows_platform(request.platform)
774
813
 
814
+ credential_proxy_enabled = bool(
815
+ request.credential_proxy and request.credential_proxy.enabled
816
+ )
817
+
818
+ if credential_proxy_enabled and egress_env.get(OPENSANDBOX_EGRESS_MITMPROXY_SSL_INSECURE):
819
+ raise ValueError(
820
+ f"'{OPENSANDBOX_EGRESS_MITMPROXY_SSL_INSECURE}' cannot be set when credential proxy is enabled"
821
+ )
822
+
823
+ if egress_env and not request.network_policy:
824
+ dropped_keys = sorted(egress_env.keys())
825
+ logger.warning(
826
+ "Sandbox %s has OPENSANDBOX_EGRESS_ env vars %s but no networkPolicy; "
827
+ "these variables will be ignored because no egress sidecar is created",
828
+ sandbox_id,
829
+ dropped_keys,
830
+ )
831
+ egress_env = {}
832
+
775
833
  if requested_windows_profile:
776
834
  validate_windows_resource_limits((request.resource_limits.root if request.resource_limits else None) or {})
777
835
  validate_windows_runtime_prerequisites()
@@ -785,9 +843,6 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
785
843
  )
786
844
 
787
845
  sidecar_container = None
788
- credential_proxy_enabled = bool(
789
- request.credential_proxy and request.credential_proxy.enabled
790
- )
791
846
  runtime_volume_name: Optional[str] = None
792
847
  try:
793
848
  # For dockur/windows profile, resourceLimits are translated to
@@ -814,21 +869,21 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
814
869
  container_exposed_ports: Optional[list[str]] = exposed_ports
815
870
 
816
871
  if request.network_policy:
817
- if credential_proxy_enabled:
818
- runtime_volume_name = f"opensandbox-runtime-{sandbox_id}"
819
- with self._docker_operation(
820
- "create credential proxy runtime volume", sandbox_id
821
- ):
822
- self.docker_client.volumes.create(
823
- name=runtime_volume_name,
824
- labels={SANDBOX_MANAGED_VOLUMES_LABEL: "server"},
825
- )
826
- auto_created_volumes = list(auto_created_volumes or [])
827
- auto_created_volumes.append(runtime_volume_name)
828
- labels[SANDBOX_MANAGED_VOLUMES_LABEL] = json.dumps(
829
- auto_created_volumes,
830
- separators=(",", ":"),
872
+ runtime_volume_name = f"opensandbox-runtime-{sandbox_id}"
873
+ with self._docker_operation(
874
+ "create egress runtime volume", sandbox_id
875
+ ):
876
+ self.docker_client.volumes.create(
877
+ name=runtime_volume_name,
878
+ labels={SANDBOX_MANAGED_VOLUMES_LABEL: "server"},
831
879
  )
880
+ auto_created_volumes = list(auto_created_volumes or [])
881
+ auto_created_volumes.append(runtime_volume_name)
882
+ labels[SANDBOX_MANAGED_VOLUMES_LABEL] = json.dumps(
883
+ auto_created_volumes,
884
+ separators=(",", ":"),
885
+ )
886
+ if credential_proxy_enabled:
832
887
  environment = [
833
888
  entry
834
889
  for entry in environment
@@ -859,6 +914,7 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
859
914
  ),
860
915
  runtime_volume_name=runtime_volume_name,
861
916
  credential_proxy_enabled=credential_proxy_enabled,
917
+ extra_env=egress_env or None,
862
918
  )
863
919
  labels[SANDBOX_EMBEDDING_PROXY_PORT_LABEL] = str(host_execd_port)
864
920
  labels[SANDBOX_HTTP_PORT_LABEL] = str(host_http_port)
@@ -891,9 +947,16 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
891
947
 
892
948
  # Inject volume bind mounts into Docker host config
893
949
  if runtime_volume_name:
894
- volume_binds.append(
895
- f"{runtime_volume_name}:{OPENSANDBOX_RUNTIME_MOUNT_PATH}:rw"
950
+ runtime_mount_suffix = f":{OPENSANDBOX_RUNTIME_MOUNT_PATH}:"
951
+ runtime_mount_end = f":{OPENSANDBOX_RUNTIME_MOUNT_PATH}"
952
+ has_runtime_mount = any(
953
+ runtime_mount_suffix in bind or bind.endswith(runtime_mount_end)
954
+ for bind in volume_binds
896
955
  )
956
+ if not has_runtime_mount:
957
+ volume_binds.append(
958
+ f"{runtime_volume_name}:{OPENSANDBOX_RUNTIME_MOUNT_PATH}:rw"
959
+ )
897
960
  if volume_binds:
898
961
  host_config_kwargs["binds"] = volume_binds
899
962
  if requested_windows_profile:
@@ -907,6 +970,26 @@ class DockerSandboxService(DockerDiagnosticsMixin, DockerRuntimeMixin, DockerVol
907
970
  )
908
971
  environment = inject_windows_user_ports(environment, exposed_ports)
909
972
 
973
+ # Inject CAP_SYS_ADMIN + unconfined AppArmor when bwrap isolation is requested.
974
+ # bwrap needs pivot_root/mount which require CAP_SYS_ADMIN and are blocked
975
+ # by Docker's default AppArmor profile.
976
+ if (request.extensions or {}).get(BOOTSTRAP_EXECD_ISOLATION_KEY) == "enable":
977
+ cap_add = set(host_config_kwargs.get("cap_add") or [])
978
+ cap_add.add("SYS_ADMIN")
979
+ host_config_kwargs["cap_add"] = sorted(cap_add)
980
+ security_opt = list(host_config_kwargs.get("security_opt") or [])
981
+ security_opt = [s for s in security_opt if not s.startswith(("apparmor=", "seccomp="))]
982
+ security_opt.append("apparmor=unconfined")
983
+ security_opt.append("seccomp=unconfined")
984
+ host_config_kwargs["security_opt"] = security_opt
985
+ tmpfs = dict(host_config_kwargs.get("tmpfs") or {})
986
+ tmpfs[ISOLATION_UPPER_MOUNT_PATH] = ""
987
+ host_config_kwargs["tmpfs"] = tmpfs
988
+ logger.warning(
989
+ "sandbox %s: granting CAP_SYS_ADMIN + apparmor/seccomp=unconfined + tmpfs for bwrap isolation (bootstrap.execd.isolation=enable)",
990
+ sandbox_id,
991
+ )
992
+
910
993
  created_container = self._create_and_start_container(
911
994
  sandbox_id,
912
995
  image_uri,
@@ -28,7 +28,7 @@ import socket
28
28
  import time
29
29
  import urllib.error
30
30
  import urllib.request
31
- from typing import Any, Optional
31
+ from typing import Any, Dict, Optional
32
32
 
33
33
  from docker.errors import DockerException, NotFound as DockerNotFound
34
34
  from fastapi import HTTPException, status
@@ -53,7 +53,7 @@ from opensandbox_server.services.endpoint_auth import (
53
53
  build_egress_auth_headers,
54
54
  merge_endpoint_headers,
55
55
  )
56
- from opensandbox_server.services.validators import ensure_egress_configured
56
+ from opensandbox_server.services.validators import ensure_egress_configured, ensure_egress_runtime_compatible
57
57
 
58
58
  logger = logging.getLogger(__name__)
59
59
 
@@ -139,6 +139,7 @@ class DockerNetworkingMixin:
139
139
 
140
140
  # Common validation: egress.image must be configured
141
141
  ensure_egress_configured(request.network_policy, self.app_config.egress)
142
+ ensure_egress_runtime_compatible(request.network_policy, self.app_config.secure_runtime)
142
143
 
143
144
  def _ensure_secure_access_support(self, request) -> None:
144
145
  """Validate that secure access can be honored under the current Docker runtime."""
@@ -370,6 +371,7 @@ class DockerNetworkingMixin:
370
371
  egress_api_host_port: Optional[int] = None,
371
372
  runtime_volume_name: Optional[str] = None,
372
373
  credential_proxy_enabled: bool = False,
374
+ extra_env: Optional[Dict[str, Optional[str]]] = None,
373
375
  ):
374
376
  sidecar_name = f"sandbox-egress-{sandbox_id}"
375
377
  sidecar_labels = {
@@ -393,6 +395,12 @@ class DockerNetworkingMixin:
393
395
  if credential_proxy_enabled:
394
396
  sidecar_env.append(f"{OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT}=true")
395
397
 
398
+ if extra_env:
399
+ skip_keys = {OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT} if credential_proxy_enabled else set()
400
+ for key, value in extra_env.items():
401
+ if key not in skip_keys and value is not None:
402
+ sidecar_env.append(f"{key}={value}")
403
+
396
404
  sidecar_port_bindings: dict[str, tuple[str, int]] = {
397
405
  "44772": ("0.0.0.0", host_execd_port),
398
406
  "8080": ("0.0.0.0", host_http_port),
@@ -117,6 +117,14 @@ class DockerRuntimeMixin:
117
117
  with self._docker_operation("execd cache read bootstrap", "execd-cache"):
118
118
  bs_stream, _ = container.get_archive("/bootstrap.sh")
119
119
  self._bootstrap_script_cache[cache_key] = b"".join(bs_stream)
120
+ # Cache bwrap binary (best-effort — may not exist in older images).
121
+ if cache_key not in self._bwrap_archive_cache:
122
+ try:
123
+ with self._docker_operation("execd cache read bwrap", "execd-cache"):
124
+ bwrap_stream, _ = container.get_archive("/usr/local/bin/bwrap")
125
+ self._bwrap_archive_cache[cache_key] = b"".join(bwrap_stream)
126
+ except DockerException:
127
+ logger.warning("bwrap not found in execd image — isolation will be unavailable, upgrade execd image to v1.1.0+")
120
128
  except DockerException as exc:
121
129
  raise HTTPException(
122
130
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -227,6 +235,33 @@ class DockerRuntimeMixin:
227
235
  },
228
236
  ) from exc
229
237
 
238
+ def _copy_bwrap_to_container(
239
+ self,
240
+ container,
241
+ sandbox_id: str,
242
+ platform: Optional[PlatformSpec] = None,
243
+ ) -> None:
244
+ """Copy bwrap binary into /opt/opensandbox/bin/ (best-effort).
245
+
246
+ Bwrap may not exist in older execd images, so failure is non-fatal —
247
+ isolation will simply be unavailable at runtime.
248
+ """
249
+ cache_key = self._normalize_platform_key(platform)
250
+ archive = self._bwrap_archive_cache.get(cache_key)
251
+ if archive is None:
252
+ logger.warning("bwrap archive not cached for %s — isolation will be unavailable, upgrade execd image to v1.1.0+", cache_key)
253
+ return
254
+
255
+ try:
256
+ with self._docker_operation("copy bwrap to sandbox", sandbox_id):
257
+ container.put_archive(path=OPENSANDBOX_DIR, data=archive)
258
+ except DockerException as exc:
259
+ logger.warning(
260
+ "Failed to copy bwrap into sandbox %s: %s (isolation will be unavailable)",
261
+ sandbox_id,
262
+ exc,
263
+ )
264
+
230
265
  def _prepare_sandbox_runtime(
231
266
  self,
232
267
  container,
@@ -236,3 +271,4 @@ class DockerRuntimeMixin:
236
271
  """Copy execd artifacts and bootstrap launcher into the sandbox container."""
237
272
  self._copy_execd_to_container(container, sandbox_id, platform)
238
273
  self._install_bootstrap_script(container, sandbox_id, platform)
274
+ self._copy_bwrap_to_container(container, sandbox_id, platform)
@@ -27,7 +27,12 @@ from datetime import datetime, timezone
27
27
  from typing import Dict, Optional
28
28
 
29
29
  from opensandbox_server.api.schema import Endpoint, Sandbox, SandboxFilter
30
- from opensandbox_server.services.constants import OPEN_SANDBOX_INGRESS_HEADER
30
+ from opensandbox_server.services.constants import (
31
+ ALLOWED_EGRESS_ENV_VARS,
32
+ EGRESS_ENV_PREFIX,
33
+ OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT,
34
+ OPEN_SANDBOX_INGRESS_HEADER,
35
+ )
31
36
  from opensandbox_server.config import (
32
37
  GATEWAY_ROUTE_MODE_HEADER,
33
38
  GATEWAY_ROUTE_MODE_URI,
@@ -234,6 +239,34 @@ def format_ingress_endpoint(
234
239
  raise RuntimeError(f"Unsupported route mode: {route_mode}")
235
240
 
236
241
 
242
+ def split_egress_env(
243
+ env: Optional[Dict[str, Optional[str]]],
244
+ ) -> tuple[Dict[str, Optional[str]], Dict[str, Optional[str]]]:
245
+ """Split request env into (sandbox_env, egress_env) by OPENSANDBOX_EGRESS_ prefix.
246
+
247
+ Only env vars listed in ALLOWED_EGRESS_ENV_VARS are forwarded to the egress
248
+ sidecar. Any other OPENSANDBOX_EGRESS_ key raises ValueError.
249
+ """
250
+ if not env:
251
+ return {}, {}
252
+
253
+ sandbox_env: Dict[str, Optional[str]] = {}
254
+ egress_env: Dict[str, Optional[str]] = {}
255
+ for key, value in env.items():
256
+ if key.startswith(EGRESS_ENV_PREFIX):
257
+ if key not in ALLOWED_EGRESS_ENV_VARS:
258
+ raise ValueError(
259
+ f"Environment variable '{key}' is not allowed; "
260
+ f"permitted OPENSANDBOX_EGRESS_ keys: {sorted(ALLOWED_EGRESS_ENV_VARS)}"
261
+ )
262
+ egress_env[key] = value
263
+ if key == OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT:
264
+ sandbox_env[key] = value
265
+ else:
266
+ sandbox_env[key] = value
267
+ return sandbox_env, egress_env
268
+
269
+
237
270
  __all__ = [
238
271
  "parse_memory_limit",
239
272
  "parse_nano_cpus",
@@ -242,4 +275,5 @@ __all__ = [
242
275
  "normalize_external_endpoint_url",
243
276
  "format_ingress_endpoint",
244
277
  "matches_filter",
278
+ "split_egress_env",
245
279
  ]