# Shared MCP Hub: identity, isolation and internal secrets requirements

Status: architecture requirements for the publication specification; **not an
implementation or a security certification**. Updated November 4, 2026.

## Product boundary

V1 is a shared MCP service hub. Users have accounts, profiles and connections.
Shared compute and a common catalog do not imply shared credentials, user data,
conversations, job results or permissions. Daedro Secret is an independently deployable module with its own API and authority.
It serves the hub internally; users do not need to learn OpenBao operations. Dedicated customer
infrastructure is a later phase and is not required to achieve V1 data isolation.

Only operator-reviewed adapters belong in the V1 shared pool. Uploading arbitrary
MCP executables into a communal worker is out of scope. A read-only tool can still
exfiltrate data; a read-only flag is not an isolation boundary.

## Required identity model

Use distinct immutable identifiers, not names, email addresses, MAC addresses or
hashes of API tokens:

| Entity | Meaning and boundary |
| --- | --- |
| Account/subject | Human identity, keyed by verified issuer + subject |
| Tenant/workspace | Authorization boundary; create a personal workspace by default |
| User profile | User-owned settings inside a workspace; not a service deployment |
| Catalog service/version | Public operator-approved MCP implementation and declared tools |
| Connection | A subject/profile's provider account, secret references and allowed operations |
| Job/run | Immutable owner tuple, selected connection/version and audit correlation |
| Worker deployment | Shared execution capacity; never the source of user authorization |

Every connection, upload, job, result, secret reference, log view, export,
pagination cursor and idempotency key must be scoped to the authoritative owner
tuple `(tenant_id, subject_id, profile_id, connection_id)` as applicable. Where a
resource belongs to a workspace rather than a person, the ACL must explicitly say
so. Merely joining the same deployment never grants access to another user's data.

Membership and permissions are resolved on the server from a verified session.
Client-supplied owner IDs are filters at most, never proof of authority. Enforce
ownership on every read/write/list/search/download/stream route and worker retry.
A random UUID prevents guessing, not unauthorized access to a known UUID. Return
consistent non-disclosing missing/foreign-resource errors.

## Human access and onboarding

The user-facing flow is browser-only: sign in, create/use a passkey, enroll Google
Authenticator with a QR code, confirm the code, then connect an MCP. OpenBao root,
unseal shares, local scripts and PEM passphrases do not appear in this workflow.

Prefer a maintained IdP (Keycloak is the current candidate) over a new bespoke
WebAuthn/TOTP/account-recovery state machine. The hub consumes verified OIDC with
PKCE, state and nonce. Enforce passkey user verification and TOTP as explicit
requirements; do not assume a default IdP flow enforces both. Configure and test
strong-authentication claims, recent-authentication requirements and step-up for
adding credentials, changing factors, exporting data and privileged actions.

The first owner must be bound to an already verified identity or a narrowly
scoped, one-use bootstrap grant. Do not let the first visitor to an empty public
site claim administration. Later registrations receive ordinary user permissions;
only an explicit workspace grant permits sharing. Bootstrap administrative
credentials are operator machinery, revoked after configuration.

Passkeys authenticate possession of a credential, not a Mac serial number. Apple
or Google password managers may synchronize passkeys. Label the interaction
"Use your passkey" or "Continue with Touch ID" where actually available; do not
claim hardware binding unless credential properties and the supported device
policy establish it. A stable domain must be chosen before enrollment; changing
the RP ID later is an account-migration event. IP-address RP IDs are unsuitable.

Recovery has two independent layers: recovering a human account and recovering
the secrets service. They must not be the same forgotten password. Offer an
additional passkey and a deliberate recovery method in the account UI. Losing
both factors must not silently fall back to a weaker public password/reset flow.
The exact recovery policy must be approved in the publication spec; a single
laptop Keychain is not sufficient organization-wide disaster recovery.

## Trusted request context across shared services

The gateway creates an authenticated internal context with stable owner IDs,
connection ID, approved service/tool version, operation scope, audience, expiry,
job ID and attempt ID. Bind it to authenticated service-to-service transport.
Consumers verify provenance and audience and derive ownership from the verified
context. Reject unsigned client `X-User`, `X-Tenant` or equivalent headers.

