Sandbox Proxy — CLI Verification Matrix
This page is how you confirm the v4 sandbox HTTPS proxy is mediating credentials correctly from the perspective of common command-line HTTPS clients. Each section is a recipe you can run by hand inside an aileron launch --sandbox=docker session against a published Aileron connector spec.
The matrix complements the Sandbox MCP walkthrough (which exercises the same data plane from the agent’s MCP transport) and the BYO Image Proxy Contract (which is what these CLIs depend on inside the container).
Per-CLI sealing properties
Each CLI has two properties that govern how Aileron seals its traffic. These are documentation properties, not config fields.
| CLI | proxy-sealable | emit-mechanism |
|---|---|---|
| curl | yes | inject |
| gh | yes | sentinel-swap |
| aws | yes | inject |
proxy-sealable means the daemon can inject the real credential at the TLS forward-proxy boundary so the container never holds it.
emit-mechanism is how the in-container client is made to emit a request the proxy can seal. It is independent of the injection scheme (bearer, basic, and so on), which is how the proxy writes the credential once the request arrives.
- The
injectmechanism: the launcher plants no token. The client emits an unauthenticated request, and the proxy injects the bound credential unconditionally at egress.curlissues the request with no token.git-over-HTTPS issues an unauthenticated request because the launcher mounts a no-op credential helper. - The
sentinel-swapmechanism: the launcher plants a non-secret, format-mimicking sentinel token. Some clients short-circuit locally when they hold no token, so they never issue the request and there is nothing for the proxy to seal. The sentinel makes the client treat itself as authenticated and issue the request. The proxy then swaps the sentinel for the real credential at egress. The proxy swaps only the sentinel it itself planted. A foreign token on asentinel-swaphost is forwarded unchanged. See ADR-0019.
Note: aws’s emit-mechanism is placeholder-plant plus sigv4-resign. The launcher plants placeholder static credentials so botocore signs locally, then the proxy strips that signature and re-signs with the real secret at egress. This is distinct from the unauthenticated-emit inject case above, where the client emits no token at all. The table marks aws as inject to keep the column to two canonical values, and the aws section below documents the sigv4-resign scheme in full.
What the proxy guarantees
When you call an installed connector operation through the proxy:
- Aileron resolves the operation against your installed connector specs.
- The matching credential binding is resolved on the daemon side and injected into the upstream request at the boundary.
- The container never sees the raw credential. The CLI inside the container only knows the proxy URL.
- A
connector.proxy.proxiedaudit event is recorded with the matched connector FQN, tool, operation, upstream host (not the full URL), and a sanitized status. No credential bytes leak into the audit log.
When the CLI calls something the proxy does not recognize (no matching connector operation, ambiguous match, oversized body, etc.), the proxy fails closed with sandbox.proxy.rejected (or connector.proxy.rejected for matched-but-rejected). The container sees the rejection envelope, never an upstream response.
Common prerequisites
Every recipe below assumes:
- A running
aileron launch --sandbox=docker <agent>session. The proxy is on by default (see ADR-0019). - At least one installed connector spec whose operation matches the request you’re about to make. Use
aileron connector listto see what’s installed; pick an operation host + path you can hit from inside the container. - The Aileron-mounted CA at
/etc/aileron/proxy/ca.pem. The launcher mounts this automatically; the BYO image’saileron-install-proxy-caadds it to the trust store before the agent starts.
The proxy URL inside the container is $HTTPS_PROXY. The launcher sets it; you can confirm with echo $HTTPS_PROXY from within the agent’s shell. It looks like http://<session-id>:<daemon-token>@host.docker.internal:<port>.
curl
curl is the canonical test target. It respects HTTPS_PROXY out of the box and lets you control the CA bundle explicitly with --cacert.
Install
ghcr.io/alrubinger/aileron-sandbox-base ships with curl. For other base images:
| Distro | Install |
|---|---|
| Debian / Ubuntu | apt-get install -y curl |
| Alpine | apk add curl |
| RHEL / Fedora | dnf install -y curl |
Configure
The launcher sets HTTPS_PROXY for you, so a bare curl call already routes through the proxy. The CA bundle is in the system trust store (installed by aileron-install-proxy-ca), so curl validates Aileron’s session CA without extra flags. You can pin the CA bundle explicitly with --cacert if you want to be defensive:
curl --cacert /etc/aileron/proxy/ca.pem https://api.example.test/pathSuccess case
Pick an installed connector operation. For a hypothetical Linear connector with GET /graphql on api.linear.app:
curl --silent --show-error \
https://api.linear.app/graphql \
-G --data-urlencode "query=query{viewer{id}}"Expected:
-
HTTP 200 with the upstream’s JSON body.
-
A
connector.proxy.proxiedaudit event. Confirm with:aileron audit list --limit 5Look for an entry like:
{ "event_type": "connector.proxy.proxied", "payload": { "aileron.connector.fqn": "github://acme/aileron-connector-linear", "aileron.connector.tool": "linear", "aileron.connector.operation": "viewer.get", "aileron.connector.boundary": "https_proxy", "aileron.connector.mediation": "https_proxy", "aileron.connector.decision": "proxied", "aileron.proxy.source": "transparent_connect_tls", "aileron.proxy.upstream.host": "api.linear.app", "aileron.proxy.upstream.status": 200 } }No credential bytes anywhere in the payload. The query string and request body are not echoed.
Failure case
Hit an unmatched upstream:
curl --silent --show-error -i https://example.com/Expected:
-
The upstream’s actual HTTP response.
example.comreturns 200 with its example page; an unreachable upstream returns 502 from the proxy. -
A
sandbox.proxy.passthroughaudit event. Confirm:aileron audit list --limit 5Look for
aileron.proxy.decision: "passthrough"andaileron.proxy.upstream.host: "example.com". The full URL is not in the payload, only the upstream host, path, and method. Under the credential-injection-only model (ADR-0019) the proxy does not inject a credential on unmatched requests, and it does not refuse them. Container-level egress hardening lives at a different layer.
gh (GitHub CLI)
gh is proxy-sealable via sentinel-swap. It honors HTTPS_PROXY once you set it, and it picks up the system CA store, so it works through the Aileron proxy with no extra config. Aileron seals it against the built-in api.github.com host binding, so you do not need an installed GitHub connector spec for gh api to work.
gh is the reason the sentinel-swap mechanism exists. gh validates its auth state locally before making a request. With no token it short-circuits and never issues the request, so the inject mechanism (plant nothing, seal the emitted request) has nothing to seal. The launcher closes this gap by planting a non-secret, format-mimicking sentinel as GH_TOKEN when a user/github entry exists in your vault. The sentinel passes gh’s own local validation, so gh issues its request. The daemon recognizes the sentinel at the TLS boundary and swaps in your real GitHub token before the request leaves the host. The real token never enters the container. The sentinel is non-secret and safe to see in echo $GH_TOKEN output; presenting it to GitHub authenticates nothing.
Store your token once with aileron auth github. The launcher does the rest on every sandbox launch.
Install
| Distro | Install |
|---|---|
| Debian / Ubuntu | See github.com/cli/cli ↗ for the official apt repo. |
| Alpine | apk add github-cli (Alpine’s community repo). |
| RHEL / Fedora | dnf install -y gh. |
Configure
There is nothing to configure inside the container. The launcher already set HTTPS_PROXY and planted the sentinel GH_TOKEN. You can confirm both:
# Set by the launcher
echo $HTTPS_PROXY
# The non-secret sentinel the launcher planted. This is NOT your real
# token; the daemon swaps it for the real one at egress.
echo $GH_TOKENDo not run gh auth login with a hand-typed placeholder. The launcher’s sentinel is the only token gh needs, and the daemon owns the swap.
Success case
gh api userExpected:
- 200 with the authenticated user’s JSON.
- A
sandbox.proxy.binding_injectedaudit event withaileron.proxy.binding.host: "api.github.com"andaileron.proxy.binding.scheme: "bearer". The daemon swapped the sentinel for the real token; the upstream sawAuthorization: Bearer <real-token>and the audit payload holds no token or sentinel bytes.
Foreign-token case
If you supply your own token instead of the sentinel (for example you ran gh auth login --with-token with a real token), the proxy does not swap it. It seals only the sentinel it planted.
echo "ghp_your_own_real_token" | gh auth login --with-token
gh api userExpected:
- The upstream sees your own token unchanged. The daemon injects nothing.
- A
sandbox.proxy.foreign_token_not_swappedaudit event withaileron.proxy.binding.host: "api.github.com". The payload holds no token bytes.
Verification status
The gh sentinel-swap is asserted by the deterministic end-to-end test TestSentinelSwap_* in internal/app/sandbox_forward_proxy_sentinel_swap_test.go, which drives a sentinel-bearing and a foreign-token request through the real CONNECT/TLS proxy boundary against a fake api.github.com upstream and asserts the swap, the no-swap, the isolation, and the audit shape. A real gh against live api.github.com over the network is not exercised in CI (it requires a real GitHub token and network egress from the test runner), so this row is asserted by the deterministic test rather than by an empirical live-network run.
aws (AWS CLI)
aws’s botocore HTTP layer reads HTTPS_PROXY from the environment, so the launcher-set proxy applies with no extra config. You only need to point aws at Aileron’s session CA. Set AWS_CA_BUNDLE to the mounted CA so botocore trusts the proxy’s intercepted TLS.
Install
| Distro | Install |
|---|---|
| Debian / Ubuntu | Follow the official bundled-installer ↗ flow. |
| Alpine | apk add aws-cli (Alpine’s community repo). |
| RHEL / Fedora | dnf install -y awscli2. |
Configure
export AWS_CA_BUNDLE=/etc/aileron/proxy/ca.pem
# HTTPS_PROXY is already set by the launcher; aws's botocore reads it.
# Like gh, aws still wants local credentials — use a placeholder. The
# real credential is injected by the daemon proxy.
aws configure set aws_access_key_id placeholder-aileron-injects-real
aws configure set aws_secret_access_key placeholder-aileron-injects-real
aws configure set region us-east-1aws is proxy-sealable via sigv4-resign. The placeholder credentials above let botocore sign the request locally so it leaves the client. At the TLS proxy boundary the daemon strips that local signature and re-signs the request with the real secret access key resolved host-side, using the access key id declared in the sigv4-resign binding. The binding carries only the access key id and is keyed by its credential identity rather than a host. The daemon derives the signing region and service from the resolved upstream host, so the binding carries no region or service and no second copy of the region can drift from the host being signed for. The real secret access key never enters the container; it is HMAC key material the daemon holds in the vault. See ADR-0019.
Success case
For a sigv4-resign host binding on s3.amazonaws.com whose credential_ref resolves the secret access key from your vault (user/aws):
aws s3api list-bucketsExpected:
- 200 with the upstream’s response body.
- A
sandbox.proxy.binding_injectedaudit event withaileron.proxy.binding.host: "s3.amazonaws.com"andaileron.proxy.binding.scheme: "sigv4-resign". The upstream received a well-formedAuthorization: AWS4-HMAC-SHA256 …header whoseCredential=<access-key-id>/.../<region>/<service>/aws4_requestscope and signature validate against the daemon’s secret. The placeholder secret you configured locally never reaches the upstream, and the audit payload holds no secret bytes.
The verification point is that the secret access key is present in the daemon vault and nowhere in the container. The container’s static credentials are placeholders. The signature the upstream accepts was computed host-side with the real secret.
Unmatched case
aws ec2 describe-instances --region us-east-1 # unmatched if no sigv4-resign binding covers ec2Expected:
awsreports the EC2 endpoint’s actual response. With nosigv4-resignbinding for that host the proxy injects nothing, so the placeholder credentials botocore signed with reach the upstream and AWS returns a SigV4 authentication error.sandbox.proxy.passthroughaudit,aileron.proxy.upstream.host: "ec2.us-east-1.amazonaws.com". The proxy does not re-sign the request and does not refuse it. A matched host gets a real signature; an unmatched host passes through unchanged.
What “no credential leak” means in practice
After each of the recipes above, run:
aileron audit list --limit 10 | jq '.[] | .payload' | grep -i -E "bearer|token|api[_-]key|secret"This should return nothing. The audit subsystem sanitizes payloads before they touch the log — even if you accidentally pasted a real credential into a curl body, the daemon’s proxy boundary strips it before recording.
Automated coverage
Two real-client subprocess integration tests live under the integration_sandbox build tag and run together with one command:
task test:integration:sandbox-proxy-clientsThe target is fail-fast and requires both curl and aws on PATH. It does not self-skip.
A curl-driven Go integration test lives at internal/app/sandbox_proxy_curl_integration_test.go. The test stands up a fake HTTPS upstream, runs curl as a host subprocess pointed at the in-process proxy with a generated session CA, and asserts:
curlperforms CONNECT-based HTTPS interception through the proxy. The test runscurlwith the--proxyflag and an empty environment (noHTTPS_PROXYset), so it proves the proxy intercepts a CONNECT tunnel and seals the TLS stream. The env-drivenHTTPS_PROXY+AWS_CA_BUNDLEconfiguration path is proven separately by theawstest below.- The proxy injects the credential at the boundary (the fake upstream sees
Authorization: Bearer <secret>, never thecurlinvocation). - The container’s
curlinvocation does not carry the credential (proves credential isolation). - A
connector.proxy.proxiedaudit event is emitted with the matched connector FQN, tool, operation, upstream host (not URL), andaileron.connector.boundary: https_proxy.
An aws-driven SigV4 integration test lives at internal/app/sandbox_proxy_aws_sigv4_integration_test.go. The test stands up a SigV4-validating fake HTTPS upstream, runs aws s3api list-buckets as a host subprocess against a sigv4-resign host binding with the secret access key in the daemon vault and placeholder static credentials in the subprocess environment, and asserts:
awshonorsHTTPS_PROXYandAWS_CA_BUNDLE(proves env-driven configuration works for the AWS CLI).- The upstream receives a syntactically and cryptographically valid
AWS4-HMAC-SHA256Authorization header whose signature the upstream recomputes with the same secret the daemon holds and confirms matches (proves a genuine valid SigV4 reached upstream, not merely a 200). - The real secret access key appears in none of the
awssubprocess environment, theawsargv, or any upstream request header, query, or body (proves credential isolation). - A
sandbox.proxy.binding_injectedaudit event is emitted withaileron.proxy.binding.scheme: "sigv4-resign"and the upstream host, and no secret bytes in the payload.
The upstream is a local SigV4-validating stub, not live AWS. The stub recomputes the canonical request and signature from what it received using the secret the daemon vault holds, which proves the proxy emitted a valid signature without requiring network egress to AWS or a real account.
The gh sentinel-swap is covered deterministically by internal/app/sandbox_forward_proxy_sentinel_swap_test.go, which drives sentinel-bearing and foreign-token requests through the in-process proxy against a fake api.github.com upstream and asserts the swap, the no-swap, the sentinel-never-reaches-upstream isolation, and the no-secret audit shape. It does not require real gh, a real token, or real network.