Shared deployment checklist
One page for the operator standing up mcp-devtools as a shared service:
several people or agents, one gateway, vendor credentials the callers do not
hold themselves. Every row is a control the threat model or the OWASP MCP
Top 10 assessment relies on, with the setting or Kubernetes object that turns
it on, what it defaults to, what a shared install needs, and the test or
procedure that verifies it. Each row points at the runbook that explains it
rather than repeating it.
The workstation posture (one operator, MCP_AUTH_MODE=off, loopback) needs
none of this: the loopback bind is the trust boundary there, and the binary
refuses to leave it without inbound authentication.
| Control | Setting or object | Default | Required for shared | Verified by |
|---|---|---|---|---|
| Inbound authentication | MCP_AUTH_MODE, MCP_OIDC_PROFILE, MCP_OIDC_ISSUER, MCP_OIDC_AUDIENCE, MCP_PUBLIC_URL (identity provider runbook) |
off; a non-loopback bind is refused |
oidc (or okta) with the issuer, audience and public URL set. Every caller presents a validated bearer token; the principal in the journal is a token claim, never a client-reported name |
auth_mode_tests, token_validator_tests::the_negative_matrix, startup gate validate_startup_security (src/server/http.rs) |
| Policy and default deny | MCP_POLICY_FILE, MCP_POLICY_PUBLIC_KEY, MCP_REVOCATION_FILE, MCP_AUTHORIZATION_GENERATION_FILE (revocation runbook, admin API runbook) |
none; required once auth is on | A signed document whose rules name exactly the reads each group needs and the writes it may make (request_risk: write is never implied). Unclassified calls are always denied. Start from deploy/policies/*.yaml |
policy_signing_tests, policy_engine_tests, authorization_generation_tests, prompt_injection_tests (a denied follow-up call sends nothing) |
| Credential selection per principal | MCP_CREDENTIAL_SELECTION (per-user credential selection) |
unset: every caller acts with the shared credential | user_then_shared or user_required for vendors where the caller’s own identity must act upstream; otherwise accept that the journal, not the vendor, attributes the call |
credential_selection_tests, credential_cache_partition_tests |
| Outbound network posture | Helm networkPolicy.enabled and networkPolicy.egress.allow[].cidr (chart README) |
false: a non-enforcing CNI silently ignores the object, so it is opt-in |
true on a CNI that enforces NetworkPolicy, with cidr narrowed from 0.0.0.0/0 to the identity provider, the SIEM and the vendor hosts. This is the only control on where git and the control-plane fetches may connect; the binary does not range-check destinations |
operations_tests::helm_network_policy_allowlists_egress_and_blocks_the_metadata_endpoint; the chart’s NOTES.txt warns at install when auth is on and the policy is off |
| Inbound WAF | Helm waf.enabled; the proxy service in deploy/compose; deploy/k8s/waf.yaml (WAF rules) |
on in every shipped deployment path; the .deb/.rpm packages and the bare image ship no proxy |
Left on, with the server reachable only through it: Helm networkPolicy.enabled=true admits only the WAF pods to the roles. Tune a false positive with a route- and argument-scoped exemption, never by disabling a rule globally. Running the packages or the bare image as a shared service needs an equivalent edge WAF |
scripts/ci/waf-smoke.sh, scripts/ci/compose-smoke.sh (CI image job); operations_tests::every_shipped_proxy_runs_the_waf, ::every_deployment_path_pins_the_same_waf_image |
| Journal checkpoints leave the host | MCP_AUDIT_JOURNAL_DIR, MCP_AUDIT_SIGNING_KEY, MCP_AUDIT_CHECKPOINT_RECORDS, MCP_AUDIT_CHECKPOINT_SECONDS, MCP_AUDIT_CHECKPOINT_EXPORT_DIR (threat model §6, release operations runbook) |
journal required once auth is on; export directory unset | An export directory shipped off the gateway host on a schedule, and a copy of the audit public key kept where the host cannot alter it. Evidence that stays on a compromised host proves nothing against that host | audit_checkpoint_tests::the_verifier_catches_edits_forgeries_wrong_keys_and_a_truncated_tail; mcp-devtools audit verify --checkpoints <dir> against the off-host copy |
| SIEM forwarding | MCP_AUDIT_FORWARD_URL and its TLS and token settings, on the control (or all) role (SIEM forwarding runbook) |
unset | A receiver the gateway operator does not administer, over TLS with the CA pinned, so a second copy of every intent and outcome exists in real time | audit_forwarding_tests; CI siem job against a live sink |
| Clone availability | Transport-specific inventory | bb_clone is stdio-only and unavailable over HTTP |
No Git binary, SSH credentials or clone volume is needed for HTTP. Direct CLI cloning remains a local operator command | http_transport_tests::modern_tools_list_is_stateless_deterministic_and_cacheable, ::http_refuses_bb_clone_before_creating_a_destination; tool_surface_parity_tests::stdio_advertises_bb_clone_when_bitbucket_is_enabled |
| Per-principal rate limit | MCP_RATE_LIMIT_PER_PRINCIPAL, MCP_RATE_LIMIT_BURST (configuration) |
0: off |
A sustained rate and burst sized to the agents’ real traffic. The bucket is per process (CF-16), so it bounds one replica, not the fleet | usage_metrics_tests::a_principal_over_its_rate_limit_gets_429_with_retry_after_and_the_metric_counts_it, ::rollup_and_rate_limit_misconfiguration_refuses_startup |
| Control plane stays private | MCP_BIND_ADDR and MCP_AUTH_MODE on the control role; Helm ingress routes /admin/ and /console to the control service (admin API runbook) |
deploy/k8s/control.yaml binds 127.0.0.1 with auth off: health only, admin disabled. The chart’s control role uses the same OIDC boundary as the gateway |
Either keep the raw manifest’s loopback bind (no admin) or enable OIDC on the control role and expose /admin/ only through the ingress rules the chart renders. Never a non-loopback bind with auth off, which the binary refuses |
operations_tests::helm_routes_console_and_custom_callback_to_control, ::kubernetes_waf_routes_administration_and_browser_to_control; startup_security_tests::non_loopback_bind_without_auth_is_refused_with_a_clear_reason |
What this page does not cover: vendor-side permissions (the credential should hold the least the policy ever grants), identity-provider hygiene, and the agent runtime’s own handling of retrieved content. Those belong to the vendor owner, the identity team and the client owner respectively; the OWASP assessment says which is which. For an AI governance audit, the governance framework map maps these controls to the NIST AI RMF, ISO/IEC 42001 and the EU AI Act.
Operator roles
Section titled “Operator roles”The controls above assume named people behind them. Assign each role to a person and a deputy before the service is shared, and record the names where the on-call rota lives; the incident response runbook refers to these roles by name.
| Role | Does | Holds | What the gateway enforces | Keep apart from |
|---|---|---|---|---|
| Caller (person or agent) | Calls tools and, where the inference path is on, models | A token with mcp:tools (MCP_REQUIRED_SCOPE) or mcp:inference, and the groups policy rules name |
mcp:tools reaches no administrative route; policy decides every call and model request |
Every administrative role, on the same token. An agent’s token never carries mcp:admin |
| Policy author | Writes rules (console editor, policy explain, drafts from activity) and submits a signed generation as a proposal |
mcp:admin |
The submission is verified and held as an admin_proposal; nothing changes until a second administrator approves (MCP_ADMIN_APPROVALS=required) |
Policy approver for the same change |
| Policy key holder | Signs policies and revocation lists, seals generations, runs the offline revoke path |
The policy private key, kept off the gateways; the gateways hold only MCP_POLICY_PUBLIC_KEY |
A document, list or manifest with a signature the public key does not verify is refused. A signed generation installed as files takes effect without approval: that is the emergency path, and the journal shows it as a generation change with no admin_mutation before it |
Journal custodian, who reviews those direct installs |
| Policy approver | Reviews a pending generation’s digest and approves or rejects it | mcp:admin, as a person |
Approver’s subject must differ from the proposer’s, in the same tenant, with a token newer than MCP_WRITE_MAX_TOKEN_AGE_SECONDS; a principal marked as a machine is refused (MCP_ADMIN_PRINCIPALS=human-or-unknown) |
Policy author for the same change |
| Call approver | Approves or rejects held tool calls, held prompts and budget holds | mcp:admin, as a person |
Same separation and freshness rules, per held call (MCP_CALL_APPROVALS=required); the approval is bound to the exact call’s hold key and spent once |
The caller whose call is held |
| Revoker | Stops a subject, a token or everyone; closes a session; purges an artifact | The policy key for revoke (as key holder); mcp:admin for admin sessions remove and admin artifacts remove |
Revocation is a signed generation and takes effect within one poll on /mcp and /v1/*; session close and artifact purge are one-person by design and journaled |
Nothing: speed matters here. The journal makes every act reviewable |
| Journal custodian | Keeps the off-host checkpoint copy and the audit public key, runs audit verify and the exports, owns the SIEM relationship |
Read access to the journal and its off-host copies; the audit public key | Checkpoints are signed with MCP_AUDIT_SIGNING_KEY on the gateway; the verifier detects edits, truncation and missing checkpoints against a copy the gateway cannot change |
Whoever administers the gateway host, so evidence survives a compromised host |
| Secret owner | Issues, scopes and rotates the vendor and model-provider credentials the gateway acts with | Write access to the secret source (vault://, awssm://, azkv://, file://) |
Credentials never pass through the admin API (CF-36); a new version is read at the next refresh and its version is recorded on each call | Callers, who never hold the shared credentials |
What the gateway separates is narrower than this table: callers from
administrators (scope), the key holder from everyone else (possession of
the private key), and one approver from the proposer (subject). Policy
author, policy approver, call approver and the online revoker all hold
mcp:admin, and the gateway does not tell them apart: any administrator
may approve any proposal in the tenant except their own. Keeping those
roles with different people is the organization’s assignment, made where
the identity provider grants mcp:admin. The identity provider must also
issue one subject per person, since two identities of one person pass the
subject check. admin_api_tests::auth_role_and_json_contracts locks the
scope boundary, unified_call_approvals_tests::self_approval_other_tenants_rejection_and_stale_tokens_are_refused
the approval separation, and
admin_principal_kind_tests::default_policy_refuses_machines_at_admin_and_nowhere_else
the refusal of machine administrators.
The cloud templates
Section titled “The cloud templates”deploy/aws/fargate.yaml (ECS on Fargate) and deploy/azure/main.bicep
(Container Apps) ship the rows above as fixed values; where a row is a
parameter, this is its name. The cloud deployment
plan records what each platform cannot express
and the cloud marketplace runbook covers
listing them. operations_tests::aws_fargate_template_preserves_the_deployment_constraints
and ::azure_container_apps_template_preserves_the_deployment_constraints
parse both offline on every PR; deploy/<cloud>/smoke.sh is the live proof.
| Control | AWS (deploy/aws) |
Azure (deploy/azure) |
|---|---|---|
| Inbound authentication | OidcProfile, OidcIssuer, OidcAudience, PublicUrl; MCP_AUTH_MODE=oidc fixed |
oidcProfile, oidcIssuer, oidcAudience, publicUrl; MCP_AUTH_MODE=oidc fixed |
| Policy and default deny | PolicyPublicKey; the signed documents under s3://<ConfigBucket>/policies/, copied by the init container |
policyPublicKey; the signed documents on the policies share, mounted read-only |
| Credential selection per principal | mcp.env in the bucket (MCP_CREDENTIAL_SELECTION) |
vendorEnvironment |
| Outbound network posture | TaskSecurityGroup: TCP 443, NFS, the SIEM port; no all-traffic rule. CreateVpcEndpoints keeps Secrets Manager, Logs and ECR inside the VPC |
The environment NSG: 169.254.169.254/32 denied first, then DNS, Storage, HTTPS and the SIEM port; everything else denied |
| Inbound WAF | The proxy sidecar is the only load balancer target; ProxyImage is the shared pin |
The proxy sidecar is the ingress target; proxyImage is the shared pin. The Application Gateway’s WAF observes in detection mode in front |
| Journal checkpoints leave the host | MCP_AUDIT_CHECKPOINT_EXPORT_DIR=/checkpoints on EFS; CheckpointShipping copies to CheckpointBucket (Object Lock, CheckpointLockMode, CheckpointRetentionDays) on CheckpointSyncSchedule |
/checkpoints on NFS; the checkpoint-sync job copies to the checkpoints container (immutability policy, checkpointRetentionDays, lockCheckpointPolicy) on checkpointSyncCron |
| SIEM forwarding | SiemForwardUrl, SiemEgressPort; CA and client certificate under policies/, named in mcp.env |
siemForwardUrl, siemEgressPort; CA and client certificate on the policies share, named in vendorEnvironment |
| Clone root | Historical /tmp/clones setting; unused by HTTP |
Historical /tmp/clones setting; unused by HTTP |
| Per-principal rate limit | mcp.env |
vendorEnvironment |
| Control plane stays private | --role all behind the WAF; /admin/ and /console are reached only through the load balancer under OIDC |
--role all behind the WAF; the environment is internal and the Application Gateway is the only way in |
| One writer per journal | DesiredCount: 1, stop-then-start deployment |
minReplicas: 1, maxReplicas: 1, single revision mode, dedicated profile |
| Secrets never in the template | VendorSecretArns names what the task role may read; AuditSigningKeySecretArn is injected by ECS into the init container only |
keyVaultName with Key Vault Secrets User for the identity; auditSigningKeySecretName by Key Vault reference; the SMB key read by listKeys() at deployment |