MCP transport sessions, SSE streams and resumption identifiers are scoped to the
same authenticated principal. Never treat an MCP session ID as authentication,
and never reuse a principal's provider client/session for a different connection.
Connection pooling is allowed only where per-request identity/credentials remain
correctly separated and tested. Global caches must include the complete owner and
connection scope; provider data cannot be cached solely by tool arguments.

Jobs record their owner before enqueueing. Workers cannot select a different
owner or provider connection from untrusted job contents. Attempts are fenced;
replayed/stale attempts cannot overwrite another attempt or another user. Stop
new work when connection access is revoked and define how already-running work
is cancelled. Revocation and expiry must also apply to result retrieval.

## Data and secret boundaries

PostgreSQL stores metadata and authoritative ACLs. Application predicates are
mandatory; row-level policies are useful defense in depth where supported. Object
storage uses scoped object keys and short-lived download/upload grants; never
accept an arbitrary bucket/key supplied by a user as an authorized object. Shared
indexes, exports, temp files and backup restoration must preserve the same scope.

The hub stores connection metadata and opaque secret references only. Personal
tokens, private contexts and secret values belong to separate private namespaces
in Daedro Secret, never the shared catalog, YAML examples or the hub database.
The browser sends secret material over authenticated TLS to the Secret API; an
optional same-origin gateway must not persist, log or replay secret request bodies.
Do not send provider keys through URL parameters, analytics or general chat/context
storage. Personal context is user data with its own retention/ACL, not automatically
shared merely because the backing service or model is communal.

Secrets are write-only from ordinary application views. Show connection metadata,
last verification and rotation state, not stored key values. Scope an OpenBao
reference to tenant + subject/profile + connection, with a pinned version for a
run. The resolver must verify the full grant before reading a secret. User input
cannot choose an arbitrary Vault path, AppRole, endpoint, executable or tool.

Separate gateway identity, credential-provisioning authority, workload access and
operator recovery. A browser session never receives an OpenBao administrative
credential. Runtime workers receive only the selected provider credential or a
short-lived provider-native credential where available. Short-lived OpenBao
tokens do not make a static provider API key short-lived. Revoking the OpenBao
token cannot revoke a provider key already read.

Do not inject different users' secrets into a shared long-lived process environment.
An adapter requiring environment secrets needs a fresh bounded child/container per
execution with cleanup, or a reviewed request-scoped credential interface. Shared
compute can allocate ephemeral execution contexts without selling dedicated
customer infrastructure. Containers and encryption alone do not prove tenant
isolation. Trusted host administrators can access runtime secrets; V1 is not a
zero-knowledge product.

Provider responses and payloads remain untrusted. Prevent endpoint/redirect SSRF,
credential forwarding to another origin and arbitrary tool execution. Define
provider egress allowlists and separately authorize paid or destructive tools.
Apply per-user/connection concurrency, storage and spend quotas to bound abuse
and noisy-neighbor effects. Do not log secrets, OTP seeds, tokens, raw provider
responses or other users' payloads in common operational logs.

## Per-action Google Authenticator authorization

This is a hard product requirement, separate from sign-in MFA. Every human action
that creates, imports, replaces, rotates, deletes, restores, exports, reveals (if
supported), grants access to, or revokes access to a secret requires a fresh Google
Authenticator authorization for that exact action. Changing authenticators or
recovery policy requires equivalent explicit step-up. Entering the secrets
metadata area also requires step-up, but a metadata-view lease grants no mutation
or reveal authority. Do not silently fall back to login MFA when step-up fails.

The Secret module, not a client flag or generic hub session, owns this protocol:

1. Authenticate the person and authorize the proposed target. Canonicalize the
   complete operation, including exact target IDs, expected versions, grant
   recipient, workload/tool scope, expiry and any batch items. Keep proposed
   secret values out of public metadata and logs; use an internal keyed digest
   when binding low-entropy sensitive input rather than exposing a plain hash.
