Skip to content

Comments

Experimental: JWE-based credential proxy prototype#39

Closed
dgellow wants to merge 2 commits intomainfrom
sam/mcp-tool-proxy
Closed

Experimental: JWE-based credential proxy prototype#39
dgellow wants to merge 2 commits intomainfrom
sam/mcp-tool-proxy

Conversation

@dgellow
Copy link
Member

@dgellow dgellow commented Feb 8, 2026

Stateless proxy that decrypts JWE tokens and injects raw API credentials at the last hop before the target API. Sandboxes running agent-generated code never see raw credentials. Only encrypted JWEs that are host-locked to prevent exfiltration.

Uses ECDH-ES+A256KW with A256GCM on P-256 keys (go-jose/v4). Includes host locking, key rotation via JWKS, revocation deny-list, per-request tracing, runtime/secret.Do integration for memory zeroing, and manual plaintext zeroing as a cross-platform fallback.

Stateless proxy that decrypts JWE tokens and injects raw API credentials
at the last hop before the target API. Sandboxes running agent-generated
code never see raw credentials — only encrypted JWEs that are host-locked
to prevent exfiltration.

Uses ECDH-ES+A256KW with A256GCM on P-256 keys (go-jose/v4). Includes
host locking, key rotation via JWKS, revocation deny-list, per-request
tracing, runtime/secret.Do integration for memory zeroing, and manual
plaintext zeroing as a cross-platform fallback.

14 integration test scenarios stress-testing credential isolation,
exfiltration attempts, key rotation, concurrency, SSE streaming,
token expiry, path traversal, and more. Benchmarks covering throughput,
latency percentiles, payload sizes, and deny-list scaling.
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @dgellow, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers an experimental JWE-based credential proxy designed to enhance security by isolating raw API credentials from potentially compromised sandboxed code. The proxy acts as an intermediary, decrypting encrypted JWE tokens containing credentials and injecting them just before forwarding requests to the target API. This approach ensures that sensitive information is never exposed to the client-side environment, while also providing robust features like host-based access control, key lifecycle management, and token revocation to maintain a secure and flexible credential delivery system.

