In a throwaway lab I ran one request: the container exits on boot, here is docker inspect, tell me why. I did not paste a stack trace. I pasted the whole JSON. The model got Env: a database URL, a cloud key, a webhook. That is the failure. Not a CVE. A trust-boundary violation on a command every platform team already runs.
Would you drop that blob into a public ticket? Then why is a remote model a safer inbox?
This walkthrough turns that paste into a regression fixture. Positive case, negative case, pinned command, expected evidence. If you strip every product name out of this article, the invariant still holds: container inspect output is a secret document until you prove otherwise.
The failing request
I keep the lab boring on purpose. One Compose service. One inspect. One chat.
# Lab only. Do not point this at a production socket.
docker inspect lab-api --format '{{json .Config.Env}}'
On Docker Engine 27.x the Env array is a list of KEY=value strings. Helpful for you. Catastrophic for a model prompt. A typical blob looks like this — every value is a canary, not a real credential:
[
"DATABASE_URL=postgres://lab:lab-canary-dbpass@db:5432/app",
"AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYCANARY",
"STRIPE_WEBHOOK_SECRET=whsec_lab_canary_do_not_ship"
]
Feed that to any remote completion API and you have crossed a boundary you cannot uncross. Logs on their side. Retention you do not control. A later fine-tune corpus you will never see. The agent did not "hack" Docker. You handed it the privileged view.
MCP filesystem tools make the same mistake with less ceremony. "Read the compose file." "Dump kubeconfig so I can reason about the cluster." Same class of failure. Different filename.
Trust boundaries that actually matter
I draw four boxes. Not a marketing architecture. A paste map.
-
Workstation / CI runner — Docker socket,
~/.kube/config,.env, shell history. - Agent harness — the process that decides which tool output becomes prompt context.
- Transport — HTTPS to a model endpoint, or a loopback server you run yourself.
- Model provider — training policy, operator access, subprocessors. Unknown unless you have a contract.
The secret does not need to "escape the VPC." It needs to enter box 3. Once it does, prevent is over. You are in detect and recover. So the invariant belongs before the harness serializes tool output, not in a model-vendor checkbox.
Ask the ugly question: who is the audience of docker inspect? You, with a TTY. Or a remote model with a retention clock?
Secret classes, not regex heroics
Regex for AKIA is a party trick. It misses DATABASE_URL, it misses docker secrets, it misses a kubeconfig client-key-data block. I classify document types, then redact or deny.
| Class | Example artifact | Default action |
|---|---|---|
| Container runtime dump |
docker inspect, docker compose config
|
Strip Env, Mounts source paths, healthcheck command env |
| Cluster identity |
~/.kube/config, kubectl config view --raw
|
Deny. Never send. |
| Cloud identity |
~/.aws/credentials, instance metadata |
Deny. |
| Process env |
printenv, GitHub Actions debug logs |
Deny or allowlist keys |
| App config |
.env, application-prod.yml
|
Deny. |
| Benign debug | Dockerfile without secrets, README, public image name | Allow. |
Is docker inspect "just metadata"? Only if you deleted Config.Env, Config.Cmd when it embeds flags, and host bind mounts that reveal home directories. Otherwise it is a secret class.
Fixture: classify, redact, deny
Unexecuted until you run it on your socket. Python 3.12. No network. The script is the gate, not the model.
# redact_inspect.py — lab fixture, not a WAF
from __future__ import annotations
import json
import posixpath
from pathlib import Path
DENY_PATH_SUFFIXES = (
"/.aws/credentials",
"/.kube/config",
"/.docker/config.json",
"/.env",
"/.env.production",
)
DENY_ENV_KEY_FRAGMENTS = (
"SECRET",
"PASSWORD",
"TOKEN",
"AWS_",
"DATABASE_URL",
"PRIVATE_KEY",
"WEBHOOK",
)
REDACT = "[REDACTED_BY_FIXTURE]"
def path_denied(path: str) -> bool:
normalized = posixpath.normpath(path.replace("\\", "/"))
return any(normalized.endswith(suffix) for suffix in DENY_PATH_SUFFIXES)
def redact_env_list(env: list[str]) -> list[str]:
out: list[str] = []
for item in env:
key, _, _value = item.partition("=")
if any(frag in key.upper() for frag in DENY_ENV_KEY_FRAGMENTS):
out.append(f"{key}={REDACT}")
else:
out.append(item)
return out
def sanitize_inspect(doc: dict) -> dict:
cfg = doc.get("Config") or {}
if "Env" in cfg and isinstance(cfg["Env"], list):
cfg = {**cfg, "Env": redact_env_list(cfg["Env"])}
return {**doc, "Config": cfg}
def gate_tool_read(path: str, raw: str) -> str:
if path_denied(path):
raise PermissionError(f"deny-listed path: {path}")
if Path(path).name == "inspect.json":
return json.dumps(sanitize_inspect(json.loads(raw)))
return raw
Wire it in front of whatever you use to stuff tool output into a prompt. MCP read_file. A shell tool. A "paste this JSON" UI. Same function. The model never sees the pre-gate bytes.
Positive and negative cases
I want the fixture to fail closed in CI. Two files. No production cluster.
Negative fixture — must raise or redact. Expected evidence: PermissionError or REDACTED_BY_FIXTURE in the serialized prompt, original canary absent.
# test_redact_inspect.py
import json
import pytest
from redact_inspect import gate_tool_read, sanitize_inspect, REDACT
CANARY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYCANARY"
def test_aws_credentials_path_denied():
with pytest.raises(PermissionError):
gate_tool_read("/home/dev/.aws/credentials", "aws_secret_access_key=x")
def test_inspect_env_redacted():
doc = {"Config": {"Env": [
"PORT=8080",
f"AWS_SECRET_ACCESS_KEY={CANARY}",
]}}
out = sanitize_inspect(doc)
blob = json.dumps(out)
assert CANARY not in blob
assert f"AWS_SECRET_ACCESS_KEY={REDACT}" in blob
assert "PORT=8080" in blob
Positive fixture — must pass. A README and a port-only env line are allowed. If your gate redacts PORT, you broke developer usefulness. Tighten keys, do not nuke the document.
def test_readme_allowed():
text = gate_tool_read("README.md", "# lab-api\\nPORT=8080")
assert "lab-api" in text
Run:
python -m pytest test_redact_inspect.py -q
If the negative test is skipped, you do not have a control. You have a blog post.
Prevent / detect / recover
| Layer | Prevent | Detect | Recover |
|---|---|---|---|
| Human paste | Refuse to copy Env / kubeconfig into chat |
Canary string in prompt logs | Rotate every value that appeared |
| Harness |
gate_tool_read on every tool result |
CI asserts canary never leaves the fixture | Kill the session, wipe local chat store |
| Network | Egress allowlist: model host only, no metadata IP | Alert on POST body size + AWS_ substring |
Provider ticket + key rotation |
| Runtime | Docker secrets / compose secrets:, not environment:
|
Scan images for leftover .env
|
Rebuild without the file |
Prevent is cheap. Recover is a weekend. I still see teams skip the canary: if you never planted CANARY in the lab inspect, you cannot prove the gate worked.
Where the model lives changes blast radius — not the invariant
A local or self-hosted completion server shrinks box 4. The JSON may never hit a third-party trainer. Good. It does not make Env safe to log inside the harness. Your laptop SSD, your CI artifact store, your "debug this agent session" dump — those are still copies.
I treat MonkeyCode here as one place that combination shows up in practice: an open-source AI development workspace with operator-supplied free model access and a free server option, so the completion hop can sit on infrastructure you chose instead of an anonymous paste into a consumer chatbot. Disclosure: This article was prepared as part of MonkeyCode's product outreach. If you evaluate that path, run the inspect fixture before you attach a model, free or not. The gate does not care which vendor is downstream.
Remote free endpoints still count as box 3 plus box 4. Self-hosted still counts as box 2. Pick the blast radius on purpose. Do not pretend the document class changed.
Limitations — who should not use this as-is
This fixture is a document classifier. It will not stop a model from inferring a password you typed in the user prompt. It will not replace IAM, it will not rotate keys, it will not audit a WAF.
Do not use it if:
- Your agent already has unrestricted Docker socket access in production. Fix that first. A redactor in front of a root socket is theater.
- You need a guaranteed zero-knowledge protocol. This is allow/deny plus string redaction.
- Your
Envkeys are adversarial (P0RTvsPORT, Unicode homoglyphs). Extend the classifier; do not trust a fragment list against a motivated insider. - You were about to send real inspect output to a public model "just to test the script." Use canaries. Always.
I also will not claim I found a vendor vulnerability. I reproduced a paste. That is enough.
Which invariant belongs in CI?
Put the negative tests in CI. They are cheap and they fail closed. Put the path deny-list in the harness, the layer that concatenates tool output into prompts. Do not ask the model to "please ignore secrets." That is not an enforcement point.
One boundary question to close: if docker inspect is too sensitive for a public GitHub issue, which layer on your team is actually stopping it from entering a model context window — and can you show the failing fixture from last week's build?
Top comments (0)