Skip to content

Feature: support vault mcp human in the loop with Oauth 2.x Authorization - #123

Open
marcellodesales wants to merge 19 commits into
hashicorp:mainfrom
marcellodesales:feature/support-vault-mcp-human-loop
Open

Feature: support vault mcp human in the loop with Oauth 2.x Authorization#123
marcellodesales wants to merge 19 commits into
hashicorp:mainfrom
marcellodesales:feature/support-vault-mcp-human-loop

Conversation

@marcellodesales

@marcellodesales marcellodesales commented Jun 26, 2026

Copy link
Copy Markdown

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

  • If applicable, I've documented the impact of any changes to security controls.

    Examples of changes to security controls include using new access control methods, adding or removing logging pipelines, etc.

If you have any questions, please contact your direct supervisor, GRC (#team-grc), or the PCI working group (#proj-pci-reboot). You can also find more information at PCI Compliance.

🔧 MCP Server Config

  • Make sure you can serve the CERT of your Vault server from https://cacerts.enterprise.io/all-certs.crt or whatever that is in the docker compose file docker-compose-ENTERPRISE_EXAMPLE.yaml... I would copy it to docker-compose-enterprise.yaml
    environment:
      ENTERPRISE_IO_CACERT_FILE: ${ENTERPRISE_IO_CACERT_FILE:-/enterprise/certs/enterprise.io.pem}
      ENTERPRISE_IO_CACERT_URL: ${ENTERPRISE_IO_CACERT_URL:-https://cacerts.enterprise.io/all-certs.crt}
  • Then bootstrap the test containers as described in the next Testing section

✅ Testing

docker compose -f docker-compose-enterprise.yaml up --build

docker compose -f docker-compose-company.yaml up 
[+] up 1/1
 ✔ Container vault-mcp-server-certs-puller-1 Recreated                                                                                                                                                        0.0s
Attaching to certs-puller-1, vault-mcp-server-1
Container vault-mcp-server-certs-puller-1 Waiting 
certs-puller-1  | private ca root fetched: /enterprise/certs/company.io.pem (42728 bytes)
certs-puller-1 exited with code 0
Container vault-mcp-server-certs-puller-1 Exited 
vault-mcp-server-1  | {"level":"info","msg":"private ca root ready","path":"/enterprise/certs/company.io.pem","size_bytes":42728,"time":"2026-07-02T21:30:53.441396922Z"}
vault-mcp-server-1  | {"addr":"0.0.0.0:8250","cors_mode":"development","endpoint":"/mcp","level":"info","msg":"http server starting","time":"2026-07-02T21:30:53.446006964Z","tls":"disabled (not recommended for production)"}
vault-mcp-server-1  | {"cacert_file":"/enterprise/certs/company.io.pem","level":"info","msg":"vault connection","time":"2026-07-02T21:30:53.446043797Z","vault_addr":"https://vault.seceng-iam.company.io","vault_namespace":""}
vault-mcp-server-1  | {"access_token_ttl":"12h0m0s","ldap_mount":"ldap","level":"info","login_methods":"ldap, userpass, token, oidc","login_page":"http://0.0.0.0:8250/vault/login","msg":"oauth enabled","oidc_callback":"http://0.0.0.0:8250/oidc/callback","oidc_mount":"oidc","oidc_role":"","time":"2026-07-02T21:30:53.446242214Z","userpass_mount":"userpass"}
Gracefully Stopping... press Ctrl+C again to force
Container vault-mcp-server-vault-mcp-server-1 Stopping 
vault-mcp-server-1  | {"level":"info","msg":"Shutting down StreamableHTTP server...","time":"2026-07-02T21:30:56.562125382Z"}
Container vault-mcp-server-vault-mcp-server-1 Stopped 
Container vault-mcp-server-certs-puller-1 Stopping 
Container vault-mcp-server-certs-puller-1 Stopped 
vault-mcp-server-1 exited with code 0

🔧 MCP Client Config

  • Just use the settings for the local MCP as usual
  • Added a docker-compose.yaml file so users can quickly bootstrap a Server, instead of a stdio process
    • Developers need to bind their PEM keys and choose oidc settings to get a server started
{
  "vault-company": {
    "timeout": 30,
    "url": "http://localhost:8250/mcp"
  }
}

🖼️ Oauth 2

  • When enabling, first the user needs to authenticate with Oauth 2, with the option for a local user or an Oauth 2
Screenshot 2026-06-30 at 10 27 15 AM
  • The Authentication stats and the user still don't see the list of tools until the Oauth 2 passes
Screenshot 2026-06-30 at 10 28 20 AM
  • Then, the Oauth 2 process goes through to your company's from the local
Screenshot 2026-06-30 at 10 27 23 AM
  • Then, the authorization passes and your MCP client will be called back with the Oauth 2 token
Screenshot 2026-06-30 at 10 27 44 AM

🖼️ MCP Use after Oauth 2

  • This still maintains it to work as before
Screenshot 2026-06-30 at 10 23 58 AM Screenshot 2026-06-30 at 10 25 37 AM Screenshot 2026-06-30 at 10 25 51 AM

Copilot AI review requested due to automatic review settings June 26, 2026 18:17
@marcellodesales
marcellodesales requested a review from a team as a code owner June 26, 2026 18:17
@hashicorp-cla-app

hashicorp-cla-app Bot commented Jun 26, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@hashicorp-cla-app

Copy link
Copy Markdown

CLA assistant check

Thank you for your submission! We require that all contributors sign our Contributor License Agreement ("CLA") before we can accept the contribution. Read and sign the agreement

Learn more about why HashiCorp requires a CLA and what the CLA includes

Have you signed the CLA already but the status is still pending? Recheck it.

@marcellodesales

Copy link
Copy Markdown
Author

I still have a couple things to fix on this one and will reopen when ready

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds optional browser-based OAuth login to the Vault MCP HTTP transport, enabling stateless Vault authentication (LDAP/userpass/token/OIDC) and sealed bearer tokens, while also improving Vault KV/mount listing behavior and TLS trust bootstrapping for private CAs.

Changes:

  • Introduces a stateless OAuth 2.1 authorization server (dynamic registration, auth-code+PKCE, token minting) plus OIDC login support and bearer middleware.
  • Improves Vault tool behavior: KV list now works without sys/mounts access; list_mounts falls back to sys/internal/ui/mounts.
  • Adds private CA bundle bootstrap + CA selection logic, plus updated docs and a docker-compose quickstart.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
README.md Documents the new browser OAuth login flow, env vars, and TLS/private CA guidance.
pkg/tools/sys/list_mounts.go Adds fallback mount listing via sys/internal/ui/mounts when sys/mounts is forbidden.
pkg/tools/kv/list_secrets.go Updates KV list to try KV v1 then fall back to KV v2 metadata path; improves key decoding robustness.
pkg/tools/kv/kv_test.go Adds tests for KV v1 listing and KV v2 fallback behavior.
pkg/oauth/types.go Adds typed payload structures for sealed OAuth artifacts.
pkg/oauth/service.go Adds typed token minting/parsing with TTL enforcement on top of the sealer.
pkg/oauth/service_test.go Tests token mint/parse, type mismatch, and expiry enforcement.
pkg/oauth/securetoken.go Implements AES-256-GCM sealing for opaque tokens (base64url encoding).
pkg/oauth/securetoken_test.go Tests sealing roundtrip, wrong-key failure, invalid token handling, and key validation.
pkg/oauth/router.go Wires discovery + OAuth routes and provides bearer middleware to inject Vault creds into request context.
pkg/oauth/pkce.go Implements PKCE S256 challenge computation/verification.
pkg/oauth/pkce_test.go Adds RFC 7636 test vector coverage for PKCE.
pkg/oauth/oidc.go Implements Vault OIDC start/callback flow with shared pending-state map for cross-port callback.
pkg/oauth/metadata.go Serves OAuth authorization server metadata and protected resource metadata plus MCP server card.
pkg/oauth/login.go Implements the interactive login page (LDAP/userpass/token + OIDC start) and auth-code issuance.
pkg/oauth/handlers.go Implements dynamic client registration, /authorize, and /token.
pkg/oauth/config.go Adds env-driven OAuth config and base URL derivation logic.
pkg/client/vaultauth.go Adds Vault login helpers for userpass/ldap/token lookup + Vault OIDC auth_url/callback helpers.
pkg/client/client.go Adds context injection helper and TLS CA bundle selection; refactors Vault client creation to use Vault API TLS config.
pkg/client/cabundle.go Adds bootstrap logic to fetch/cache the Viasat private CA bundle on startup when missing.
docker-compose.yaml Adds a StreamableHTTP + optional OAuth quickstart configuration with CA bundle volume priming.
cmd/vault-mcp-server/main.go Wires OAuth router/middleware, optional OIDC callback listener, CA bootstrap, and improved startup logging summary.
cmd/vault-mcp-server/init.go Switches logging to JSON+stdout by default and adds LOG_LEVEL support.
.gitignore Ignores local data/ directory (cert cache).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/oauth/pkce.go
Comment on lines +6 to +23
import (
"crypto/sha256"
"encoding/base64"
"strings"
)

// CodeChallengeS256 computes the PKCE S256 challenge for a verifier.
func CodeChallengeS256(codeVerifier string) string {
sum := sha256.Sum256([]byte(codeVerifier))
return base64.RawURLEncoding.EncodeToString(sum[:])
}

// VerifyCodeChallengeS256 reports whether codeVerifier matches expectedChallenge
// under the PKCE S256 method.
func VerifyCodeChallengeS256(codeVerifier string, expectedChallenge string) bool {
got := CodeChallengeS256(codeVerifier)
return strings.EqualFold(got, expectedChallenge)
}
Comment thread cmd/vault-mcp-server/main.go Outdated
Comment on lines +176 to +178
callbackServer := &http.Server{
Addr: fmt.Sprintf(":%d", oauthCfg.OIDCCallbackPort),
Handler: oauthRouter.OIDCCallbackMux(),
Comment on lines +58 to +65
secret, readErr := vault.Logical().Read("sys/internal/ui/mounts")
if readErr != nil {
logger.WithError(readErr).Error("Failed to list mounts via sys/internal/ui/mounts")
return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil
}
if secret == nil || secret.Data == nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil
}
Comment thread pkg/oauth/oidc.go
Comment on lines +85 to +91
vaultState := u.Query().Get("state")

oidcStateMap.Store(vaultState, oidcPending{
SealedAuthState: encState,
ClientNonce: clientNonce,
ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL),
})
Comment thread pkg/oauth/oidc.go
Comment on lines +135 to +150
vaultToken, err := client.OIDCCallback(ctx, r.vaultAuthParams(), r.cfg.OIDCMount, vaultState, code, pending.ClientNonce)
if err != nil {
r.logger.WithError(err).Warn("vault oidc callback failed")
// Can't render the login page from the callback port — redirect to it.
loginURL := r.cfg.BaseURL(req) + "/vault/login?auth_state=" +
url.QueryEscape(pending.SealedAuthState) + "&error=" +
url.QueryEscape("OIDC sign-in failed: "+truncateErr(err))
// BaseURL from port 8250 would be wrong; use the configured server URL.
if r.cfg.ServerURL != "" {
loginURL = r.cfg.ServerURL + "/vault/login?auth_state=" +
url.QueryEscape(pending.SealedAuthState) + "&error=" +
url.QueryEscape("OIDC sign-in failed: "+truncateErr(err))
}
http.Redirect(w, req, loginURL, http.StatusFound)
return
}
Comment thread pkg/oauth/handlers.go
Comment on lines +68 to +72
method := in.TokenEndpointAuthMethod
if method == "" {
method = "none"
}

Comment thread pkg/oauth/handlers.go
Comment on lines +219 to +225

writeJSON(w, tokenResponse{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: int64(time.Until(meta.ExpiresAt).Seconds()),
Scope: strings.Join(cd.Scopes, " "),
})
@marcellodesales
marcellodesales requested a review from Copilot June 30, 2026 17:52
@marcellodesales marcellodesales changed the title Feature/support vault mcp human loop Feature: support vault mcp human in the loop with Oauth 2.x Authorization Jun 30, 2026
@marcellodesales
marcellodesales marked this pull request as ready for review June 30, 2026 17:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 9 comments.

Comment thread pkg/oauth/pkce.go
Comment on lines +6 to +23
import (
"crypto/sha256"
"encoding/base64"
"strings"
)

// CodeChallengeS256 computes the PKCE S256 challenge for a verifier.
func CodeChallengeS256(codeVerifier string) string {
sum := sha256.Sum256([]byte(codeVerifier))
return base64.RawURLEncoding.EncodeToString(sum[:])
}

// VerifyCodeChallengeS256 reports whether codeVerifier matches expectedChallenge
// under the PKCE S256 method.
func VerifyCodeChallengeS256(codeVerifier string, expectedChallenge string) bool {
got := CodeChallengeS256(codeVerifier)
return strings.EqualFold(got, expectedChallenge)
}
Comment on lines +58 to +62
secret, readErr := vault.Logical().Read("sys/internal/ui/mounts")
if readErr != nil {
logger.WithError(readErr).Error("Failed to list mounts via sys/internal/ui/mounts")
return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil
}
Comment on lines +63 to +66
if secret == nil || secret.Data == nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil
}

