docs: initial architecture and design for MCP Nexus
MCP Nexus is a self-hosted control plane for MCP servers — "Kubernetes for
MCP." Agents connect to one endpoint; Nexus discovers services, installs the
right MCP servers, aggregates them behind a namespaced router, secures access
with auth/RBAC, and heals/updates them via a reconcile loop.
This first commit is design-phase only (no runnable code yet):
- README.md project front door + module map
- docs/ARCHITECTURE.md target design: tenets, two-plane split, reconcile
loop, domain types, storage, security, deployment
- docs/ROADMAP.md phased delivery (foundations -> walking skeleton ->
discover+install -> secure -> operate -> extend/scale)
- docs/modules/01-20 one design doc per module, all cross-linked
Backend stack decision: Go (single static binary, embedded SQLite + embedded
React dashboard). Repo initialized in /root with a whitelist .gitignore so
only project files are tracked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
127
docs/modules/06-authentication.md
Normal file
127
docs/modules/06-authentication.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Authentication
|
||||
|
||||
> Module 06 · Plane: Data · Roadmap phase: 3
|
||||
> Part of [MCP Nexus architecture](../ARCHITECTURE.md).
|
||||
|
||||
## Purpose
|
||||
Authentication turns an inbound connection at the [API Gateway](04-gateway.md) into a **verified principal** — a stable identity for either a human user (dashboard/API operator) or an AI agent (Claude, Cursor, …). It answers exactly one question: *"Who is calling?"* It does **not** decide what they may do; that is [RBAC](08-rbac.md). AuthN is the seam through which every request passes before it can be scoped, routed, or audited (Architecture §5, §9). It is pluggable by tenet #3: API keys first (Phase 3), OAuth2/OIDC and JWT sessions next, and LDAP/Active Directory/SAML (enabling SSO) later.
|
||||
|
||||
## Responsibilities
|
||||
- Own a set of pluggable **AuthN providers** that each attempt to resolve request credentials into a `Principal`. Starter provider: **API keys**. Follow-ons: OAuth2, OpenID Connect (authorization-code + PKCE), JWT session validation, then LDAP, Active Directory, and SAML (SSO/enterprise IdP federation).
|
||||
- Extract credentials from the request in a provider-appropriate way: `Authorization: Bearer <token>`, `X-API-Key`, a session cookie, or an MCP `initialize` handshake carrying an agent credential.
|
||||
- Establish and validate **sessions**: mint short-lived JWT access tokens (with a refresh path) after an interactive login; validate their signature, expiry, audience, and issuer on every request.
|
||||
- Issue, hash, store, list, and **revoke API keys**; keys are stored only as salted hashes, shown in plaintext exactly once at creation.
|
||||
- Attach the resolved `Principal` (with its provider, credential id, and asserted identity claims) to the request context so downstream RBAC, Router, and Audit all read one canonical identity.
|
||||
- Map an agent's presented credential to its [AgentProfile](14-agent-profiles.md) identity so profile→role resolution can proceed.
|
||||
- Emit an audit event for every authentication decision — success, failure, and revocation — with principal, provider, and source address (Architecture §9).
|
||||
|
||||
## Non-goals
|
||||
- **Authorization.** Deciding which tools/namespaces a principal may see or call is [RBAC](08-rbac.md); this module only proves identity.
|
||||
- **The agent-facing identity binding.** Modeling per-agent policy objects is [AI Agent Profiles](14-agent-profiles.md); AuthN merely resolves a credential to the profile's identity.
|
||||
- **Credential storage for upstream services.** MCP-server secrets live in the [Secrets Manager](07-secrets-manager.md); AuthN secures *access to Nexus*, not Nexus's access to backends.
|
||||
- **Transport security.** TLS termination is the [Gateway](04-gateway.md) / [Security](19-security.md) baseline.
|
||||
- **Plugin transport.** Out-of-process auth-backend plugins are wired via the [Plugin System](11-plugin-system.md) in Phase 5; this module defines only the in-process interface.
|
||||
|
||||
## Interfaces
|
||||
```go
|
||||
// A resolved, verified identity. Attached to every request context after AuthN.
|
||||
type Principal struct {
|
||||
ID string // stable identity key, e.g. "user:alice", "agent:claude-01"
|
||||
Kind PrincipalKind // KindUser | KindAgent | KindService
|
||||
Provider string // "apikey", "oidc", "jwt", "ldap", "saml"
|
||||
Claims map[string]string // sub, email, groups, agent_id, ...
|
||||
Expires time.Time // zero for non-expiring credentials (e.g. API key)
|
||||
}
|
||||
|
||||
type PrincipalKind string
|
||||
|
||||
const (
|
||||
KindUser PrincipalKind = "user"
|
||||
KindAgent PrincipalKind = "agent"
|
||||
KindService PrincipalKind = "service"
|
||||
)
|
||||
|
||||
// A pluggable authentication backend (tenet #3).
|
||||
type Provider interface {
|
||||
Name() string // "apikey", "oidc", ...
|
||||
// Authenticate inspects the request; ok=false if this provider does not
|
||||
// recognize the credential, err for a malformed/invalid one.
|
||||
Authenticate(ctx context.Context, r *http.Request) (Principal, bool, error)
|
||||
}
|
||||
|
||||
// The authenticator runs providers in order and returns the first match.
|
||||
type Authenticator interface {
|
||||
Register(p Provider) error
|
||||
Resolve(ctx context.Context, r *http.Request) (Principal, error) // ErrUnauthenticated if none match
|
||||
}
|
||||
|
||||
// API-key lifecycle. Keys are stored hashed; plaintext is returned once.
|
||||
type KeyManager interface {
|
||||
Issue(ctx context.Context, subject string, opts KeyOpts) (plaintext string, id string, err error)
|
||||
Verify(ctx context.Context, presented string) (Principal, error)
|
||||
Revoke(ctx context.Context, id string) error
|
||||
List(ctx context.Context, subject string) ([]KeyMeta, error)
|
||||
}
|
||||
```
|
||||
|
||||
HTTP/API surface (behind the Gateway; TLS-terminated):
|
||||
- `POST /api/v1/auth/login` — interactive login → sets session cookie / returns JWT.
|
||||
- `POST /api/v1/auth/refresh` — exchange a refresh token for a fresh access token.
|
||||
- `POST /api/v1/auth/logout` — invalidate the current session.
|
||||
- `GET /api/v1/auth/oidc/callback` — OAuth2/OIDC authorization-code redirect target.
|
||||
- `GET /api/v1/auth/saml/acs` — SAML assertion consumer service (SSO, later).
|
||||
- `POST /api/v1/auth/keys` · `GET /api/v1/auth/keys` · `DELETE /api/v1/auth/keys/{id}` — API-key CRUD (RBAC-gated).
|
||||
|
||||
MCP handshake: an agent presents its credential in the `initialize` request (bearer token or API key header on the Streamable HTTP transport); the resolved `Principal` is pinned to the MCP session for its lifetime.
|
||||
|
||||
Request-resolution flow (data plane, per request):
|
||||
|
||||
```
|
||||
1. Gateway receives request (MCP over Streamable HTTP, or dashboard/API call).
|
||||
2. Authenticator.Resolve iterates registered providers in priority order:
|
||||
apikey → jwt/session → oidc → (ldap/ad/saml later)
|
||||
3. First provider that recognizes the credential returns (Principal, true).
|
||||
- none recognize it → ErrUnauthenticated → 401 (audited)
|
||||
- one recognizes but it's bad → error → 401 (audited, no fall-through)
|
||||
4. Principal attached to request context (id, kind, provider, claims).
|
||||
5. For agents: Principal.ID feeds AgentProfile.Identify (Module 14).
|
||||
6. RBAC (Module 08) reads the Principal to scope the request.
|
||||
7. Audit records the decision: {principal, provider, source_ip, outcome}.
|
||||
```
|
||||
|
||||
## Data
|
||||
- **Reads/writes** the **Identity** store (Architecture §8): `Users`, `AgentProfiles`, `Roles`, and **API keys** (stored as `{id, subject, hash, salt, created, last_used, expires, revoked}`).
|
||||
- **Reads** OIDC/SAML provider config (issuer URL, client id, JWKS endpoint, metadata) from the **Config** store; the OAuth **client secret** is a ref into the [Secrets Manager](07-secrets-manager.md), never inlined.
|
||||
- **Writes** authentication events to the **Audit** store (append-only).
|
||||
- JWT signing keys live in the **Secrets** store; the JWKS for verifying externally issued tokens is fetched and cached from the IdP.
|
||||
|
||||
## Dependencies
|
||||
- Sits behind the [Gateway](04-gateway.md), which invokes `Resolve` as the first middleware on the data-plane hot path.
|
||||
- Feeds the `Principal` to [RBAC](08-rbac.md) for authorization and to [AI Agent Profiles](14-agent-profiles.md) for identity→profile binding.
|
||||
- OAuth client secrets and JWT signing keys come from [Secrets](07-secrets-manager.md).
|
||||
- Every decision is recorded per [Security](19-security.md) §9 audit rules.
|
||||
- Backend providers are plugin points via the [Plugin System](11-plugin-system.md) (Phase 5).
|
||||
|
||||
## Failure modes & handling
|
||||
| Failure | Behavior |
|
||||
|---|---|
|
||||
| No provider recognizes the credential | `ErrUnauthenticated` → `401`; audited as a failed attempt with source IP. |
|
||||
| Malformed/expired JWT | `401`; client directed to `/auth/refresh` or re-login; never falls through to another provider as valid. |
|
||||
| IdP (OIDC/SAML) unreachable | Interactive login fails closed; already-issued, still-valid JWTs continue to work until expiry (no data-plane outage). |
|
||||
| JWKS fetch fails | Serve from cached keys; refuse tokens signed by unknown `kid` rather than trusting blindly. |
|
||||
| API key leaked/compromised | Immediate `Revoke`; hash comparison is constant-time; keys are rate-limited and can carry an expiry. |
|
||||
| Brute-force credential guessing | Per-source and per-subject rate limiting (Security §9) + exponential backoff; repeated failures alert via [Notifications](13-notifications.md). |
|
||||
| Clock skew on token validation | Bounded leeway (e.g. ±60s) on `nbf`/`exp`; beyond that, reject. |
|
||||
|
||||
## Security notes
|
||||
Honors Architecture §9. Credentials are never logged and never written into `DiscoveredResource`, config, or audit payloads — only credential **ids** and outcomes are recorded. API keys are stored as salted hashes (argon2id/bcrypt), compared in constant time, and shown in plaintext once. JWTs are short-lived, signed with a key from [Secrets](07-secrets-manager.md), and validated for signature, `iss`, `aud`, and expiry on every request. OAuth flows use authorization-code + PKCE; client secrets are secret-refs. All auth traffic is TLS-only (fail closed if terminated plaintext). Sessions bind to the `Principal` for their lifetime so a mid-session privilege change forces re-resolution. Fail closed everywhere: an unresolvable request is unauthenticated, never anonymous-with-access.
|
||||
|
||||
## Open questions
|
||||
- Should agent credentials be first-class API keys scoped to an [AgentProfile](14-agent-profiles.md), or a distinct credential type with its own lifecycle?
|
||||
- mTLS as an additional agent authentication provider for zero-shared-secret deployments?
|
||||
- Session store: stateless JWT only, or a server-side session table to enable instant global revocation (at a hot-path lookup cost)?
|
||||
- How to federate group/role claims from multiple IdPs into one [RBAC](08-rbac.md) role space without collisions?
|
||||
- Do we support step-up auth (re-authentication) for high-risk control-plane mutations?
|
||||
|
||||
## Milestone
|
||||
Delivered in **Phase 3** (Secure — data-plane hardening). Thin slice first: **API-key authentication** end to end — issue a key, present it on the MCP handshake and the control API, resolve a `Principal`, and audit the decision — so two differently keyed agents can be told apart. OIDC/OAuth + JWT sessions follow within the phase; LDAP/AD/SAML SSO are deferred (Roadmap "not early"). Contributes to the Phase 3 exit proof: two agents with different identities connect and are distinguished before RBAC scopes them.
|
||||
Reference in New Issue
Block a user