feat(docker): terminal.docker_snap_compat opt-out for snap-packaged Docker under AppArmor (#9730)

On hosts where Docker ships as a snap (Ubuntu cloud images / Azure VMs), the
snap's AppArmor confinement turns two sandbox hardening flags into a dead
container at start: `--init` fails with "exec /sbin/docker-init: operation not
permitted" and `--security-opt no-new-privileges` then fails every exec the
same way ("exec /usr/bin/sleep: operation not permitted"). This is snapd
LP#1908448 — not probeable from the client, and docker_extra_args cannot remove
flags we add.

`terminal.docker_snap_compat: true` drops exactly those two flags; cap-drop ALL,
the tmpfs hardening, PID limits and the privdrop caps are unchanged, and a
warning is logged at container start. Bridged everywhere the other docker_*
keys are (CLI env map, gateway env map, `hermes config set` sync, terminal_tool
env read, the shared container_config shaper, DEFAULT_CONFIG).
This commit is contained in:
kshitijk4poor
2026-09-05 20:54:30 +05:30
committed by kshitij
parent 431978d47b
commit 256bd1adc9
7 changed files with 28 additions and 9 deletions
+1
View File
@@ -293,6 +293,7 @@ _TERMINAL_ENV_MAPPINGS = {
"ssh_host", "ssh_user", "ssh_port", "ssh_key", "container_cpu", "container_memory",
"container_disk", "container_persistent", "docker_volumes", "docker_env", "docker_extra_args",
"docker_shm_size", "docker_mount_cwd_to_workspace", "docker_network", "docker_run_as_host_user",
"docker_snap_compat",
"docker_persist_across_processes", "docker_shared_container_key", "docker_orphan_reaper",
"sandbox_dir", "persistent_shell",
)
+1
View File
@@ -1841,6 +1841,7 @@ def _bridge_terminal_config_to_env(_terminal_cfg: dict) -> None:
"docker_mount_cwd_to_workspace": "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE",
"docker_network": "TERMINAL_DOCKER_NETWORK",
"docker_run_as_host_user": "TERMINAL_DOCKER_RUN_AS_HOST_USER",
"docker_snap_compat": "TERMINAL_DOCKER_SNAP_COMPAT",
"docker_persist_across_processes": "TERMINAL_DOCKER_PERSIST_ACROSS_PROCESSES",
"docker_shared_container_key": "TERMINAL_DOCKER_SHARED_CONTAINER_KEY",
"docker_orphan_reaper": "TERMINAL_DOCKER_ORPHAN_REAPER",
+1 -1
View File
@@ -2055,7 +2055,7 @@ TERMINAL_CONFIG_ENV_MAP = {
"daytona_image", "vercel_runtime", "ssh_host", "ssh_user", "ssh_port", "ssh_key",
"container_cpu", "container_memory", "container_disk", "container_persistent",
"docker_volumes", "docker_env", "docker_mount_cwd_to_workspace", "docker_network",
"docker_extra_args", "docker_shm_size", "docker_run_as_host_user",
"docker_extra_args", "docker_shm_size", "docker_run_as_host_user", "docker_snap_compat",
"docker_persist_across_processes", "docker_shared_container_key",
"docker_orphan_reaper", "sandbox_dir", "persistent_shell")}}
+4
View File
@@ -332,6 +332,10 @@ DEFAULT_CONFIG = {
# default for images whose entrypoints must start as root (e.g. the bundled Hermes image,
# which drops to `hermes` via s6-setuidgid). When on, SETUID/SETGID caps are omitted.
"docker_run_as_host_user": False,
# Snap-packaged Docker under AppArmor (Ubuntu cloud images; LP#1908448) refuses to exec
# anything under `--init` or `--security-opt no-new-privileges` ("operation not
# permitted"). True drops those two flags; every other hardening stays. See #9730.
"docker_snap_compat": False,
# Trusted profiles sharing one Docker container identity; empty = per-profile boundary.
"docker_shared_container_key": "",
# Keep a long-lived bash shell across execute() calls so cwd/env/shell variables survive.
+18 -7
View File
@@ -239,7 +239,6 @@ _BASE_SECURITY_ARGS = [
"--cap-add", "DAC_OVERRIDE",
"--cap-add", "CHOWN",
"--cap-add", "FOWNER",
"--security-opt", "no-new-privileges",
"--tmpfs", "/tmp:rw,nosuid,size=512m",
"--tmpfs", "/var/tmp:rw,noexec,nosuid,size=256m"]
@@ -295,9 +294,15 @@ _PRIVDROP_CAP_ARGS = ["--cap-add", "SETUID", "--cap-add", "SETGID"]
_S6_INIT_ENTRYPOINTS = ("/init", "/package/admin/s6-overlay/command/init")
def _build_security_args(run_as_host_user: bool, run_exec: bool = False) -> list[str]:
"""Security/cap/tmpfs args for the privilege mode; ``run_exec`` mounts /run exec for s6 images."""
args = list(_BASE_SECURITY_ARGS) + list(_RUN_TMPFS_EXEC if run_exec else _RUN_TMPFS_NOEXEC)
_NO_NEW_PRIVILEGES_ARGS = ["--security-opt", "no-new-privileges"]
def _build_security_args(run_as_host_user: bool, run_exec: bool = False, snap_compat: bool = False) -> list[str]:
"""Security/cap/tmpfs args for the privilege mode; ``run_exec`` mounts /run exec for s6 images.
``snap_compat`` drops no-new-privileges: snap-packaged Docker's AppArmor profile turns it into
"exec: operation not permitted" for every process in the container (#9730, LP#1908448)."""
args = list(_BASE_SECURITY_ARGS) + ([] if snap_compat else list(_NO_NEW_PRIVILEGES_ARGS))
args += list(_RUN_TMPFS_EXEC if run_exec else _RUN_TMPFS_NOEXEC)
return args if run_as_host_user else args + list(_PRIVDROP_CAP_ARGS)
@@ -501,7 +506,8 @@ class DockerEnvironment(BaseEnvironment):
extra_args: list = None,
persist_across_processes: bool = True,
shm_size: str = _DEFAULT_SHM_SIZE,
shared_container_key: str = ""):
shared_container_key: str = "",
snap_compat: bool = False):
if cwd == "~":
cwd = "/root"
super().__init__(cwd=cwd, timeout=timeout)
@@ -547,7 +553,12 @@ class DockerEnvironment(BaseEnvironment):
"Docker: image %s uses /init (s6-overlay) as entrypoint — "
"skipping --init and mounting /run with exec.",
image)
security_args = _build_security_args(run_as_host_user and bool(user_args), run_exec=image_uses_s6_init)
security_args = _build_security_args(
run_as_host_user and bool(user_args), run_exec=image_uses_s6_init, snap_compat=snap_compat)
self._snap_compat = snap_compat
if snap_compat:
logger.warning(
"docker_snap_compat: running without --init and no-new-privileges (snap Docker under AppArmor)")
logger.info("Docker volume_args: %s", volume_args)
# docker_extra_args go last so they can override defaults.
@@ -751,7 +762,7 @@ class DockerEnvironment(BaseEnvironment):
# own /init PID 1, so adding --init there creates two competing inits and breaks startup
# (#34628).
self._docker_exe, "run", "-d",
*([] if self._image_uses_s6_init else ["--init"]),
*([] if self._image_uses_s6_init or self._snap_compat else ["--init"]),
"--name", name,
*label_args,
"-w", workdir,
+1
View File
@@ -654,6 +654,7 @@ def _get_env_config() -> Dict[str, Any]:
"docker_volumes": docker_volumes,
"docker_env": docker_env,
"docker_run_as_host_user": _tenv_bool("TERMINAL_DOCKER_RUN_AS_HOST_USER", "false"),
"docker_snap_compat": _tenv_bool("TERMINAL_DOCKER_SNAP_COMPAT", "false"),
"docker_network": _tenv_bool("TERMINAL_DOCKER_NETWORK", "true"),
"docker_extra_args": docker_extra_args,
"docker_shm_size": docker_shm_size,
+2 -1
View File
@@ -40,7 +40,7 @@ _CONTAINER_KEYS = (
("docker_volumes", []), ("docker_mount_cwd_to_workspace", False), ("docker_forward_env", []),
("docker_env", {}), ("docker_run_as_host_user", False), ("docker_extra_args", []),
("docker_shm_size", "1g"), ("docker_network", True), ("docker_persist_across_processes", True),
("docker_shared_container_key", ""), ("docker_orphan_reaper", True),
("docker_shared_container_key", ""), ("docker_orphan_reaper", True), ("docker_snap_compat", False),
)
_DOCKER_KWARGS = (
("volumes", "docker_volumes", []), ("auto_mount_cwd", "docker_mount_cwd_to_workspace", False),
@@ -48,6 +48,7 @@ _DOCKER_KWARGS = (
("run_as_host_user", "docker_run_as_host_user", False), ("network", "docker_network", True),
("extra_args", "docker_extra_args", []), ("persist_across_processes", "docker_persist_across_processes", True),
("shared_container_key", "docker_shared_container_key", ""), ("shm_size", "docker_shm_size", "1g"),
("snap_compat", "docker_snap_compat", False),
)