Comment thread pkg/oauth/handlers.go
Comment on lines +68 to +72
method := in.TokenEndpointAuthMethod
if method == "" {
method = "none"
}

Comment thread pkg/oauth/oidc.go
Comment on lines +87 to +92
oidcStateMap.Store(vaultState, oidcPending{
SealedAuthState: encState,
ClientNonce: clientNonce,
ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL),
})

Comment thread cmd/vault-mcp-server/main.go Outdated
Comment on lines +176 to +178
callbackServer := &http.Server{
Addr: fmt.Sprintf(":%d", oauthCfg.OIDCCallbackPort),
Handler: oauthRouter.OIDCCallbackMux(),
Comment thread cmd/vault-mcp-server/main.go Outdated
Comment on lines +197 to +199
// Apply middleware (innermost first). The bearer middleware unseals the OAuth
// token into the context; VaultContextMiddleware then leaves those values
// intact (it only overrides when a header/env value is present).
Comment thread docker-compose.yaml Outdated
Comment on lines +59 to +62
# --- TLS trust (Viasat private CA) ---
VAULT_CACERT: ${VAULT_CACERT:-/company/certs/company.io.pem}
VIASAT_IO_CACERT_FILE: ${VIASAT_IO_CACERT_FILE:-/company/certs/company.io.pem}
VIASAT_IO_CACERT_URL: ${VIASAT_IO_CACERT_URL:-https://company.com/all-certs.crt}
Comment thread pkg/oauth/handlers.go
Comment on lines +103 to +107
func (r *Router) authorize(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
This commit shows how to run the MCP server that performs a localhost
OIDC authentication. When you setup the server, make sure that your
server will point to your enterprise Vault and so you can run it
locally. Then, as shown in README.md of the fetc-certs dir, the container
that loads the local cert will download and store it in a private
volume share where the Vault container can bootstrap and fully load.
Copilot AI review requested due to automatic review settings July 3, 2026 04:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated 10 comments.

Comment thread pkg/oauth/pkce.go
Comment on lines +6 to +23
import (
"crypto/sha256"
"encoding/base64"
"strings"
)

// CodeChallengeS256 computes the PKCE S256 challenge for a verifier.
func CodeChallengeS256(codeVerifier string) string {
sum := sha256.Sum256([]byte(codeVerifier))
return base64.RawURLEncoding.EncodeToString(sum[:])
}

// VerifyCodeChallengeS256 reports whether codeVerifier matches expectedChallenge
// under the PKCE S256 method.
func VerifyCodeChallengeS256(codeVerifier string, expectedChallenge string) bool {
got := CodeChallengeS256(codeVerifier)
return strings.EqualFold(got, expectedChallenge)
}
Comment on lines +165 to 169
logger.WithFields(log.Fields{
"mcp_auth_secret": secret,
"hint": "set MCP_AUTH_SECRET to persist bearer tokens across restarts",
}).Info("generated MCP_AUTH_SECRET")
}
Comment on lines +171 to +174
// Require the configured private CA bundle to be present on disk before booting.
// Bootstrap (download) should be performed out-of-process (e.g. docker-compose-viasat.yaml's
// certs-puller or scripts/fetch-secrets).
if err := requireConfiguredCACert(logger); err != nil {
}
}

return fmt.Errorf("private ca root missing: expected one of %v to exist; bootstrap it (e.g. scripts/fetch-secrets) or mount it into the container", candidates)
@@ -0,0 +1,75 @@
# fetch-secrets (certs-puller)
Comment on lines +58 to +65
secret, readErr := vault.Logical().Read("sys/internal/ui/mounts")
if readErr != nil {
logger.WithError(readErr).Error("Failed to list mounts via sys/internal/ui/mounts")
return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil
}
if secret == nil || secret.Data == nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil
}
Comment thread pkg/oauth/oidc.go
Comment on lines +87 to +91
oidcStateMap.Store(vaultState, oidcPending{
SealedAuthState: encState,
ClientNonce: clientNonce,
ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL),
})
Comment thread README.md
Comment on lines +89 to +91
- `VAULT_CACERT`: PEM CA bundle path used to verify the upstream Vault TLS cert (e.g. `/viasat/certs/viasat.io.pem`)
- `VIASAT_IO_CACERT_FILE` / `VIASAT_IO_CACERT_URL`: location and source URL of the Viasat private CA bundle; bootstrap it out-of-process (see `scripts/fetch-secrets/` or `docker-compose-viasat.yaml`)
- `VAULT_AUTH_LDAP_MOUNT` (default `ldap`), `VAULT_AUTH_USERPASS_MOUNT` (default `userpass`), `VAULT_OIDC_MOUNT` (default `oidc`), `VAULT_OIDC_ROLE` (optional): Vault auth method mounts used by the login page
Comment thread README.md
Comment on lines +83 to +86
OAuth / browser login (see [Browser OAuth](#browser-oauth-vault-login)):

- `MCP_AUTH_SECRET`: base64url (32-byte) key that seals OAuth tokens. Optional; if unset the server generates one at startup and logs it at INFO. Set it to keep bearer tokens valid across restarts.
- `MCP_SERVER_URL`: public base URL advertised in OAuth metadata/redirects (default: derived per-request from the `Host` / `X-Forwarded-*` headers)
Comment thread README.md
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 3, 2026 07:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated 12 comments.

Comment thread e2e/cors_e2e_test.go
Comment on lines +49 to +52
func testOAuthSecret() string {
key := make([]byte, 32)
return base64.RawURLEncoding.EncodeToString(key)
}
Comment thread pkg/oauth/router.go
Comment on lines +88 to +94
// External token bypass (for clients that cannot do OAuth).
if strings.TrimSpace(req.Header.Get(client.VaultHeaderToken)) != "" ||
strings.TrimSpace(req.Header.Get(client.VaultToken)) != "" ||
strings.TrimSpace(os.Getenv(client.VaultToken)) != "" {
next.ServeHTTP(w, req)
return
}
Comment thread pkg/oauth/handlers.go
Comment on lines +68 to +72
method := in.TokenEndpointAuthMethod
if method == "" {
method = "none"
}

Comment thread pkg/oauth/oidc.go
Comment on lines +87 to +91
oidcStateMap.Store(vaultState, oidcPending{
SealedAuthState: encState,
ClientNonce: clientNonce,
ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL),
})
Comment thread pkg/oauth/config.go
Comment on lines +16 to +20
// environment. OAuth is enabled only when MCPAuthSecret is set.
type Config struct {
// MCPAuthSecret is the base64url (32-byte) symmetric key used to seal all
// stateless OAuth tokens. Empty disables OAuth entirely.
MCPAuthSecret string
}
}

