opsgateApprovalWatchToolsConfigurationGitHub

Security policy

한국어

opsgate is a released 0.7.x project implementing remote HTTP, authentication and human-approved writes. The optional note provider is experimental. It has not yet been security-reviewed by third parties. Review the implementation you deploy.

Threat model

One opsgate instance is one trust group. ops:read reads every enabled read provider: servers, cluster resources and notes. Provider allowlists constrain resources; server.tools and per-user/token tools globs can narrow tool access. They are not separate credential or process boundaries. For different trust levels run separate instances and accounts, for example production operations and personal notes. Keep each instance's credentials and reachable data appropriate for its entire trust group.

The intended boundary is a constrained MCP interface with read tools and explicitly approved write actions: explicit provider opt-in, namespace allowlists, namespace denies, scope checks, bounded calls and output, best-effort redaction and per-call audit records. No arbitrary shell or exec interface is exposed. Kubernetes Secrets are excluded; ConfigMap values are hidden by default. These controls reduce accidental disclosure and the operations a client can request through opsgate.

They do not make untrusted output safe. Logs, events, annotations and resource names can contain prompt injection. Treat output as data, never instructions. A malicious or compromised client can request every operation its configured scopes and allowlists permit, copy returned data, or attack other connected tools. MCP annotations are hints, not authorization. Stdio trusts the OS account launching the server; its configured user name is only a label.

Over-broad RBAC or kubeconfig permissions expand the consequences of bugs and compromise. Use dedicated credentials with only required read and optional write permissions, and review the supplied RBAC manifests. Kubeconfig exec and auth-provider credential plugins are rejected. Keep kubeconfig files and config paths under trusted ownership. Namespace allow/deny rules filter namespaced resources and Namespace objects by name; they do not restrict Nodes or PersistentVolumes, including nodes in k8s_overview. Omit namespace for cluster-scoped kinds. The supplied ClusterRoleBinding grants cluster-wide reads; prefer namespace RoleBindings where possible. Providers and their credentials must not be attacker-controlled. Running as root is discouraged; use an unprivileged account with only the cluster access required.

The tool reference details the implemented text patterns and object sanitization. Task-log pages, note line pages and search snippets use sensitive ranges detected in the full bounded source, including secrets crossing page boundaries. Fragments preserve line endings within masked spans. Private-key PEM blocks missing a BEGIN or END marker are masked from or through the available source boundary; material with neither marker cannot be identified as PEM. ConfigMap data and binaryData values are hidden by default. Pod and workload literal environment values are masked for case-insensitive name substrings pass, secret, token, key, credential, auth, dsn, private, cert and database_url; other values may remain visible. valueFrom references are visible and are not resolved. Redaction is best effort, not a guarantee that secrets cannot escape. Logs, metadata and process information can contain credentials in unexpected formats. Avoid collecting secret material in the first place. Protect audit logs too: arguments and errors can retain sensitive information. Auditing is not a tamper-proof ledger and does not guarantee durable delivery when writes fail. Context deadlines require handlers and dependencies to cooperate; output limits do not eliminate all resource exhaustion risks.

opsgate does not defend against a compromised host, kernel, server binary, dependency, administrator or credential store. It does not sandbox the client or revoke the launching user's permissions. It does not protect against stolen bearer tokens used within their lifetime, denial of service at the network edge, or disclosure by an authorized client.

Remote exposure and authorization

HTTP exposes /mcp with Protected Resource Metadata. Authentication is mandatory; HTTPS is required for the public origin except loopback development. Terminate TLS at a trusted proxy, keep the backend private and forward the discovery, OAuth and approval routes described in deployment. Do not log bearer tokens, cookies, authorization codes, client secrets or approval URLs. The Host check replaces the SDK's localhost check: public requests must match public_url's host, including any explicit port, or receive 421. Loopback listeners additionally accept loopback Host values for probes. When signed inbox notices are configured, only the exact POST /approvals/notify endpoint also accepts other syntactically valid Hosts for private service delivery. It ignores browser credentials and authenticates with constant-time HMAC verification, signed freshness and replay checks, a 64 KiB body cap and a global rate limit. It stores bounded identifier-only notices and never creates approvals or executes actions. All browser and MCP routes retain the Host policy. A Host-preserving reverse proxy works with a loopback backend; wildcard listeners have no local exception. CORS and MCP annotations are not authorization boundaries.

