Network Operations Guide
For: Network operations and platform teams configuring policies, managing credentials, and monitoring Axemere Gateway health.
Table of Contents
- Admin API Authentication
- Policy Configuration
- Proxy Mode Credential Setup
- Common Policy Patterns
- Credential Configuration
- Workload Management
- Connector Management
- Node Inventory
- Risk Scoring
- Approval Workflows
- Quarantine Management
- Rate Limiting
- Monitoring and Observability
- Execution Record Fields
- Transparent Proxy and Delegation Tokens
- SSL MITM Proxy
- Admin API Reference
Admin API Authentication
All admin endpoints require one of:
MVGC-Admin-Token: <token>headerAuthorization: Bearer <token>header
The token value is set via the MVGC_ADMIN_TOKEN environment variable on the gateway. In a
Docker Compose deployment it can also be found in docker-compose.yaml.
Policy Configuration
Policies are defined as YAML files under MVGC_POLICIES_DIR (default configs/policies/).
A policy bundle (mvp_policy.yaml) references individual rule files that are evaluated in a
defined layer order.
Evaluation Layers
Evaluation stops on the first deny or require_approval match by default (stop_on is
configurable per bundle). The default decision when no rule matches is deny.
Supported Operators
| Operator | Description |
|---|---|
equals | Exact string match |
in | Value is in a list |
not_in | Value is not in a list |
exists | Field is present and non-empty |
regex | Regular expression match |
prefix | String has given prefix |
suffix | String has given suffix |
lt, lte, gt, gte | Numeric comparisons |
Logical combinators: all (AND), any (OR). These can nest to arbitrary depth.
Decisions
| Decision | HTTP Status | Description |
|---|---|---|
allow | 200 | Permit the request |
deny | 403 | Block the request with a reason |
downgrade | 200 | Allow with mutations (e.g. swap to a cheaper model) |
require_approval | 202 | Hold for manual approval; returns approval_id |
require_attribution | 403 | Block the request; required attribution fields are missing (see require_attribution) |
rate_limit | 429 | Reject with Retry-After header |
quarantine | 403 | Block and quarantine for review due to risk signals |
Set
MVGC_APPROVAL_ENABLED=falseto revertrequire_approvalto the legacy 403 behavior.
require_attribution and stop_on: The require_attribution decision is included in the bundle stop_on list (alongside deny and require_approval), so evaluation halts immediately when a rule produces it. Use effect.decision: require_attribution in identity or delegation layer rules to block requests that are missing required attribution fields before further evaluation proceeds.
Hot-Loading a Policy Bundle
Push a bundle that references rule files already present on the gateway filesystem under
MVGC_POLICIES_DIR:
curl -X PUT http://localhost:7080/v1/admin/policies \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/yaml" \ --data-binary @configs/examples/mvp_policy.yaml
Push a fully self-contained bundle with rules embedded inline (no files need to exist on the
filesystem). inline_rules takes precedence over files when both are present:
curl -X PUT http://localhost:7080/v1/admin/policies \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/yaml" \ --data-binary @- << 'EOF' schema: mvgc.policy_bundle.v1 bundle_id: bundle-inline-001 version: 1.0.0 defaults: decision: deny evaluation: order: [identity, targets, credentials, budgets, risk] stop_on: [deny, require_approval] merge_strategy: first_match inline_rules: identity: - id: identity.allow.all.explicit priority: 100 when: field: context.connection_type equals: direct_api effect: decision: allow risk: - id: risk.require_approval.high_cost priority: 100 when: field: context.action.params.risk_tier equals: "high" effect: decision: require_approval reason: "risk tier requires manual approval" EOF
Both variants respond with {status, bundle_id, version} on success.
Reloading Policies from Disk (including add-ons)
If you add, remove, or enable a new policy add-on by placing or symlinking a file into
configs/policies/addons/ while the gateway is running, use the disk reload endpoint to
pick up the changes without restarting:
curl -s -X POST http://localhost:7080/v1/admin/policies/reload \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
Response:
{ "status": "ok", "bundle_id": "bundle-prod-v2", "version": "2.1.0", "source": "file" }
This re-reads bundle.yaml and all files in addons/ from MVGC_POLICIES_DIR. Any
previously API-pushed override (from PUT /v1/admin/policies) is cleared; the gateway
returns to reading from disk on every subsequent request evaluation. Returns 400 if the
gateway is running in distributed (CP remote) mode, where bundles are managed by the
control plane.
Add-on workflow: copy or symlink the add-on from
configs/policies/available/intoconfigs/policies/addons/, then call this endpoint. No restart needed.
Verifying the Active Policy Bundle
Use GET /v1/admin/policies to confirm which bundle is currently loaded; useful immediately after a reload to verify it took effect:
curl -s http://localhost:7080/v1/admin/policies \ -H "MVGC-Admin-Token: $ADMIN_TOKEN" | jq .
Response:
{ "bundle_id": "bundle-prod-v2", "version": "2.1.0", "source": "override", "loaded_at": "2026-03-15T14:22:10Z" }
| Field | Description |
|---|---|
bundle_id | The bundle_id from the active bundle YAML |
version | The version field from the active bundle |
source | "override": hot-loaded via PUT; "file": loaded from MVGC_POLICIES_DIR on disk (including after a POST /reload); "default": built-in deny-all fallback |
loaded_at | ISO-8601 timestamp when the override was activated; omitted for file and default sources |
See configs/policies/available/ for the add-on catalog and configs/policies/ for the per-layer rule files used with mvp_policy.yaml. Use POST /v1/admin/policies/reload to pick up new add-ons without restarting.
Proxy Mode Credential Setup
When proxy_enabled: true, the gateway acts as a forwarding proxy -- clients send HTTP
traffic to the gateway, which runs policy evaluation and injects credentials before
forwarding to the upstream provider. API keys are not injected automatically; you must
register credentials and load a policy bundle that explicitly selects them per target host.
The four required steps are described below. A ready-to-use example bundle is provided at
configs/examples/proxy-credentials.yaml.
Step 1 -- Register credentials
Register one credential per provider. The secret_ref names the environment variable that
holds the actual API key -- the key itself is never stored in the database.
# OpenAI curl -X PUT http://localhost:7080/v1/admin/credentials \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"credential_id":"cred-openai","provider":"openai","mode":"alias", "billing_owner":"platform","secret_ref":"OPENAI_API_KEY"}' # Anthropic curl -X PUT http://localhost:7080/v1/admin/credentials \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"credential_id":"cred-anthropic","provider":"anthropic","mode":"alias", "billing_owner":"platform","secret_ref":"ANTHROPIC_API_KEY"}'
Repeat for every provider you need. See Key Alias for the full field reference.
Step 2 -- Set the API key environment variables
Set the environment variable named in each secret_ref before (or after, via a restart)
starting the gateway. In mvgc.yaml there is no direct place for secrets; use environment
variables or a secrets manager:
export OPENAI_API_KEY=sk-... export ANTHROPIC_API_KEY=sk-ant-...
For Homebrew deployments, add these to your shell profile or the launchd environment before
running brew services start mvgc-gateway.
Step 3 -- Load a policy bundle with select_credential rules
The policy connectors layer is the right place to bind credentials to target hosts.
Load the example bundle from configs/examples/proxy-credentials.yaml, or push an inline
bundle directly:
curl -X PUT http://localhost:7080/v1/admin/policies \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/yaml" \ --data-binary @- << 'EOF' schema: mvgc.policy_bundle.v1 bundle_id: proxy-with-creds version: "1.0.0" layer_order: [connectors] inline_rules: connectors: - id: proxy.allow.openai priority: 100 when: field: action.target_host equals: api.openai.com effect: decision: allow select_credential: cred-openai - id: proxy.allow.anthropic priority: 100 when: field: action.target_host equals: api.anthropic.com effect: decision: allow select_credential: cred-anthropic - id: proxy.deny.unknown-host priority: 500 when: field: action.target_host exists: true effect: decision: deny reason: "no credential configured for this target host" EOF
The default decision is
deny, so any target host not matched by an explicit rule will be blocked. Add a rule for each provider you want to allow.
Step 4 -- Verify the setup
Confirm the bundle is loaded and credentials are registered:
# Active bundle curl -s http://localhost:7080/v1/admin/policies \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq . # Registered credentials (keys are never returned) curl -s http://localhost:7080/v1/admin/credentials \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
Step 5 -- Test a proxied request
Send a request through the proxy using the /proxy/{provider}/ path prefix to identify the
upstream provider. The gateway resolves openai to api.openai.com automatically:
curl http://localhost:7080/proxy/openai/v1/chat/completions \ -H "X-MVGC-Org-ID: my-org" \ -H "X-MVGC-Workload-ID: my-app" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4","messages":[{"role":"user","content":"hello"}]}'
The gateway evaluates policy, injects the OPENAI_API_KEY from the environment, and
forwards the request. The caller never sees the API key.
Supported path-prefix providers: openai, anthropic, gemini, cohere. For Azure OpenAI
or any target not in this list, pass the X-MVGC-Target-Host header explicitly:
curl http://localhost:7080/v1/chat/completions \ -H "X-MVGC-Org-ID: my-org" \ -H "X-MVGC-Workload-ID: my-app" \ -H "X-MVGC-Target-Host: my-resource.openai.azure.com" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4","messages":[{"role":"user","content":"hello"}]}'
Target host resolution order: (1) X-MVGC-Target-Host header, (2) request Host header,
(3) /proxy/{provider}/ path prefix, (4) 400 error if none of the above resolves.
Delegation tokens in proxy mode: In transparent proxy mode, the
X-MVGC-Delegation-IDheader carries only the token ID -- the signed payload is not transmitted. Delegation scope constraints are therefore not enforced. For full scope enforcement, use explicitPOST /v1/actions:executewith thedelegation_tokenfield.
Common Policy Patterns
Below are concrete YAML examples for frequent policy use cases. Each rule goes in the
appropriate layer file under MVGC_POLICIES_DIR or inline in a bundle via inline_rules.
Allow only specific models:
targets: - id: targets.deny.disallowed_models priority: 100 when: field: context.action.params.model not_in: ["gpt-4o-mini", "claude-3-haiku-20240307"] effect: decision: deny reason: "model not in approved list"
Rate-limit a workload to 10 requests per minute:
identity: - id: identity.rate_limit.wl_prod_app priority: 90 when: field: context.workload_id equals: "wl-prod-app-1" effect: decision: rate_limit reason: "workload rate limit: 10 req/min"
The token bucket refill rate is controlled by the policy decision;
the rate_limit decision triggers the gateway's built-in token bucket limiter keyed by
workload_id.
Require approval for high-cost requests:
budgets: - id: budgets.require_approval.high_cost priority: 80 when: field: context.action.params.model in: ["gpt-4o", "claude-3-5-sonnet-20241022"] effect: decision: require_approval reason: "high-cost model requires operator approval"
Deny requests outside business hours (combined with risk):
risk: - id: risk.deny.outside_hours priority: 70 when: field: context.risk.signals # time_of_day signal active means outside MVGC_RISK_BUSINESS_HOURS in: ["time_of_day"] effect: decision: deny reason: "requests not permitted outside business hours"
Downgrade model on budget pressure:
transforms: - id: transforms.downgrade.budget_pressure priority: 60 when: field: context.risk.signals in: ["cost_anomaly"] effect: decision: downgrade mutations: - key: action.params.model value: "gpt-4o-mini"
Credential Configuration
BYOK (Bring Your Own Key)
The caller supplies the API key in the request via credential_hint. The gateway passes it
through to the provider without storing it. No server-side configuration is required.
Use byok when callers bring their own provider accounts and the gateway should not manage keys.
Key Alias
The gateway manages API keys server-side. Credentials are registered with a
secret_ref pointing to an environment variable. The
caller references a credential_id and the gateway
resolves the secret at runtime.
Credential fields:
| Field | Required | Description |
|---|---|---|
credential_id | Yes | Unique identifier for the credential |
provider | Yes | Provider name (e.g. openai, anthropic, gemini) |
mode | Yes | "alias" for key alias mode |
billing_owner | Yes | "customer" or "platform" |
secret_ref | Yes | Environment variable name holding the API key |
org_id | No | Scopes the credential to a specific organization; when empty, available to all orgs |
connector_id | No | Binds this credential to a specific connector ID (e.g. "openai"); enforced at credential metadata level |
scopes | No | Human-readable scope list (e.g. ["chat", "embeddings"]) for auditing and future machine-policy use |
Register an alias credential:
curl -X PUT http://localhost:7080/v1/admin/credentials \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "credential_id": "cred-openai", "provider": "openai", "mode": "alias", "billing_owner": "customer", "secret_ref": "OPENAI_API_KEY", "org_id": "org-example-001", "connector_id": "openai", "scopes": ["chat", "embeddings"] }'
The org_id field scopes the credential to a specific organization for multi-tenant deployments.
When set, the credential is only resolvable by requests matching that org_id. When empty
(default), the credential is available to all organizations.
The value of OPENAI_API_KEY must be set in the gateway's environment. The calling
application never sees the API key -- it references only the credential ID.
List registered credentials:
curl -X GET http://localhost:7080/v1/admin/credentials \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
Delete a credential:
curl -X DELETE http://localhost:7080/v1/admin/credentials/cred-openai \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" # 204 No Content on success; 404 if credential not found
Reloading credentials from file
If you update your credentials.yaml on disk after the gateway is already running (e.g. to change
an inline key or switch modes), the file changes are not picked up automatically; the gateway
seeds from the file only on first boot when the database is empty.
Use the reload endpoint to apply your current file to the running gateway without a restart. This does a full upsert (overwriting any existing DB values) for every credential defined in the file:
curl -s -X POST http://localhost:7080/v1/admin/credentials/reload \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
Response:
{ "count": 5, "status": "ok", "upserted": ["cred-anthropic", "cred-azure-openai", "cred-cohere", "cred-gemini", "cred-openai"] }
The gateway reads all *.yaml files from MVGC_CREDENTIALS_DIR and upserts each credential. If
any file fails to parse or any upsert fails, the response is 400/500 with an error message and
the remaining credentials are not processed (fix the error and re-run).
See configs/credentials/credentials.yaml for examples. The seed directory is configurable via
MVGC_CREDENTIALS_DIR (default: configs/credentials; deb/rpm: /etc/mvgc/credentials).
Workload Management
Workloads represent logical applications or services that submit requests through the gateway.
Each workload carries default_attribution fields (used
when the caller omits them) and an allowed_connection_types list that controls which
connection types the workload may use.
Workload Fields
| Field | Required | Description |
|---|---|---|
workload_id | Yes | Unique identifier for the workload (see workload_id) |
org_id | Yes | The organization this workload belongs to |
name | No | Human-readable display name |
default_attribution | No | Fallback attribution fields when the caller omits them |
allowed_connection_types | No | List of permitted connection types (direct_api, sdk_redirect, connect_proxy); if omitted, all are allowed |
Creating or Updating a Workload
curl -X PUT http://localhost:7080/v1/admin/workloads \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "workload_id": "wl-prod-app-1", "org_id": "org-example-001", "name": "Production Application", "default_attribution": { "customer_id": "cust-default", "account_id": "acct-default" }, "allowed_connection_types": ["direct_api"] }'
allowed_connection_types accepts any combination of direct_api, sdk_redirect, and connect_proxy.
default_attribution fills in any attribution fields not supplied by the caller.
Listing Workloads
curl -s http://localhost:7080/v1/admin/workloads \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
Returns an array of all registered workloads with their workload_id, org_id, name,
default_attribution, and allowed_connection_types.
Deleting a Workload
curl -X DELETE http://localhost:7080/v1/admin/workloads/wl-prod-app-1 \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN"
Returns 204 No Content on success, 404 Not Found if the workload does not exist.
Alternatively, to soft-decommission without deleting, update allowed_connection_types to []
(empty list) to block all requests while retaining the workload record.
Connector Management
Connectors are stateless route handlers that map
action.target_host values to upstream AI provider
endpoints. The gateway ships with built-in connectors for OpenAI, Anthropic, Gemini,
Azure OpenAI, and a generic HTTP fallback.
The connectors policy layer is evaluated before targets. In the default add-on configuration, provider routing rules in addons/provider-*.yaml and addons/deny-unknown-host.yaml handle this layer. For custom connector-level rules (e.g., allowlisting by connector ID), add rules to the connectors layer via an add-on:
connectors: - id: connectors.deny.non_openai priority: 100 when: field: context.candidates.connectors not_in: ["openai", "anthropic"] effect: decision: deny reason: "connector not in approved list"
Interpreting Connector Health
GET /v1/admin/connectors returns a list of registered connectors with metadata and health
status:
curl -s http://localhost:7080/v1/admin/connectors \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
Example response:
[ {"connector_id": "openai", "provider": "openai", "target_host": "api.openai.com", "status": "ok"}, {"connector_id": "anthropic", "provider": "anthropic", "target_host": "api.anthropic.com", "status": "ok"}, {"connector_id": "gemini", "provider": "gemini", "target_host": "generativelanguage.googleapis.com", "status": "ok"}, {"connector_id": "azure_openai","provider": "azure", "target_host": "your-resource.openai.azure.com", "status": "ok"}, {"connector_id": "cohere", "provider": "cohere", "target_host": "api.cohere.com", "status": "ok"}, {"connector_id": "generic_http","provider": "generic", "target_host": "(any)", "status": "ok"} ]
SSE streaming: OpenAI, Anthropic, Gemini, Azure OpenAI, and Cohere connectors all support server-sent events (SSE) streaming when
stream: trueis set inaction.params.
| Status | Meaning | Action |
|---|---|---|
ok | Connector is registered and configuration looks correct | No action needed |
no_credential | No credential is configured or resolvable for this connector | Register a credential via PUT /v1/admin/credentials |
error | Last request to this connector returned an upstream error | Check provider status page; verify the API key is valid |
Connectors are stateless route handlers -- "health" reflects configuration state, not a live ping to the upstream provider.
Generic HTTP Connector Configuration
The generic HTTP connector (generic_http) supports header filtering via two complementary
lists:
| Config Field | Description |
|---|---|
AllowedHeaders | Positive allowlist of header names to forward upstream. When non-empty, only headers in this list are forwarded. Evaluated before DeniedHeaders. |
DeniedHeaders | Headers to strip before forwarding upstream (e.g. internal auth headers). Applied after AllowedHeaders filtering. |
When both are configured, AllowedHeaders is evaluated first (only listed headers pass
through), then DeniedHeaders removes any remaining unwanted headers. When AllowedHeaders
is empty, all headers are forwarded except those in DeniedHeaders.
Action Type Routing
The connector manager supports routing by action.type in addition to hostname-based routing.
Use RegisterActionType to map action type strings to connector IDs. This is step 2 in the
routing precedence defined by spec §4.1:
- Policy-selected --
select_connectoreffect from a matching policy rule action.type-- type-based routing via registered mappingstarget_host-- hostname match against built-in provider connectors- Fallback --
generic_httpconnector
Type mappings are registered programmatically in the gateway setup:
connectorManager.RegisterActionType("openai_chat", "openai") connectorManager.RegisterActionType("custom.embedding", "embedding_service")
When a request's action.type matches a registered mapping, the request routes to the
associated connector without requiring a hostname match. This enables custom action types
that don't correspond to a specific provider hostname.
Custom action type mappings are registered during gateway initialization. Contact support if you need to add a custom action type mapping for a non-standard provider.
Node Inventory
The GET /v1/admin/nodes endpoint returns the list of gateway nodes (currently single-node):
curl -s http://localhost:7080/v1/admin/nodes \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
[ { "node_id": "node-001", "registered_at": "2026-03-12T10:00:00Z", "mode": "embedded" } ]
| Field | Description |
|---|---|
node_id | Gateway node identifier (from MVGC_NODE_ID) |
registered_at | When the node started (server boot time) |
mode | "embedded" (standalone) or "distributed" (connected to CP via gRPC) |
Risk Scoring
The gateway computes a composite risk score (0.0 to 1.0) for each request based on four signals:
| Signal | Weight | Trigger |
|---|---|---|
rate_spike | 0.40 | Current request rate exceeds baseline by MVGC_RISK_RATE_THRESHOLD× |
cost_anomaly | 0.30 | Current cost rate exceeds baseline by MVGC_RISK_COST_THRESHOLD× |
target_diversity | 0.15 | Distinct target hosts in recent window exceeds baseline by MVGC_RISK_TARGET_DIVERSITY_THRESHOLD× |
time_of_day | 0.15 | Request is outside configured MVGC_RISK_BUSINESS_HOURS |
Baseline warm-up:
rate_spikeandcost_anomalyrequire at least 5 observations in the baseline window before they can fire. New workloads and fresh gateway restarts will not trigger these signals until enough history has accumulated. This prevents false positives during startup or low-traffic periods where any single request would otherwise appear anomalous against an empty baseline.
Risk context is available in DSL rules under the context.risk namespace:
| DSL Field | Description |
|---|---|
context.risk.score | Composite risk score (0.0-1.0) |
context.risk.request_rate | Requests per minute in the current window |
context.risk.cost_rate | Estimated cost per minute in the current window |
context.risk.signals | Array of active risk signal names |
Caller-supplied context is available under the context namespace:
| DSL Field | Description |
|---|---|
context.purpose | Caller intent string from ActionRequest.Context.Purpose |
context.labels.<key> | Value of a caller-supplied label (e.g. context.labels.env) |
Example rule using caller context:
identity: - id: identity.deny.no_purpose priority: 100 when: field: context.purpose operator: exists negate: true effect: decision: deny reason: "requests must declare a purpose"
Example rule that quarantines high-risk requests:
risk: - id: risk.quarantine.high_score priority: 100 when: field: context.risk.score gt: "0.8" effect: decision: quarantine reason: "composite risk score too high"
The MVGC_RISK_* environment variables are fully wired into the execution flow. Risk scores
are computed per-request and injected into the EvaluationContext before policy evaluation,
so context.risk.* DSL conditions work in all policy layers. Configure the risk thresholds
via environment variables -- see
Observability and Risk Variables.
Delegation depth limit: Delegation chains deeper than 3 levels are denied before policy
evaluation. The gateway returns a 403 with reason "delegation depth exceeds maximum (3)".
This prevents unbounded delegation chains and limits blast radius if a delegation token is
compromised.
Approval Workflows
When a policy rule returns require_approval, the gateway creates an approval request and
returns HTTP 202 with an approval_id. The request is held until an operator approves or
denies it. Approved requests are cached so subsequent identical requests execute normally.
Full lifecycle example:
Step 1 -- Submit a request that triggers require_approval:
curl -s -X POST http://localhost:7080/v1/actions:execute \ -H "Content-Type: application/json" \ -d '{ "schema": "mvgc.action_request.v2", "request_id": "req-needs-approval", "org_id": "org-example-001", "workload_id": "wl-prod-app-1", "action": { "type": "ai.infer", "method": "POST", "target_host": "api.openai.com", "params": { "model": "gpt-4o", "risk_tier": "high" } }, "attribution": { "project_id": "proj-123" } }' | jq . # HTTP 202 -- response includes approval_id # { "decision": "require_approval", "approval_id": "a1b2c3d4-...", ... }
Step 2 -- List pending approvals:
curl -s "http://localhost:7080/v1/admin/approvals?org_id=org-example-001&status=pending" \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
Step 3 -- Approve the request:
APPROVAL_ID="a1b2c3d4-..." # from Step 1 response curl -s -X POST "http://localhost:7080/v1/admin/approvals/$APPROVAL_ID/approve" \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"decided_by": "ops-admin"}' | jq .
Step 4 -- Re-submit the original request (now executes normally):
# Same body as Step 1 -- gateway finds the pre-approval and allows execution curl -s -X POST http://localhost:7080/v1/actions:execute \ -H "Content-Type: application/json" \ -d '{ ... same body as Step 1 ... }' | jq . # HTTP 200 -- { "decision": "allow", ... }
Deny instead of approve:
curl -s -X POST "http://localhost:7080/v1/admin/approvals/$APPROVAL_ID/deny" \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"decided_by": "ops-admin"}' | jq .
Quarantine Management
A quarantine policy decision returns HTTP 403 and records the entry server-side. Use the
admin endpoints to review and release quarantined requests.
# List quarantined requests for an org curl -s "http://localhost:7080/v1/admin/quarantine?org_id=org-example-001&limit=20" \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq . # Release a quarantined entry QUARANTINE_ID="q9z8y7x6-..." curl -s -X POST "http://localhost:7080/v1/admin/quarantine/$QUARANTINE_ID/release" \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"released_by": "ops-admin"}' | jq .
The quarantine_id is not included in the execute response (the entry is written server-side);
use the list endpoint to find and review quarantine entries.
Rate Limiting
When a request is rate-limited by policy, the gateway returns HTTP 429 with a Retry-After
header indicating how many seconds to wait:
curl -v -X POST http://localhost:7080/v1/actions:execute \ -H "Content-Type: application/json" \ -d '{ ... }' 2>&1 | grep -E "< HTTP|Retry-After|decision" # < HTTP/1.1 429 Too Many Requests # < Retry-After: 60 # "decision": "rate_limit"
Rate limits are defined in policy DSL rules using the rate_limit decision. The token bucket
implementation refills based on elapsed time and is keyed per workload.
Runtime Rate Limit Configuration
The rate limiter's token bucket parameters can be inspected and adjusted at runtime without restarting the gateway.
Get current configuration:
curl -s http://localhost:7080/v1/admin/ratelimit/config \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
{"refill_rate": 10, "max_tokens": 10, "window_duration": 60000000000}
Update configuration:
curl -s -X PUT http://localhost:7080/v1/admin/ratelimit/config \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"refill_rate": 20, "max_tokens": 50, "window_duration": 120000000000}' | jq .
Both refill_rate and max_tokens must be greater than 0. window_duration is in
nanoseconds (e.g. 60000000000 = 1 minute).
List active bucket keys:
curl -s http://localhost:7080/v1/admin/ratelimit/keys \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq .
Returns an array of workload IDs that currently have tracked rate limit state:
["wl-prod-app-1", "wl-staging-2"]
Monitoring and Observability
Gateway Health (GET /healthz)
The GET /healthz endpoint returns enriched status information (no admin token required):
curl -s http://localhost:7080/healthz | jq .
Example response:
{ "status": "ok", "node_id": "node-001", "connectors": [ {"id": "openai", "version": "1.0.0", "healthy": true}, {"id": "anthropic", "version": "1.0.0", "healthy": true} ], "cp_status": "connected", "bundle_id": "bundle-prod-001", "bundle_version": "2.1.0", "last_hash_submit_at": "2026-03-12T14:30:00Z" }
| Field | Description |
|---|---|
status | "starting" during the startup grace period (MVGC_HEALTHZ_STARTUP_GRACE, default 10s), then "ok" once the gateway is fully initialized |
node_id | This gateway's node identifier |
connectors | Array of registered connectors with id, version, and healthy boolean |
cp_status | Control plane connection: "connected", "offline", or "unconfigured" |
bundle_id | Active policy bundle ID (omitted when no bundle loaded) |
bundle_version | Active policy bundle version (omitted when no bundle loaded) |
last_hash_submit_at | RFC3339 timestamp of the last successful hash submission to the CP ledger (omitted when not applicable) |
Use cp_status to detect connectivity issues with the control plane in distributed mode.
Use last_hash_submit_at to detect stale hash submission pipelines.
Prometheus Metrics
Prometheus metrics are available at GET /metrics when MVGC_METRICS_ENABLED=true (default).
curl -s http://localhost:7080/metrics | grep mvgc_
Example output:
mvgc_requests_total{action_type="ai.infer",decision="allow",org_id="org-example-001"} 42
mvgc_request_duration_ms_bucket{action_type="ai.infer",decision="allow",le="100"} 38
mvgc_risk_score{org_id="org-example-001"} 0.32
mvgc_approvals_pending 3
mvgc_rate_limit_hits{org_id="org-example-001"} 1
mvgc_quarantine_total{org_id="org-example-001"} 0
mvgc_auth_failures_total{reason="invalid_key",org_id=""} 2
Metrics Reference
| Metric | Type | Labels | Description |
|---|---|---|---|
mvgc_auth_failures_total | Counter | reason, org_id | Authentication failures before policy evaluation (missing_header, invalid_key, revoked, expired, suspended) |
mvgc_auth_key_lookup_total | Counter | result | API key cache lookup results (hit, miss, fallback) |
mvgc_requests_total | Counter | action_type, decision, org_id | Total policy decisions by action type, decision, and org |
mvgc_request_duration_ms | Histogram | action_type, decision | End-to-end request latency in milliseconds (buckets: 5, 10, 25, 50, 100, 250, 500, 1000) |
mvgc_upstream_latency_seconds | Histogram | provider | Time spent waiting for the upstream provider response |
mvgc_risk_score | Gauge | org_id | Last computed composite risk score per org (0.0--1.0) |
mvgc_risk_signals | Counter | signal_type, org_id | Risk signals detected per type per org |
mvgc_approvals_pending | Gauge | (none) | Current number of open require_approval decisions awaiting a decision |
mvgc_rate_limit_hits | Counter | org_id | Requests rejected by the rate limiter per org |
mvgc_quarantine_total | Counter | org_id | Workloads quarantined per org |
mvgc_policy_deny_total | Counter | org_id, reason_code | Policy deny decisions per org and reason code |
mvgc_org_overage_enforced_total | Counter | org_id | Requests denied due to monthly overage per org |
mvgc_pending_hash_submissions | Gauge | (none) | Hash submissions pending delivery to the control plane |
mvgc_config_sync_lag_seconds | Histogram | (none) | Lag between config event timestamp and local processing |
Dashboard API
The dashboard endpoints provide aggregated summaries without requiring a separate analytics stack. Both require the admin token.
# Summary: total requests, allow/deny counts for an org over a time window curl -s "http://localhost:7080/v1/dashboard/summary?org_id=org-example-001&from=2026-03-03T00:00:00Z&to=2026-03-10T00:00:00Z" \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq . # Decision breakdown: counts grouped by decision type curl -s "http://localhost:7080/v1/dashboard/decisions?org_id=org-example-001" \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" | jq . # [{"decision":"allow","count":38},{"decision":"deny","count":4},{"decision":"rate_limit","count":1}]
GCP Cloud Monitoring (Infrastructure Alerts)
For GCP deployments, infrastructure-level alerts are provisioned at the infrastructure level and managed independently of the gateway application. These complement the Prometheus and SIEM layers above.
What fires an alert:
| Alert | Condition | Channel |
|---|---|---|
| Cloud SQL memory | > 80% for 5 min | Email + Google Chat (when configured) |
| Cloud SQL connections | > 20 active for 5 min | Email + Google Chat |
| Cloud SQL disk | > 80% for 5 min | Email + Google Chat |
| Cloud SQL CPU | > 80% for 10 min | Email + Google Chat |
| CP gRPC unreachable | TCP check failing from 2+ regions | Email + Google Chat |
| Telemetry HTTPS down | /healthz failing from 2+ regions | Email + Google Chat |
| LB 5xx error rate | > 5 errors/min for 5 min | Email + Google Chat |
| LB p99 latency | > 2000ms for 5 min | Email + Google Chat |
Google Chat webhook integration is configured at the infrastructure level. Contact your platform team to add or update webhook URLs.
Dashboard: GCP Console → Monitoring → Dashboards → Axemere Infrastructure shows Cloud SQL utilization, uptime check pass rates, LB request count and latency percentiles, and pod restart counts in a single view.
Responding to alerts: Each alert policy includes a documentation block with a runbook link. The most common responses:
- Cloud SQL connections alert: check for connection pool leaks; add PgBouncer or upgrade the instance tier
- Uptime check alert: check gateway health via
/healthz; contact support if the gateway is unreachable - LB 5xx rate alert: correlate with Cloud SQL memory/connection alerts; often indicates DB unreachable
- OOMKill alert: upgrade the gateway host instance tier; check for unbounded queries
SIEM Export
The gateway can stream events to external security systems. Configure one or both:
- Webhook: set
MVGC_EXPORT_WEBHOOK_URL(and optionallyMVGC_EXPORT_WEBHOOK_TOKEN). Events are POST'd as JSON with retry logic (3 attempts, exponential backoff). - Syslog: set
MVGC_EXPORT_SYSLOG_ADDRtotcp://host:portorudp://host:port.
Both can be configured simultaneously; events fan out to all configured exporters.
Exported event types: execution, approval, quarantine, risk_alert.
The risk_alert event is emitted whenever a request's composite risk score reaches 0.7 or
higher. The event payload includes the risk score, active signals, and the request context
(org, workload, caller). Use these events to trigger SIEM alerts for anomalous activity
patterns without waiting for a quarantine policy decision.
Each execution event payload includes the full ExecutionRecord, which contains
caller_ip, the source IP of the request as observed
at the gateway. Use this field in your SIEM to correlate requests with network telemetry
and identify anomalous source IPs.
Execution Record Fields
Every request processed by the gateway produces an execution record. This includes
denied requests: every deny, rate_limit, and quarantine decision is recorded
with full context (caller IP, workload, target host, reason). To identify services that
need new routing rules, filter execution records by decision = 'deny' and inspect the
action_type and target host fields. Key fields relevant to security investigation:
| Field | Description |
|---|---|
record_id | Unique UUIDv7 identifier for the record |
org_id | Organization that owns the request |
workload_id | Workload that submitted the request |
caller_id | Caller identifier within the workload (application-supplied) |
caller_ip | Source IP address, extracted server-side from the HTTP connection |
connection_type | How the request reached the gateway: direct_api, sdk_redirect, or connect_proxy; indexed for efficient filtering |
action_type | Action type (e.g. ai.infer) |
decision | Policy decision: allow, deny, downgrade, require_approval, require_attribution, rate_limit, quarantine |
event_time | RFC3339 UTC timestamp of the request |
record_hash | JCS SHA-256 hash of the canonical record, use with GET /v1/verify/{record_hash} |
connector_version | Version of the connector that executed the request |
idempotency_key | Client-supplied idempotency key (if provided); same key returns cached result |
tokens_in | Input token count (separate from aggregate model_tokens_used) |
tokens_out | Output token count (separate from aggregate model_tokens_used) |
The full record JSON (including the Ed25519 signature envelope) is stored in record_json.
Ledger attribution context:
When the gateway submits record hashes to the control plane ledger (via SubmitRecordHashRequest),
the request now carries full attribution context:
| Field | Description |
|---|---|
credential_id | The credential used for this request (key alias credential ID) |
project_id | Project identifier from the request attribution |
account_id | Account identifier from the request attribution |
These fields flow into ledger entries, enabling audit queries by credential, project, or account in CP-connected mode.
Filtering records by source IP or connection type:
Use the Reports API or the console (Govern → Records) to filter execution records by
caller_ip, connection_type, traffic_class, or any other field. See the
Admin API Reference for query parameters.
Proxy Mode and Delegation Tokens
System proxy mode (connection_type: connect_proxy) and SDK redirect mode (connection_type: sdk_redirect) support passing a delegation
token reference via the X-MVGC-Delegation-ID header. However, the full signed delegation
token bytes cannot be transmitted in proxy mode -- the header carries only the token ID, not
the signed payload.
Consequence: Delegation token scope constraints (actions_allow, targets_allow, budget
limits) are not enforced for transparent proxy requests. The delegation_id is recorded
in the execution record for audit purposes, but the gateway does not verify the token signature
or check scope compliance.
For full delegation token enforcement -- including scope validation, budget constraints, and
signature verification -- use explicit ActionRequest mode (POST /v1/actions:execute) with
the delegation_token field.
SSL MITM Proxy
When MVGC_PROXY_MITM_ENABLED=true, the gateway intercepts HTTPS CONNECT tunnels to
managed domains: it terminates TLS using a dynamically generated leaf certificate signed
by the gateway's root CA, runs the decrypted request through the full policy pipeline
(credential injection, budget enforcement, record writing), then re-encrypts the
connection to the upstream provider. Traffic to non-managed domains passes through as
an opaque TCP tunnel.
Environment variables
| Variable | Required | Description |
|---|---|---|
MVGC_PROXY_MITM_ENABLED | yes | Set to true to activate MITM mode. Default: false. |
MVGC_MANAGED_DOMAINS | yes | Comma-separated hostnames or *. wildcard prefixes to intercept, e.g. api.openai.com,*.anthropic.com. |
MVGC_PROXY_CA_KEY | no | Absolute path to the root CA private key PEM. Default: <MVGC_KEY_DIR>/proxy-ca.key. |
MVGC_PROXY_CA_CERT | no | Absolute path to the root CA certificate PEM. Default: <MVGC_KEY_DIR>/proxy-ca.crt. |
MVGC_PROXY_CA_KEY_TYPE | no | Key algorithm: ecdsa (P-256, default) or rsa (2048-bit). |
MVGC_PROXY_LEAF_CERT_TTL | no | Leaf certificate cache TTL, e.g. 24h. Default: 24h. |
Example mvgc.yaml stanza:
proxy_enabled: true mitm_enabled: true managed_domains: - api.openai.com - "*.anthropic.com" - generativelanguage.googleapis.com
The root CA key and certificate are written to MVGC_KEY_DIR on first start (mode 0600
for the key, 0644 for the cert) and reloaded on subsequent starts. Rotating the root CA
requires re-distributing the new certificate to all client trust stores.
Distributing the CA certificate
Clients must trust the gateway's root CA before HTTPS traffic will succeed through MITM mode. Retrieve the certificate from the gateway:
curl http://localhost:7080/v1/proxy/ca.crt > mvgc-proxy-ca.crt
This endpoint returns 404 when MVGC_PROXY_MITM_ENABLED is false.
Trust store installation by platform:
# macOS sudo security add-trusted-cert -d -r trustRoot \ -k /Library/Keychains/System.keychain mvgc-proxy-ca.crt # Ubuntu / Debian sudo cp mvgc-proxy-ca.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates # RHEL / CentOS / Fedora sudo cp mvgc-proxy-ca.crt /etc/pki/ca-trust/source/anchors/ sudo update-ca-trust
For tooling that manages its own trust bundle:
# curl (per-invocation) curl --cacert mvgc-proxy-ca.crt --proxy http://localhost:7080 https://api.openai.com/... # Python requests (per-process) export REQUESTS_CA_BUNDLE=/path/to/mvgc-proxy-ca.crt export SSL_CERT_FILE=/path/to/mvgc-proxy-ca.crt
Per-workload domain scope
The MVGC_MANAGED_DOMAINS environment variable sets the gateway-level default. Individual
policy bundles can narrow or extend this list using the proxy: bundle key:
proxy: managed_domains: - api.openai.com - "*.anthropic.com" bypass_domains: - telemetry.anthropic.com # always pass through
bypass_domains is evaluated first; a domain matching both lists is treated as bypassed.
See proxy-mitm.yaml in the
configuration reference for a complete bundle example.
Admin API Reference
The full endpoint listing -- credentials, workloads, policies, connectors, approvals, quarantine, nodes, rate limiting, metrics, and verification -- is in the Admin API Reference.
All endpoints require the MVGC-Admin-Token or Authorization: Bearer header (set via
MVGC_ADMIN_TOKEN). Quick reference for the most common operations:
| Method | Endpoint | Use |
|---|---|---|
GET | /v1/admin/policies | Check active bundle |
PUT | /v1/admin/policies | Hot-load a bundle |
POST | /v1/admin/policies/reload | Reload from disk (picks up add-ons) |
PUT | /v1/admin/credentials | Register a credential |
PUT | /v1/admin/workloads | Register a workload |
GET | /v1/admin/approvals | List pending approvals |
GET | /v1/admin/quarantine | List quarantine entries |
GET | /metrics | Prometheus metrics |
GET | /healthz | Gateway health (no token required) |