refactor(egress): make TLS inspection explicit
tracker-policy-pr / check-pr (pull_request) Successful in 9s
test / unit (pull_request) Successful in 45s
test / integration-docker (pull_request) Successful in 45s
lint / lint (push) Failing after 59s
test / integration-firecracker (pull_request) Successful in 3m28s
test / coverage (pull_request) Successful in 16s
test / publish-infra (pull_request) Has been skipped

This commit is contained in:
2026-07-23 17:38:08 +00:00
parent 21084d8313
commit 984434a9db
14 changed files with 312 additions and 213 deletions
+47 -32
View File
@@ -147,6 +147,7 @@ def egress_manifest_routes(
inbound_detectors=r.InboundDetectors,
outbound_on_match=r.OutboundOnMatch,
preserve_auth=r.PreserveAuth,
inspect=r.Inspect,
))
return tuple(out)
@@ -226,9 +227,13 @@ def _yaml_str_escape(s: str) -> str:
def _route_to_yaml_fields(r: Route) -> dict[str, object]:
fields: dict[str, object] = {"host": r.host}
if not r.inspect:
fields["inspect"] = False
return fields
inspect: dict[str, object] = {}
if r.auth_scheme and r.token_env:
fields["auth_scheme"] = r.auth_scheme
fields["token_env"] = r.token_env
inspect["auth_scheme"] = r.auth_scheme
inspect["token_env"] = r.token_env
if r.matches:
matches_data: list[dict[str, object]] = []
for entry in r.matches:
@@ -252,28 +257,30 @@ def _route_to_yaml_fields(r: Route) -> dict[str, object]:
headers_data.append(hd)
entry_data["headers"] = headers_data
matches_data.append(entry_data)
fields["matches"] = matches_data
inspect["matches"] = matches_data
if r.git_fetch:
fields["git"] = {"fetch": True}
inspect["git"] = {"fetch": True}
if r.preserve_auth:
inspect["preserve_auth"] = True
if (
r.outbound_detectors is not None
or r.inbound_detectors is not None
or r.outbound_on_match
):
dlp: dict[str, object] = {}
if r.outbound_detectors is not None:
dlp["outbound_detectors"] = (
inspect["outbound_detectors"] = (
False if not r.outbound_detectors
else list(r.outbound_detectors)
)
if r.inbound_detectors is not None:
dlp["inbound_detectors"] = (
inspect["inbound_detectors"] = (
False if not r.inbound_detectors
else list(r.inbound_detectors)
)
if r.outbound_on_match:
dlp["outbound_on_match"] = r.outbound_on_match
fields["dlp"] = dlp
inspect["outbound_on_match"] = r.outbound_on_match
if inspect:
fields["inspect"] = inspect
return fields
@@ -281,30 +288,30 @@ def _render_match_entry(entry: dict[str, object]) -> list[str]:
lines: list[str] = []
first_key = True
if "paths" in entry:
lines.append(" - paths:")
lines.append(" - paths:")
first_key = False
for pd in entry["paths"]: # type: ignore[union-attr]
pd_dict: dict[str, str] = pd # type: ignore[assignment]
if "type" in pd_dict:
lines.append(f' - type: "{_yaml_str_escape(pd_dict["type"])}"')
lines.append(f' value: "{_yaml_str_escape(pd_dict["value"])}"')
lines.append(f' - type: "{_yaml_str_escape(pd_dict["type"])}"')
lines.append(f' value: "{_yaml_str_escape(pd_dict["value"])}"')
else:
lines.append(f' - value: "{_yaml_str_escape(pd_dict["value"])}"')
lines.append(f' - value: "{_yaml_str_escape(pd_dict["value"])}"')
if "methods" in entry:
methods_str = ", ".join(f'"{_yaml_str_escape(m)}"' for m in entry["methods"]) # type: ignore[union-attr]
prefix = " - " if first_key else " "
prefix = " - " if first_key else " "
lines.append(f'{prefix}methods: [{methods_str}]')
first_key = False
if "headers" in entry:
prefix = " - " if first_key else " "
prefix = " - " if first_key else " "
lines.append(f"{prefix}headers:")
first_key = False
for hd in entry["headers"]: # type: ignore[union-attr]
hd_dict: dict[str, str] = hd # type: ignore[assignment]
lines.append(f' - name: "{_yaml_str_escape(hd_dict["name"])}"')
lines.append(f' value: "{_yaml_str_escape(hd_dict["value"])}"')
lines.append(f' - name: "{_yaml_str_escape(hd_dict["name"])}"')
lines.append(f' value: "{_yaml_str_escape(hd_dict["value"])}"')
if first_key:
lines.append(" - {}")
lines.append(" - {}")
return lines
@@ -323,22 +330,30 @@ def egress_render_routes(
for r in routes:
f = _route_to_yaml_fields(r)
lines.append(f' - host: "{_yaml_str_escape(str(f["host"]))}"')
if "auth_scheme" in f:
lines.append(f' auth_scheme: "{_yaml_str_escape(str(f["auth_scheme"]))}"')
lines.append(f' token_env: "{_yaml_str_escape(str(f["token_env"]))}"')
if "matches" in f:
lines.append(" matches:")
for entry in f["matches"]: # type: ignore[union-attr]
if f.get("inspect") is False:
lines.append(" inspect: false")
continue
inspect: dict[str, object] = f.get("inspect", {}) # type: ignore[assignment]
if not inspect:
continue
lines.append(" inspect:")
if "auth_scheme" in inspect:
lines.append(f' auth_scheme: "{_yaml_str_escape(str(inspect["auth_scheme"]))}"')
lines.append(f' token_env: "{_yaml_str_escape(str(inspect["token_env"]))}"')
if "matches" in inspect:
lines.append(" matches:")
for entry in inspect["matches"]: # type: ignore[union-attr]
lines.extend(_render_match_entry(entry)) # type: ignore[arg-type]
if "git" in f:
git_dict: dict[str, object] = f["git"] # type: ignore
lines.append(" git:")
if "git" in inspect:
git_dict: dict[str, object] = inspect["git"] # type: ignore
lines.append(" git:")
if git_dict.get("fetch") is True:
lines.append(" fetch: true")
if "dlp" in f:
dlp_dict: dict[str, object] = f["dlp"] # type: ignore
lines.append(" dlp:")
for dk, dv in dlp_dict.items():
lines.append(" fetch: true")
if inspect.get("preserve_auth") is True:
lines.append(" preserve_auth: true")
for dk in ("outbound_detectors", "inbound_detectors", "outbound_on_match"):
if dk in inspect:
dv = inspect[dk]
if dv is False:
lines.append(f" {dk}: false")
elif isinstance(dv, list):
+8 -8
View File
@@ -126,7 +126,7 @@ class EgressAddon:
# `Proxy-Authorization` (HTTPS tunnels don't repeat it on the bumped
# inner requests). Keyed by client_conn.id; cleared on disconnect.
self._conn_tokens: dict[str, str] = {}
# Connections whose route carries `dlp: false` — mitmproxy tunnels
# Connections whose route carries `inspect: false` — mitmproxy tunnels
# these without TLS interception so the client sees the server's real
# cert. Keyed by client_conn.id; cleared on disconnect.
self._passthrough_conns: set[str] = set()
@@ -312,7 +312,7 @@ class EgressAddon:
bumped requests won't carry `Proxy-Authorization`), keyed by client
connection, and strip it so it never reaches upstream.
For `dlp: false` routes, also resolve the policy here to make the
For `inspect: false` routes, also resolve the policy here to make the
allowlist decision before the TLS handshake: the tunnel is either
blocked immediately or marked for passthrough in `_passthrough_conns`
so `tls_clienthello` skips interception."""
@@ -333,7 +333,7 @@ class EgressAddon:
self._stash_flow_ctx(flow, config, slug, env)
host = flow.request.pretty_host
route = match_route(config.routes, host)
if route is not None and route.dlp_passthrough:
if route is not None and not route.inspect:
decision = decide(config.routes, host, "/", env, deny_reason=config.deny_reason)
if decision.action == "block":
flow.response = http.Response.make(
@@ -346,7 +346,7 @@ class EgressAddon:
self._passthrough_conns.add(conn_id)
def tls_clienthello(self, client_hello: typing.Any) -> None:
"""Skip TLS interception for `dlp: false` routes so the client sees
"""Skip TLS interception for `inspect: false` routes so the client sees
the server's real certificate rather than the MITM CA's leaf."""
conn_id = getattr(client_hello.context.client, "id", "")
if conn_id in self._passthrough_conns:
@@ -383,10 +383,10 @@ class EgressAddon:
# DLP outbound scan BEFORE stripping auth — catches tokens the
# agent tried to smuggle in any header, path, query param, or body.
# Hostname is included to catch DNS-tunnelling exfiltration attempts.
# `dlp: false` routes skip scanning entirely (TLS is also not
# `inspect: false` routes skip scanning entirely (TLS is also not
# intercepted for HTTPS, so this branch only fires for plain HTTP).
route = match_route(config.routes, flow.request.pretty_host)
if route is not None and not route.dlp_passthrough:
if route is not None and route.inspect:
if not await self._handle_outbound_dlp(flow, route, slug, env):
return
# The redact policy may have rewritten the request line; recompute
@@ -656,7 +656,7 @@ class EgressAddon:
bottle's resolved config (`request()` stashed it — see `_flow_ctx`)."""
config, _slug, env = self._flow_ctx(flow)
route = match_route(config.routes, flow.request.pretty_host)
if route is None or route.dlp_passthrough:
if route is None or not route.inspect:
return
if flow.response is None:
return
@@ -702,7 +702,7 @@ class EgressAddon:
return
config, slug, env = self._flow_ctx(flow)
route = match_route(config.routes, flow.request.pretty_host)
if route is None or route.dlp_passthrough:
if route is None or not route.inspect:
return
message = flow.websocket.messages[-1] # type: ignore[union-attr]
content = message.content.decode("utf-8", errors="replace")
+62 -33
View File
@@ -28,7 +28,7 @@ from .egress_dlp_config import (
ON_MATCH_SUPERVISE,
OUTBOUND_DETECTOR_NAMES,
OUTBOUND_ON_MATCH_VALUES,
parse_dlp_block,
parse_inspect_block,
)
@@ -79,8 +79,8 @@ class Route:
# "" means unset → DEFAULT_OUTBOUND_ON_MATCH. See OUTBOUND_ON_MATCH_VALUES.
outbound_on_match: str = ""
preserve_auth: bool = False
# dlp: false — skip all scanning; HTTPS flows tunnel without TLS interception.
dlp_passthrough: bool = False
# False tunnels HTTPS without TLS interception or HTTP-level controls.
inspect: bool = True
LOG_OFF = 0 # no logging
@@ -261,10 +261,31 @@ def _parse_one(idx: int, raw: object) -> Route:
host: object = raw_dict.get("host")
if not isinstance(host, str) or not host:
raise ValueError(f"{label}: 'host' must be a non-empty string")
legacy_flat = "inspect" not in raw_dict
inspect_raw = raw_dict.get("inspect", {})
if inspect_raw is False:
inspect = False
settings: dict[str, object] = {}
elif isinstance(inspect_raw, dict):
inspect = True
settings = (
{k: v for k, v in raw_dict.items() if k != "host"}
if legacy_flat
else typing.cast(dict[str, object], inspect_raw)
)
legacy_dlp = settings.pop("dlp", None)
if isinstance(legacy_dlp, dict):
settings.update(typing.cast(dict[str, object], legacy_dlp))
elif legacy_dlp is not None:
raise ValueError(
f"{label} ({host}): legacy 'dlp' must be an object"
)
else:
raise ValueError(f"{label} ({host}): 'inspect' must be false or an object")
# matches
matches: tuple[MatchEntry, ...] = ()
matches_raw = raw_dict.get("matches")
matches_raw = settings.get("matches")
if matches_raw is not None:
if not isinstance(matches_raw, list):
raise ValueError(f"{label} ({host}): 'matches' must be a list")
@@ -274,8 +295,8 @@ def _parse_one(idx: int, raw: object) -> Route:
)
# auth (unchanged wire format)
auth_scheme: object = raw_dict.get("auth_scheme", "")
token_env: object = raw_dict.get("token_env", "")
auth_scheme: object = settings.get("auth_scheme", "")
token_env: object = settings.get("token_env", "")
if not isinstance(auth_scheme, str):
raise ValueError(f"{label} ({host}): 'auth_scheme' must be a string")
if not isinstance(token_env, str):
@@ -289,7 +310,7 @@ def _parse_one(idx: int, raw: object) -> Route:
# git-over-HTTPS policy
git_fetch = False
git_raw = raw_dict.get("git")
git_raw = settings.get("git")
if git_raw is not None:
if not isinstance(git_raw, dict):
raise ValueError(f"{label} ({host}): 'git' must be an object")
@@ -307,22 +328,30 @@ def _parse_one(idx: int, raw: object) -> Route:
)
# dlp detectors
outbound_detectors, inbound_detectors, outbound_on_match, dlp_passthrough = parse_dlp_block(
idx, host, raw_dict,
outbound_detectors, inbound_detectors, outbound_on_match = parse_inspect_block(
idx, host, settings,
)
preserve_auth_raw = raw_dict.get("preserve_auth", False)
preserve_auth_raw = settings.get("preserve_auth", False)
if preserve_auth_raw is not True and preserve_auth_raw is not False:
raise ValueError(
f"{label} ({host}): 'preserve_auth' must be a boolean"
)
preserve_auth: bool = preserve_auth_raw
for k in settings:
if k not in (
"matches", "auth_scheme", "token_env", "git", "preserve_auth",
"outbound_detectors", "inbound_detectors", "outbound_on_match",
):
raise ValueError(
f"{label} ({host}): inspect has unknown key {k!r}"
)
for k in raw_dict:
if k not in ("host", "matches", "auth_scheme", "token_env", "dlp", "git", "preserve_auth"):
if not legacy_flat and k not in ("host", "inspect"):
raise ValueError(
f"{label} ({host}): unknown key {k!r}; accepted keys "
f"are 'host', 'matches', 'auth_scheme', 'token_env', 'dlp', 'git', 'preserve_auth'"
f"are 'host' and 'inspect'"
)
return Route(
@@ -335,7 +364,7 @@ def _parse_one(idx: int, raw: object) -> Route:
inbound_detectors=inbound_detectors,
outbound_on_match=outbound_on_match,
preserve_auth=preserve_auth,
dlp_passthrough=dlp_passthrough,
inspect=inspect,
)
@@ -372,27 +401,27 @@ def route_to_yaml_dict(r: Route) -> dict[str, object]:
proposal without translation. Fields that are empty/default are
omitted so the agent doesn't copy irrelevant keys."""
d: dict[str, object] = {"host": r.host}
if not r.inspect:
d["inspect"] = False
return d
inspected: dict[str, object] = {}
if r.auth_scheme:
d["auth_scheme"] = r.auth_scheme
d["token_env"] = r.token_env
inspected["auth_scheme"] = r.auth_scheme
inspected["token_env"] = r.token_env
if r.matches:
d["matches"] = [_match_entry_to_dict(m) for m in r.matches]
inspected["matches"] = [_match_entry_to_dict(m) for m in r.matches]
if r.git_fetch:
d["git"] = {"fetch": True}
if r.dlp_passthrough:
d["dlp"] = False
else:
dlp: dict[str, object] = {}
if r.outbound_detectors is not None:
dlp["outbound_detectors"] = list(r.outbound_detectors)
if r.inbound_detectors is not None:
dlp["inbound_detectors"] = list(r.inbound_detectors)
if r.outbound_on_match:
dlp["outbound_on_match"] = r.outbound_on_match
if dlp:
d["dlp"] = dlp
inspected["git"] = {"fetch": True}
if r.outbound_detectors is not None:
inspected["outbound_detectors"] = list(r.outbound_detectors)
if r.inbound_detectors is not None:
inspected["inbound_detectors"] = list(r.inbound_detectors)
if r.outbound_on_match:
inspected["outbound_on_match"] = r.outbound_on_match
if r.preserve_auth:
d["preserve_auth"] = True
inspected["preserve_auth"] = True
if inspected:
d["inspect"] = inspected
return d
@@ -764,7 +793,7 @@ def scan_outbound(
safe_tokens: typing.AbstractSet[str] | None = None,
crlf_text: str | None = None,
) -> ScanResult | None:
if route.dlp_passthrough:
if not route.inspect:
return None
# Lazy import to avoid circular deps and keep dlp_detectors optional
# at import time (the gateway copies it flat alongside this file).
@@ -863,7 +892,7 @@ def scan_inbound(
route: Route,
body: str | bytes,
) -> ScanResult | None:
if route.dlp_passthrough:
if not route.inspect:
return None
try:
from dlp_detectors import scan_naive_injection # type: ignore[import-not-found]
@@ -892,7 +921,7 @@ __all__ = [
"DEFAULT_OUTBOUND_ON_MATCH",
"OUTBOUND_DETECTOR_NAMES",
"INBOUND_DETECTOR_NAMES",
"parse_dlp_block",
"parse_inspect_block",
"Config",
"Decision",
"HeaderMatch",
+12 -30
View File
@@ -1,6 +1,6 @@
"""DLP detector-config parsing for egress routes (PRD 0053, PRD 0062).
"""Inspection and DLP configuration parsing for egress routes.
A route's optional `dlp:` block names which outbound/inbound detectors run
A route's optional `inspect:` object names which outbound/inbound detectors run
and what the proxy does when an outbound detector matches a token
(`outbound_on_match`). This module owns parsing and validating that block,
kept apart from the request-time scan/decision flow in `egress_addon_core`
@@ -26,25 +26,14 @@ OUTBOUND_ON_MATCH_VALUES = (ON_MATCH_BLOCK, ON_MATCH_REDACT, ON_MATCH_SUPERVISE)
DEFAULT_OUTBOUND_ON_MATCH = ON_MATCH_SUPERVISE
def parse_dlp_block(
def parse_inspect_block(
idx: int,
host: str,
raw_dict: dict[str, object],
) -> tuple[tuple[str, ...] | None, tuple[str, ...] | None, str, bool]:
"""Parse the optional `dlp` block on a route, returning
(outbound_detectors, inbound_detectors, outbound_on_match, passthrough).
`dlp: false` sets passthrough=True: all scanning is skipped and HTTPS
connections are tunnelled without TLS interception."""
dlp_raw = raw_dict.get("dlp")
if dlp_raw is None:
return None, None, "", False
inspect: dict[str, object],
) -> tuple[tuple[str, ...] | None, tuple[str, ...] | None, str]:
"""Parse DLP settings from an inspected route."""
label = f"route[{idx}] ({host})"
if dlp_raw is False:
return None, None, "", True
if not isinstance(dlp_raw, dict):
raise ValueError(f"{label}: 'dlp' must be false or an object")
dlp = typing.cast(dict[str, object], dlp_raw)
dlp = inspect
def _parse_detector_field(
field: str,
@@ -57,18 +46,18 @@ def parse_dlp_block(
return ()
if not isinstance(val, list):
raise ValueError(
f"{label}: dlp.{field} must be false, a list, or omitted"
f"{label}: inspect.{field} must be false, a list, or omitted"
)
items = typing.cast(list[object], val)
names: list[str] = []
for j, item in enumerate(items):
if not isinstance(item, str):
raise ValueError(
f"{label}: dlp.{field}[{j}] must be a string"
f"{label}: inspect.{field}[{j}] must be a string"
)
if item not in valid_names:
raise ValueError(
f"{label}: dlp.{field}[{j}] {item!r} is not a valid "
f"{label}: inspect.{field}[{j}] {item!r} is not a valid "
f"detector name; valid names: {', '.join(sorted(valid_names))}"
)
names.append(item)
@@ -82,16 +71,9 @@ def parse_dlp_block(
if on_match_raw is not None:
if not isinstance(on_match_raw, str) or on_match_raw not in OUTBOUND_ON_MATCH_VALUES:
raise ValueError(
f"{label}: dlp.outbound_on_match must be one of "
f"{label}: inspect.outbound_on_match must be one of "
f"{', '.join(OUTBOUND_ON_MATCH_VALUES)} (got {on_match_raw!r})"
)
on_match = on_match_raw
for k in dlp:
if k not in ("outbound_detectors", "inbound_detectors", "outbound_on_match"):
raise ValueError(
f"{label}: dlp has unknown key {k!r}; accepted keys "
f"are 'outbound_detectors', 'inbound_detectors', "
f"'outbound_on_match'"
)
return outbound, inbound, on_match, False
return outbound, inbound, on_match
+42 -36
View File
@@ -72,7 +72,7 @@ class ManifestEgressRoute:
InboundDetectors: tuple[str, ...] | None = None
OutboundOnMatch: str = ""
PreserveAuth: bool = False
DlpPassthrough: bool = False
Inspect: bool = True
@classmethod
def from_dict(cls, bottle_name: str, idx: int, raw: object) -> "ManifestEgressRoute":
@@ -82,9 +82,17 @@ class ManifestEgressRoute:
if not isinstance(host, str) or not host:
raise ManifestError(f"{label} missing required string field 'host'")
inspect_raw = d.get("inspect", {})
if inspect_raw is False:
inspect = False
inspect_d: dict[str, object] = {}
else:
inspect = True
inspect_d = as_json_object(inspect_raw, f"{label} inspect")
# --- matches ---
matches: tuple[ManifestMatchEntry, ...] = ()
matches_raw = d.get("matches")
matches_raw = inspect_d.get("matches")
if matches_raw is not None:
if not isinstance(matches_raw, list):
raise ManifestError(
@@ -102,9 +110,9 @@ class ManifestEgressRoute:
# --- auth ---
auth_scheme = ""
token_ref = ""
if "auth" in d:
auth_raw = d.get("auth")
auth_d = as_json_object(auth_raw, f"{label} auth")
if "auth" in inspect_d:
auth_raw = inspect_d.get("auth")
auth_d = as_json_object(auth_raw, f"{label} inspect.auth")
if not auth_d:
raise ManifestError(
f"{label} auth is empty ({{}}); omit the 'auth' key "
@@ -164,20 +172,19 @@ class ManifestEgressRoute:
f"the 'role' field is reserved for future use"
)
# --- dlp ---
# --- DLP settings (inspection-only) ---
outbound_detectors: tuple[str, ...] | None = None
inbound_detectors: tuple[str, ...] | None = None
outbound_on_match = ""
dlp_passthrough = False
if "dlp" in d:
outbound_detectors, inbound_detectors, outbound_on_match, dlp_passthrough = _parse_dlp_block(
label, d.get("dlp"),
if inspect:
outbound_detectors, inbound_detectors, outbound_on_match = _parse_inspect_block(
label, inspect_d,
)
# --- git-over-HTTPS policy ---
git_fetch = False
if "git" in d:
git_d = as_json_object(d.get("git"), f"{label} git")
if "git" in inspect_d:
git_d = as_json_object(inspect_d.get("git"), f"{label} inspect.git")
raw_fetch = git_d.get("fetch", False)
if isinstance(raw_fetch, bool):
git_fetch = raw_fetch
@@ -195,8 +202,8 @@ class ManifestEgressRoute:
# --- preserve_auth ---
preserve_auth = False
if "preserve_auth" in d:
raw_preserve_auth = d.get("preserve_auth")
if "preserve_auth" in inspect_d:
raw_preserve_auth = inspect_d.get("preserve_auth")
if not isinstance(raw_preserve_auth, bool):
raise ManifestError(
f"{label} preserve_auth must be a boolean "
@@ -204,11 +211,22 @@ class ManifestEgressRoute:
)
preserve_auth = raw_preserve_auth
for k in inspect_d:
if k not in (
"matches", "auth", "git", "preserve_auth",
"outbound_detectors", "inbound_detectors", "outbound_on_match",
):
raise ManifestError(
f"{label} inspect has unknown key {k!r}; accepted keys are "
f"'matches', 'auth', 'git', 'preserve_auth', "
f"'outbound_detectors', 'inbound_detectors', "
f"'outbound_on_match'"
)
for k in d:
if k not in ("host", "matches", "auth", "role", "dlp", "git", "preserve_auth"):
if k not in ("host", "role", "inspect"):
raise ManifestError(
f"{label} has unknown key {k!r}; accepted keys are "
f"'host', 'matches', 'auth', 'role', 'dlp', 'git', 'preserve_auth'"
f"'host', 'role', and 'inspect'"
)
return cls(
@@ -222,7 +240,7 @@ class ManifestEgressRoute:
InboundDetectors=inbound_detectors,
OutboundOnMatch=outbound_on_match,
PreserveAuth=preserve_auth,
DlpPassthrough=dlp_passthrough,
Inspect=inspect,
)
@@ -342,18 +360,13 @@ def _parse_header_match(
return ManifestHeaderMatch(Name=name, Value=value, Type=htype)
def _parse_dlp_block(
def _parse_inspect_block(
route_label: str,
raw: object,
) -> tuple[tuple[str, ...] | None, tuple[str, ...] | None, str, bool]:
"""Parse the `dlp` value on a route.
`dlp: false` is a full bypass: no scanning, HTTPS tunnelled without
TLS interception. Returns (outbound, inbound, on_match, passthrough)."""
if raw is False:
return None, None, "", True
label = f"{route_label} dlp"
d = as_json_object(raw, label)
inspect: dict[str, object],
) -> tuple[tuple[str, ...] | None, tuple[str, ...] | None, str]:
"""Parse DLP settings from an inspected route."""
label = f"{route_label} inspect"
d = inspect
def _parse_field(
field: str,
@@ -396,14 +409,7 @@ def _parse_dlp_block(
)
on_match = on_match_raw
for k in d:
if k not in ("outbound_detectors", "inbound_detectors", "outbound_on_match"):
raise ManifestError(
f"{label} has unknown key {k!r}; accepted keys are "
f"'outbound_detectors', 'inbound_detectors', "
f"'outbound_on_match'"
)
return outbound, inbound, on_match, False
return outbound, inbound, on_match
LOG_LEVELS = frozenset({0, 1, 2})
+10 -9
View File
@@ -168,15 +168,16 @@ _ROUTES_YAML_DESCRIPTION = (
"Full proposed /etc/egress/routes.yaml content. "
"Each route entry accepts these keys:\n"
" host: <hostname> (required)\n"
" auth_scheme: Bearer|token (must pair with token_env)\n"
" token_env: <ENV_VAR_NAME> (must pair with auth_scheme)\n"
" matches: (optional list of match entries)\n"
" - paths: [{type: prefix|exact|regex, value: /...}]\n"
" methods: [GET, POST, ...]\n"
" headers: [{name: X-Hdr, value: val, type: exact|regex}]\n"
" git: (optional; omit to block git clone/fetch)\n"
" fetch: true\n"
" dlp: (optional DLP scanner overrides)\n"
" inspect: false (opaque whole-host TLS tunnel; no HTTP controls)\n"
" inspect: (omit for inspected defaults)\n"
" auth_scheme: Bearer|token (must pair with token_env)\n"
" token_env: <ENV_VAR_NAME> (must pair with auth_scheme)\n"
" matches: (optional list of match entries)\n"
" - paths: [{type: prefix|exact|regex, value: /...}]\n"
" methods: [GET, POST, ...]\n"
" headers: [{name: X-Hdr, value: val, type: exact|regex}]\n"
" git: (optional; omit to block git clone/fetch)\n"
" fetch: true\n"
" outbound_detectors: [token_patterns, known_secrets]\n"
" inbound_detectors: [naive_injection_detection]\n"
" outbound_on_match: block|redact|supervise (default supervise)\n"