Skip to content

q15-proxy

q15-proxy sits between q15-exec and the internet. Its primary job is credential injection: attaching the right secrets to the right requests at the network layer, so the agent never sees them.

q15’s philosophy is broad access. The proxy does not maintain a whitelist of allowed destinations. The agent can reach most of the internet freely. What it cannot do is access credentials — those belong to the proxy.

A man-in-the-middle (MITM) proxy intercepts network traffic between a client and a server. In web security, “MITM” usually describes an attacker. q15 turns the concept inside out: the MITM is the defense, and it is controlled by you, not by the model.

Here is how it works mechanically:

  1. HTTP CONNECT — When q15-exec makes an HTTPS request, the HTTP client first sends a CONNECT request to the proxy, asking it to establish a TCP tunnel to the destination host.
  2. Selective TLS interception — For hosts that have matching proxy rules, the proxy intercepts the TLS handshake. It generates a leaf certificate signed by its own CA, presents it to the client, and terminates TLS on its end. This lets the proxy read and modify the plaintext HTTP request before forwarding it over a new TLS connection to the real server. For hosts without rules, the proxy transparently tunnels the connection — it never sees inside the TLS.
  3. Request mutation — While the request is in plaintext (between the client and the proxy), the proxy can inject headers, replace placeholders, or add basic auth credentials.
  4. Forward — The proxy forwards the mutated request to the destination server with the correct credentials attached.

The proxy generates its own CA certificate (ECDSA, 365-day validity) on first start and persists it in /var/lib/q15/proxy/. The CA certificate is exported to the exec environment so that HTTP clients inside q15-exec trust the proxy’s leaf certificates. Without this trust, TLS interception would produce certificate errors.

flowchart TD
    Client["q15-exec<br/>HTTP client"] -->|"CONNECT api.github.com:443"| Proxy["q15-proxy"]
    Proxy -->|"Has rule for api.github.com?<br/>Yes → MITM"| Intercept["TLS intercepted<br/>plaintext visible"]
    Intercept -->|"Inject GH_TOKEN header"| Forward["Forward to api.github.com<br/>with credentials"]
    Proxy -->|"Has rule?<br/>No → transparent tunnel"| Tunnel["Plain CONNECT tunnel<br/>no inspection"]
    Tunnel --> PassThrough["example.com<br/>passed through untouched"]
    style Proxy fill:#bbf7d0,stroke:#16a34a,color:#16a34a
    style Intercept fill:#dbeafe,stroke:#2563eb,color:#2563eb
    style Tunnel fill:#f3f4f6,stroke:#6b7280
    style PassThrough fill:#f3f4f6,stroke:#6b7280

Credential injection is the proxy’s core function. The idea is simple: the agent says “fetch from api.github.com” and the proxy attaches the GitHub token to that request. The agent never sees the token.

Rules define which hosts get credentials and how those credentials are attached. Three injection mechanisms are available:

The most common mechanism. A rule can set arbitrary headers on matching requests, with {{ secret.alias }} template syntax for embedding secret values:

rules:
- name: github-api
match_hosts:
- api.github.com
set_header:
Authorization: 'Bearer {{ secret.github_token }}'

When a request to api.github.com passes through, the proxy renders the template, resolves github_token from its secret store, and sets Authorization: Bearer ghp_... on the request. The agent’s original request had no auth header — the proxy added it.

For services that use HTTP Basic authentication:

rules:
- name: internal-api
match_hosts:
- internal.example.com
set_basic_auth:
username: agent
secret: internal_api_password

The proxy base64-encodes username:secret_value and sets the Authorization: Basic ... header.

For credentials that need to appear in query parameters, paths, or non-standard headers, the proxy can replace opaque placeholder strings with real secret values:

rules:
- name: search-api
match_hosts:
- search.example.com
replace_placeholder:
- placeholder: '__API_KEY__'
secret: search_api_key
in:
- query
- header

The agent sends a request with ?key=__API_KEY__ in the query string. The proxy replaces __API_KEY__ with the real key before forwarding. The agent only ever saw the placeholder.

As a defense-in-depth measure, the proxy strips credential-bearing headers from responses before they reach the agent:

  • Authorization
  • Proxy-Authorization
  • X-Api-Key

This prevents credentials from leaking back into the agent’s context through response content.

proxy:
no_proxy:
- localhost
- 127.0.0.1
- ::1
- q15-proxy
- q15-exec
set_lowercase_proxy_env: true
secrets:
- github_token
rules:
- name: github-api
match_hosts:
- api.github.com
set_header:
Authorization: 'Bearer {{ secret.github_token }}'

In this example, any HTTPS request to api.github.com gets TLS-intercepted and an Authorization header injected from the github_token secret alias. Requests to any other host pass through the proxy untouched — no interception, no mutation.

Proxy secret aliases resolve from either the uppercased alias name as an environment variable, or its _FILE companion. For example, the alias github_token resolves from:

  • GITHUB_TOKEN (env var)
  • GITHUB_TOKEN_FILE (file path containing the secret value)

This means secrets never appear in the proxy config file itself. They are injected at runtime from the deployment environment.

The proxy’s design embodies a specific principle: secrets belong to the proxy, not the agent.

In a typical agent setup, API credentials live in environment variables or config files that the model can read. A successful prompt injection attack can ask the agent to read and exfiltrate those credentials.

In q15, the agent never sees the raw secret values. It requests an action (“fetch from api.github.com”) and the proxy attaches the credential at the network layer. The agent’s context contains no credentials to steal — because the proxy owns them, and the proxy is a separate service the agent cannot influence.

This is the defense-in-depth principle in practice: even if the model is fully compromised, the attacker cannot exfiltrate credentials the agent never had. The agent can make requests to the internet — that is by design — but it does so without holding any secrets.