Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FROM python:3.10-slim AS builder
FROM python:3.10-alpine AS builder

# Optional: inject the release version when .git is unavailable so hatch-vcs
# resolves the real version instead of falling back to 0.1.0.dev0. Passed by
Expand All @@ -28,9 +28,7 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl ca-certificates

RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:/root/.cargo/bin:${PATH}"
Expand All @@ -44,7 +42,7 @@ COPY LICENSE README.md ./
# Install the project itself into the venv (deps already synced)
RUN uv pip install --no-deps --editable .

FROM python:3.10-slim AS runtime
FROM python:3.10-alpine AS runtime

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PYTHONDONTWRITEBYTECODE=1 \
Expand All @@ -55,6 +53,15 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \

WORKDIR /app

# The service runs entirely from the copied virtualenv. The base image's
# system setuptools is build tooling, not a runtime dependency, and its
# vendored packages needlessly expand the production vulnerability surface.
RUN rm -rf \
/usr/local/lib/python3.10/site-packages/_distutils_hack \
/usr/local/lib/python3.10/site-packages/pkg_resources \
/usr/local/lib/python3.10/site-packages/setuptools \
/usr/local/lib/python3.10/site-packages/setuptools-*.dist-info

COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/opensandbox_server /app/opensandbox_server
COPY --from=builder /app/opensandbox_server/examples/example.config.k8s.toml /etc/opensandbox/config.toml
Expand Down
17 changes: 15 additions & 2 deletions server/opensandbox_server/services/k8s/egress_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,19 @@ def prep_execd_init_for_egress(exec_install_script: str) -> tuple[str, Dict[str,
security context dict must be applied to the execd init container (typically via
``build_security_context_from_dict`` in ``security_context``).

A pod-level non-root UID otherwise overrides this init container and makes
the sysctl write fail with EPERM. Keep the root exception explicit and
local to the privileged init container.

Returns:
``(prefixed_shell_script, {"privileged": True})``
The prefixed shell script and its privileged root security context.
"""
script = f"set -e; echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6 && {exec_install_script}"
return script, {"privileged": True}
return script, {
"privileged": True,
"runAsNonRoot": False,
"runAsUser": 0,
}


def build_security_context_for_sandbox_container(
Expand Down Expand Up @@ -113,6 +121,11 @@ def apply_egress_to_spec(
"env": env,
"securityContext": {
"capabilities": {"add": ["NET_ADMIN"]},
# A pod-level non-root UID clears NET_ADMIN from the effective
# capability set. The sidecar must retain it to install the
# nftables/iptables policy, so scope the root exception here.
"runAsNonRoot": False,
"runAsUser": 0,
},
"ports": [{"name": "egress-api", "containerPort": 18080}],
"readinessProbe": {
Expand Down
19 changes: 17 additions & 2 deletions server/opensandbox_server/services/k8s/security_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,22 @@ def build_security_context_from_dict(
)

privileged = security_context_dict.get("privileged")

if capabilities is None and privileged is None:
run_as_non_root = security_context_dict.get("runAsNonRoot")
run_as_user = security_context_dict.get("runAsUser")

if (
capabilities is None
and privileged is None
and run_as_non_root is None
and run_as_user is None
):
return None

return V1SecurityContext(
capabilities=capabilities,
privileged=privileged,
run_as_non_root=run_as_non_root,
run_as_user=run_as_user,
)


Expand All @@ -72,6 +81,12 @@ def serialize_security_context_to_dict(
if security_context.privileged is not None:
result["privileged"] = security_context.privileged

if getattr(security_context, "run_as_non_root", None) is not None:
result["runAsNonRoot"] = security_context.run_as_non_root

if getattr(security_context, "run_as_user", None) is not None:
result["runAsUser"] = security_context.run_as_user

if getattr(security_context, "seccomp_profile", None) is not None:
sp = security_context.seccomp_profile
profile_dict: Dict[str, Any] = {"type": sp.type}
Expand Down
52 changes: 52 additions & 0 deletions server/opensandbox_server/services/k8s/template_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,59 @@ def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any
result[key] = BaseSandboxTemplateManager._deep_merge(
result[key], override_value
)
elif BaseSandboxTemplateManager._are_named_object_lists(
result[key], override_value
):
result[key] = BaseSandboxTemplateManager._merge_named_object_lists(
result[key], override_value
)
else:
result[key] = BaseSandboxTemplateManager._deep_copy(override_value)

return result

@staticmethod
def _are_named_object_lists(base: Any, override: Any) -> bool:
"""Return whether both values are non-empty lists keyed by unique names.

Kubernetes uses ``name`` as the merge key for containers, init
containers, environment variables, volumes, and several related pod
fields. Restricting this behavior to unambiguously named object lists
keeps ordinary lists, such as commands and tolerations, replace-only.
"""
if not isinstance(base, list) or not isinstance(override, list):
return False
if not base or not override:
return False

for items in (base, override):
names = [item.get("name") for item in items if isinstance(item, dict)]
if len(names) != len(items):
return False
if any(not isinstance(name, str) or not name for name in names):
return False
if len(set(names)) != len(names):
return False

return True

@staticmethod
def _merge_named_object_lists(
base: list[Dict[str, Any]], override: list[Dict[str, Any]]
) -> list[Dict[str, Any]]:
"""Merge runtime objects into template objects with the same name."""
result = BaseSandboxTemplateManager._deep_copy(base)
indexes = {item["name"]: index for index, item in enumerate(result)}

for override_item in override:
name = override_item["name"]
if name in indexes:
index = indexes[name]
result[index] = BaseSandboxTemplateManager._deep_merge(
result[index], override_item
)
else:
indexes[name] = len(result)
result.append(BaseSandboxTemplateManager._deep_copy(override_item))

return result
4 changes: 4 additions & 0 deletions server/tests/k8s/test_batchsandbox_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1787,6 +1787,8 @@ def test_create_workload_with_network_policy_adds_sidecar(self, mock_k8s_client)
caps = sidecar.get("securityContext", {}).get("capabilities", {})
assert "NET_ADMIN" in caps.get("add", [])
assert sidecar.get("securityContext", {}).get("privileged") is not True
assert sidecar["securityContext"]["runAsNonRoot"] is False
assert sidecar["securityContext"]["runAsUser"] == 0
assert "command" not in sidecar
assert sidecar["readinessProbe"]["httpGet"]["path"] == "/healthz"
assert sidecar["readinessProbe"]["httpGet"]["port"] == 18080
Expand All @@ -1797,6 +1799,8 @@ def test_create_workload_with_network_policy_adds_sidecar(self, mock_k8s_client)
assert execd_init["name"] == "execd-installer"
assert execd_init["image"] == "execd:latest"
assert execd_init.get("securityContext", {}).get("privileged") is True
assert execd_init["securityContext"]["runAsNonRoot"] is False
assert execd_init["securityContext"]["runAsUser"] == 0
assert "/proc/sys/net/ipv6/conf/all/disable_ipv6" in execd_init["args"][0]

main = next(c for c in containers if c["name"] == "sandbox")
Expand Down
52 changes: 52 additions & 0 deletions server/tests/k8s/test_batchsandbox_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,58 @@ def test_deep_merge_replaces_lists_not_merges(self):
result = BatchSandboxTemplateManager._deep_merge(base, override)

assert result == {"spec": {"tolerations": [{"key": "b"}]}}

def test_deep_merge_merges_named_kubernetes_objects(self):
base = {
"spec": {
"containers": [
{
"name": "sandbox",
"securityContext": {
"allowPrivilegeEscalation": False,
"capabilities": {"drop": ["ALL"]},
},
"resources": {"limits": {"memory": "6Gi"}},
},
{"name": "template-sidecar", "image": "template:latest"},
]
}
}
override = {
"spec": {
"containers": [
{
"name": "sandbox",
"image": "runtime:latest",
"resources": {"requests": {"cpu": "100m"}},
},
{"name": "runtime-sidecar", "image": "runtime-sidecar:latest"},
]
}
}

result = BatchSandboxTemplateManager._deep_merge(base, override)

assert result["spec"]["containers"] == [
{
"name": "sandbox",
"image": "runtime:latest",
"securityContext": {
"allowPrivilegeEscalation": False,
"capabilities": {"drop": ["ALL"]},
},
"resources": {
"limits": {"memory": "6Gi"},
"requests": {"cpu": "100m"},
},
},
{"name": "template-sidecar", "image": "template:latest"},
{"name": "runtime-sidecar", "image": "runtime-sidecar:latest"},
]

# The merge must not mutate either input template.
assert "image" not in base["spec"]["containers"][0]
assert "securityContext" not in override["spec"]["containers"][0]

def test_deep_merge_none_values_do_not_override(self):
base = {"spec": {"expireTime": "2024-12-31"}}
Expand Down
10 changes: 8 additions & 2 deletions server/tests/k8s/test_egress_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def test_handles_missing_default_action(self):
assert "egress" in policy_dict

def test_security_context_adds_net_admin_not_privileged(self):
"""Egress sidecar uses NET_ADMIN only (IPv6 is disabled in execd init when egress is on)."""
"""Egress uses the narrow NET_ADMIN + root exception, not privileged mode."""
egress_image = "opensandbox/egress:v1.1.6"
network_policy = NetworkPolicy(
default_action="deny",
Expand All @@ -233,6 +233,8 @@ def test_security_context_adds_net_admin_not_privileged(self):
security_context = container["securityContext"]
assert security_context.get("privileged") is not True
assert "NET_ADMIN" in security_context.get("capabilities", {}).get("add", [])
assert security_context["runAsNonRoot"] is False
assert security_context["runAsUser"] == 0

def test_no_command_uses_image_entrypoint(self):
container = _egress_container(
Expand Down Expand Up @@ -513,7 +515,11 @@ class TestPrepExecdInitForEgress:
def test_returns_privileged_security_dict_and_prefixed_script(self):
base = "cp ./execd /opt/opensandbox/execd"
script, sc = prep_execd_init_for_egress(base)
assert sc == {"privileged": True}
assert sc == {
"privileged": True,
"runAsNonRoot": False,
"runAsUser": 0,
}
assert "/proc/sys/net/ipv6/conf/all/disable_ipv6" in script
assert script.endswith(base)

Expand Down
Loading