Configuration governance, quorum and operation identity
Enable independent review of configuration changes in remote mode:
admin:
approvals:
policy_changes:
enabled: true # Default false for existing deployments.
required_scopes: [mcphub:security]
subjects: [security-reviewer-sub] # Optional exact OIDC subjects.
require_step_up: true # Requires step_up_acr_values above.
notifications:
url: https://notify.example.com/mcphub
secret: ${MCPHUB_WEBHOOK_SECRET} # At least 32 bytes.
audit_archive:
url: https://audit.example.com/mcphub
key_id: audit-2026-01
signing_key: ${MCPHUB_AUDIT_SIGNING_KEY}
Configuration administrators propose creates/updates to backends, tool groups, HTTP tools and OpenAPI imports; APIs and mcpbridge admin return 202 with pending_approval, approval_id and approval_url. The web editor opens the proposal. No configuration is activated yet. Another subject with the security scope must sign in using Sign in as a security administrator (/auth/login?role=security), review the redacted before/after snapshot, and Approve and apply. Credential changes are marked without revealing values. A security role alone cannot directly edit configuration, and a write reviewer alone cannot approve configuration. Review requires a browser session and CSRF checks; API tokens cannot approve. Deletes and a pure enabled→disabled change take effect immediately. Re-enabling and changing other fields require review. YAML/operator access and database access remain trusted; these static governance settings cannot be edited by the management API.
Proposals bind target revisions and, for tools/imports, their group revision. A concurrent modification makes application fail instead of overwriting it. OpenAPI proposals include the resolved document and generated definitions; approval never refetches the specification. Changed automatic refreshes also become proposals and retain the last approved definitions until reviewed. Restart invalidates unstarted proposals; interrupted application is marked unknown and requires inspection. A failed/expired proposal needs a fresh proposal. At most 20 active requests per issuer/subject and 32 MiB per complete configuration proposal are accepted.
For a production write, configure the matching tool rule:
approval:
required_approvals: 2
require_step_up: true
operation_id_argument: /operation_id
status_tool: get_operation_status
approvers:
- subjects: [reviewer-a-sub, reviewer-b-sub]
# Optional conditions can only increase the base quorum/authentication strength.
# Set the base to 1 if only these resource values require two reviewers.
risk_rules:
- resources:
- argument: /project
allowed_values: [production]
required_approvals: 2
require_step_up: true
Quorum defaults to one and permits one or two. Every vote must come from a distinct authorized subject; quorum two always excludes the requester. Required step-up is performed separately by each reviewer. The first vote leaves a two-reviewer request pending; the execution deadline starts only at quorum. Any authorized reviewer can reject while pending or revoke before execution. The UI shows vote count and reviewers. Risk conditions use all-of JSON Pointer conditions with exact string values; an array matches if any member has the configured risk value. Missing, empty or mistyped risk arguments fail closed. All matching policies combine with the strongest quorum/step-up requirement. For deletion/bulk tools or a production-only endpoint, set the base quorum on the actual tool: an agent-supplied label such as risk: low is not a trustworthy risk boundary.
operation_id_argument selects a required 1–128 character string using letters, digits, ., _, : or -. Generate it once per business operation and reuse it across client retries. MCPHub binds it to issuer, subject, source and tool. Identical normalized execution parameters return the existing approval/status; different arguments or business metadata conflict. Only transport progressToken is excluded from comparison. Completed, rejected, expired and unknown operations cannot reuse the ID for a fresh write. Retention removes detailed results but retains a small hashed identity tombstone, so old IDs remain unavailable; tombstones grow with the number of unique operations. Different caller identities are intentionally isolated. Without an operation ID, identical active requests share one approval; after completion, the same arguments may represent a new operation.
The operation ID remains in the saved tool arguments and is sent upstream. An HTTP Header parameter can map it to Idempotency-Key; MCP tools must implement their own key handling. Gateway deduplication cannot prevent writes made outside MCPHub, or retries with a new ID. Backend idempotency and atomic version checks are still required for business-level guarantees.
Clients should poll the explicitly read-only tool:
{"name":"mcphub_approval_status","arguments":{"approval_id":"..."}}
It returns state, quorum, voters, deadlines and an available cached result, without claiming or executing the operation. Optional query_upstream: true invokes status_tool, an explicitly published read-only original tool in the same backend/group, using the saved arguments. It must accept those arguments, including the operation ID, and return structured content describing the upstream state/receipt. Scope, resource, configuration and rate-limit checks still apply. The response is an upstream_observation; it does not reset an unknown outcome or authorize replay. Status lookup works across restart when the source/tool configuration is unchanged; changed configuration suppresses cached results and upstream lookup. Basic status remains private to the original issuer/subject. Use mcphub_resume_approval only when deliberately executing an approved write.
Approval notifications and independent audit archive
Both integrations are optional HTTPS endpoints, configured outside the management API. URLs cannot contain credentials, query parameters or fragments. Approval events and delivery records commit in the same SQLite/PostgreSQL transaction. A single worker sends them in order, with a 10-second timeout, no redirects and durable retry backoff from 5 seconds to one hour. Receivers must deduplicate event_id: a lost receipt can cause redelivery. A failed archive entry blocks later archive entries to preserve chain order. Events include requests, votes, decisions, cancellation, execution and expiry; pending/approved requests receive one reminder within five minutes of expiry, on the minute-based maintenance tick. Notifications contain only event_id, approval_id, action, approval_url, and expires_at. Links open the authenticated review page; webhooks cannot approve.
Verify X-MCPHub-Signature: sha256=<hex> as HMAC-SHA256 with the webhook secret over X-MCPHub-Timestamp + "." + raw_body. Check timestamp freshness and deduplicate the event ID. A 2xx response acknowledges notification delivery. Keep the shared secret out of URLs and logs.
The audit key is a Base64-encoded 64-byte Ed25519 private key (seed followed by public key); the corresponding 32-byte public key must be distributed independently to the archive verifier. Each envelope contains entry, hash (SHA-256 of the exact serialized entry), and signature (Base64 Ed25519 signature of the 32-byte hash). Entries contain a sequence, previous hash, key ID, event/approval IDs, action, actor, timestamp, decision reason/authentication evidence, and hashes binding the encrypted local intent/result. Tool arguments, configuration secrets and access tokens are not exported; decision reasons should not contain secrets. The external receiver verifies the signature and chain, durably stores the exact JSON envelope, then returns 2xx with {"sequence":123,"hash":"matching-envelope-hash"}. A different/missing receipt is retried. Do not pretty-print or reserialize the signed entry.
mcphub verify-audit --file archive.jsonl --key "audit-2026-01=$AUDIT_PUBLIC_KEY"
# For rotated signing keys, repeat --key ID=BASE64_PUBLIC_KEY.
# For a partial file, pass the externally trusted preceding checkpoint:
mcphub verify-audit --file next.jsonl --key "audit-2026-01=$AUDIT_PUBLIC_KEY" --after-sequence 123 --after-hash "$TRUSTED_PREVIOUS_HASH"
The verifier rejects altered records, missing/reordered internal entries, wrong keys and broken links, and prints the final sequence/hash checkpoint. Preserve and compare checkpoints outside the MCPHub database to detect rollback/deleted tails; a valid prefix alone cannot prove completeness. Use a separately controlled append-only/WORM sink and protect the signing key. This does not defend against an operator controlling both MCPHub's signer and the archive, and it archives approval events rather than every general activity event.
GET /api/v1/approvals/delivery exposes enabled flags and pending/failed counts to configuration/security browser roles. The approval page and stderr logs flag delivery failures. Unacknowledged archive entries prevent retention from deleting their approval details; successful external archives outlive local retention. Monitor queue/database growth during outages. No external webhook, production OIDC provider or archive service is provisioned automatically.