2. Create a random server-side challenge with subject, tenant, session binding,
   action, target, payload digest, creation/expiry, failure count and consumed
   status. Initial maximum lifetime: 90 seconds; never refresh by polling.
3. Display the concrete action for review, then require a fresh TOTP code from
   the enrolled Google Authenticator. The code belongs only in the secure form.
   The six-digit code is not itself a signature over the action: the server must
   enforce the binding. Do not advertise transaction signing from TOTP alone.
4. Verify the factor with the trusted identity system or the module's reviewed
   TOTP verifier. Consume the accepted authenticator time-step globally for that
   credential, and atomically consume the challenge with the operation/approval
   record. The same code must not approve a different challenge or replay an
   already-approved action. Initial failure limit: five attempts per challenge,
   with account and source rate limits and bounded backoff.
5. Execute only the canonical action approved. Re-check ownership, grant policy
   and expected version immediately before commit. Reject payload substitution,
   stale target versions and concurrent reuse. Idempotency may return the original
   result; it must not repeat a mutation or transform approval into another action.
6. Persist a metadata-only audit record with actor, target, action, factor event,
   outcome and correlation ID. Never record TOTP codes/seeds or secret contents.
   On unavailable MFA, policy service or required audit persistence, fail closed.

A success receipt is short-lived, single-use and audience-bound to the Secret
module/action. It is not a general bearer permission to manipulate secrets for
several minutes. Exact batch import may be one action only if the user sees the
complete batch and its scope before approval; additions or changes invalidate it.
If a TOTP period was already consumed, explain that the next code is needed.

If using OIDC step-up, bind state and nonce to the action intent and require a flow
that demonstrably performs a fresh OTP challenge. A cached SSO session, a static
`amr` claim or a prior `auth_time` is insufficient evidence of this action's OTP.
The selected IdP integration must prove this behavior before publication; do not
assume the provider exposes a direct OTP-validation API.

Automated MCP reads are different: a human first approves an explicit execution
grant with TOTP. The machine can then obtain only its allowed secret version,
for the approved subject/profile/connection, tool/workload and finite TTL or run
budget. Each machine read has workload authentication, fresh grant checks and an
audit event. It cannot create, rotate, reveal to a human, or broaden its grant.
Grant expiry, revocation or verification outage prevents new reads. Background
reads do not require a person to type an OTP for every request, and must not be
misrepresented as an OTP-approved human action.

## Availability and internal OpenBao operations

Choose automatic unsealing independent of a user's laptop. On GCP, a narrowly
scoped VM identity plus a dedicated software Cloud KMS key is the preferred
candidate. OpenBao 2.7 requires the external GCP KMS plugin; pin and verify it.
Protect the KMS key from deletion and test restart and backup restoration. A
retained encrypted old volume is not a usable rollback when its unseal material
is inaccessible. Never claim otherwise.

Operator recovery material must be independently recoverable and tested. Do not
leave a root token as the normal application's credential. Remove temporary
bootstrap grants after testing scoped management and runtime identities. Avoid a
Mac-only IAP tunnel and 24-hour VM stop policy as hidden dependencies of a public
service. Budget alerts are not a spending cap; quotas and bounded execution must
implement the intended limits.

## Verified implementation gaps

These findings come from code inspection; this is not an exhaustive audit.

| Location | Current behavior | Publication requirement |
| --- | --- | --- |
| `internal/secretsui/handler.go` | Static entity-to-workspace mapping; no self-service accounts/profiles | Persistent identity, membership and profile/connection authorization |
| `internal/secrets/resolver.go` | Grant key is `(workspace, deployment)` | Add user/profile/connection scope; deployment is capacity, not owner |
| `deploy/openbao/connect-existing.py` | Imports operator credentials into `demo`, passes all operations through that workspace | Replace demo migration glue with authenticated per-user connection lifecycle |
| `internal/secretmcp/http.go` | One operator-selected bearer per configured upstream operation | Authenticated per-user context and connection-scoped upstream identity |
| Voice processor `server.py:244` | `processor_owner = SHA256(bearer token)` | Stable owner independent of credential rotation; shared hub token must not collapse every user into one upstream owner |
| Voice processor `repository.py` | Jobs/uploads are queried by owner, which is currently the token-derived identity | Preserve predicates, replace owner derivation, test two-user cross-access |
| `internal/secretmcp/runner.go` | Approved child/container, environment secrets, coarse completion result | Cross-run cleanup, request-scoped secrets and observable per-user run/result contracts |
| `internal/transport/fleet.go` | Shared/private deployment management exists, and creation defaults private | V1 product contract must prioritize shared services and must not imply instance ownership solves data isolation |
| `docs/connected-mcp-rollout.md` | Real S3 listing and audio/video capabilities only; no media workflow | Test real upload → transcription → result and all corresponding owner checks |

