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:
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")}}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user