langgenius / langgenius/dify

Proposal: Hardening environment variables in agent runtime

Open
#39,278 10 comments 2 reactions 1 assignee Claimed by @wylswz View on GitHub
πŸ’ͺ enhancement project#dify
Dominant language
TypeScript
Stars
156k
Forks
24.6k
Avg merge
22h 9m
Merged PRs (30d)
610

Description

### Self Checks

- [x] I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
- [x] I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
- [x] I confirm that I am using English to submit this report, otherwise it will be closed.
- [x] 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.

```yaml
# /run/secrets//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/
└── /
β”œβ”€β”€ providers.yaml ← injection profiles (read by proxy)
└── /
└── ← 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.