MAQPNADocs

Secret vaults and per-user tokens

Keep MCP and model credentials out of sandboxes, connect users' SaaS accounts to the gateway's per-user token vault, and source MAQPNA's own secrets and keys from HashiCorp Vault, OpenBao, cloud KMS and cloud secret managers.

An agent should never hold a long-lived credential. In MAQPNA, credentials stay at the gateway: the sandbox gets a short-lived session token, and the gateway adds the real credential when it forwards a call. This page covers the three places secrets come from:

  1. Upstream credentials for MCP servers and model routes, from Kubernetes Secrets.
  2. The per-user token vault: users connect their own GitHub, Microsoft 365, Google, Salesforce, Slack or other OAuth accounts once, and the gateway injects their token on calls made on their behalf.
  3. External vaults and key managers for MAQPNA's own secrets and signing keys: HashiCorp Vault and OpenBao (Transit), AWS KMS, Cloud KMS, Azure Key Vault, and any provider of the Secrets Store CSI driver or External Secrets Operator.
flowchart LR
    A["Agent in sandbox<br/>session token only"] --> G["MAQPNA gateway"]
    S[("Kubernetes Secret<br/>MCPServer auth")] -->|"bearer, header,<br/>OAuth client, mTLS"| G
    V[("token vault<br/>sealed AES-256-GCM")] -->|"user's token<br/>perUser: true"| G
    K["KMS / Vault Transit<br/>key never leaves"] -->|"sign, seal"| G
    CSI["Secrets Store CSI<br/>or External Secrets"] -->|"files"| G
    G -->|"credential added here"| U["MCP server or model"]
    U2["User in browser"] -->|"/v1/connect/github"| V

When to use it#

  • An MCP server needs an API key, OAuth client or client certificate: use MCPServer.spec.auth.
  • A tool must act as the user (their GitHub repos, their mailbox): use the token vault with perUser: true.
  • Your security policy says secrets live in Vault or a cloud secret manager, and keys in a KMS or HSM.

Upstream credentials on an MCP server#

MCPServer.spec.auth.type is one of none, bearerSecret, headerSecret, oauthClientCredentials, tokenExchange or mtls. The operator copies the referenced Secret into the gateway-only Secret maqpna-upstream-credentials; it never reaches the sandbox.

apiVersion: maqpna.com/v1alpha1
kind: MCPServer
metadata:
  name: tickets
  namespace: team-a
spec:
  url: https://tickets.example.eu/mcp
  auth:
    type: headerSecret
    headerName: X-Api-Key
    secretRef: {name: tickets-api, key: api-key}

See MCP servers and tool pinning and MCPServer reference. Model route keys work the same way; see Connect model providers.

The per-user token vault#

How it works#

A user connects a provider once in the browser. The gateway signs them in with your identity provider (OIDC, PKCE), then runs the provider's OAuth consent (PKCE). The resulting tokens are sealed with AES-256-GCM under a key you hold and stored under the verified user. When a session acting for that user calls an MCP server with auth.perUser: true, the gateway injects the user's access token, refreshing and rotating it as needed. The agent never sees it.

Endpoint Purpose
GET /v1/connect/{provider} Start consent in the browser
GET /v1/connect/login/callback Identity provider sign-in callback
GET /v1/connect/{provider}/callback Provider callback; register <resourceBaseURL>/v1/connect/<provider>/callback as the redirect URI
POST /v1/connect/{provider} Connect without a browser with an ID token (Identity Assertion JWT grant, ID-JAG; for example Okta Cross App Access)
DELETE /v1/connect/{provider} Disconnect

Built-in presets: github, microsoft (with tenant), google, salesforce (with domain), slack and generic (set authorizationURL, tokenURL and optionally revocationURL yourself).

Prerequisites#

  • OIDC set up for your users (Keycloak and OIDC), and delegation.resourceBaseURL set to the gateway's public https URL.
  • An OAuth app at each provider, with its client ID and secret.
  • Verified session users (principalBinding), because tokens are injected only for verified principals unless you set allowUnverifiedPrincipal: true.

Step 1: Create the key and client secrets#

head -c32 /dev/urandom > kek
kubectl create secret generic maqpna-vault-kek -n maqpna-system --from-file=kek=kek
rm kek
kubectl create secret generic maqpna-delegation -n maqpna-system \
  --from-literal=login='<idp-client-secret>' \
  --from-literal=github='<github-client-secret>'

Step 2: Set the Helm values#

