Skip to content

Proposal: Hardening environment variables in agent runtime #39278

Description

@wylswz

Self Checks

  • I have read the Contributing Guide and Language Policy.
  • I have searched for existing issues search for existing issues, including closed ones.
  • I confirm that I am using English to submit this report, otherwise it will be closed.
  • Please do not modify this template :) and fill in all the required fields.

1. Is this request related to a challenge you're experiencing? Tell me about your story.

Environment Hardening: Secret Injection Without Exposure

Problem

Secret environment variables in shell outputs are currently redacted via a configurable pattern list, but leakage remains possible:

  • Encoding bypass — apply a transformation (e.g. base64) to the sensitive envvar, then decode it externally.
  • Exfiltration via network — transmit the secret directly to untrusted sites (e.g. as an HTTP header value).

Pattern-based redaction is inherently an arms race; the root cause is that secrets live in the agent's process address space at all.


Proposed Solution

Remove secrets from the agent's process entirely. Instead of injecting real values as environment variables, maintain a mapping outside the agent's reachable filesystem and resolve placeholders at the network boundary.

Provider Profiles

Each provider is defined by a complete injection profile — not just a secret value, but the full mapping of which env var is injected into which HTTP header for which target domain(s). The proxy uses these profiles as an allowlist; secrets are never injected into requests that don't match.

# /run/secrets/<session-id>/providers.yaml
providers:
  dify:
    secrets:
      - env: INTERNAL_ACCESS_TOKEN        # placeholder exposed to agent
        value_file: dify/internal_token    # actual secret (file ref)
        inject:
          header: Authorization            # HTTP header to populate
          prefix: "Bearer "                # optional prefix before value
          domains:                         # allowlisted target domains
            - "*.dify.internal"
            - "api.dify.ai"

  github:
    secrets:
      - env: GITHUB_TOKEN
        value_file: github/token
        inject:
          header: Authorization
          prefix: "Bearer "
          domains:
            - "api.github.com"
            - "uploads.github.com"
      - env: GITHUB_WEBHOOK_SECRET
        value_file: github/webhook_secret
        inject:
          header: X-Hub-Signature-256
          prefix: "sha256="
          domains:
            - "*.github.com"

  aws:
    secrets:
      - env: AWS_ACCESS_KEY_ID
        value_file: aws/access_key_id
        inject:
          header: X-Amz-Access-Key        # resolved by SigV4 signing
          domains:
            - "*.amazonaws.com"
            - "*.aws.amazon.com"
      - env: AWS_SECRET_ACCESS_KEY
        value_file: aws/secret_access_key
        inject:
          header: X-Amz-Secret-Key        # consumed by proxy's SigV4 signer
          domains:
            - "*.amazonaws.com"
            - "*.aws.amazon.com"
      - env: AWS_SESSION_TOKEN
        value_file: aws/session_token
        inject:
          header: X-Amz-Security-Token
          domains:
            - "*.amazonaws.com"

  openai:
    secrets:
      - env: OPENAI_API_KEY
        value_file: openai/api_key
        inject:
          header: Authorization
          prefix: "Bearer "
          domains:
            - "api.openai.com"

  gcloud:
    secrets:
      - env: GCLOUD_ACCESS_TOKEN
        value_file: gcloud/access_token
        inject:
          header: Authorization
          prefix: "Bearer "
          domains:
            - "*.googleapis.com"
            - "*.google.com"

Secret Store Layout

Provider profiles and secret value files live outside the agent's Landlock-restricted workspace:

/run/secrets/
└── <session-id>/
    ├── providers.yaml                    ← injection profiles (read by proxy)
    └── <provider>/
        └── <secret-name>                 ← file contains the real secret value

Example:

/run/secrets/session-1/providers.yaml
/run/secrets/session-1/dify/internal_token
/run/secrets/session-1/github/token
/run/secrets/session-1/aws/access_key_id