## Mandatory public release gates

1. Two independently authenticated users and at least two profiles/connections
   each can use the same service deployment without observing each other's
   connection metadata, secrets, uploads, jobs, results, logs, cache entries or
   streams. Test guessed and known IDs, cursors, stale sessions and direct API
   requests, not just the normal UI.
2. Register, authenticate, revoke and recover accounts in a real browser. Test
   missing factor, replay, invalid origin/RP ID, CSRF and privileged-action
   step-up. Confirm passkey alone cannot satisfy a route requiring passkey+TOTP.
3. For every human Secret API mutation, test missing OTP, wrong OTP, expired
   challenge, reused time-step, same/different-action replay, payload substitution,
   simultaneous submissions, changed ownership/version and MFA/audit outage.
   Existing login MFA alone must never authorize the operation. Verify that an
   explicitly approved workload grant permits only its exact automated reads.
4. Authenticate every hop gateway → worker → provider; spoofed owner fields,
   reused session identifiers and cross-tenant job messages are rejected.
5. Rotate a provider key without changing data ownership. Delete/revoke a
   connection, verify queued/running/read-result behavior, and verify no unrelated
   connection changes. Test missing/expired leases and provider errors safely.
6. Restart the VM and OpenBao without owner intervention; restore a backup into
   an isolated environment and verify ownership and secret grants. Prove cleanup
   on crash/timeout and quota enforcement under parallel users.
7. A real S3 read and real audio/video processing each succeed through the UI
   with the selected connection. Unsupported/offline adapters remain unavailable,
   not green. Provider charges and data retention are visible before submission.
8. Publish the threat model, privacy/retention rules and actual acceptance
   evidence. Do not claim "no leaks", "hardware-bound", "production ready" or
   "zero knowledge" from a screenshot, capability probe or encrypted storage alone.

## Architecture decision notes and sources

Keycloak supports passwordless/discoverable WebAuthn and OTP enrollment. Its
default conditional flow can skip second-factor work after passkey authentication,
so the required combined flow needs deliberate configuration and negative tests.
See the [Keycloak administration guide](https://www.keycloak.org/docs/latest/server_admin/).

Keycloak's container guide recommends a 2 GB limit for small production deployments;
750 MB is only a comparison to its previous heap setting. Co-location with a
768 MiB OpenBao limit plus PostgreSQL and the hub on a 2 GB VM has insufficient
credible headroom. Do not use development-mode H2 to hide that constraint. See
[Keycloak container memory guidance](https://www.keycloak.org/server/containers).

Cloud KMS software key versions are listed at $0.06/month, plus operations; this
is a component estimate, not the whole deployment budget. See
[Google Cloud KMS pricing](https://cloud.google.com/security/products/security-key-management).
OpenBao documents [auto-unseal lifecycle dependencies](https://openbao.org/docs/next/concepts/seal/)
and the [GCP KMS plugin transition](https://openbao.org/docs/2.6.x/configuration/seal/gcpckms/).

A Go implementation using an established WebAuthn verification library can reduce
resident infrastructure, but it still owns enrollment, challenge/session state,
TOTP storage/replay control, revocation and account recovery. That is a distinct
security product scope, not a quick replacement of the login form. For public V1,
prefer an existing maintained IdP unless the publication spec explicitly funds
and tests that ownership. The existing Go OIDC dependency can support integration.