delegation:
  resourceBaseURL: https://maqpna.example.eu
  clientSecretsSecret: maqpna-delegation     # mounted at /etc/maqpna/delegation-secrets
  tokenVault:
    enabled: true
    keySecret: maqpna-vault-kek              # key "kek": 32 random bytes
    login:
      issuer: https://idp.example.eu/realms/maqpna
      clientID: maqpna-connect
      clientSecretFile: /etc/maqpna/delegation-secrets/login
    providers:
      - name: github
        preset: github
        clientID: Iv1.abc
        clientSecretFile: /etc/maqpna/delegation-secrets/github
        allowedServers: [api.githubcopilot.com]

allowedServers (host globs) is required. MCPServer objects are written by namespace users, so without it anyone could point a per-user server at their own URL and receive other users' tokens. Per-user servers on other hosts never receive the token. allowedNamespaces can restrict it further. allowAnyServer: true turns the check off and is not recommended.

Add each provider's endpoints to sovereignty.allowedEgressHosts when that list is set.

Step 3: Mark the MCP server per-user#

spec:
  url: https://api.githubcopilot.com/mcp/
  auth:
    type: none          # or mtls
    perUser: true
    provider: github

Step 4: Connect an account#

The user opens https://maqpna.example.eu/v1/connect/github in a browser and approves the consent. A session for a user who has not connected gets consent_required.

Verify#

maqpna accounts list --user [email protected]

With gateway.activity.enabled and delegation.tokenVault.mirror: true, the gateway also mirrors each account as a ConnectedAccount object (status only, no secrets) with phase Connected, ReauthRequired or Revoked:

kubectl get connectedaccounts -n maqpna-system

To revoke a user's account at the provider and delete it:

maqpna accounts revoke [email protected] github

A cluster-wide terminate revocation that matches the user also revokes their tokens, unless keepOnKillSwitch: true. Rotate the sealing key with maqpna keys rotate vault.

HashiCorp Vault, OpenBao and cloud KMS for keys#

Everywhere MAQPNA signs (audit checkpoints, evidence packs, usage reports, licences, identities), it takes a key URI. The private key stays in the key manager:

URI Key manager
kms://vault/<mount>/keys/<name> HashiCorp Vault Transit
kms://openbao/<mount>/keys/<name> OpenBao Transit
kms://aws/<key-id, ARN or alias/name> AWS KMS
kms://gcp/projects/P/locations/L/keyRings/R/cryptoKeys/K/cryptoKeyVersions/V Google Cloud KMS
kms://azure/<vault>/<key>[/<version>] or azurekv://<vault>/<key> Azure Key Vault
pkcs11:... HSM (images built with -tags pkcs11 only)

For Vault and OpenBao the gateway reads VAULT_ADDR, VAULT_NAMESPACE, VAULT_CACERT and VAULT_TOKEN_FILE (or VAULT_TOKEN). Without a token it uses Kubernetes auth with MAQPNA_VAULT_K8S_ROLE (mount MAQPNA_VAULT_K8S_MOUNT, default kubernetes). Set them with gateway.extraEnv.

audit:
  checkpointSigning:
    enabled: true
    keyURI: kms://vault/transit/keys/maqpna-audit
gateway:
  extraEnv:
    - {name: VAULT_ADDR, value: https://vault.example.eu:8200}
    - {name: MAQPNA_VAULT_K8S_ROLE, value: maqpna-gateway}

Key types, Transit policies and permissions are in Cloud identity & keys.

Secrets from a secret manager#

Every secret MAQPNA reads can come from a file, and every secret environment variable has a _FILE twin (for example MAQPNA_ADMIN_TOKEN_FILE). So both common bridges work:

  • External Secrets Operator writes ordinary Kubernetes Secrets from Vault KV, AWS Secrets Manager, Google Secret Manager or Azure Key Vault. The chart's Secret references work unchanged.
  • Secrets Store CSI driver mounts secrets as files. Enable gateway.secretsStoreCSI (secretProviderClass, mountPath default /mnt/secrets-store) and point *File settings or _FILE variables at it.

Examples for each provider are in Cloud identity & keys.

Troubleshooting#

Symptom Cause Fix
Helm fails: tokenVault.enabled requires delegation.tokenVault.keySecret No sealing key Create the kek Secret (Step 1)
consent_required on a per-user call The user has not connected the provider Send them to /v1/connect/<provider>
server_not_allowed_for_provider The MCP server's host is not in allowedServers Add the host glob to the provider
principal_unverified The session's user is not verified Set up principalBinding, or (not recommended) allowUnverifiedPrincipal: true
Account phase ReauthRequired The provider refused the refresh token The user connects again
MCPServer rejected: perUser requires provider and type none or mtls Wrong auth.type with perUser Use type: none or mtls and set provider
Vault signing fails at start-up No VAULT_ADDR, or the token lacks Transit sign Set the env vars; check the Transit policy

Next steps#