return fmt.Errorf("private ca root missing: expected one of %v to exist; bootstrap it (e.g. scripts/fetch-secrets) or mount it into the container", candidates)
Comment thread README.md
- `MCP_AUTH_CODE_TTL`: lifetime of authorization codes / login state (default: `5m`)
- `MCP_AUTH_ACCESS_TTL`: lifetime of bearer access tokens (default: `12h`)
- `VAULT_CACERT`: PEM CA bundle path used to verify the upstream Vault TLS cert (e.g. `/viasat/certs/viasat.io.pem`)
- `VIASAT_IO_CACERT_FILE` / `VIASAT_IO_CACERT_URL`: location and source URL of the Viasat private CA bundle; bootstrap it out-of-process (see `scripts/fetch-secrets/` or `docker-compose-viasat.yaml`)
@@ -0,0 +1,75 @@
# fetch-secrets (certs-puller)
Comment thread pkg/client/client.go
Comment on lines +79 to +81
if _, err := os.Stat(path); err == nil {
return path
}
Comment thread pkg/oauth/handlers.go
Comment on lines +220 to +225
writeJSON(w, tokenResponse{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: int64(time.Until(meta.ExpiresAt).Seconds()),
Scope: strings.Join(cd.Scopes, " "),
})
Copilot AI review requested due to automatic review settings July 7, 2026 18:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

