Skip to content

feat: add credential substitution with global credentials support#26

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

feat: add credential substitution with global credentials support#26
tito merged 14 commits intomainfrom
mathieu/env-substitution

Conversation

@tito
Copy link
Copy Markdown
Contributor

@tito tito commented Mar 25, 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:

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:

{
  "session_id": "gw-abc123",
  "container_name": "opencode",
  "global_credentials": ["ANTHROPIC_API_KEY"],
  "ttl_seconds": 900
}

Example response:

{
  "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

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

Dependencies

Companion greywall PR: GreyhavenHQ/greywall#63

tito added 5 commits March 25, 2026 11:47
Add in-memory credential store with encrypted DB persistence for
credential placeholder substitution in the MITM pipeline.

- DB migration 8: sessions and global_credentials tables
- AES-256-GCM encryption for credential storage with master key management
- CredentialStore with sync.RWMutex for concurrent header/query substitution
- Session CRUD: create/upsert, heartbeat, delete, TTL expiry sweep
- Global credential CRUD: create, list, delete with value masking
- Placeholder format: greyproxy:credential:v1:<session>:<hex>
- Cleanup goroutine for expired session removal and count flushing
- Comprehensive tests: crypto round-trip, concurrent access, DB reload,
  key rotation purge, substitution correctness
…phases 2-3)

Phase 2: REST API endpoints for credential substitution sessions and
global credentials. Sessions support create (upsert), heartbeat, delete,
and list. Global credentials support create, list, and delete. The API
never returns raw credential values.

Phase 3: Wire credential substitution into the MITM pipeline. Placeholders
in HTTP headers and URL query parameters are replaced with real credentials
just before forwarding upstream, in both HTTP/1.1 and HTTP/2 paths.
Stored transactions retain placeholder values (headers cloned before
substitution). The credential store is initialized on startup with
encryption key management and periodic cleanup.
Adds a "Credential Protection" section to the settings page with:
- Active sessions panel showing container name, credential labels,
  and substitution counts, with force-expire button
- Global credentials panel with add/delete forms (password input,
  labels, masked previews)
- Real-time updates via WebSocket session events
…tings tabs

Track which credentials were substituted per HTTP transaction by piping
label names through the MITM pipeline. Each transaction now stores the
substituted credential labels and session ID, visible in the traffic
detail view with green badge chips.

Restructure the settings page into three tabs (General / MITM /
Credentials) with a protection status banner that reflects TLS
interception state. Sessions now display metadata (PWD, CMD, PID, etc.)
sent by greywall.

Key changes:
- Migration 9: substituted_credentials + session_id on http_transactions,
  metadata_json on sessions
- SubstituteRequest returns SubstitutionResult with labels and session IDs
- GlobalCredentialSubstituter returns CredentialSubstitutionInfo piped
  through HTTPRoundTripInfo to transaction storage
- SessionCreateInput accepts metadata map for greywall integration
- Transaction filtering by session_id via API and UI
- API contract documented in docs/credential-substitution-api.md
- Session creation API accepts `global_credentials` (list of labels) and
  returns resolved placeholders so greywall can inject them as env vars
- Publish EventSessionSubstitution after flushing counts to DB so the
  settings UI refreshes substitution counts in real-time
- Show credential substitution info in activity and traffic tables (shield
  icon inline, label badges in expanded details)
- Add `credLabels` template helper and `SubstitutedCredentials` to the
  unified ActivityItem query
- Update settings UI to explain the --cred workflow, .env rewriting, and
  that all placeholder occurrences are replaced
- Add session creation time and active duration to settings session cards
@tito tito changed the title feat: add credential substitution feat: add credential substitution with global credentials support Mar 25, 2026
tito added 9 commits March 26, 2026 07:55
Replace inline onclick handlers with data attributes and event
delegation. The old escapeHtml (DOM-based) did not escape quotes,
allowing injection via crafted session/credential IDs in onclick
attributes. The new escapeHtml escapes all five HTML special
characters (&, <, >, ", '). Also add encodeURIComponent on IDs
in fetch URLs to prevent path traversal.
… key rotation

LoadOrGenerateKey now returns an error when the key file exists but
has the wrong size, instead of silently overwriting it (which would
make all stored credentials permanently unreadable).

PurgeUnreadableSessions is renamed to PurgeUnreadableCredentials and
now also purges global credentials that cannot be decrypted after
key rotation, not just sessions.
Global credential real values were decrypted and merged into each
session's mappings_enc, meaning deleting a global credential did not
remove its value from existing sessions. Global credentials are
already loaded separately from the global_credentials table at
startup and on create/delete, so the duplication was unnecessary.

Now sessions only store labels for global credentials (for dashboard
display); real values are resolved at substitution time from the
global store. Deleting a global credential immediately stops
substitution for all sessions.
The SELECT and DELETE used separate datetime('now') calls, creating
a window where a heartbeat could extend a session between the two
queries. The session ID would still be returned as expired but not
actually deleted, causing an inconsistency between the in-memory
store and the database. Now both queries use the same snapshot.
The credential substituter hook was a plain global variable read on
every MITM request and written during setup, creating a potential
data race. Replace with atomic.Pointer and getter/setter functions
to ensure safe concurrent access. This is particularly important
since the substituter handles security-sensitive credential values.
…up log format

- Remove unused gcmNonceSize constant in credential_crypto.go
- Fix alphabetical import ordering in program.go (time after strings)
- Normalize log.Printf format in credential_store.go to use
  [credential_store] prefix consistently
The usage instructions for --inject were taking up vertical space
by default. Now they are hidden behind a toggle so the credentials
list is immediately visible.
@tito tito self-assigned this Mar 27, 2026
@tito tito marked this pull request as ready for review March 27, 2026 00:39
@tito tito merged commit 4f9b6b4 into main Mar 27, 2026
@tito tito deleted the mathieu/env-substitution branch March 27, 2026 00:43
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