Skip to content

feat: add credential substitution for sandboxed environments#63

Merged
tito merged 15 commits intomainfrom
mathieu/env-substitution
Mar 27, 2026
Merged

feat: add credential substitution for sandboxed environments#63
tito merged 15 commits intomainfrom
mathieu/env-substitution

Conversation

@tito
Copy link
Copy Markdown
Contributor

@tito tito commented Mar 25, 2026

Summary

End-to-end credential protection for sandboxed commands. Real API keys are replaced with opaque placeholders before entering the sandbox; greyproxy transparently injects real values into HTTP requests before they reach upstream servers.

Auto-detection

Greywall scans environment variables against 100+ well-known names and suffix patterns (_API_KEY, _TOKEN, _SECRET, _PASSWORD). Detected credentials are automatically protected.

greywall -- opencode    # credentials auto-detected and protected

--secret and --inject flags

# Protect an env var not in the auto-detection list
greywall --secret LITELLM_NOTRACK_API_KEY -- opencode

# Inject a credential stored in the greyproxy dashboard
greywall --inject ANTHROPIC_API_KEY -- opencode

# Both together
greywall --secret MY_VAR --inject DASHBOARD_KEY -- command

Config file support

Flags can also be set in the config file or per-profile:

{
  "credentials": {
    "secrets": ["LITELLM_NOTRACK_API_KEY"],
    "inject": ["ANTHROPIC_API_KEY"]
  }
}

Session lifecycle

  • Session registered with greyproxy at launch; heartbeats every 60s
  • Auto re-registers if greyproxy restarts
  • Session deleted on sandbox exit

Sandbox hardening

session.key and ca-key.pem denied inside the sandbox on both Linux (bwrap) and macOS (Seatbelt).

Changes

  • internal/sandbox/credentials.go: credential detection, placeholder generation, session registration with global_credentials support, heartbeat loop, SubstituteEnv (replaces + appends)
  • internal/sandbox/credentials_test.go: detection, substitution, suffix patterns, extra vars, env appending
  • internal/config/config.go: CredentialConfig with secrets and inject fields, merge support
  • cmd/greywall/main.go: --secret, --inject, --no-credential-protection, --skip-version-check; config merging; greyproxy version check (>= v0.3.4 for --inject)
  • internal/sandbox/dangerous.go: greyproxy sensitive file paths
  • internal/sandbox/{linux,macos}.go: deny-read rules for key files
  • docs/credential-protection.md: full documentation

Dependencies

Requires greyproxy PR: GreyhavenHQ/greyproxy#26

The global_credentials field in POST /api/sessions is implemented there. Auto-detected credentials and --secret work with any greyproxy version; --inject requires >= v0.3.4.

Test plan

  • go test ./... passes
  • greywall -- env shows placeholders for detected credentials
  • greywall --secret CUSTOM_VAR -- env shows placeholder for the custom var
  • greywall --inject LABEL -- env shows placeholder for injected credential
  • greywall --no-credential-protection -- env shows real values
  • HTTP requests with placeholders substituted correctly (shield icon in activity view)
  • Config file credentials.secrets and credentials.inject merged with CLI flags

tito added 4 commits March 25, 2026 12:37
Detects credential env vars (100+ well-known names + suffix patterns),
generates opaque placeholders, registers them with greyproxy's session
API, and rewrites the sandbox environment. Real credentials are injected
transparently at the proxy's MITM layer.

Includes:
- Credential detection with well-known list and suffix pattern matching
- Session registration, heartbeat loop, and cleanup on exit
- Environment variable substitution with placeholders
- Deny rules for session.key and ca-key.pem in both Linux (bwrap)
  and macOS (Seatbelt) sandboxes
- --no-credential-protection flag to opt out
- Unit tests for detection, substitution, and placeholder generation
Add --cred <LABEL> flag (repeatable) to inject global credentials stored
in the greyproxy dashboard. At session creation, requested labels are sent
via global_credentials field; the proxy resolves them and returns
placeholders. Greywall sets the placeholder as an env var in the sandbox.

SubstituteEnv now appends env vars that don't already exist, so --cred
works even when the variable is not in the host environment.

Requires greyproxy with global_credentials support in POST /api/sessions.
When --cred is used, verify greyproxy is running and is v0.3.4 or later
(the version that adds global_credentials support to POST /api/sessions).
Shows a clear error with upgrade instructions if the check fails.
@tito tito changed the title feat: add --cred flag for global credential injection feat: add credential substitution for sandboxed environments Mar 26, 2026
tito added 3 commits March 26, 2026 07:39
Allows protecting env vars that don't match the well-known list or
suffix patterns (e.g. LITELLM_NOTRACK_API_KEY). The flag is repeatable:

  greywall --protect MY_CUSTOM_VAR --protect ANOTHER_VAR -- command

Unlike --cred (which fetches values from the proxy dashboard), --protect
works with values already in the host environment.
… support

Rename flags for clarity:
  --cred    -> --inject  (inject credential from proxy dashboard)
  --protect -> --secret  (mark env var as a secret)

Add credentials section to config file so these can be set per-profile:

  {
    "credentials": {
      "secrets": ["LITELLM_NOTRACK_API_KEY"],
      "inject": ["ANTHROPIC_API_KEY"]
    }
  }

Config values are merged with CLI flags (deduplicated).
tito added 5 commits March 26, 2026 08:02
The greyproxy dashboard can display context about what command is
running in each sandbox session. Pass working directory, command
name, arguments, binary path, and PID when registering and
re-registering sessions.
Allow users to exclude specific env vars from credential detection.
Useful for variables that match suffix patterns (e.g. _TOKEN) but
are not actually secrets. Can be set via --ignore-secret CLI flag
or credentials.ignore in the config file. Both sources are merged
and deduplicated.
Previously, .env files were masked (bind-mounted as empty) inside the
sandbox, breaking tools that read .env files at runtime. Now, when
credential substitution is active, .env files are rewritten with
placeholder values and mounted read-only into the sandbox.

Each .env file gets unique per-file placeholders so that different
files with the same key but different values (e.g., .env has KEY=a,
.env.local has KEY=b) each map to the correct real value on the proxy
side. Environment variable substitution uses only env-detected mappings
to avoid placeholder collisions.

When credential substitution is not active (disabled or registration
failed), the old empty-file masking behavior is preserved with a
warning printed to stderr.
tito added a commit to GreyhavenHQ/greyproxy that referenced this pull request Mar 27, 2026
## Summary

End-to-end credential substitution: real API keys never reach sandboxed
processes. The proxy transparently replaces opaque placeholders with
real values before forwarding HTTP requests upstream.

### Global credentials

Global credentials are stored in the dashboard and injected on demand
via `--inject`:

```bash
greywall --inject ANTHROPIC_API_KEY --inject OPENAI_API_KEY -- opencode
```

The session creation API (`POST /api/sessions`) accepts a
`global_credentials` field (list of labels). The proxy resolves each
label, merges placeholder-to-value mappings into the session, and
returns the placeholders so greywall can set them as environment
variables.

**Example request:**
```json
{
  "session_id": "gw-abc123",
  "container_name": "opencode",
  "global_credentials": ["ANTHROPIC_API_KEY"],
  "ttl_seconds": 900
}
```

**Example response:**
```json
{
  "session_id": "gw-abc123",
  "expires_at": "2026-03-25T23:00:00Z",
  "credential_count": 1,
  "global_credentials": {
    "ANTHROPIC_API_KEY": "greyproxy:credential:v1:global:a1b2c3..."
  }
}
```

### Substitution tracking

- Substitution counts flushed to DB every 60s, broadcast via WebSocket
(`session.substitution` event)
- Activity and traffic tables show shield icon for substituted requests,
credential labels as badges in expanded details
- Session cards show creation time and active duration

### Changes

- **API**: `POST /api/sessions` accepts `global_credentials`, resolves
labels, returns placeholders
- **Credential store**: publishes `session.substitution` events after
flushing counts
- **Activity**: `ActivityItem` includes `SubstitutedCredentials`; shield
icon and label badges in both activity and traffic tables
- **Settings UI**: global credentials section explains `--inject`
workflow and substitution behavior
- **Tests**: CRUD, API handler, and end-to-end substitution tests for
global credentials
- **Docs**: `docs/credential-substitution.md` covering the full API,
substitution behavior, and dashboard UI

## Test plan

- [x] `go test ./internal/greyproxy/...` passes
- [x] `go test ./internal/greyproxy/api/...` passes (session creation
tests)
- [x] `greywall --inject LABEL -- env` shows placeholder in output
- [x] HTTP request with placeholder substituted (shield icon in activity
view)
- [x] Settings > Credentials > Active Sessions shows substitution count
updating

## Dependencies

Companion greywall PR: GreyhavenHQ/greywall#63
@tito tito marked this pull request as ready for review March 27, 2026 00:44
tito added 3 commits March 27, 2026 15:45
- credential-protection.md: add .env file rewriting section explaining
  full Linux support (bind-mount) vs macOS limitations (files denied)
- platform-support.md: add credential substitution rows to feature
  comparison table and macOS-specific explanation
- Update limitations section to reflect current state
- errcheck: wrap resp.Body.Close() in deferred closure to handle return value
- gosec G117: rename SessionMetadata.Pwd to WorkDir to avoid secret pattern match
- gosec G704/G703: add nolint annotations for local API calls and temp file cleanup
- Add RewrittenEnvFiles field to LinuxSandboxOptions stub (linux_stub.go) to fix
  macOS compilation error
- gofumpt: align struct literal fields in main.go
- gosec G704: move nolint annotation to http.DefaultClient.Do() call
@tito tito merged commit 1906877 into main Mar 27, 2026
4 checks passed
@tito tito deleted the mathieu/env-substitution branch March 27, 2026 23:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant