opsgateApprovalWatchToolsConfigurationGitHub

Deploying opsgate

한국어

Use the container for Kubernetes inspection. Darwin builds are provided for local clients accessing clusters through kubeconfig. There are no Windows builds.

Streamable HTTP, OAuth, authentication, human approval and write tools are implemented. This checkout is preparing for its first public release; third-party security review is pending. The optional note provider is experimental. Verify authentication, approval and proxy behavior before exposure. The upstream login callback is exactly <public_url>/oauth/callback.

Verify a release before installing

Install cosign and GitHub CLI from their official distributions. Select a real release tag and the archive for your OS and architecture:

TAG=v0.7.0 # Replace with the release you intend to install.
ARCH=amd64 # Or arm64.
gh release download "$TAG" --repo gkdis6/opsgate \
  --pattern "opsgate_${TAG#v}_linux_${ARCH}.tar.gz" \
  --pattern checksums.txt --pattern checksums.txt.sigstore.json
cosign verify-blob \
  --certificate-identity-regexp '^https://github\.com/gkdis6/opsgate/\.github/workflows/release\.yml@refs/tags/v[^/]+$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --bundle checksums.txt.sigstore.json checksums.txt
sha256sum --check --ignore-missing checksums.txt
gh attestation verify "opsgate_${TAG#v}_linux_${ARCH}.tar.gz" --repo gkdis6/opsgate
gh attestation verify checksums.txt --repo gkdis6/opsgate

Run each command only after the previous command succeeds. For tighter identity verification, replace the regexp flag with --certificate-identity and the full workflow URL ending in @refs/tags/$TAG. On macOS use shasum -a 256 to compare the selected archive's digest with its entry in the verified checksums file. Only extract and execute an archive after signature, checksum and attestation verification. Release assets also contain archive SBOMs generated by Syft.

For containers, obtain the manifest digest from the release or registry and verify that immutable reference before putting it in the deployment:

IMAGE=ghcr.io/gkdis6/opsgate@sha256:REPLACE_WITH_MANIFEST_DIGEST
cosign verify \
  --certificate-identity-regexp '^https://github\.com/gkdis6/opsgate/\.github/workflows/release\.yml@refs/tags/v[^/]+$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com "$IMAGE"

GoReleaser uses keyless sign-blob with a Sigstore bundle for checksums and signs container digests through docker_signs; see its signing documentation.

TLS reverse proxy and Google login

Set http.public_url to the external HTTPS origin, for example https://mcp.example.com. Do not append /mcp or another path. Clients connect to https://mcp.example.com/mcp; this URL is also the OAuth resource identifier. Google authenticates the human; opsgate issues the MCP access tokens.

Create a Google OIDC web application, use issuer https://accounts.google.com, and register the exact upstream callback URL <public_url>/oauth/callback (for example, https://mcp.example.com/oauth/callback) at the identity provider. Set the client ID and an explicit allowlist of verified email subjects (email:operator@example.com) or exact oidc:<sub> subjects. Google email identities require Gmail/Googlemail or a Workspace hd claim matching the email domain; other Google accounts must use oidc:<sub>. Store the client secret in a file or Kubernetes Secret, never directly in YAML, a command argument, or source control. Give ops:write only to operators who need it. Each write also requires an allowed target, OS/RBAC permissions and human approval.