Injection & Resolution

  1. The agent process receives only a placeholder in its environment, e.g. GITHUB_TOKEN=__secret:github:GITHUB_TOKEN__.
  2. The outbound proxy intercepts every request, matches the target domain against provider profiles, and only injects the secret into the declared header if the domain matches.
  3. If no profile matches the target domain, the placeholder is stripped (never forwarded raw).
  4. Each injection event is logged for audit (provider, env key, target domain, timestamp).

Third-Party CLI Tools

For tools that require authenticated credentials (e.g. gh, aws, gcloud, kubectl), the flow is:

  1. Authenticate with the 3rd-party provider (OAuth, API key exchange, etc.) — this happens outside the agent runtime.
  2. Convert the resulting credential into a mapped secret file in the secret store.
  3. Deploy the mapped file into the agent runtime's secret mount so the proxy can inject it on outbound calls.
┌──────────────────────────────────────────────────────────────────────────┐
│                       OUTSIDE AGENT RUNTIME                              │
│                                                                          │
│  ┌──────────┐       ┌───────────────────┐       ┌────────────────────┐  │
│  │ 3rd-Party│       │  Credential Mgr   │       │   Secret Store     │  │
│  │   App    │◄─────►│  (auth + convert) │──────►│   /run/secrets/    │  │
│  │(GitHub,  │ OAuth │                   │ write │    session-1/      │  │
│  │ AWS …)   │ / key │                   │       │  ├─ providers.yaml │  │
│  └──────────┘       └───────────────────┘       │  ├─ github/token   │  │
│                                                 │  ├─ aws/access_key │  │
│                                                 │  └─ openai/api_key │  │
│                                                 └─────────┬──────────┘  │
│                                                           │ mount       │
├───────────────────────────────────────────────────────────┼─────────────┤
│                       AGENT RUNTIME                       │             │
│                                                           ▼             │
│  ┌──────────────┐       ┌──────────────────────────┐  ┌────────────┐    │
│  │    Agent     │──────►│     Outbound Proxy       │─►│  Internet  │    │
│  │   Process    │ req   │                          │  │            │    │
│  │              │       │  1. match target domain  │  │ github.com │    │
│  │ env:         │       │     against profiles     │  │ aws.com    │    │
│  │ GITHUB_TOKEN │       │  2. lookup value_file    │  │ openai.com │    │
│  │ = __secret:  │       │  3. inject into header   │  │            │    │
│  │ github:      │       │  4. strip all remaining  │  │  ✗ evil.com│    │
│  │ GITHUB_TOKEN │       │     placeholders         │  │  (blocked) │    │
│  │ __           │       │  5. log audit event      │  │            │    │
│  └──────────────┘       └──────────────────────────┘  └────────────┘    │
└─────────────────────────────────────────────────────────────────────────┘

Flow Summary

authenticate        convert              deploy             resolve
─────────────►  ─────────────────►  ──────────────────►  ────────────►
  3rd-party        credential →         mount secret       proxy injects
  OAuth/key        secret file          into runtime       real value

Design Decisions

Decision Resolution
Injection scope Per-domain. Each secret is only injected into requests matching the profile's domains allowlist.
Credential lifecycle Session-scoped by default. Profiles are created at session start and destroyed at session end.
Secret rotation The credential manager can overwrite value_file contents without restarting the session; the proxy reads on each request.
Audit trail Every injection event is logged: (timestamp, session_id, provider, env, target_domain, header).
Fallback behavior If a placeholder appears in a request body or non-declared header, the proxy blocks the request and logs a warning.
Profile validation Profiles are validated at session start — invalid YAML, missing value files, or overlapping domain rules are rejected.

Security Properties

  • No secret in agent address space — the agent only sees opaque placeholders.
  • Domain-pinned injection — even if the agent crafts a request to evil.com with a placeholder header, the proxy refuses injection.
  • No global resolution — there is no wildcard "inject everywhere" mode; every injection requires an explicit domain match.
  • Placeholder stripping — unresolved placeholders are never forwarded to any upstream.

2. Additional context or comments

No response

3. Can you help us with this feature?

  • I am interested in contributing to this feature.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions