Configuration reference
Defaults below come from config.Default(); a mode changes some of them (see Modes). YAML is loaded over defaults with strict known-field checking; unknown keys fail. HTTP, authentication and approval settings are included below. Empty slices are shown as [] (some are nil in Go).
Modes and opsgate init
mode names where opsgate runs as a service and fills in defaults for it.
Values written in the file always win. Without a mode the defaults below apply
and the transport defaults to stdio.
| Key | mode: kubernetes (pod) |
|---|---|
-transport default |
http |
http.listen |
0.0.0.0:8787 |
auth.mode |
builtin |
auth.builtin.upstream.provider |
passkey |
auth.builtin.state_file |
/var/lib/opsgate/oauth.json |
approval.web |
true while auth.mode is builtin |
server.audit_log |
"" (stderr, for the cluster's log collection) |
providers.kubernetes.enabled |
true, with the pod's ServiceAccount (kubeconfig: "") |
The mode does not fill in namespaces, users or write targets.
opsgate init asks for namespaces, users, the public URL and sign-in (passkey,
Google or GitHub), and writes a short mode: kubernetes file with mode 0600.
Write tools stay off until targets are listed in configuration. It also asks whether
to grant the user ops:write. It refuses
to overwrite a file without -force. Every question has a flag (opsgate init -h); without a terminal, or with -yes, unanswered questions take their
defaults, and the public URL and user must be given as flags. The result is
checked as the HTTP server would check it before it is written. When Kubernetes
tools are enabled, opsgate init defaults the namespace deny list to
[kube-system, kube-public, kube-node-lease, opsgate]; -deny-namespaces overrides it.
It also asks which workloads you may restart or roll back from your phone
(-recover, comma-separated namespace/name globs; empty by default). Answering
grants ops:write, lists them in rollout_restart and restore_image, sets
action_approval_url, enables approval.push (-language en or ko) and
approval.inbox_notify with the key file /etc/opsgate/inbox/key, and writes a
matching watch configuration to -watch-o (default opsgate-watch.yaml) that
sends warnings to the inbox through http://opsgate.opsgate.svc:8787. Both
files are checked before either is written, and the command prints the
remaining deployment steps.
opsgate config show -config <file> prints the effective configuration,
mode defaults included, as YAML that opsgate loads unchanged. With web approval enabled,
this prints require_web: true explicitly. Running that output with
-transport stdio denies writes; remove the key or set it to false to allow elicitation.
| YAML key | Default | Meaning |
|---|---|---|
mode |
"" |
Empty or kubernetes; see above |
server.name |
opsgate |
MCP server name |
server.audit_log |
"" |
Empty writes to stderr; otherwise JSONL path |
server.max_output_bytes |
65536 |
Tool output byte limit; must be positive |
server.tool_timeout |
30s |
Cooperative call timeout; separate planning/applying deadlines for writes; positive |
stdio.user |
local |
Audit identity label; OS user is the trust boundary |
stdio.scopes |
[ops:read] |
Accepted scopes: ops:read, ops:write; write does not enable actions |
providers.kubernetes.enabled |
false |
Enable Kubernetes provider |
providers.kubernetes.clusters |
omitted | Ordered connection/policy entries; mutually exclusive with single-cluster fields |
providers.kubernetes.name / clusters[].name |
default / required |
Name clients select with the cluster argument |
providers.kubernetes.protected / clusters[].protected |
false |
Require fresh approval on every write; no approval windows |
providers.kubernetes.kubeconfig |
"" |
Empty: in-cluster, then $KUBECONFIG, then ~/.kube/config |
providers.kubernetes.context |
"" |
Context override; empty uses selected context |
providers.kubernetes.namespaces |
[] |
Namespace glob allowlist; empty allows none; * allows all |
providers.kubernetes.deny_namespaces |
[] |
Deny globs override allow globs |
providers.kubernetes.allow_logs |
false |
Enable pod logs |
providers.kubernetes.show_configmap_data |
false |
False exposes keys only, hides values |
providers.kubernetes.max_log_lines |
500 |
Pod log lines per call cap |
http.listen |
127.0.0.1:8787 |
Backend TCP address |
http.public_url |
"" |
Required HTTP origin; HTTPS except loopback HTTP; resource is origin + /mcp |
auth.mode |
"" |
Required for HTTP: builtin, external or static |
auth.builtin.state_file |
"" |
Optional 0600 persistence for clients and token hashes; empty loses state on restart |
auth.builtin.users |
[] |
Allowed upstream users; empty denies everyone |
auth.builtin.users[].subject |
"" |
passkey:name, github:login, email:verified-email or oidc:sub (exact OIDC subject) |
auth.builtin.users[].scopes |
[] |
Grants: ops:read, ops:write |
auth.builtin.upstream.provider |
"" |
passkey (no identity provider; needs no client ID or secret), github or oidc |
auth.builtin.upstream.issuer |
"" |
Required OIDC issuer, e.g. https://accounts.google.com |
auth.builtin.upstream.client_id |
"" |
Upstream application client ID |
auth.builtin.upstream.client_secret_file |
"" |
Secret file; use this or client_secret_env |
auth.builtin.upstream.client_secret_env |
"" |
Environment variable name containing the secret |
auth.builtin.access_token_ttl |
1h |
Access token lifetime |
auth.builtin.refresh_token_ttl |
720h |
Refresh token lifetime |
auth.builtin.max_clients |
100 |
Dynamic Client Registration capacity |
auth.external.issuer |
"" |
Trusted issuer for discovery and JWKS |
auth.external.audience |
"" |
Empty resolves to public_url + /mcp |
auth.external.user_claim |
sub |
String claim identifying the user |
auth.external.users |
[] |
Empty trusts token scopes; otherwise restrict users and intersect token/granted scopes |
auth.external.users[].subject |
"" |
claim:value for the configured user_claim |
auth.external.users[].scopes |
[] |
Maximum permitted scopes for this identity |
auth.static |
[] |
Configured bearer tokens; no OAuth login in this mode |
auth.static[].name |
"" |
Token identity/audit label |
auth.static[].sha256 |
"" |
Hex SHA-256 digest, never the raw token |
auth.static[].scopes |
[] |
Token grants; include ops:read for HTTP access |
approval.wait |
25s when web approval is enabled explicitly or by a mode, otherwise 0s |
Wait for decision and execution, 0s–55s; zero returns the link immediately; cancellation ends only the wait |
approval.ttl |
10m |
Pending approval/input lifetime |
approval.web |
true with builtin auth, otherwise false |
Enable browser approval; requires builtin auth |
approval.require_web |
true when approval.web is true; otherwise false |
Require browser approval on HTTP; explicit false allows elicitation; explicit true also denies stdio writes |
approval.inbox_notify.secret_file |
empty (disabled) | Shared watch notice HMAC key file (16–4096 bytes, trimmed); requires builtin web approval and subjects |
approval.inbox_notify.subjects |
[] |
1–100 unique configured builtin users to receive identifier-only notices; memory-only, max 50/user for 1h; pushes coalesce for 1m |
watch.inbox.url |
empty (disabled) | Server origin for signed POST /approvals/notify; HTTPS or webhook-compatible private/loopback HTTP; own queue and 1s batching (group batch overrides it) |
watch.inbox.secret_file |
empty | Required shared HMAC key file (16–4096 bytes, trimmed) |
watch.inbox.allow_private_http |
false |
Permit private HTTP with dial-time address checks, as for watch outputs |
approval.push.enabled |
false |
Web Push for MCP web approvals and configured inbox notices; requires approval.web |
approval.push.subject |
empty | Required contact mailto: or https: URL when enabled |
approval.push.state_file |
/var/lib/opsgate/push.json with a mode, otherwise empty |
Durable 0600 subscriptions and VAPID keys; required when enabled, separate from OAuth state |
approval.push.allowed_hosts |
[fcm.googleapis.com, updates.push.services.mozilla.com, web.push.apple.com, '*.notify.windows.com'] |
HTTPS push hosts; wildcard matches subdomains only; dial-time public addresses, no redirects or proxies |
approval.push.language |
en |
Generic notification language: en or ko; push carries no content |
approval.window |
0s |
Optional user/tool/target auto-approval lifetime; 1m–12h when enabled |
approval.window_tools |
[] |
path.Match tool-name globs eligible for windows; empty allows none |
providers.kubernetes.rollout_restart |
[] |
namespace/name globs; empty disables tool |
providers.kubernetes.scale |
[] |
namespace/name globs; empty disables tool |
providers.kubernetes.delete_pod |
[] |
namespace/name globs; empty disables tool |
providers.kubernetes.max_replicas |
10 |
Scale maximum; nonnegative, zero permits only zero replicas |
providers.host.enabled |
false |
Enable the optional host provider; an error with mode: kubernetes |
providers.host.poll_interval |
30s |
How often opsgate watch polls units and containers (10s–5m) |
providers.host.systemd.units / .restart |
[] / [] |
path.Match globs of visible units and of units that may be restarted (both must match) |
providers.host.docker.endpoint |
"" |
unix:///absolute/socket or http://<loopback IP>:<port> of a Docker socket proxy |
providers.host.docker.containers / .restart |
[] / [] |
Container name globs or compose:<project>/<service> globs; visible and restartable |
providers.note.enabled |
false |
Enable the note provider for a Markdown folder or Obsidian vault; no note app or CLI needed |
providers.note.vault |
"" |
Absolute existing vault directory; all access confined with os.Root |
providers.note.exclude |
[] |
Additional vault-relative path.Match globs matching a path or any parent; dot-prefixed segments and sync-conflict copies always excluded |
providers.note.allow_write |
false |
Register four note write tools; each requires ops:write and human approval |
providers.note.max_note_bytes |
1048576 |
Positive per-note size limit for reads/writes; oversized notes skipped in content scans |
server.tools.allow / server.tools.deny |
[] / [] |
Tool-name path.Match globs; empty allow permits all offered tools, deny wins; excluded tools are absent from tools/list |
auth.builtin.users[].tools |
[] |
Optional tool-name globs narrowing a user's calls; empty adds no limit |
auth.external.users[].tools |
[] |
Optional tool-name globs narrowing a mapped user's calls; empty adds no limit |
auth.static[].tools |
[] |
Optional tool-name globs narrowing this token's calls; empty adds no limit |
auth.builtin.allowed_redirect_hosts |
[] |
Exact redirect hostnames (no ports); localhost covers loopback IPs/any port; supported private schemes use e.g. cursor:; empty accepts any otherwise valid redirect host |
auth.builtin.unknown_redirect_hosts |
"" |
With a host list: empty/deny rejects unlisted destinations; approve holds clients pending admin review |
auth.builtin.admins |
[] |
Allowed upstream subjects who manage clients at /admin/clients; must be in users; required for approve mode |
auth.builtin.notify_url |
"" |
Optional JSON POST for pending registrations; HTTPS or loopback HTTP, no URL credentials/fragments |
Server policy patterns must match a tool offered by an enabled provider after its action gates, or startup fails. Per-user/token tool patterns are syntax-checked and enforced on calls; they do not filter tools/list. Scopes, server policy, per-user tool policy and provider resource allowlists all apply. Empty tool allowlists add no restriction, unlike empty provider resource allowlists. Configuration changes require a restart.
All providers are disabled by default. Enabling Kubernetes permits cluster-scoped reads subject to RBAC. ops:read covers all enabled read providers, including the optional experimental note provider. See the trust model.
Note writes run as the server account, so notes it creates or rewrites are owned by that account. Grant it access with ACLs rather than ownership, and keep the vault owner able to replace those files (write access to their directories). A sync tool running as the vault owner cannot chmod files it does not own: in Syncthing, set the vault folder's Ignore Permissions option, or permission-only changes from other devices fail with "operation not permitted".
See the complete example for a read-only starting point and separate commented write opt-ins. The tool reference describes argument, approval and output limits.
Kubernetes clusters
The existing single-cluster form remains valid: put kubeconfig, context and policy fields directly under providers.kubernetes. Its name defaults to default; protected defaults to false. For several clusters, use clusters instead. Each entry takes the same connection and policy fields and requires a unique name matching ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ (1–63 characters). Do not combine clusters with single-cluster fields, apart from numeric defaults. An enabled provider rejects clusters: []. In clusters entries, an omitted or zero max_log_lines or max_replicas means 500 or 10; leave scale empty to disable scaling on a cluster.
providers:
kubernetes:
enabled: true
clusters:
- name: kind
kubeconfig: /etc/opsgate/kind.yaml
context: kind-kind
namespaces: [apps]
allow_logs: true
rollout_restart: [apps/web]
scale: [apps/web]
max_replicas: 3
- name: prod
kubeconfig: /etc/opsgate/prod.yaml
protected: true
namespaces: [apps]
deny_namespaces: [kube-system]
allow_logs: true
# Empty write allowlists keep production read-only.
protected: true requires fresh human approval for every write, even if the user has an approval window. Policy settings apply only to their own cluster. Logs and write tools appear if any cluster enables them; calls targeting a cluster that disables them are denied. With several clusters, pass cluster on every call except k8s_clusters. With one cluster, it may be omitted, but an explicit name must match. Use k8s_clusters to review policy and reachability before choosing a target; state the target cluster to the user before a write.
Managed clusters (EKS, GKE, AKS)
opsgate refuses kubeconfig credential plugins (exec and auth-provider, including aws eks get-token) because they can run programs from a kubeconfig. Use a dedicated ServiceAccount and a separate kubeconfig with a token. From an administrator context pointing at the intended cluster, apply the existing read-only RBAC. It grants cluster-wide reads without Secrets; use namespace RoleBindings for narrower access.
kubectl create namespace opsgate
kubectl apply -f deploy/kubernetes/rbac-readonly.yaml
umask 077
kubectl --kubeconfig=opsgate.kubeconfig config set-cluster managed \
--server=https://API_ENDPOINT --certificate-authority=cluster-ca.crt --embed-certs=true
kubectl --kubeconfig=opsgate.kubeconfig config set-credentials opsgate \
--token="$(kubectl -n opsgate create token opsgate --duration=24h)"
kubectl --kubeconfig=opsgate.kubeconfig config set-context managed \
--cluster=managed --user=opsgate --namespace=opsgate
kubectl --kubeconfig=opsgate.kubeconfig config use-context managed
Replace API_ENDPOINT and cluster-ca.crt with the cluster's API endpoint and CA certificate from your administrator kubeconfig. Skip namespace creation if it already exists. Keep the new kubeconfig private (mode 0600) and point opsgate at it. The API server may shorten the requested token lifetime; rotate the token and restart opsgate before it expires. This kubeconfig contains no credential plugin and needs no cloud CLI at runtime.
Authentication and human approval
The upstream application callback is exactly <public_url>/oauth/callback. See deployment for Host-preserving proxies and local probe rules.
builtin signs people in with passkeys or delegates login to GitHub or OIDC (including Google), then issues opaque MCP tokens. With passkeys, registration needs a one-time invite: opsgate logs one per user without a passkey at startup (24h), and opsgate passkey invite -config <file> -subject <user> [-ttl 24h] issues more; run it as the server account because invites are files next to the state file. It supports authorization code with PKCE S256 only, Dynamic Client Registration (DCR), per-client consent, refresh rotation and reuse revocation. The issuer is http.public_url; the resource is <public_url>/mcp. Optional state persistence retains client registrations and token hashes across restarts; protect its directory and backups, and use one process per state file. State reads and writes have a 512 MiB limit; an oversized write preserves the previous snapshot and makes the running instance fail closed.
Builtin OIDC users may match a verified email:<address> or an exact, case-sensitive immutable oidc:<sub> from the configured issuer. If both match, oidc:<sub> takes precedence and needs no email claim. Google email subjects require gmail.com/googlemail.com, or an hd claim matching the email domain (Google Workspace); email_verified alone is insufficient for other Google addresses. For the Google example in the README, use a Workspace address or configure oidc:<sub>.
OAuth GET routes reject request bodies; POST body reads have a 10-second deadline. Authorization errors stay on a local page before consent; Deny on the consent page may redirect. At DCR capacity, registrations older than 10 minutes that never issued tokens and have no outstanding authorization are evicted oldest first. Pending authorizations are limited to 20 per client and 1000 overall, evicting the oldest affected entry; browser approval and admin logins have a separate pool of 256, with a burst of 20 and one new admission every 3 seconds, and only expired login entries are removed.
external validates JWT signatures using issuer discovery and JWKS, with issuer, audience, expiry and allowed algorithm checks. Issuer, discovery and JWKS URLs require HTTPS, except loopback HTTP. JWKS fetch attempts are shared across requests and limited to one per minute, including failed fetches. Cached valid keys continue to work; a new signing key may be rejected until the cooldown expires. Audience defaults to the MCP resource URL. Scopes come from scope or scp; an optional claim:<value> user allowlist further restricts identities and intersects their scopes. static accepts pre-shared bearer tokens configured by SHA-256 digest; generate a token and digest with opsgate -gen-token. Store the raw token only in the client's secret storage. Static mode provides no DCR or OAuth login.
Kubernetes and note read tools require ops:read. Kubernetes writes require their own action allowlist; note writes require allow_write. These operations require ops:write, underlying OS/RBAC permission and human confirmation. HTTP access additionally requires ops:read, so remote writers need both scopes. Namespace allow/deny rules still apply to Kubernetes writes.
The server validates inputs and builds a side-effect-free plan before confirmation. With builtin auth and approval.web: true, HTTP defaults to browser approval and returns an approval link and approval_id. Setting approval.require_web: false explicitly allows clients supporting MCP elicitation to use an in-client prompt, including multi round-trip input requests across protocol versions; other clients still use the web link. The human opens it, signs in upstream as the same user and reviews the exact operation. Approving immediately re-plans with the original caller and arguments, checks the approved summary and target, and runs the write once. The waiting call receives the result; after approval.wait ends, read it with approval_status or the same arguments plus approval_id. Result retries never apply again. Approvals expire, are single-use and bind the user, tool, SHA-256 of canonical unredacted arguments (excluding approval_id) and plan. Writes are denied if redaction changes the summary, so the human sees an unambiguous target. Kubernetes plans include the target UID; execution uses that invocation’s plan and identity/version preconditions. Without a confirmation channel, writes are denied; stdio supports elicitation only.
The signed-in /approvals inbox lists your live requests, retained results and watch notices addressed to you. Install it and enable optional device notifications as described in Approval inbox and notifications. Only accounts with ops:write may subscribe (10 devices per user, newest wins).
Optional time-window approvals require both approval.window (1m–12h; 0s disables the feature) and approval.window_tools (a path.Match glob allowlist; empty allows no tools). For example, window: 30m and window_tools: [k8s_rollout_restart] let a human choose Approve + allow for 30m in the browser, or the optional Also auto-approve k8s_rollout_restart for 30m checkbox in elicitation. The grant covers that exact user, tool and target, including later calls from another client or with different arguments. It does not cover another target. It does not grant scopes, bypass action allowlists, skip input validation or skip planning. Grants live only in memory, expire without renewal on use, are capped at 100, and disappear on restart. Each call still has its own audit record with approval: window; grant creation emits a separate window_granted event with the source (web or elicitation), duration and expiry. With require_web, only the browser can obtain a grant and stdio writes remain denied.
Approval pages show the plan with diff highlighting, requester, transport, expiry and collapsed arguments. After approving, the page shows the result. You can return to the chat; the assistant receives it while waiting or when it checks the approval. Results stay readable until the original expiry and at least 10 minutes after execution; the 100-record bound includes retained outcomes. Approval summaries allow up to max(64 KiB, argument limit + 16 KiB). Raw JSON arguments default to 16 KiB; note content tools allow 528 KiB (256 KiB per content/replacement field). JSON escaping counts toward the wire limit.
approval.require_web defaults to true whenever approval.web is enabled, including by a mode with builtin auth. Explicit approval.require_web: false opts in to elicitation: a compromised client can approve its own writes. External/static auth and approval.web: false keep the default false because web approval is unavailable. With -transport stdio, an omitted require_web allows elicitation even for mode-based files, since the user starts the process locally and there is no web channel. Explicit true still denies stdio writes. HTTP startup warns when elicitation is allowed. Prompt injection can still make a model request writes: humans must read the summary before approving. See SECURITY.md.
Client registration can be restricted with allowed_redirect_hosts. With a nonempty list, unknown_redirect_hosts: deny rejects other hosts; approve keeps such clients pending until an allowlisted administrator approves at /admin/clients. Pending clients cannot authorize. notify_url optionally receives a JSON POST containing event, client ID/name, redirect hosts and the admin URL; notifications are bounded and best effort. Client names are self-reported. Approval of a registration does not grant user scopes or skip consent. An empty host list accepts any otherwise valid redirect destination and ignores the unknown-host policy.
Watch alerts
The watch section is read only by opsgate watch, a separate process; the MCP server ignores it. opsgate watch requires providers.kubernetes.enabled and at least one of watch.outputs, watch.email.host and watch.inbox.url.
| Key | Default | Meaning |
|---|---|---|
approval_url |
empty | Public https origin for choices/restart links; defaults to the inbox origin when inbox is configured |
groups |
absent (broadcast) | Ordered namespace routes: name, namespaces, outputs, problems_only and batch; see Groups |
clusters |
all | Configured cluster names to watch |
problems_only |
false |
Produce warnings only for every channel |
batch |
30s |
Webhook batch interval (1s–10m) |
cooldown |
15m |
Repeat warnings for one object are counted, not sent (0–24h) |
max_alerts |
50 |
Alerts per channel batch (1–500) |
email.host |
empty | Enables SMTP; no whitespace, scheme, port or slash |
email.port |
587 |
1–65535; 465 uses implicit TLS, every other port requires STARTTLS |
email.username |
empty | Optional; set together with email.password_file |
email.password_file |
empty | SMTP password, read at startup and trimmed; unreadable or empty files fail startup |
email.from |
empty | Required bare address, no display name or CR/LF |
email.to |
empty | 1–20 bare addresses, no display names or CR/LF |
email.batch |
5m |
Email batch interval (1m–1h) |
email.problems_only |
false |
Send warnings only by email |
Alert groups
Optional watch.groups selects the first namespace match before applying group
and output warning filters. It replaces output batch intervals and keeps batches,
cooldown and retries independent per (group, output). Namespaces use
path.Match; namespace-less alerts match only literal "" or "*". Unmatched
alerts are dropped and counted in local minute/shutdown summaries. Configure
1–50 uniquely named groups, valid globs and existing outputs; email/inbox
refer to enabled dedicated settings and are reserved output names with groups.
Use 1s–10m batches for webhook/Discord/inbox, 1m–1h for email, or their
intersection for mixed groups. All existing global and output warning filters
still apply. See Groups for the example and complete limits.
Omit groups to retain existing broadcast behavior; groups: [] is invalid.
Direct webhook destinations
watch.outputs is an optional list of up to 20 named destinations, alongside
watch.email for SMTP delivery.
Each destination has an independent bounded queue, cooldown and retry state;
a failing receiver does not block the others. Outputs use watch.batch,
watch.cooldown and watch.max_alerts. Global watch.problems_only applies
before fan-out; an output can additionally select warnings only.
| Output key | Default | Meaning |
|---|---|---|
name |
required | Unique 1–64 ASCII letters, digits, _ or -; identifies delivery errors |
type |
webhook |
webhook for native/custom JSON; discord for Discord embeds |
language |
en |
ko or en; inherits watch.language; adds webhook display fields while preserving raw reason/summary |
url |
empty | Receiver URL: https or loopback http; private http requires allow_private_http; no credentials or fragments |
url_file |
empty | Read and trim the receiver URL at startup; set exactly one of url and url_file |
headers |
empty | Static HTTP headers; prefer files for credentials |
header_files |
empty | Header name to file path; read and trim each value at startup |
secret_file |
empty | Per-output HMAC key, at least 16 bytes after trimming |
body_template |
empty | Go text/template for JSON POST body; only with type: webhook, at most 64 KiB |
allow_private_http |
false |
Per-output opt-in to private plain HTTP, checked on every connection |
problems_only |
false |
Send warnings only to this output |
watch:
outputs:
- name: discord
type: discord
url_file: /etc/opsgate/discord-webhook-url
- name: custom
type: webhook
url: https://alerts.example.com/ingest
header_files:
Authorization: /etc/opsgate/alerts-authorization
body_template: |
{"message": {{json .Text}}, "sent_at": {{json .SentAt}}, "alerts": {{json .Alerts}}}
The authorization file contains the full value, for example Bearer TOKEN.
Files are loaded once at startup; restart watch after rotating them. Unreadable
files, invalid URLs, header values or template syntax fail startup. Files keep
credentials out of effective configuration output; inline URLs and headers are
shown as configured. Configure the HMAC key and private-HTTP flag per output.
Routing/framing headers such as Host and Content-Length, and
X-Opsgate-Signature, are reserved; duplicate header names are rejected case-insensitively.
Templates receive the exported Go fields of alert.Batch: .Source, .SentAt,
.Alerts, .Dropped, .Text, .Group (empty without groups); alerts have .Time, .Cluster, .Namespace,
.Kind, .Name, .Severity, .Reason, .Summary, .Hint, .Restart and
.Suppressed. Use {{json VALUE}} without surrounding quotes to safely encode
strings, arrays and objects. Alert text is data, never another template.
Rendered bodies must be valid JSON and at most 1 MiB; execution or JSON errors
lose that batch without posting it. POST defaults to application/json;
headers.Content-Type can override it. HMAC signs the actual rendered bytes.
Discord needs no bot or n8n: create an incoming channel webhook and store its
URL in url_file. The built-in embed shows UTC occurrence and sending times,
cluster, severity, kind, namespace/name, reason, summary, suppressed repeats,
hints, restart suggestions and dropped count. Automatic mentions are disabled.
Descriptions over Discord's 4096 UTF-16-unit limit are visibly truncated;
use a smaller batch/max_alerts or a parallel native JSON receiver when complete
large batches are needed. Custom Discord payloads can instead use type: webhook
with a template, where the operator is responsible for Discord limits and mentions.
All webhook destinations reject redirects and use a 10-second request timeout. Network failures, HTTP 429 and 5xx receive two retries after 1s and 4s; other non-2xx responses are not retried. There is no persistent delivery queue or exactly-once guarantee; retries can duplicate a delivery. HTTP 429 uses the same bounded backoff, without a separate server-directed rate-limit scheduler.
Email fields require email.host. Webhook and email are independent: configure
either or both, with separate batching, cooldown state and retries. Both use
watch.cooldown and watch.max_alerts. TLS and certificate verification are
mandatory (TLS 1.2 or newer): port 465 uses implicit TLS, all other ports require
STARTTLS. Use a dedicated credential, such as an app password, that can be
revoked on its own. Alerts are not stored and are lost after retries, just as
with webhook delivery.
Namespace, pod and container names and redacted summaries pass through the mail
provider outside the cluster. Worst-case volume is one message per batch
interval, per group when groups are configured: 5m can send up to 288
messages a day and 15m up to 96. Check the account's sending limit, especially
when other services share it.
See watch alerts for what is reported and the payload.
Audit format
The shared wrapper writes one JSON object per handled tool call, appended to server.audit_log, or stderr when empty. New files are created with mode 0600 (subject to umask); existing permissions are retained. Symlinks, non-regular files and group/world-writable files are rejected. Stdout is reserved for MCP messages.
{"time":"2026-10-24T12:00:00Z","user":"local","transport":"stdio","tool":"k8s_get","args":{"kind":"deployment","namespace":"default","name":"web"},"outcome":"ok","duration_ms":12,"output_bytes":120}
Fields of audit.Event: time (RFC3339 timestamp with optional fractional seconds), user, transport, tool, optional redacted args, outcome (ok, error, denied, pending), optional redacted error, duration_ms, and output_bytes (returned success text size; zero on error). Write events additionally include optional redacted plan (the summary shown to the human) and approval (the confirmation method, elicitation, web or window; may also be present on a refusal or pending call). pending means human input is outstanding. Web approval creation records pending before waiting, without text output; result retrieval does not duplicate the apply event. Elicitation input requests do not return text output. Planning and applying each receive a separate tool timeout; human confirmation may take longer. Web decisions emit separate audit events with outcome approved or rejected, user, tool and only an eight-character args.approval_id_prefix. Denied multi round-trip approval verifications emit denied events without request state. Link approval creation, decisions and execution also record optional source (rule or ai), separately from tool arguments. Tool output bodies are not stored. Audit write failures are reported to stderr without failing the tool call; audit is not tamper-proof. Protect and rotate logs, which may still contain sensitive information.
Window creation emits window_granted with the source approval method and args.window/args.expires_at; later calls using the grant record approval: window.
Analyzer configuration
opsgate-analyzer uses a separate YAML file, not a section in the MCP server
configuration. See the analyzer settings table
and example.
watch.language: ko|en selects the default display language for all webhook/Discord outputs.
Kubernetes restore_image: ["namespace/deployment"] and action_approval_url: https://mcp.example.com
opt in to verified image restoration approvals. action_approval_url is also the
per-cluster HTTPS origin used by action_link for all four catalog actions. No
new setting is required: set this URL and at least one of rollout_restart,
restore_image, scale or delete_pod; server tool policy still controls
registration. Both default to disabled; see analyzer.
Command line
Run opsgate -h to list commands and flags, or opsgate help <command> for command help. opsgate version (also -version) prints the build version. Help and successful commands exit 0; invalid commands, flags or extra arguments exit 2 before loading configuration; operational errors exit 1. Help goes to stdout and errors to stderr.