pkg/tools/kv/read_secret.go:93

  • The new optional key argument is not used when constructing the response. If key is intended to scope the output to a single field, apply it before marshaling; otherwise remove the parameter to avoid dead/lying API surface.
	jsonData, err := json.Marshal(kvSecret.Data)
	if err != nil {
		logger.WithError(err).Error("Failed to marshal secret to JSON")
		return mcp.NewToolResultError(fmt.Sprintf("Error marshaling JSON: %v", err)), nil
	}

Comment on lines +165 to +168
logger.WithFields(log.Fields{
"mcp_auth_secret": secret,
"hint": "set MCP_AUTH_SECRET to persist bearer tokens across restarts",
}).Info("generated MCP_AUTH_SECRET")
Comment on lines +171 to +174
// Require the configured private CA bundle to be present on disk before booting.
// Bootstrap (download) should be performed out-of-process (e.g. docker-compose-viasat.yaml's
// certs-puller or scripts/fetch-secrets).
if err := requireConfiguredCACert(logger); err != nil {
}
}

return fmt.Errorf("private ca root missing: expected one of %v to exist; bootstrap it (e.g. scripts/fetch-secrets) or mount it into the container", candidates)
@@ -0,0 +1,75 @@
# fetch-secrets (certs-puller)
Comment thread README.md
Comment on lines +89 to +91
- `VAULT_CACERT`: PEM CA bundle path used to verify the upstream Vault TLS cert (e.g. `/viasat/certs/viasat.io.pem`)
- `VIASAT_IO_CACERT_FILE` / `VIASAT_IO_CACERT_URL`: location and source URL of the Viasat private CA bundle; bootstrap it out-of-process (see `scripts/fetch-secrets/` or `docker-compose-viasat.yaml`)
- `VAULT_AUTH_LDAP_MOUNT` (default `ldap`), `VAULT_AUTH_USERPASS_MOUNT` (default `userpass`), `VAULT_OIDC_MOUNT` (default `oidc`), `VAULT_OIDC_ROLE` (optional): Vault auth method mounts used by the login page
Comment on lines +58 to +65
secret, readErr := vault.Logical().Read("sys/internal/ui/mounts")
if readErr != nil {
logger.WithError(readErr).Error("Failed to list mounts via sys/internal/ui/mounts")
return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil
}
if secret == nil || secret.Data == nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to list mounts: %v", err)), nil
}
Comment on lines +93 to +100
if cfg, ok := m["config"].(map[string]interface{}); ok {
if v, ok := cfg["default_lease_ttl"].(float64); ok {
mt.DefaultLeaseTTL = int(v)
}
if v, ok := cfg["max_lease_ttl"].(float64); ok {
mt.MaxLeaseTTL = int(v)
}
}
Comment thread pkg/oauth/config.go
Comment on lines +15 to +20
// Config holds the OAuth Authorization Server configuration, loaded from the
// environment. OAuth is enabled only when MCPAuthSecret is set.
type Config struct {
// MCPAuthSecret is the base64url (32-byte) symmetric key used to seal all
// stateless OAuth tokens. Empty disables OAuth entirely.
MCPAuthSecret string
Comment thread pkg/oauth/oidc.go
Comment on lines +87 to +91
oidcStateMap.Store(vaultState, oidcPending{
SealedAuthState: encState,
ClientNonce: clientNonce,
ExpiresAt: time.Now().Add(r.cfg.AuthCodeTTL),
})
Comment on lines +42 to +46
Tool: mcp.NewTool("read_folder",
mcp.WithDescription("Read ALL secrets under a KV mount path in one call. "+
"Defaults to key names + SHA-256 of each value (safe, no plaintext exposure). "+
"Set include_values=true to return actual secret values."),
mcp.WithToolAnnotation(mcp.ToolAnnotation{ReadOnlyHint: utils.ToBoolPtr(true)}),
The interactive login page hard-coded root-relative form actions
(action="/vault/login", action="/vault/oidc/start"). Behind a reverse-proxy path
prefix (e.g. https://host/mcps/vault-mcp), the browser resolves those against the
HOST ROOT and POSTs to /vault/oidc/start -> 404, so OIDC/LDAP sign-in never starts.
Terminal MCP clients (Claude Code, WARP) surfaced this after OAuth discovery
succeeded. It only worked on localhost because the server is at the root there.

Fix: derive Config.BasePath() from MCP_SERVER_URL (the public mount point; a proxy
strips the prefix before it reaches us, so the request can't reveal it) and prefix
the login form actions with it. Empty at the host root, so localhost is unchanged.
All server-issued redirects already build from ServerURL and were unaffected.

- pkg/oauth/config.go: add Config.BasePath()
- pkg/oauth/login.go: loginPageData.BasePath; forms use {{.BasePath}}/vault/...
- tests: BasePath cases + login template renders prefixed vs root actions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot Bot review requested due to automatic review settings July 10, 2026 21:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 11 comments.

Comment on lines +165 to +168
logger.WithFields(log.Fields{
"mcp_auth_secret": secret,
"hint": "set MCP_AUTH_SECRET to persist bearer tokens across restarts",
}).Info("generated MCP_AUTH_SECRET")
Comment thread pkg/oauth/handlers.go
Comment on lines +134 to +157
var reg ClientRegistrationData
if _, err := r.tokenSvc.Parse(string(TokenTypeClientID), clientID, &reg); err != nil {
// The client_id is a sealed token minted with the server's current
// MCP_AUTH_SECRET. If decryption fails the server most likely restarted
// (generating a new secret), invalidating the stored registration.
// Rather than surfacing an error the user cannot act on, re-register the
// client on-the-fly using the redirect_uri already present in the request
// so the OIDC flow continues uninterrupted.
if redirectURI == "" {
r.renderCallbackError(w, "MCP client registration is no longer valid and no redirect_uri was provided — please reconnect your MCP client.")
return
}
u, parseErr := url.Parse(redirectURI)
if parseErr != nil || u.Scheme == "" || u.Host == "" {
r.renderCallbackError(w, "MCP client registration is no longer valid and the redirect_uri is not a valid URL — please reconnect your MCP client.")
return
}
r.logger.WithField("redirect_uri", redirectURI).Info("stale client_id — transparently re-registering MCP client for this authorize request")
reg = ClientRegistrationData{
ClientIDIssuedAt: time.Now().UTC().Unix(),
RedirectURIs: []string{redirectURI},
TokenEndpointAuthMethod: "none",
}
}
Comment thread pkg/oauth/handlers.go
Comment on lines +68 to +71
method := in.TokenEndpointAuthMethod
if method == "" {
method = "none"
}
@@ -0,0 +1,75 @@
# fetch-secrets (certs-puller)
Comment thread README.md
- `MCP_AUTH_CODE_TTL`: lifetime of authorization codes / login state (default: `5m`)
- `MCP_AUTH_ACCESS_TTL`: lifetime of bearer access tokens (default: `12h`)
- `VAULT_CACERT`: PEM CA bundle path used to verify the upstream Vault TLS cert (e.g. `/viasat/certs/viasat.io.pem`)
- `VIASAT_IO_CACERT_FILE` / `VIASAT_IO_CACERT_URL`: location and source URL of the Viasat private CA bundle; bootstrap it out-of-process (see `scripts/fetch-secrets/` or `docker-compose-viasat.yaml`)
Comment on lines +32 to +35
mcp.WithString("key",
mcp.DefaultString(""),
mcp.Description("A optional key in the secret to delete. If not specified, all keys in the the secret will be deleted."),
),

// Read the secret
secret, err := vault.Logical().Read(fullPath)
kvSecret, err := vault.KVv2(mount).Get(ctx, strings.TrimPrefix(path, "/"))
Comment on lines +42 to +46
Tool: mcp.NewTool("read_folder",
mcp.WithDescription("Read ALL secrets under a KV mount path in one call. "+
"Defaults to key names + SHA-256 of each value (safe, no plaintext exposure). "+
"Set include_values=true to return actual secret values."),
mcp.WithToolAnnotation(mcp.ToolAnnotation{ReadOnlyHint: utils.ToBoolPtr(true)}),
Comment on lines +78 to +80
func readFolderHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
logger.Debug("Handling read_folder request")

Comment on lines +93 to +100
if cfg, ok := m["config"].(map[string]interface{}); ok {
if v, ok := cfg["default_lease_ttl"].(float64); ok {
mt.DefaultLeaseTTL = int(v)
}
if v, ok := cfg["max_lease_ttl"].(float64); ok {
mt.MaxLeaseTTL = int(v)
}
}
…rm@main

Reusable workflow re-runs pin the original resolved SHA; an empty commit
forces a fresh pull_request run that resolves docker-multiarch-cicd.yaml@main
(now containing the env-based bake-metadata digest fix).
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.

2 participants