Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
commit
cb5be898ea
663 files changed
+82674
No files matched your search
@@ -0,0 +1,55 @@
|
||||
# ChatGPT OAuth connection persistence
|
||||
|
||||
The public MCP endpoint is expected to remain connected in ChatGPT without periodic interactive re-authorization while the OAuth grant remains valid.
|
||||
|
||||
## Required provider contract
|
||||
|
||||
The Authentik OAuth2/OIDC provider used by the ChatGPT MCP app must expose the `offline_access` scope mapping in addition to `openid`, `profile`, and `email`. This lets a compliant OAuth client request a refresh token instead of depending only on the original access token.
|
||||
|
||||
Production configuration therefore requires:
|
||||
|
||||
```text
|
||||
AUTHENTIK_SCOPES=openid,profile,email,groups,offline_access
|
||||
```
|
||||
|
||||
`groups` remains required by the Hub's existing role mapping. The ChatGPT
|
||||
provider itself uses `openid profile email offline_access`. The public
|
||||
protected-resource metadata and the MCP `401 WWW-Authenticate` challenge must
|
||||
advertise that same refresh-capable scope set. `promotion_preflight.py` fails
|
||||
closed when either `openid` or `offline_access` is absent.
|
||||
|
||||
## Authentik one-time reconciliation
|
||||
|
||||
In the Authentik admin interface, open the OAuth2/OIDC provider backing the
|
||||
ChatGPT application (`Provider for ChatGPT - ITWorx Gitea` in production) and
|
||||
include the built-in `offline_access` scope mapping under Scope mappings. This
|
||||
is distinct from the browser-facing Hub provider. Keep the authorization-code
|
||||
flow enabled and keep refresh-token support enabled. Do not weaken redirect
|
||||
URI, PKCE, audience, or group restrictions to obtain persistence.
|
||||
|
||||
After changing the provider, verify its OpenID discovery metadata advertises `offline_access` in `scopes_supported` and verify an authorization requesting that scope receives a refresh token.
|
||||
|
||||
Run production preflight with `--verify-oidc-discovery`. This checks both the
|
||||
browser-facing Hub issuer and the ChatGPT issuer for an exact issuer, HTTPS
|
||||
authorization/token endpoints, authorization-code and refresh-token grants,
|
||||
S256 PKCE, and `openid` plus `offline_access` publication.
|
||||
|
||||
## Validated production settings
|
||||
|
||||
The production ChatGPT provider is confidential and uses one strict
|
||||
`https://chatgpt.com/connector/oauth/...` redirect. Its access tokens are valid
|
||||
for five minutes. Refresh tokens are valid for 365 days and the refresh
|
||||
threshold is `seconds=0`; on the deployed Authentik version this rotates the
|
||||
refresh token on every successful refresh and revokes the prior token. These
|
||||
settings preserve short access-token exposure, revocability, and durable
|
||||
connections without disabling PKCE or widening redirect matching.
|
||||
|
||||
## ChatGPT reconciliation
|
||||
|
||||
OAuth metadata can be cached at app creation time. After the provider is corrected, recreate or reconnect the ChatGPT MCP app once so ChatGPT reads the updated provider metadata. Thereafter access-token renewal should use the refresh token and should not require routine interactive reconnects.
|
||||
|
||||
A future reconnect can still be legitimate after explicit revocation, refresh-token expiry or rotation failure, provider/client reconfiguration, security-policy changes, or deletion/recreation of the ChatGPT app.
|
||||
|
||||
## Operational rule
|
||||
|
||||
Do not work around reconnect prompts by making access tokens effectively permanent or by replacing user OAuth with a shared long-lived bearer token. Refresh-token based OAuth persistence preserves revocability and the existing identity boundary.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Docker read broker runtime
|
||||
|
||||
`docker-read-broker` is the only service with `/var/run/docker.sock`. Its public compose surface has no host port and the broker is attached only to the internal broker network. The root filesystem is read-only, capabilities are dropped, and the registration file and service token are mounted read-only.
|
||||
|
||||
The broker accepts only authenticated `GET` requests for host status, bounded storage, registered container list/inspect/health/logs, and bounded network metadata. Mutation, exec, image, volume, secret/config, arbitrary filters, and arbitrary Engine API paths are absent or denied. A stable registered ID is resolved to an immutable container ID and verified against compose project/service labels before inspection. Caller project and environment are taken from a short-lived HMAC service token, never from a free-form container request.
|
||||
|
||||
The deterministic fake backend and acceptance tests cover registration isolation, label spoofing, mutation/raw-route denial, secret/control-character log redaction, bounded logs, token expiry, and socket-unavailable degradation. No live Docker socket or Unraid host was modified for WP-100.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Docker Read Broker Security
|
||||
|
||||
## Purpose
|
||||
|
||||
The broker contains the unavoidable Docker socket trust boundary and prevents MCP-facing services from receiving raw Engine API access.
|
||||
|
||||
## v1 API allowlist
|
||||
|
||||
- list registered containers with bounded fields;
|
||||
- inspect one registered container;
|
||||
- read health/status;
|
||||
- read bounded recent logs;
|
||||
- list registered networks needed for topology display;
|
||||
- return host Docker version and non-sensitive capacity summary.
|
||||
|
||||
## Explicitly absent
|
||||
|
||||
- create/start/stop/restart/remove;
|
||||
- exec;
|
||||
- image build/pull/push/remove;
|
||||
- volume content access;
|
||||
- secret/config access;
|
||||
- arbitrary Docker API path;
|
||||
- arbitrary filters that reveal unrelated containers;
|
||||
- host filesystem mounts.
|
||||
|
||||
## Registration
|
||||
|
||||
The Hub passes a signed or mutually authenticated request containing a stable project/environment registration. The broker maps that registration to container IDs or immutable labels discovered during reconciliation. A client-supplied container name is never trusted alone.
|
||||
|
||||
## Hardening
|
||||
|
||||
- internal connector network only;
|
||||
- mTLS or short-lived internal service token;
|
||||
- no published host port;
|
||||
- read-only root filesystem and no package manager;
|
||||
- minimal dependencies;
|
||||
- bounded log bytes and time range;
|
||||
- reject containers not registered to the project;
|
||||
- structured Docker SDK calls, never shell commands;
|
||||
- dedicated security tests against write methods.
|
||||
|
||||
Mounting the socket read-only does not make Docker access read-only by itself. The broker's application-level API is the controlling boundary.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Identity and Authorization
|
||||
|
||||
## Human management users
|
||||
|
||||
Use Authentik OIDC authorization code flow with PKCE and a same-origin BFF session. Browser JavaScript receives only user/session metadata and CSRF protection, never reusable access or refresh tokens.
|
||||
|
||||
## Machine clients
|
||||
|
||||
Each MCP client has its own principal and credential. Preferred flow is client credentials or an Authentik service-account equivalent with audience restricted to the MCP Hub.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
svc-mcp-codex-workstation
|
||||
svc-mcp-claude-workstation
|
||||
svc-mcp-ludarium-prod
|
||||
svc-mcp-inspector
|
||||
```
|
||||
|
||||
Never reuse one broad token across projects.
|
||||
|
||||
After machine authentication and database-backed binding authorization, the
|
||||
dataplane replaces all caller-supplied identity headers and signs the exact
|
||||
project, environment, client, trace, and timestamp tuple. ContextForge forwards
|
||||
that assertion without receiving the signing key. Every connector verifies the
|
||||
HMAC and the 60-second age bound before repeating its own project, environment,
|
||||
and resource-registration checks. Missing, expired, or modified assertions deny.
|
||||
|
||||
## Effective authorization inputs
|
||||
|
||||
- trusted issuer and audience;
|
||||
- principal subject;
|
||||
- client identity;
|
||||
- workspace;
|
||||
- project;
|
||||
- environment;
|
||||
- endpoint/virtual server;
|
||||
- capability or tool;
|
||||
- risk class;
|
||||
- enabled status and revocation time;
|
||||
- matching policies and role bindings.
|
||||
|
||||
## Decision semantics
|
||||
|
||||
- explicit global deny wins;
|
||||
- disabled entities deny;
|
||||
- missing project/environment binding denies;
|
||||
- missing capability binding denies;
|
||||
- any matched deny wins over allows;
|
||||
- allow decisions include a trace explaining matched rules;
|
||||
- clients never select their own project through an untrusted body field when the endpoint already scopes it.
|
||||
|
||||
## Revocation
|
||||
|
||||
Client disable or binding removal must affect new requests immediately or within a documented cache ceiling of at most 60 seconds. Cache invalidation is event-driven where possible and always bounded.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Output Safety
|
||||
|
||||
MCP results may be consumed by agents and therefore are an untrusted content channel.
|
||||
|
||||
## Required result envelope
|
||||
|
||||
Every tool result should provide structured data and metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {},
|
||||
"meta": {
|
||||
"project": "...",
|
||||
"environment": "...",
|
||||
"source": "...",
|
||||
"truncated": false,
|
||||
"nextCursor": null,
|
||||
"generatedAt": "..."
|
||||
},
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- mark truncation explicitly;
|
||||
- never silently omit authorization failures;
|
||||
- distinguish target unavailability from empty results;
|
||||
- normalize control characters in logs;
|
||||
- return plain text only when the contract requires it;
|
||||
- cap nested depth and collection size;
|
||||
- do not render untrusted HTML in the UI;
|
||||
- resource descriptions and tool metadata come from managed manifests, not target content;
|
||||
- include source identifiers so an agent can verify where information came from.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Policy governance runtime
|
||||
|
||||
WP-110 provides one deterministic evaluator for management simulation, the dataplane router, and first-party connector invocation adapters. Rules are parsed into typed selectors and evaluated in this order:
|
||||
|
||||
1. missing binding is denied;
|
||||
2. terminal global denials are evaluated before any allow;
|
||||
3. explicit denials are evaluated;
|
||||
4. typed allows and their enumerated conditions are evaluated;
|
||||
5. the default is deny.
|
||||
|
||||
Rules contain selector values and bounded conditions only. They cannot execute scripts, perform regex evaluation, make outbound calls, or expand a project scope implicitly. A development selector matches only development; it does not imply production.
|
||||
|
||||
The `Governance` boundary carries the same `InvocationContext` into the rate limiter and append-only audit sink. A permitted call emits one success or error event; a denied or rate-limited call emits one corresponding event. The audit projection stores a SHA-256 argument hash, redacted bounded arguments, matched rule IDs, and a sanitized decision trace. Credentials and authorization headers are never included.
|
||||
|
||||
Connectors still perform their own project, environment, repository, document, and registered-container checks. Gateway policy is an additional boundary, not a substitute for connector authorization.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Secrets and Redaction
|
||||
|
||||
## Storage
|
||||
|
||||
Secrets are supplied through Docker secrets or read-only files below `/run/secrets`. Configuration stores only a secret reference.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
credentials:
|
||||
type: docker_secret
|
||||
reference: gitea_mcp_read_token
|
||||
```
|
||||
|
||||
No real secret appears in Git, catalog YAML, `.env.example`, test fixtures, screenshots, evidence, or generated client configuration.
|
||||
|
||||
## Redaction stages
|
||||
|
||||
1. reject secret-shaped input where a field should not accept it;
|
||||
2. omit sensitive headers from logs;
|
||||
3. sanitize target-system output in the connector;
|
||||
4. apply a final result redaction profile;
|
||||
5. sanitize audit arguments and response summaries;
|
||||
6. run automated secret scanning over repository and evidence.
|
||||
|
||||
## Patterns
|
||||
|
||||
At minimum detect:
|
||||
|
||||
- bearer/JWT tokens;
|
||||
- basic auth values;
|
||||
- private-key blocks;
|
||||
- common API key formats;
|
||||
- connection strings with passwords;
|
||||
- cookies and session IDs;
|
||||
- Authentik/Gitea tokens;
|
||||
- environment variable names marked sensitive.
|
||||
|
||||
## Audit representation
|
||||
|
||||
Use a stable argument hash and a small sanitized projection. Never solve auditability by storing the entire raw request.
|
||||
|
||||
## UI
|
||||
|
||||
Secret fields are write-only. The UI shows state such as `configured`, `missing`, or `last rotated`, never the value or a reversible masked form.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Security Invariants
|
||||
|
||||
These rules override every lower-level requirement.
|
||||
|
||||
1. **No arbitrary execution.** No generic shell, command, script, or interpreter tool may be exposed.
|
||||
2. **No raw Docker capability.** The Docker socket is mounted only into the dedicated read broker. No raw Docker API proxy exists.
|
||||
3. **No arbitrary file access.** Every readable path is registered, canonicalized, allowlisted, and mounted read-only.
|
||||
4. **No unrestricted SQL.** v1 has no general database query tool. Future database access must use a dedicated least-privilege account and query guard.
|
||||
5. **No secret retrieval.** Secrets can be consumed internally but never returned, indexed, audited verbatim, or shown in UI.
|
||||
6. **Deny by default.** Absence of an explicit binding or allow decision means deny.
|
||||
7. **Project and environment isolation.** Every invocation is checked against both dimensions; dev access never implies prod access.
|
||||
8. **Connector revalidation.** Each connector validates caller context and target allowlists; gateway visibility is insufficient.
|
||||
9. **Audience-bound tokens.** A token must be issued for the Hub resource and trusted issuer. Token forwarding to unrelated services is prohibited.
|
||||
10. **Bounded input and output.** Requests, logs, repository files, search results, and lists have strict limits and pagination.
|
||||
11. **Sanitized audit.** Audit records identify the action without storing access tokens, passwords, private keys, connection strings, or unlimited payloads.
|
||||
12. **No destructive tools.** v1.0 target-system capabilities are read-only. Post-v1 bounded writes must remain reversible and may not expose merge, delete, force-push, default-branch mutation or repository administration.
|
||||
13. **Reversible deployment.** Migrations and upgrades require backup, health verification, and a tested rollback or restore path.
|
||||
14. **No implicit permission expansion.** Adding a connector or tool does not expose it to existing clients until explicit bindings are reconciled.
|
||||
15. **Untrusted content remains data.** Repository files, docs, logs, and MCP results may contain prompt injection; they never modify policy or authorization.
|
||||
|
||||
|
||||
## WP-160 additions
|
||||
|
||||
Connector and dataplane target URLs must use HTTPS and must not resolve from an unsafe IP literal or metadata/localhost hostname. The security scan is fail-closed for non-fixture secret patterns, raw Docker mounts, mutable third-party images, unsafe host/container settings, and unintended published ports.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Security Test Plan
|
||||
|
||||
## Identity
|
||||
|
||||
- invalid signature, issuer, audience, expiry, not-before;
|
||||
- revoked client and disabled project;
|
||||
- dev token against prod alias;
|
||||
- user session fixation, CSRF, logout, refresh expiry;
|
||||
- duplicate and conflicting group/role bindings.
|
||||
|
||||
## Router
|
||||
|
||||
- unknown alias;
|
||||
- encoded path traversal;
|
||||
- oversized body;
|
||||
- hop-by-hop header injection;
|
||||
- attacker-supplied upstream host;
|
||||
- authorization header logging test;
|
||||
- timeout and partial-stream handling.
|
||||
|
||||
## Connectors
|
||||
|
||||
- path traversal and symlink escape;
|
||||
- SSRF and redirect to disallowed address;
|
||||
- output containing tokens, cookies, ANSI/control characters, and HTML;
|
||||
- target resource from another project;
|
||||
- pagination abuse and oversized output;
|
||||
- malicious document prompt injection retained as data only.
|
||||
|
||||
## Docker broker
|
||||
|
||||
- every write/exec endpoint absent;
|
||||
- raw API path denied;
|
||||
- unregistered container denied;
|
||||
- Docker label spoof resistance;
|
||||
- bounded logs;
|
||||
- socket unavailable degradation.
|
||||
|
||||
## Persistence and audit
|
||||
|
||||
- migration rollback/restore;
|
||||
- sanitized arguments;
|
||||
- audit cannot be modified through normal API;
|
||||
- retention job does not delete within protected window;
|
||||
- cache flush does not alter authorization truth.
|
||||
|
||||
## Supply chain
|
||||
|
||||
- lockfile integrity;
|
||||
- exact container digests in release manifest;
|
||||
- SBOM generated;
|
||||
- secret scan clean;
|
||||
- dependency and image scan policy enforced.
|
||||
|
||||
|
||||
## WP-160 executable gates
|
||||
|
||||
`tests/test_wp160_security.py` covers prompt injection as data, secret-like content rejection, registered-root traversal, SSRF host literals, token-reference boundaries, gateway drift, Compose socket/public-port invariants, and SBOM contents. `scripts/security_scan.py` performs the deterministic source/Compose policy scan; `scripts/generate_sbom.py` emits a CycloneDX 1.5 inventory from locked Python/Node manifests and declared container images. Fixture-only findings are explicitly classified and do not become production evidence.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Session store: production decision (WP-230E)
|
||||
|
||||
## Decision
|
||||
|
||||
`control-api`'s management-session store (`services/control_api/auth/bff.py`
|
||||
`SessionStore`) is an **in-memory, single-process store, and this is the
|
||||
accepted v1 production design** -- not a placeholder awaiting a Redis
|
||||
migration. `control-api` MUST run as exactly one instance/replica. This
|
||||
document is the load-bearing artifact for that constraint: code and Compose
|
||||
must stay consistent with it, and `scripts/compose_release_check.py` enforces
|
||||
the Compose side of it statically (see below), not just the docstring.
|
||||
|
||||
## Why this, not a Redis-backed store
|
||||
|
||||
`redis` is already a hard Compose dependency of `control-api`
|
||||
(`docker-compose.yml`'s `control-api.depends_on.redis`), which reads as an
|
||||
implied promise that sessions are Redis-backed. They are not: no
|
||||
`HUB_REDIS_*` setting exists, and no code path talks to Redis for sessions.
|
||||
That gap is exactly what this decision closes.
|
||||
|
||||
A real Redis-backed store was evaluated and deferred rather than built now,
|
||||
for a concrete, checkable reason: it requires a new production runtime
|
||||
dependency (`redis`, or an async client) added to `pyproject.toml` and
|
||||
locked via `uv lock`. At the time of this decision `uv` was not reliably
|
||||
resolvable in the environment this work was executed in, so a lockfile
|
||||
change could not be produced with real confidence -- and a session store
|
||||
touches authentication, the most security-sensitive path in this service.
|
||||
Shipping unlocked or hand-edited dependency pins for that specific subsystem
|
||||
is a worse outcome than shipping the honestly-documented current design.
|
||||
This is a tooling-availability constraint on *when*, not a judgment that
|
||||
Redis-backed sessions are undesirable; re-scoping this work is the correct
|
||||
next step once dependency tooling is confirmed reliable (see "Revisiting
|
||||
this decision" below).
|
||||
|
||||
## What "single instance" actually means here
|
||||
|
||||
- Every session and every CSRF token lives in one process's memory
|
||||
(`SessionStore._records`, a plain `dict` guarded by a `threading.RLock`).
|
||||
- A restart, redeploy, or crash of `control-api` invalidates every active
|
||||
session. Users see this as being signed out and must re-authenticate
|
||||
through Authentik; no data is lost, no state is corrupted, and the BFF's
|
||||
same-origin cookie/CSRF model means there is nothing to reconcile on the
|
||||
client side.
|
||||
- Running two or more `control-api` replicas behind a load balancer would
|
||||
silently split sessions and CSRF tokens across disjoint in-memory stores.
|
||||
A request routed to instance B with a session cookie issued by instance A
|
||||
would see "session expired" even though the session is, from the user's
|
||||
perspective, still valid -- and worse, CSRF token mismatches on that same
|
||||
split would be indistinguishable from a real CSRF attack in the logs.
|
||||
This is a correctness and security-signal problem, not just an HA gap, so
|
||||
it is enforced, not left to operational discipline.
|
||||
|
||||
## Enforcement (not just documentation)
|
||||
|
||||
- `docker-compose.yml` and `docker-compose.prod.yml` do not declare
|
||||
`deploy.replicas` for `control-api` (or anywhere); Compose's default is
|
||||
one container per service, so the constraint already holds today.
|
||||
- `scripts/compose_release_check.py::check_compose` now fails the release
|
||||
gate if `control-api` ever declares `deploy.replicas != 1`. This is a
|
||||
static, automated check (`tests/test_wp150_operations.py` and the new
|
||||
`tests/test_wp230_session_store.py` exercise it), run as part of
|
||||
`python scripts/compose_release_check.py` and the release-readiness gate
|
||||
-- so a future change that tries to scale `control-api` fails loudly
|
||||
before it reaches production, instead of failing silently at 2am when a
|
||||
real user hits the split-brain session bug above.
|
||||
- `SessionStore`'s docstring states the single-instance constraint directly
|
||||
at the point where a future change is most likely to be made, and points
|
||||
back to this document.
|
||||
|
||||
## Revisiting this decision
|
||||
|
||||
Move to a real shared store (Redis-backed, encrypted-at-rest session
|
||||
payloads, TTL-matched key expiry, explicit logout invalidation, and defined
|
||||
behavior for a Redis outage -- fail closed, forcing re-authentication,
|
||||
rather than silently falling back to per-instance memory) when:
|
||||
|
||||
1. `uv lock` can be run and verified in the environment doing the work, so
|
||||
the new dependency's lock entry is trustworthy, and
|
||||
2. there is an actual operational need for more than one `control-api`
|
||||
replica (there is none today -- Tower runs a single instance of every
|
||||
owned service).
|
||||
|
||||
Until then, this document and the `compose_release_check.py` gate are the
|
||||
enforced contract: single instance, in-memory, sessions do not survive a
|
||||
restart, and that is by design.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Threat Model
|
||||
|
||||
## Assets
|
||||
|
||||
- Authentik tokens and service identities;
|
||||
- connector credentials;
|
||||
- project source and documentation;
|
||||
- container metadata and logs;
|
||||
- Docker socket access;
|
||||
- policy and client bindings;
|
||||
- audit integrity;
|
||||
- gateway configuration;
|
||||
- backup material.
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
1. external MCP client to NPM/router;
|
||||
2. browser to web/control API;
|
||||
3. control plane to Authentik;
|
||||
4. router/control plane to ContextForge;
|
||||
5. ContextForge to connectors;
|
||||
6. connector to target system;
|
||||
7. Docker broker to Docker socket;
|
||||
8. services to PostgreSQL/Redis;
|
||||
9. backup process to storage.
|
||||
|
||||
## Primary threats and controls
|
||||
|
||||
| Threat | Example | Required controls |
|
||||
|---|---|---|
|
||||
| Token substitution | token issued for another API reused at MCP endpoint | issuer, audience, expiry, signature, resource validation |
|
||||
| Confused deputy | connector called with a target from another project | connector-side project/resource allowlist |
|
||||
| Tool poisoning | malicious description or output tries to alter agent behavior | managed manifests, sanitize metadata, treat content as data |
|
||||
| Prompt injection | README tells agent to reveal secrets | never authorize from content; explicit tool policies |
|
||||
| SSRF | connector endpoint points to metadata service | registered endpoints, URL parser, private-range policy, no arbitrary URL |
|
||||
| Path traversal | `../../secrets` in resource URI | canonical path and root containment checks |
|
||||
| Command injection | container name placed into shell command | no shell; structured Docker SDK calls |
|
||||
| Excessive data exfiltration | huge log/file response | pagination, byte/row/time limits, redaction |
|
||||
| Secret leakage | token appears in logs/tool output | structured logging, secret scanners, redaction profiles |
|
||||
| Approval replay | old operation approval reused | plan hash, actor, target, expiry, single-use state |
|
||||
| Audit tampering | operator deletes incriminating event | append semantics, restricted DB role, export/retention |
|
||||
| Supply-chain compromise | mutable `latest` image changes | exact tags/digests, SBOM, scans, update gate |
|
||||
| DoS | expensive repeated search/log calls | per-client rate limit, timeouts, concurrency caps |
|
||||
| Cross-project cache bleed | cached result returned to wrong project | project/environment/client in cache key |
|
||||
| Docker breakout | compromised broker abuses socket | minimal API, read-only logic, network isolation, no arbitrary labels/exec |
|
||||
|
||||
## Abuse cases to test
|
||||
|
||||
- a VacatureRadar token requests a Ludarium endpoint;
|
||||
- a production-observer client invokes an unbound write name;
|
||||
- a document path escapes its registered root through symlinks;
|
||||
- a Gitea file contains a fake token and malicious instructions;
|
||||
- Docker logs contain an Authorization header;
|
||||
- an unknown alias attempts to proxy to an attacker-controlled URL;
|
||||
- a revoked client reuses a cached token;
|
||||
- ContextForge returns a tool not present in the desired catalog;
|
||||
- Redis is flushed during reconciliation;
|
||||
- a connector returns more than the output budget.
|
||||
|
||||
|
||||
## WP-160 implementation notes
|
||||
|
||||
The Gitea and MCP dataplane URL contracts reject localhost, metadata hostnames, and private, loopback, link-local, reserved, unspecified, or multicast IP literals. Redirects remain same-origin and bounded. Repository test fixtures may contain deliberately fake secret-like values; the security scan classifies those paths as fixture placeholders and fails on non-fixture findings.
|
||||
@@ -0,0 +1,22 @@
|
||||
# MCP client onboarding and bridge
|
||||
|
||||
## Client lifecycle
|
||||
|
||||
Each machine client is an independently scoped Hub record bound to one project and environment. New UI records are `pending` and their binding is disabled until an exact, real Authentik subject is activated. The control API returns a client ID, status, last-seen metadata, an Authentik service-account plan, and a safe configuration projection. It never returns a bearer token, app password, refresh token, or shared credential.
|
||||
|
||||
Disable and revoke are separate audited actions. Both deny new requests immediately in the registry and remain bounded by a revocation cache ceiling of 60 seconds. A client cannot choose a different project or environment through an MCP request body.
|
||||
|
||||
## Generated profiles
|
||||
|
||||
- Native MCP 2026 uses Streamable HTTP and protocol revision `2026-07-28`.
|
||||
- Compatibility clients use the same scoped endpoint with revision `2025-11-25`.
|
||||
- The Codex stdio profile reads an Authentik app password from a user-only file, obtains a short-lived M2M token and refreshes it before expiry. Static bearer-token environment variables remain supported for compatibility.
|
||||
- Inspector uses an explicit LAN/internal endpoint and is enabled only through the Compose `inspector` profile.
|
||||
|
||||
Configuration contains endpoint, protocol, transport, scopes, client ID, Authentik username and credential reference only. `scripts/authentik_provision_mcp_service_account.py`, `scripts/provision_machine_client.py`, and `scripts/install_codex_mcp.ps1` form the reproducible operator path.
|
||||
|
||||
## Bridge safety
|
||||
|
||||
The bridge accepts only MCP initialization/notification/ping, bounded tool, resource and prompt methods; rejects oversized or invalid messages; never shells out; never logs credentials; handles JSON and SSE responses; and never includes authorization material in responses. Direct remote OAuth remains the preferred future path once the deployed Authentik supports policy-bound Dynamic Client Registration.
|
||||
|
||||
Reconciliation preserves only explicitly managed active clients. Pending clients remain disabled, operator-disabled clients stay disabled, and revoked clients cannot be silently reprovisioned.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Codex integration
|
||||
|
||||
## Production profile
|
||||
|
||||
Codex uses one MCP server per project/environment. Production endpoints are:
|
||||
|
||||
```text
|
||||
https://mcp.example.com/mcp/ludarium/prod
|
||||
https://mcp.example.com/mcp/vacatureradar/prod
|
||||
https://mcp.example.com/mcp/mobilityops/prod
|
||||
```
|
||||
|
||||
The deployed Authentik `2025.12.6` supports M2M client credentials and app
|
||||
passwords, but not the policy-bound Dynamic Client Registration introduced in
|
||||
Authentik `2026.8`. Codex therefore uses the repository's stdio bridge. The
|
||||
bridge retrieves five-minute Authentik access tokens and refreshes them without
|
||||
putting tokens or app passwords in Codex configuration.
|
||||
|
||||
Each profile requires:
|
||||
|
||||
- a dedicated Authentik service account and app password;
|
||||
- the exact Authentik user UUID as the Hub client subject;
|
||||
- one active Hub binding to one project/environment;
|
||||
- a local credential file readable only by the current OS user;
|
||||
- a Codex stdio entry created by `scripts/install_codex_mcp.ps1`.
|
||||
|
||||
## Operator sequence
|
||||
|
||||
1. Generate the local credential with `scripts/new_codex_credential.ps1`.
|
||||
2. Pipe `scripts/authentik_provision_mcp_service_account.py` into the Authentik
|
||||
container's `ak shell`, passing the credential through a temporary file.
|
||||
3. Run `scripts/provision_machine_client.py` in the control API container with
|
||||
the exact returned subject UUID. Supply `--client-id` when activating a
|
||||
pending UI-created record.
|
||||
4. Install the profile:
|
||||
|
||||
```powershell
|
||||
.\scripts\install_codex_mcp.ps1 `
|
||||
-Project '<project>' `
|
||||
-Environment 'prod' `
|
||||
-HubClientId '<hub-client-uuid>' `
|
||||
-OAuthUsername 'svc-mcp-codex-<project>' `
|
||||
-SecretFile '<user-only-credential-file>'
|
||||
```
|
||||
|
||||
5. Restart Codex and verify the server with `codex mcp list`.
|
||||
6. Prove `initialize`, `tools/list`, one read-only invocation, audit creation,
|
||||
and a cross-project denial.
|
||||
|
||||
Never copy an app password into `config.toml`, a command argument, Git,
|
||||
evidence, UI output, logs or chat. Disabling/revoking the Hub client denies new
|
||||
calls even if an Authentik token has not yet expired.
|
||||
|
||||
## Useful workflows
|
||||
|
||||
Runtime connectors publish these user-invoked prompts:
|
||||
|
||||
- `project-context-pack` and `pre-change-impact-review`;
|
||||
- `repository-change-review` and `release-readiness`;
|
||||
- `production-incident-triage` and `deployment-health-review`;
|
||||
- `fleet-operations-morning-brief`.
|
||||
|
||||
The Gitea surface also exposes pull-request summaries, bounded changed-file
|
||||
metadata, and commit/CI statuses. The Unraid surface exposes an aggregate
|
||||
project summary in addition to container health, inspect and bounded logs.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Compatibility and Versioning
|
||||
|
||||
## Tool lifecycle
|
||||
|
||||
- additive optional output fields are allowed within a major version;
|
||||
- required input changes or semantic breaks require a new major contract;
|
||||
- deprecated tools remain discoverable only for clients still bound to them and include replacement metadata;
|
||||
- removals require usage evidence and an announced date.
|
||||
|
||||
## Gateway upgrades
|
||||
|
||||
A ContextForge or SDK upgrade requires:
|
||||
|
||||
1. backup;
|
||||
2. release-note review;
|
||||
3. clean-room test environment;
|
||||
4. MCP discovery and invocation matrix across supported revisions;
|
||||
5. auth/header/proxy tests;
|
||||
6. connector contract suite;
|
||||
7. rollback/restore proof;
|
||||
8. recorded version/digest update.
|
||||
|
||||
## Client matrix
|
||||
|
||||
At minimum test:
|
||||
|
||||
- native 2026 remote MCP client;
|
||||
- compatible 2025-era client;
|
||||
- stdio bridge;
|
||||
- MCP Inspector;
|
||||
- project-bound machine client;
|
||||
- revoked/invalid client negative cases.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Error Model
|
||||
|
||||
## Stable categories
|
||||
|
||||
- `AUTHENTICATION_REQUIRED`
|
||||
- `TOKEN_INVALID`
|
||||
- `ACCESS_DENIED`
|
||||
- `PROJECT_NOT_FOUND`
|
||||
- `RESOURCE_NOT_REGISTERED`
|
||||
- `CONNECTOR_DISABLED`
|
||||
- `CONNECTOR_UNAVAILABLE`
|
||||
- `TARGET_UNAVAILABLE`
|
||||
- `VALIDATION_FAILED`
|
||||
- `RATE_LIMITED`
|
||||
- `TIMEOUT`
|
||||
- `RESULT_TOO_LARGE`
|
||||
- `UPSTREAM_PROTOCOL_ERROR`
|
||||
- `INTERNAL_ERROR`
|
||||
|
||||
## Response rules
|
||||
|
||||
- do not reveal whether a forbidden resource exists when that leaks scope;
|
||||
- include a trace ID;
|
||||
- use retryability metadata;
|
||||
- keep user-facing messages useful but sanitized;
|
||||
- log detailed internal exceptions only in protected logs;
|
||||
- translate target-specific errors into stable categories while preserving a safe diagnostic code.
|
||||
- separate `retryable` from `safeToRetry`: transient writes may be retryable only
|
||||
after their exact state is reconciled, never by blind transport replay;
|
||||
- identify the failed operation and subsystem without returning target URLs,
|
||||
credentials, raw headers or stack traces;
|
||||
- include expected/actual state and a bounded recovery hint when optimistic state
|
||||
changed;
|
||||
- keep the stateless MCP session usable after an operation error.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "CONNECTOR_UNAVAILABLE",
|
||||
"category": "transient",
|
||||
"message": "The Unraid inventory connector is temporarily unavailable.",
|
||||
"operation": "unraid.container.health",
|
||||
"subsystem": "upstream_unraid",
|
||||
"retryable": true,
|
||||
"safeToRetry": true,
|
||||
"correlationId": "...",
|
||||
"sessionId": null,
|
||||
"upstreamStatus": 503,
|
||||
"expectedState": null,
|
||||
"actualState": null,
|
||||
"recoveryHint": "Retry the unchanged read after the bounded backoff."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The dataplane retries only MCP protocol reads and catalogued R1 tools, once.
|
||||
Connector-local Gitea GETs use at most three bounded attempts. R2, R3 and unknown
|
||||
operations are never automatically retried.
|
||||
@@ -0,0 +1,34 @@
|
||||
# ChatGPT to Knowledge Hub document intake
|
||||
|
||||
The separate MCP Hub exposes ten focused tools at `/mcp/knowledge-hub/prod`: search and project context; scanned attachment text extraction; workspace roots, children and current page reads; attachment and caller-supplied Gitea text proposals; review submission; and status. `analyze_knowledge_files` and `extract_knowledge_files` advertise `_meta.openai/fileParams: ["files"]`. ContextForge omits connector tool metadata, so the dataplane restores this exact binding only for these two vetted attachment schemas on `knowledge-hub/prod`. The connector scans every source within fixed limits. Extraction returns real text in bounded pages with per-file coverage and an actor-bound authenticated continuation cursor; it has no proposal side effects. A proposal includes the selected create or update publication, and only a human reviewer in Knowledge Hub can approve or publish it.
|
||||
|
||||
For early attachment validation failures, connector audit may include a fixed `input_reason` label such as `attachment_file_id_invalid`. The label identifies the rejected check only; attachment IDs, URLs, names, and content are never recorded.
|
||||
|
||||
## Access boundaries
|
||||
|
||||
- The ChatGPT OAuth identity must be bound in MCP Hub to the exact `knowledge-hub` project and `prod` environment. MCP Hub signs the connector identity assertion and applies the tool policy. A supplied target Knowledge Hub `project_id` must be in `KNOWLEDGE_HUB_ALLOWED_PROJECT_IDS`. Project context always requires this ID. When `project_id` is omitted, search, extraction, proposal creation, review submission and status use the token-scoped workspace only if `KNOWLEDGE_HUB_WORKSPACE_READ_ENABLED=true` and the signed connector identity is exactly `knowledge-hub/prod`. This setting defaults to `false`; an empty target project allowlist is accepted only when workspace access is enabled.
|
||||
- Workspace roots and children are cursor paged. Current page Markdown is character paged and carries an ETag; later slices require the same ETag. Select a parent page or workspace root for a create, or compare the complete current page and include its ETag for an update.
|
||||
- For Gitea sources, first read the complete file through the separate Gitea MCP. Pass the bounded UTF-8 text and caller-supplied `itworx-git://` URI, ref, commit and full Git blob SHA to `create_text_source_proposal`. The intake connector checks that the supplied text matches that SHA and scans it. `source_complete=true` and repository provenance remain caller assertions; the connector does not fetch a repository or independently verify its contents.
|
||||
- A publication Markdown body is limited to 32,000 characters and analysis to 32,000 characters. An attachment or PDF with no extractable text is rejected before a proposal can be created; OCR is outside this path. DOCX extraction reads the main document text and may omit header/footer content, so review those sections separately before submitting a proposal.
|
||||
- The connector uses a separate Knowledge Hub service account with exactly `knowledge:search`, `projects:read`, `pages:read`, and `ingestion:write`. It cannot approve or publish proposals. Its identity assertion uses a dedicated `knowledge_hub_ingestion_identity_key` secret mounted only in the dataplane and intake connector, separate from the other connector HMAC key.
|
||||
- The Knowledge Hub token is loaded only from `/run/secrets/knowledge_hub_ingestion_token`. Signed attachment download URLs and token values must not appear in logs, audit events, or persisted proposal analysis.
|
||||
- An internal retry of a failed proposal is available only with the current ETag, `retryEligible=true`, and a fresh operation key. It is not exposed as an MCP tool or a publication action.
|
||||
- The connector and scanner are opt in. The default `INGESTION_CONNECTOR_ENABLED=false` excludes the new project and gateway from reconciliation, leaving existing MCP routes available during staging. The default dataplane does not mount or require the new identity key; the final intake production overlay adds that mount when the intake route is activated.
|
||||
|
||||
## Production activation
|
||||
|
||||
The live `wiki.example.com` Authentik proxy requires a narrow unauthenticated-path exception for the connector's API routes. Its proxy provider uses:
|
||||
|
||||
```text
|
||||
^/api/v1/(?:search|projects/[0-9a-fA-F-]{36}/context|pages(?:/roots|/[0-9a-fA-F-]{36}(?:/children)?)|ingestion-proposals(?:/[0-9a-fA-F-]{36}(?:/(?:sources|retry))?)?)$
|
||||
```
|
||||
|
||||
This only passes requests to Knowledge Hub's own bearer authorization. An anonymous JSON request to each allowed route must return 401; `/` and unrelated API routes must continue to redirect to Authentik. Keep the exception aligned with the connector's fixed route allowlist.
|
||||
|
||||
1. Deploy the Knowledge Hub admin preset that issues the four-scope intake token, including `pages:read`. Create the dedicated token in Beheer and place it in the operator-owned secret directory as `knowledge_hub_ingestion_token` with ownership matching the MCP Hub service UID/GID. Generate a separate random 32-byte-or-longer `knowledge_hub_ingestion_identity_key` in that directory. Keep both secrets out of Git and deployment logs.
|
||||
2. Verify a current backup and the pinned images. Set `KNOWLEDGE_HUB_ENDPOINT` to the actual HTTPS Knowledge Hub origin. Set `KNOWLEDGE_HUB_ALLOWED_PROJECT_IDS` to comma-separated UUIDs if project-scoped intake is needed; for workspace-only courses, keep it empty and explicitly set `KNOWLEDGE_HUB_WORKSPACE_READ_ENABLED=true` after reviewing the workspace-wide permission. Pin `CONNECTOR_INGESTION_IMAGE` to an immutable digest. Render Compose with `docker-compose.ingestion.prod.yml` **last**, after `docker-compose.prod.yml` and `docker-compose.autodeploy.yml`; inspect that the final connector image is the digest and `build` is absent before starting it.
|
||||
3. Start `ingestion-clamd` and `connector-ingestion` with the `knowledge-intake` Compose profile while `INGESTION_CONNECTOR_ENABLED=false`. Wait for current scanner signatures and connector readiness. Verify the four token scopes, a project context read when project intake is configured, and roots/current-page reads when workspace access is enabled.
|
||||
4. Add an enabled, exact OAuth client binding for `knowledge-hub/prod`. Set `INGESTION_CONNECTOR_ENABLED=true` on control API and worker, then restart only those services and verify gateway tool parity, project route, and all ten advertised tool schemas.
|
||||
5. Connect the MCP URL in ChatGPT. Exercise search, roots/children and a complete current-page read; attach a benign test PDF and read every extraction page; then create a complete proposal and submit it. Confirm it reaches `ReadyForReview`; approve/publish through the Knowledge Hub UI only. Check that a rejected file leaves no proposal or unscanned persisted source.
|
||||
|
||||
If staging fails, disable the `knowledge-hub/prod` OAuth client binding, set `INGESTION_CONNECTOR_ENABLED=false`, and reconcile again before stopping the connector. This disables an already created intake virtual server as well. Retain proposal IDs and audit evidence for human review. Do not delete Knowledge Hub data to roll back the connector.
|
||||
@@ -0,0 +1,34 @@
|
||||
# MCP Protocol Profile
|
||||
|
||||
## Supported revisions
|
||||
|
||||
- primary: `2026-07-28`;
|
||||
- compatibility: `2025-11-25` and earlier revisions supported by ContextForge/SDK v2.
|
||||
|
||||
## Transports
|
||||
|
||||
- remote dataplane and connectors: Streamable HTTP;
|
||||
- local bridge and tests: stdio;
|
||||
- no custom WebSocket protocol.
|
||||
|
||||
## Server behavior
|
||||
|
||||
- explicit server identity and version;
|
||||
- tool/resource list responses are cacheable only within correct project/client scope;
|
||||
- structured outputs are validated against declared output schemas;
|
||||
- requests and results include correlation metadata;
|
||||
- maximum request body at or below 4 MiB;
|
||||
- per-tool timeouts and output budgets are stricter than global limits;
|
||||
- protocol errors are distinct from target-system errors.
|
||||
|
||||
## 2026 headers
|
||||
|
||||
Where available, `Mcp-Method` and `Mcp-Name` can support routing, metrics, and rate limits. They are hints from a validated protocol path, not the sole authorization input. Compatibility traffic may require bounded body parsing by trusted components.
|
||||
|
||||
## Durable work
|
||||
|
||||
Do not depend on MCP Tasks in v1. Use `operation_id` in the management domain and expose status through ordinary Hub APIs/resources when needed. Add a protocol adapter only after the official SDK supports the extension.
|
||||
|
||||
## Client bridge
|
||||
|
||||
A local bridge may translate stdio to remote HTTPS. It stores credentials in the local OS-protected environment/configuration and never prints tokens. The bridge is optional; native remote MCP is preferred.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Resources and Prompts
|
||||
|
||||
## Resource URI schemes
|
||||
|
||||
```text
|
||||
itworx-project://<project>/<environment>/status
|
||||
itworx-docs://<project>/<source>/<path-or-id>
|
||||
itworx-git://<project>/<repository>/<ref>/<path>
|
||||
itworx-unraid://<project>/<environment>/container/<registered-ref>
|
||||
itworx-audit://event/<id>
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- custom URIs do not reveal physical server paths;
|
||||
- identifiers are canonical and encoded once;
|
||||
- resource access repeats project/client authorization;
|
||||
- large resources use metadata plus bounded retrieval;
|
||||
- symlink and path containment rules apply before reading content.
|
||||
|
||||
## Initial prompts
|
||||
|
||||
- `project-health-review`;
|
||||
- `incident-triage`;
|
||||
- `repository-onboarding`;
|
||||
- `release-readiness-review`;
|
||||
- `security-posture-review`.
|
||||
|
||||
Prompts structure a user-initiated workflow. They do not grant permissions, hide tool calls, or autonomously execute write actions.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Tool Contracts
|
||||
|
||||
## Naming
|
||||
|
||||
```text
|
||||
<domain>.<resource>.<action>
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
project.status.get
|
||||
gitea.repository.list
|
||||
gitea.file.read
|
||||
knowledge.document.search
|
||||
unraid.container.list
|
||||
unraid.container.inspect
|
||||
unraid.container.logs
|
||||
```
|
||||
|
||||
Names are lowercase ASCII, dot-separated, stable, and globally unique within the Hub. Do not include project names in canonical tool names; project scope comes from the virtual server and identity context.
|
||||
|
||||
## Required manifest fields
|
||||
|
||||
- canonical name and semantic version;
|
||||
- title and managed description;
|
||||
- connector type;
|
||||
- input and output JSON Schema;
|
||||
- risk class;
|
||||
- required capability and scopes;
|
||||
- allowed environment classes;
|
||||
- timeout, rate, row/item, and byte limits;
|
||||
- redaction profile;
|
||||
- idempotency and side-effect annotation;
|
||||
- lifecycle status and replacement when deprecated.
|
||||
|
||||
## Inputs
|
||||
|
||||
- reject unknown fields by default;
|
||||
- use enums and bounded strings;
|
||||
- use opaque registered references rather than raw paths/URLs;
|
||||
- pagination uses bounded `limit` and opaque cursor;
|
||||
- environment/project are not accepted when endpoint context already determines them.
|
||||
|
||||
## Outputs
|
||||
|
||||
- structured data first;
|
||||
- stable machine-readable error code;
|
||||
- explicit truncation and cursor;
|
||||
- source and generated time metadata;
|
||||
- no secrets, raw auth headers, or unbounded logs;
|
||||
- validate against the output schema before returning.
|
||||
|
||||
## Side effects
|
||||
|
||||
Every v1.0 tool is `readOnly: true`. The v1.1 Gitea exception is an exact R2
|
||||
allowlist of six tools: branch, issue, comment, bounded file and pull-request
|
||||
creation/update. MCP annotations must truthfully mark them non-read-only and
|
||||
the client must confirm the external action. The server independently enforces
|
||||
the project binding, R2 policy, separate write credential, safe change-branch
|
||||
namespace, input budgets and target allowlist. A tool whose target API might
|
||||
mutate through a GET-like endpoint remains prohibited.
|
||||
Reference in new issue
Block a user