Highlights

  • New JWE-based Credential Proxy: Introduces a stateless proxy that decrypts JWE tokens and injects raw API credentials at the last hop, preventing sandboxed environments from seeing sensitive data.
  • Secure Credential Handling: Utilizes ECDH-ES+A256KW with A256GCM on P-256 keys for encryption, and integrates with runtime/secret.Do (or a cross-platform fallback) for memory zeroing of plaintext credentials.
  • Host Locking and Exfiltration Prevention: Implements host locking to ensure JWEs are only valid for specified target hosts, mitigating credential exfiltration attempts.
  • Key Management and Rotation: Includes a keystore for managing ECDSA keys, supporting key generation, loading from PEM files, and exposing JWKS for key rotation.
  • Token Revocation: Features a deny-list mechanism to revoke JWE tokens, preventing their reuse after compromise or expiry.
  • Comprehensive Testing and Benchmarking: Adds extensive unit, integration, and benchmark tests to validate functionality, security scenarios, and performance under various loads and configurations.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • stainless-proxy/cmd/mint/main.go
    • Added a command-line utility to mint JWE tokens, allowing for easy generation of encrypted credentials for testing and deployment.
  • stainless-proxy/cmd/stainless-proxy/main.go
    • Introduced the main entry point for the proxy server, handling configuration loading, keystore initialization, and setting up the HTTP server.
  • stainless-proxy/config.example.json
    • Added an example configuration file to guide users on setting up the proxy, including options for address, key directory, and minting.
  • stainless-proxy/go.mod
    • Added Go module definition and dependencies, including github.com/go-jose/go-jose/v4 for JWE operations and github.com/stretchr/testify for testing.
  • stainless-proxy/go.sum
    • Added Go module checksums for direct and indirect dependencies.
  • stainless-proxy/internal/config/config.go
    • Implemented configuration loading and parsing from JSON files, with support for environment variable references for sensitive values like MintSecret.
  • stainless-proxy/internal/hostmatch/hostmatch.go
    • Added a utility for matching hostnames against a list of patterns, supporting exact matches and wildcard subdomains.
  • stainless-proxy/internal/hostmatch/hostmatch_test.go
    • Added unit tests for the host matching logic, covering various scenarios including exact, wildcard, case-insensitivity, and port stripping.
  • stainless-proxy/internal/jwe/jwe.go
    • Implemented JWE encryption and decryption logic, defining Credential and Payload structures, and integrating secretutil.Do for secure memory handling.
  • stainless-proxy/internal/jwe/jwe_test.go
    • Added unit tests for JWE round-trip encryption/decryption, handling of expired tokens, incorrect keys, multiple keys, and missing payload fields.
  • stainless-proxy/internal/keystore/keystore.go
    • Implemented a keystore for managing ECDSA private/public key pairs, including key generation, loading from PEM files, and generating JWKS for public key distribution.
  • stainless-proxy/internal/proxy/bench_test.go
    • Added comprehensive benchmarks to measure the performance of JWE crypto operations, sequential and parallel proxy throughput, impact of payload sizes, credential counts, deny-list overhead, and network latency.
  • stainless-proxy/internal/proxy/integration_test.go
    • Added extensive integration tests covering various real-world scenarios such as multi-header authentication, exfiltration attempts, token revocation, key rotation, concurrent requests, large POST bodies, SSE streaming, token expiry, wildcard host matching, context cancellation, query parameter preservation, and backend error passthrough.
  • stainless-proxy/internal/proxy/proxy.go
    • Implemented the core proxy HTTP handler, responsible for extracting and decrypting JWE tokens, performing host matching, injecting credentials into upstream requests, and forwarding responses.
  • stainless-proxy/internal/proxy/proxy_test.go
    • Added unit tests for the core proxy functionality, including credential injection, host blocking, authentication failures, token revocation, and request body forwarding.
  • stainless-proxy/internal/proxy/trace.go
    • Defined structures and a callback mechanism for tracing request processing phases within the proxy, enabling detailed performance monitoring.
  • stainless-proxy/internal/revocation/revocation.go
    • Implemented a deny-list for JWE tokens, allowing tokens to be marked as revoked with an expiry, and includes a background cleanup process.
  • stainless-proxy/internal/revocation/revocation_test.go
    • Added unit tests for the deny-list functionality, verifying token revocation, expiry, and cleanup.
  • stainless-proxy/internal/secretutil/do_default.go
    • Added a default implementation for secretutil.Do that simply executes the function, used when goexperiment.runtimesecret is not enabled.
  • stainless-proxy/internal/secretutil/do_experiment.go
    • Added an experimental implementation for secretutil.Do that leverages runtime/secret.Do for enhanced memory zeroing, active when goexperiment.runtimesecret is enabled.
  • stainless-proxy/internal/server/handlers.go
    • Implemented HTTP handlers for various server endpoints, including JWKS, health checks, JWE revocation, and JWE minting.
  • stainless-proxy/internal/server/middleware.go
    • Implemented common HTTP middleware for logging, panic recovery, and authentication for the minting endpoint.
  • stainless-proxy/internal/server/server.go
    • Implemented the main HTTP server setup, including routing for proxy and management endpoints, and graceful shutdown handling.
Activity
  • The pull request introduces a new stainless-proxy application, along with a mint command-line utility, to provide an experimental JWE-based credential proxy.
  • It establishes a robust internal architecture with dedicated packages for configuration, host matching, JWE handling, keystore management, proxy logic, token revocation, and HTTP server components.
  • Significant effort has been put into testing, with new unit tests for core components, extensive integration tests covering various security and functional scenarios, and performance benchmarks.
  • The proxy is designed with security in mind, incorporating host locking, JWE token revocation, and secure memory handling for sensitive credentials.
  • The server setup includes middleware for logging, panic recovery, and authentication for administrative endpoints like JWE minting and revocation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an impressive initial commit for the stainless-proxy. The project is well-structured, with a clear separation of concerns between proxying, cryptography, key management, and server logic. The inclusion of comprehensive integration tests, benchmarks, and security considerations like memory zeroing for secrets (runtime/secret) from the outset is excellent. My review focuses on a few areas to further enhance security, robustness, and correctness.

Comment on lines 51 to 54
if auth != "Bearer "+string(secret) {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Using the != operator for comparing secrets is vulnerable to timing attacks, which could allow an attacker to reconstruct the secret by measuring response time differences. You should use a constant-time comparison function.

Please use subtle.ConstantTimeCompare for this check. You will need to add "crypto/subtle" to your imports.

Note: This approach still leaks the length of the expected token. A more secure (but more complex) alternative is to store a hash of the mint secret and compare the hash of the incoming token against it.

expectedAuth := "Bearer " + string(secret)
// Use constant-time comparison to prevent timing attacks.
if len(auth) != len(expectedAuth) || subtle.ConstantTimeCompare([]byte(auth), []byte(expectedAuth)) != 1 {
	http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
	return
}

if err := os.MkdirAll(keyDir, 0700); err != nil {
return nil, fmt.Errorf("creating key directory: %w", err)
}
path := filepath.Join(keyDir, fmt.Sprintf("key-%s.pem", time.Now().Format("2006-01-02")))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The filename format for generated keys, key-YYYY-MM-DD.pem, is not unique enough. If the service is restarted multiple times on the same day, it will overwrite the key file generated earlier that day. This could lead to an availability issue if an old key that is still in use gets overwritten. Using a more granular, unique timestamp (e.g., including time) would prevent this.

Suggested change
path := filepath.Join(keyDir, fmt.Sprintf("key-%s.pem", time.Now().Format("2006-01-02")))
path := filepath.Join(keyDir, fmt.Sprintf("key-%s.pem", time.Now().UTC().Format("20060102T150405Z")))

Comment on lines 27 to 39
d.mu.RLock()
exp, ok := d.entries[hash]
d.mu.RUnlock()
if !ok {
return false
}
if time.Now().After(exp) {
d.mu.Lock()
delete(d.entries, hash)
d.mu.Unlock()
return false
}
return true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation of IsRevoked has a race condition and is inefficient. It releases the read lock and then takes a write lock to delete an entry. Between the unlock and lock, another goroutine could modify the map, potentially causing a valid token to be deleted.

A safer and more performant approach is to perform the entire check under a single read lock and rely on the periodic Cleanup goroutine to handle the deletion of expired entries. This avoids the complexity and performance cost of lock upgrading on a read-heavy path.

d.mu.RLock()
defer d.mu.RUnlock()
exp, ok := d.entries[hash]
if !ok {
	return false
}
// Rely on the periodic cleanup task to remove expired entries.
// This avoids a lock upgrade on a read path.
return time.Now().Before(exp)

Comment on lines 54 to 58
resp, err := http.Get(*jwksURL)
if err != nil {
fmt.Fprintf(os.Stderr, "error: fetching JWKS: %v\n", err)
os.Exit(1)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

http.Get uses the default HTTP client, which has no timeout. This can cause the CLI tool to hang indefinitely if the JWKS endpoint is slow or unresponsive. It's good practice to use an http.Client with a reasonable timeout.

Suggested change
resp, err := http.Get(*jwksURL)
if err != nil {
fmt.Fprintf(os.Stderr, "error: fetching JWKS: %v\n", err)
os.Exit(1)
}
client := http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(*jwksURL)
if err != nil {
fmt.Fprintf(os.Stderr, "error: fetching JWKS: %v\n", err)
os.Exit(1)
}

Comment on lines +103 to +106
if (value[0] == '"' && value[len(value)-1] == '"') ||
(value[0] == '\'' && value[len(value)-1] == '\'') {
value = value[1 : len(value)-1]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic to trim quotes from environment variable values can be simplified and made more robust by using strings.Trim. This avoids manual index checking and handles both single and double quotes more cleanly.

value = strings.Trim(value, "\"'")

Comment on lines 263 to 266
case "Connection", "Keep-Alive", "Proxy-Authenticate",
"Proxy-Authorization", "Te", "Trailers",
"Transfer-Encoding", "Upgrade":
return true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a small typo in the list of hop-by-hop headers. According to RFC 2616 and RFC 7230, the header is Trailer, not Trailers. While not a critical issue, correcting it ensures better protocol compliance.

Suggested change
case "Connection", "Keep-Alive", "Proxy-Authenticate",
"Proxy-Authorization", "Te", "Trailers",
"Transfer-Encoding", "Upgrade":
return true
case "Connection", "Keep-Alive", "Proxy-Authenticate",
"Proxy-Authorization", "Te", "Trailer",
"Transfer-Encoding", "Upgrade":

…http timeout

- Use constant-time comparison for mint secret auth
- Make DenyList.IsRevoked read-only to fix race between RUnlock and Lock
  where a concurrent Add could be deleted; expired entry cleanup is left
  to the Cleanup goroutine
- Add 30s timeout to JWKS fetch in mint CLI
- Fix hop-by-hop header name: Trailers → Trailer per RFC 7230
@dgellow
Copy link
Member Author

dgellow commented Feb 9, 2026

Moved to its own repo https://github.com/stainless-api/derouge

@dgellow dgellow closed this Feb 9, 2026
@dgellow dgellow deleted the sam/mcp-tool-proxy branch February 9, 2026 23:03
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