Terminate TLS at an ingress or reverse proxy in front of Service opsgate:8787. Proxy /mcp, /.well-known/*, /oauth/*, /passkey/*, /passkey.js, /approve/* and /admin/clients without changing the path, query, Host, Authorization, Origin or MCP headers. Preserve response headers, cookies and redirects. Support GET, POST, DELETE and streaming responses; disable buffering and caching, and allow long-lived MCP connections. Do not log authorization headers, cookies, OAuth query strings, request bodies or approval URLs. Keep the backend port inaccessible from untrusted networks.

A flood can delay new browser approval and admin sign-ins until the shared login limiter recovers (HTTP 429 with Retry-After), while existing sign-ins remain valid until expiry; apply a rate limit at the reverse proxy.

opsgate replaces the MCP SDK's localhost check with its own Host validation. Public requests must use the host from http.public_url, including any explicit port (comparison is case-insensitive); mismatches receive HTTP 421. Binding to 127.0.0.1 behind a proxy that preserves the public Host therefore works. A loopback listener also accepts loopback Host values for local probes. On a wildcard listener, loopback Host values receive no such exception, even when the connection arrives on loopback. Forwarded-Host headers do not replace Host.

Restrict builtin registration with allowed_redirect_hosts; an empty list accepts any otherwise valid redirect host. With a nonempty list, unknown_redirect_hosts: deny rejects other hosts; approve holds them pending review at /admin/clients by subjects in admins (also present in users). notify_url can send a best-effort JSON notification for pending clients. Review client destinations and never treat a self-reported client name as verified.

For example, Caddy running in the opsgate namespace obtains TLS certificates and streams responses with:

mcp.example.com {
    @opsgate path /mcp /.well-known/* /oauth/* /passkey/* /passkey.js /approve/* /admin/clients
    reverse_proxy @opsgate opsgate:8787 {
        flush_interval -1
    }
    respond 404
}

No access log is enabled in this example. /healthz is for internal probes; it need not be exposed by the public proxy. Restrict Kubernetes Service access to the ingress controller and monitoring using your cluster's network policy. Permit required egress to DNS, Google OIDC and the Kubernetes API.

Install in Kubernetes

The deployment templates are in the source checkout, not the binary archive.

Namespace allowlists supplement RBAC. The ClusterRoleBinding in rbac-readonly.yaml lets the ServiceAccount read allowed resource kinds across all namespaces at the Kubernetes API level, including namespaces denied by opsgate policy. The watcher shares this role. Application policy is the namespace boundary with this binding; replace it with per-namespace RoleBindings for a stricter setup. Its permissions do not authorize optional write actions; add only the required namespace-scoped permissions if writes are needed.

Use deploy/kubernetes/opsgate-deployment.yaml and the existing deploy/kubernetes/rbac-readonly.yaml. The deployment uses the latter's ServiceAccount opsgate, enables only reads, and mounts a PVC for OAuth state. Keep one replica and the Recreate strategy; concurrent processes must not share the state file. A default StorageClass is required, or set one explicitly on the PVC. Protect the PVC and its backups as credentials.

Edit the manifest's Google client ID, HTTPS origin, identity and namespace allowlists. Replace its illustrative image tag with the verified image digest, and update the supplied Host header on every HTTP probe to match your public host. Create the namespace and provision the Secret from a protected local file:

kubectl create namespace opsgate --dry-run=client -o yaml | kubectl apply -f -
kubectl -n opsgate create secret generic opsgate-upstream \
  --from-file=client-secret=/secure/path/google-client-secret
kubectl apply -f deploy/kubernetes/rbac-readonly.yaml
kubectl apply -f deploy/kubernetes/opsgate-deployment.yaml
kubectl -n opsgate rollout status deployment/opsgate

The Secret is required and mounted read-only; no client secret is embedded in the manifest. Restrict Secret RBAC and enable encryption at rest. Logs go to stderr for your cluster log collector. Configure audit retention and restricted collection in that collector, and avoid dropping events. Configure a TLS ingress/reverse proxy to Service opsgate:8787, preserving the routes and streaming behavior above. Probes use the internal /healthz endpoint. The manifest binds to 0.0.0.0, so each supplied HTTP probe includes httpHeaders: [{name: Host, value: mcp.example.com}] under httpGet. The Host value must match the host in http.public_url, including any explicit port. Without that header, the kubelet uses the Pod IP as Host and receives 421. The same requirement applies to direct Service/Pod-IP health checks.

For disposable development only, replace the state volume's persistentVolumeClaim with emptyDir: {} and omit the PVC document. Pod replacement then loses registrations and tokens and forces clients to reconnect. The read-only root filesystem needs no other writable mount.

Write actions remain disabled. To enable them, add an exact action allowlist and ops:write for selected users, plus a narrow Role/RoleBinding for that action's resources and verbs in the target namespace. Avoid cluster-wide write grants. The supplied rbac-operator.yaml Role is namespace-scoped, but its workload patch permission allows changes to more fields than the restart tool changes; RBAC does not restrict that permission to the restart annotation. Restart after changing the ConfigMap (the subPath mount does not hot reload):

kubectl -n opsgate rollout restart deployment/opsgate

Verify discovery through the public origin, that unauthenticated /mcp requests are rejected, and that an unlisted identity cannot sign in. Exercise one read and, if enabled, one approved write against a disposable allowed target.

Install on a host without Kubernetes

Run opsgate as a systemd service under its own opsgate account with deploy/host/opsgate.service and, for alerts, opsgate-watch.service. Enable the optional host provider for unit and container tools, install the polkit rule for restarts and put a socket proxy in front of Docker. Do not use mode: kubernetes there; set http, auth and approval explicitly.

Upgrades and local builds

Read release notes and verify the new artifacts first. For Kubernetes, stop the deployment and securely back up OAuth state, config and audit logs, then update the verified image digest and apply. Recreate intentionally incurs a short outage. Confirm probes, discovery, login and policy checks before resuming traffic. Keep the prior verified image and a compatible state backup for rollback; rollback may require restoring state compatible with the old version. Never run old and new processes against the same state file.

make docker VERSION=dev builds locally from Go 1.26 into a nonroot distroless image. make snapshot uses GoReleaser v2 (the release workflow pins v2.19.0), Syft and Docker with Buildx; release signing requires cosign and publishing requires registry/GitHub credentials. Snapshots skip publishing and signing. Tagged v* releases run race tests and vulnerability scanning before building archives, SBOMs and multi-architecture images, signing checksums/images, and attaching GitHub build provenance.