feat: add credential substitution with global credentials support#26
Merged
feat: add credential substitution with global credentials support#26
Conversation
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
7 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:The session creation API (
POST /api/sessions) accepts aglobal_credentialsfield (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
session.substitutionevent)Changes
POST /api/sessionsacceptsglobal_credentials, resolves labels, returns placeholderssession.substitutionevents after flushing countsActivityItemincludesSubstitutedCredentials; shield icon and label badges in both activity and traffic tables--injectworkflow and substitution behaviordocs/credential-substitution.mdcovering the full API, substitution behavior, and dashboard UITest plan
go test ./internal/greyproxy/...passesgo test ./internal/greyproxy/api/...passes (session creation tests)greywall --inject LABEL -- envshows placeholder in outputDependencies
Companion greywall PR: GreyhavenHQ/greywall#63