fix(macos): persist gateway CA on host
test / integration-docker (push) Successful in 19s
Update Quality Badges / update-badges (push) Failing after 42s
lint / lint (push) Successful in 52s
test / unit (push) Successful in 1m43s
test / integration-firecracker (push) Successful in 4m56s
test / coverage (push) Successful in 17s
test / publish-infra (push) Successful in 1m49s
test / integration-docker (push) Successful in 19s
Update Quality Badges / update-badges (push) Failing after 42s
lint / lint (push) Successful in 52s
test / unit (push) Successful in 1m43s
test / integration-firecracker (push) Successful in 4m56s
test / coverage (push) Successful in 17s
test / publish-infra (push) Successful in 1m49s
This commit was merged in pull request #454.
This commit is contained in:
@@ -41,7 +41,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from ... import log
|
||||
from ...orchestrator.gateway import GATEWAY_CA_CERT
|
||||
from ...orchestrator.gateway import GATEWAY_CA_CERT, MITMPROXY_HOME
|
||||
from ...orchestrator.lifecycle import (
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_STARTUP_TIMEOUT_SECONDS,
|
||||
@@ -52,6 +52,7 @@ from ...paths import (
|
||||
CONTROL_PLANE_TOKEN_ENV,
|
||||
HOST_DB_FILENAME,
|
||||
host_control_plane_token,
|
||||
host_gateway_ca_dir,
|
||||
)
|
||||
from .. import util as backend_util
|
||||
from . import util as container_mod
|
||||
@@ -218,6 +219,14 @@ class MacosInfraService:
|
||||
# Container-only DB volume: one kernel writes bot-bottle.db, never
|
||||
# shared with the host or another guest.
|
||||
"--volume", f"{self._db_volume}:{_DB_ROOT_IN_CONTAINER}",
|
||||
# The DB needs a container-only ext4 volume for coherent SQLite
|
||||
# locking, but the CA has no such constraint. Keep it in the host
|
||||
# app-data root so infra-container recreation and Apple Container
|
||||
# volume pruning cannot silently rotate every bottle's trust
|
||||
# anchor (issue #450).
|
||||
"--mount",
|
||||
container_mod.bind_mount_spec(
|
||||
str(host_gateway_ca_dir()), MITMPROXY_HOME),
|
||||
# Bind-mount the control-plane source (read-only); a code change
|
||||
# takes effect on relaunch with no image rebuild.
|
||||
"--mount",
|
||||
@@ -261,9 +270,9 @@ class MacosInfraService:
|
||||
|
||||
def ca_cert_pem(self, *, timeout: float = DEFAULT_CA_TIMEOUT_SECONDS) -> str:
|
||||
"""The gateway's mitmproxy CA (PEM) agents install to trust its TLS
|
||||
interception. Read out of the container (the CA lives on a
|
||||
container-internal path, not a host mount); polls because mitmproxy
|
||||
writes it a beat after start."""
|
||||
interception. Read through the container path backed by the persistent
|
||||
host CA directory; polls because mitmproxy writes it a beat after
|
||||
start."""
|
||||
def _fetch() -> str | None:
|
||||
result = container_mod.run_container_argv(
|
||||
["container", "exec", self._name, "cat", GATEWAY_CA_CERT])
|
||||
|
||||
@@ -324,15 +324,24 @@ re-attachment blocker distinct from #443/#445).
|
||||
|
||||
The CA lives on the **host filesystem** at `bot_bottle_root()/gateway-ca`
|
||||
(`host_gateway_ca_dir()`), bind-mounted into the container at mitmproxy's
|
||||
confdir. This is deliberately a host bind-mount, **not a Docker named volume**:
|
||||
a named volume survives `docker rm` but is silently wiped by
|
||||
`docker volume prune` / `docker system prune --volumes` during routine host
|
||||
maintenance, which is exactly how the ephemeral-CA symptom shows up in
|
||||
practice. A path under the app-data root is never pruned by docker, and stays
|
||||
directly inspectable and rotatable from the host. mitmproxy reuses an existing
|
||||
CA and generates one only on first run, so the bind-mount alone gives
|
||||
confdir. This is deliberately a host bind-mount, **not a container-runtime
|
||||
named volume**: a named volume survives ordinary container removal but can be
|
||||
silently wiped by Docker's or Apple Container's volume-prune commands during
|
||||
routine host maintenance, which is exactly how the ephemeral-CA symptom shows
|
||||
up in practice. A path under the app-data root is not managed or pruned by the
|
||||
container runtime, and stays directly inspectable and rotatable from the host.
|
||||
mitmproxy reuses an existing CA and generates one only on first run, so the
|
||||
bind-mount alone gives
|
||||
"adopt-existing, generate-on-first-run" for free.
|
||||
|
||||
The macOS backend uses the same host-resident CA directory and bind-mounts it
|
||||
into the consolidated Apple infra container. Its `bot-bottle-mac-db` named
|
||||
volume remains container-only because that prevents incoherent cross-kernel
|
||||
SQLite locking, but the CA is deliberately not stored there: Apple Container
|
||||
also has a `container volume prune` operation, and the named volume is
|
||||
temporarily unreferenced while the infra container is recreated. Keeping the
|
||||
CA on the host makes both ordinary recreation and volume pruning safe.
|
||||
|
||||
**Deliberate rollover** is the explicit inverse: `rotate_gateway_ca()` removes
|
||||
the persisted CA material so the next start remints it, and the
|
||||
`python -m bot_bottle.orchestrator.rotate_ca` one-shot wires that together with
|
||||
|
||||
@@ -61,6 +61,20 @@ class TestInfraRun(unittest.TestCase):
|
||||
mounts = [argv[i + 1] for i, a in enumerate(argv) if a == "--mount"]
|
||||
self.assertTrue(all("bot-bottle.db" not in m for m in mounts))
|
||||
|
||||
def test_ca_is_persisted_on_the_host_not_the_container_volume(self) -> None:
|
||||
"""The CA survives infra recreation and cannot be removed by Apple
|
||||
Container's volume-prune command."""
|
||||
argv = self._run_container(MacosInfraService(repo_root=Path("/r")))
|
||||
mounts = [argv[i + 1] for i, a in enumerate(argv) if a == "--mount"]
|
||||
ca_mounts = [
|
||||
m for m in mounts
|
||||
if "target=/home/mitmproxy/.mitmproxy" in m
|
||||
]
|
||||
self.assertEqual(1, len(ca_mounts))
|
||||
self.assertIn("source=", ca_mounts[0])
|
||||
self.assertIn("/gateway-ca", ca_mounts[0])
|
||||
self.assertNotIn(",readonly", ca_mounts[0])
|
||||
|
||||
def test_nat_network_precedes_the_host_only_network(self) -> None:
|
||||
argv = self._run_container(MacosInfraService(repo_root=Path("/r")))
|
||||
nets = [argv[i + 1] for i, a in enumerate(argv) if a == "--network"]
|
||||
|
||||
Reference in New Issue
Block a user