Ctrl K
ComunidadGuías…

docs/authentication.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Authentication

Checked against the current MCP/OpenAI OAuth model on 2026-09-27.

## Supported product path

The supported public ChatGPT path is **integrated self-hosted OAuth/OIDC**.

The package runs a dedicated ZITADEL instance plus PostgreSQL beside the Gateway. The supported path is self-contained and does not require a third-party identity service or separate tunnel.

Local/lab verification still uses the static bearer token generated by `scripts/init.sh`. That token never becomes the public ChatGPT credential.

## Public topology

~~~text
ChatGPT
   |
   | HTTPS + OAuth 2.x / OIDC
   v
public domain
   |-------------------------------|
   |                               |
   v                               v
Gateway                         ZITADEL
protected resource             authorization server
   |                               |
   v                               v
Broker                       PostgreSQL identity state
   |
   v
VPS
~~~

The same public hostname can serve both roles. Router priority sends `/mcp`, `/healthz` and RFC 9728 protected-resource metadata to the Gateway; OAuth/OIDC discovery, login, token, user-info and DCR endpoints go to ZITADEL.

## Bootstrap

Run:

~~~bash
bash scripts/setup-integrated-auth.sh
~~~

The script generates the identity-stack secrets, reuses an existing Traefik when possible, starts the bundled edge only on a clean host, starts ZITADEL/PostgreSQL, enables open Dynamic Client Registration required by MCP clients, creates a dedicated non-admin operator identity, records its stable subject in `.env`, then recreates Gateway/Broker in integrated mode.

The operator password is read without terminal echo. It is never written to `.env` or the state marker.

The temporary bootstrap IAM owner is deleted after the dedicated operator is created. The bootstrap machine PAT is then removed from the bootstrap volume. The internal login-client PAT remains in the private Docker volume because ZITADEL Login requires it.

## MCP protected-resource metadata

The Gateway exposes:

~~~text
/.well-known/oauth-protected-resource
~~~

For integrated mode it normally advertises:

~~~text
resource: https://mcp.example.com/mcp
authorization_servers:
  - https://mcp.example.com
scopes_supported:
  - openid
bearer_methods_supported:
  - header
~~~

`scripts/verify-public.sh` verifies that metadata, required resource scopes, OIDC discovery, DCR advertisement, S256 PKCE, refresh-token support, private introspection credentials, HTTPS and unauthenticated MCP denial.

## Resource binding and private token introspection

The integrated Authorization Server uses a dedicated ZITADEL project as the MCP resource audience.

During bootstrap the package creates:

~~~text
MCP VPS Agent Resource
  └── API application: MCP VPS Agent Introspector
~~~

The project ID becomes a required OAuth scope:

~~~text
urn:zitadel:iam:org:project:id:<resource-project-id>:aud
~~~

ZITADEL adds that project ID to the access-token audience when the scope is granted. The Gateway then validates every opaque bearer token over the private Docker identity network using ZITADEL's RFC 7662 introspection endpoint.

A token is accepted only when all of these are true:

- introspection returns `active: true`;
- `sub` is present;
- `iss` exactly matches the public integrated issuer;
- `exp` is still in the future;
- `aud` contains the dedicated MCP resource project;
- the token scopes include every scope advertised as required by the MCP protected resource.

The introspection API client belongs to that same resource project. ZITADEL independently rejects introspection when the token audience does not contain that API client's client ID or project ID. The Gateway performs its own audience check as a second boundary.

The introspection endpoint itself is not exposed as a management surface by the package. Gateway reaches it through the private `integrated-auth` Docker network. The introspection client secret is stored only in the local root-readable operator configuration and is redacted from diagnostic bundles.

## Dynamic Client Registration

MCP clients register before an operator has authenticated, so ZITADEL DCR is enabled with unauthenticated registration. The dedicated DCR endpoint is rate-limited at the Traefik layer. Dynamically registered clients live in ZITADEL's dedicated DCR project.

The operator still has to authenticate before ChatGPT receives a usable access token.

## Subject binding

The setup script creates a dedicated regular operator user for the VPS operator and writes that user's chosen stable ID to:

~~~dotenv
VPS_AGENT_SUBJECT=<operator-user-id>
~~~

The bootstrap IAM owner exists only during first-instance setup and is removed before installation proceeds. It is never the identity accepted by the Broker.

## Fail-closed rules

- The local Gateway starts on loopback/static auth for deterministic local acceptance.
- A public MCP URL is rejected unless the Gateway runs in integrated mode.
- `verify-public.sh` rejects non-HTTPS public URLs.
- public metadata must identify the expected resource and issuer.
- integrated OIDC discovery must advertise DCR, S256 PKCE and refresh-token support.
- protected-resource metadata must advertise the dedicated ZITADEL resource-audience scope.
- every accepted access token must be active, unexpired, from the expected issuer and bound to the dedicated resource project audience.
- unauthenticated `/mcp` must return HTTP 401 with a `WWW-Authenticate: Bearer` challenge pointing to the protected-resource metadata.
- the Broker rechecks the exact expected subject and policy after Gateway authentication.
- the Gateway still has no host root and no Docker socket.

## Edge proxy

If exactly one running Traefik is detected, the installer joins its Docker network and reuses its ACME resolver. If no Traefik is present and host ports 80/443 are free, the package starts its own pinned Traefik.

The automatic installer refuses to replace an unknown web server already occupying 80/443.

## Local/lab static bearer

Static bearer remains only for local CI and pre-public verification:

~~~dotenv
VPS_AGENT_AUTH_MODE=static
~~~

It is intentionally not the public ChatGPT path.