Builtin OAuth addresses these threats:

OAuth responses are sent after releasing the shared state lock. OAuth GET routes reject bodies and POST body reads have a 10-second deadline. Pending authorizations evict oldest entries at 20 per client or 1000 overall; browser approval and admin logins use a separate pool of 256, with a burst of 20 and one new admission every 3 seconds over a 10-minute transaction lifetime, and only expired login entries are removed. A flood can delay new sign-ins until the shared limiter recovers (HTTP 429 with Retry-After), while existing sign-ins remain valid until expiry; apply a rate limit at the reverse proxy. HTTP access logs mask /approve/<id> as /approve/* and omit query strings.

Passkey login (upstream passkey) needs no external identity provider. Registration requires a one-time invite: a random 256-bit token whose SHA-256 is stored in a 0600 file next to the state file, valid for its TTL (24h by default, at most 7 days), bound to one configured subject and deleted on use. At first start opsgate writes such invites to its log for users without a passkey, so anyone who can read the service log can enroll as those users until they do: restrict log access, or issue invites with opsgate passkey invite instead. Credentials require user verification and a resident key; the relying party ID is the public_url host and the only accepted origin is public_url. Each login challenge is single-use and bound to the browser's pending authorization cookie; a signature counter that fails to advance is treated as a cloned authenticator and rejected. Losing a passkey means issuing a new invite; removing a user from users immediately blocks sign-in. Passkey pages are the only pages that run script: the same-origin /passkey.js under script-src 'self', with no inline script.

Builtin login accepts only configured passkey users (passkey:<name>), GitHub logins, verified OIDC email identities or exact, case-sensitive oidc:<sub> identities from the configured issuer. Google email identities additionally require a Gmail/Googlemail domain or an hd claim matching the email domain; other Google addresses must use an immutable subject. GitHub login renames can change who matches a login allowlist: prefer verified email via OIDC or keep the GitHub allowlist current. External mode verifies JWT signatures via discovered JWKS and checks issuer, audience, expiry and allowed signing algorithms. Issuer, discovery and JWKS URLs require HTTPS, except loopback HTTP. Optional external user entries restrict identities and scopes. Static mode stores only SHA-256 token digests; it has no interactive OAuth flow.

Kubernetes writes need a separate action allowlist; note writes need allow_write. These tools need ops:write, OS/RBAC permission and human confirmation. HTTP also requires ops:read. Planning validates the target and produces the summary before execution. Elicitation obtains an in-client decision through MCP input requests. With builtin auth and approval.web: true, HTTP defaults to browser approval even for clients supporting elicitation; the client receives an approval link and the approver signs in upstream as the same user. The approval expires, is single-use and binds the SHA-256 of canonical unredacted arguments (excluding approval_id) and the exact plan. A summary changed by redaction is denied because its target cannot be shown unambiguously. Kubernetes summaries include the UID, and Apply receives the plan from the same invocation; rollout uses a JSON patch UID test, scale retains resourceVersion, and pod deletion retains a UID precondition. A retry with changed arguments cannot reuse it. Without confirmation, writes are denied.

Time-window approvals are off by default. Enabling them requires approval.window between 1m and 12h and explicit tool-name path.Match globs in approval.window_tools; an empty allowlist allows none. A human can opt in while approving an eligible call through the browser or elicitation. A grant binds one exact user, tool and target, and allows later valid calls from other clients with different arguments on that same target until expiry. A different target requires a new approval. Use narrow tool allowlists and short windows. Grants do not bypass scope checks, provider policy, planning, argument limits or summary redaction checks. They are held only in memory, capped at 100 live grants, never extended by use, and cleared on restart. Creation emits a separate window_granted audit event (source method, duration and expiry), and every use is audited by the write wrapper with approval: window. require_web still requires the browser to obtain a grant and denies stdio writes. Compromised clients can opt into elicitation windows just as they can accept individual elicitation prompts.

Raw write JSON defaults to 16 KiB; note create/append/replace allow 528 KiB (256 KiB per content/replacement input). JSON escaping counts toward the wire limit. Approval summaries allow max(64 KiB, argument limit + 16 KiB); redacted display arguments must fit the tool argument limit. Provider output above 4 MiB is omitted entirely; other output is redacted before truncation to the configured response cap (default 64 KiB). Approval and consent pages share escaped HTML templates, inline CSS, no scripts or external assets, and retain their CSRF, cross-origin, CSP and cache protections. Approval links and CSRF tokens are not included in grant audit events.

approval.require_web defaults to true when 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, and the server cannot establish that a real person clicked a client-controlled prompt. External/static auth and approval.web: false keep require_web false by default because no web channel is available. HTTP startup warns when elicitation is allowed. When the file omits require_web, -transport stdio allows elicitation even with a mode, since the user starts the process locally and there is no web channel. Explicit true still denies stdio writes. config show prints the HTTP default explicitly; its require_web: true must be removed or set to false to allow stdio writes. Prompt injection can still cause the model to request writes. Humans must read the summary and target before approving. Approval does not make an operation safe or guarantee the target's state remains unchanged; OS privileges, RBAC and narrow allowlists remain essential. Audit records include plans, approval methods, pending outcomes, web decisions (with only an approval ID prefix) and denied multi round-trip verifications, but audit delivery is not a prerequisite for execution.

Known limitations

Note provider

Vault access is confined with os.Root, excludes dot paths and sync-conflict copies, and applies configured exclusions. Safe in-vault symlinks are supported. Note text and filenames are untrusted. Note write plans show the target, hashes of masked current/result text, and a content preview or diff bounded near 8 KiB with omitted lines reported. Without an existing eligible approval window, a truncated preview requires web approval showing the complete masked arguments; if web approval is unavailable, split the change into smaller writes.

When full review is required, any redaction change to the arguments (excluding approval_id) denies the write before confirmation or approval-window reuse; split the change or remove the masked value.

Apply rechecks the approved file identity, hash, size and modification time. Updates then publish a synced temporary file with rename. This is an optimistic check, not a transaction against external editors: a concurrent external edit between the check and rename can be overwritten. Use version history in sync tools and backups. Creates publish without overwriting a racing creator; trash retains a copy locally rather than permanently deleting a note. Protect the vault and its backups with filesystem permissions.

Host provider

The optional host provider is off by default and refused with mode: kubernetes. It takes only concrete, validated unit and container names from allowlists and runs systemctl/journalctl with fixed argv, never a shell, with timeouts and output caps. Restarts need both the visible and the restart allowlist and human approval. Unit restarts go through systemd's D-Bus API and a polkit rule limited to the restart verb on listed units, so opsgate needs no sudo and keeps NoNewPrivileges. Docker access must go through a socket proxy that allows only list, inspect, logs and restart; the Docker socket itself is root-equivalent. A container restart is bound to the container ID in the approved plan. See host provider.

Supported versions

While the project is 0.x, security fixes and support are best effort, focused on the latest development version. There is no guaranteed maintenance window for older 0.x releases.

Reporting a vulnerability

Please report privately using GitHub Security Advisories: choose Report a vulnerability on the repository's Security page. Include the affected revision, configuration with secrets removed, impact and reproducible steps. Do not open public issues for vulnerabilities or publish credentials. Maintainers will coordinate investigation and disclosure privately; response times are best effort.

Optional analyzer model backends

The analyzer can use an API model or a configured Claude Code or Codex CLI executable. CLI mode launches only that operator-controlled executable with fixed flags, without a shell. Alert text and evidence arrive on stdin, not command arguments. Native CLI tools, direct MCP servers, user/project settings, hooks and persisted sessions are disabled through CLI flags; managed organizational settings may still apply. All requested MCP calls return to the shared read-only tool policy. The process has a deadline, bounded output and process-group cancellation on Linux/macOS. These controls do not sandbox a malicious executable or protect its OS account from that executable: secure the CLI installation and auth directory. CLI credentials and account limits are independent of the MCP read-only token; the MCP token is not given to the CLI. -check checks executable availability, not login freshness or provider access. The default container image does not bundle Claude Code or a subscription login.

Codex mode also uses a per-invocation local model catalog to remove model-owned shell/patch tools, explicit feature/config overrides, and a JSONL event guard that cancels unexpected native-tool events. Its format is tested against CLI 0.166.0, including a local fake Responses-server contract test that asserts the outgoing native tool list is empty. This metadata/config contract must be rechecked when upgrading Codex; read-only sandboxing alone does not constrain remote MCP writes. Codex receives no MCP token or direct MCP server. Common MCP policies remain enforced by the analyzer. Existing CLI auth can be shared with a properly protected mount; refresh may write to that directory.