Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Gatekeeper is pre-1.0. The configuration schema and credential source interface

## v0.17.2 — 2026-07-15

### Added

- **Eight new deployment and how-to guides** — the docs audit found the guide catalog stopped at reference material for several features with real setup friction. New task-oriented walkthroughs, each verified against code (all YAML parse-checked through `gatekeeper.LoadConfig`, Go snippets compile-checked, and the container guide exercised live against the published image): deploying behind a TCP load balancer with PROXY protocol (GCP `proxyHeader: PROXY_V1` backend config, gatekeeper-first rollout ordering, client-IP-forgery caveats); running the `ghcr.io/majorcontext/gatekeeper` container image (config/CA mounting, client wiring, and why in-container Docker healthchecks can't work on the distroless image — it has no shell); the Postgres data plane with Neon end-to-end (account- vs project-scoped API keys, the libpq `host=`/`hostaddr=` split for local testing without DNS); host command credentials with worked 1Password and `pass` examples; GCP service account tokens (including the precise key-rotation semantics: only the Google token endpoint rejecting a JWT drops the cached key, and only in Secret Manager mode — a downstream 401 rides out the TTL); MCP relay setup (honest about MCP having no `gatekeeper.yaml` surface — it's Go-library-only via `RunContextData`); the full credential lifecycle (startup fetch, 75%-of-TTL refresh whose backoff jitter applies after the 60s cap so waits can reach ~75s, token-exchange's 1-minute cache, 401/403 eviction); and the three Keep policy scopes (`http`, `mcp-<server>`, `llm-gateway`), including that `mcp-*` denials set no `X-Moat-Blocked` header unlike the other two scopes

### Changed

- **Docs corpus now complies with its own style guide** — a compliance sweep nobody had run found `concepts/` and `guides/` used Title Case headings throughout while `getting-started/` and `reference/` were sentence case per `docs/STYLE-GUIDE.md`; 106 headings across 18 files converted, plus scattered terminology and abbreviation-order fixes. Systemic gaps flagged for a policy call rather than fixed: first-use abbreviation expansion is ignored corpus-wide, guide files use `--` where the rest of the corpus uses em dashes (new guides written to the em-dash convention), and frontmatter title casing is uncodified

### Fixed

- **Gatekeeper's own OTel diagnostics no longer feed back into the failing OTel log-export pipeline** — `configureLogging` fans every slog record out to `otelslog.NewHandler("gatekeeper")` unconditionally: the bridge's `Enabled` defers entirely to the OTel logger, not the console handler's configured level, so nothing before v0.17.0's DEBUG demotion (and nothing since) kept the bridge from seeing a record just because it was noisy. That meant `logOTelError` ([#47](https://github.com/majorcontext/gatekeeper/pull/47))'s own DEBUG record on a failed export was itself enqueued right back into the OTel log-export pipeline that had just failed: failed export → DEBUG diagnostic → diagnostic fanned out to the bridge → re-enqueued for the next export → that export fails too, now carrying the diagnostic → another diagnostic logged → re-enqueued again, and so on for as long as the collector stays unreachable. Bounded — one record per export attempt, and the batch queue drops on overflow rather than growing without limit — but a genuine feedback loop, not merely repeated independent failures. `gatekeeper.OTelDiagnosticKey` is a new exported marker attribute; `logOTelError` tags its DEBUG record with it, and `configureLogging`'s otelslog bridge handler is now wrapped in a filter that drops any record carrying the marker before forwarding — the console/file handler is unaffected and still logs every record, marked or not, at its configured level. Diagnostics about the OTel pipeline itself are now visible locally but never re-enter the pipeline they're reporting on ([#48](https://github.com/majorcontext/gatekeeper/issues/48))
Expand Down
8 changes: 8 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
- [OpenTelemetry](./content/guides/08-opentelemetry.md) — Distributed tracing, metrics, and logs
- [Go Library](./content/guides/09-go-library.md) — Embed the proxy engine in a custom Go application
- [WebSocket Support](./content/guides/10-websockets.md) — WebSocket upgrades through TLS interception
- [Deploying Behind a TCP Load Balancer](./content/guides/11-load-balancer-proxy-protocol.md) — recover real client IPs with PROXY protocol behind a TCP-terminating load balancer
- [Running the Container Image](./content/guides/12-docker-deployment.md) — pull and run the published container image with config and CA material mounted
- [Postgres Data Plane with Neon](./content/guides/13-postgres-neon.md) — run the Postgres listener against a Neon project with run-token authentication
- [Host Command Credentials](./content/guides/14-process-credentials.md) — inject credentials from any secret manager that has a CLI via the process source
- [GCP Service Account Tokens](./content/guides/15-gcp-service-account.md) — mint and refresh GCP OAuth2 access tokens from a service account key
- [MCP Relay Setup](./content/guides/16-mcp-relay.md) — configure the MCP relay end-to-end using the Go library
- [Credential Caching, Refresh, and Invalidation](./content/guides/17-credential-lifecycle.md) — the full credential lifecycle from startup fetch through 401/403-triggered eviction
- [Keep Policy Scopes](./content/guides/18-keep-policy-scopes.md) — the http, mcp-<server>, and llm-gateway policy scopes and their denial logs

### Reference

Expand Down
18 changes: 9 additions & 9 deletions docs/content/concepts/01-tls-interception.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ description: "How Gatekeeper terminates TLS connections, generates per-host cert
keywords: ["gatekeeper", "TLS interception", "MITM proxy", "certificate generation"]
---

# TLS Interception
# TLS interception

Gatekeeper is a TLS-intercepting proxy. It terminates the client's TLS connection, reads the plaintext HTTP request, injects credentials, and forwards the request to the real server over a separate TLS connection. This is a man-in-the-middle architecture — the client must trust gatekeeper's CA certificate.

This page explains why TLS interception is necessary and how the certificate chain works.

## Why MITM Is Necessary
## Why MITM is necessary

HTTP proxies see CONNECT tunnels as opaque byte streams. Without interception, the proxy knows the destination host but cannot read or modify the encrypted HTTP request inside the tunnel. Credential injection requires access to the plaintext request headers — so gatekeeper must terminate the client's TLS, read the request, inject headers, and re-encrypt for the upstream server.

## The CONNECT Flow
## The CONNECT flow

When a client sends an HTTPS request through gatekeeper, the flow has five stages:

Expand All @@ -37,7 +37,7 @@ Client Gatekeeper api.github.com
|<-- 200 OK ---------------| |
```

## Two Separate TLS Connections
## Two separate TLS connections

The proxy maintains two independent TLS sessions per intercepted request:

Expand All @@ -48,31 +48,31 @@ The proxy maintains two independent TLS sessions per intercepted request:

These connections use independent keys and cipher suites. The client never sees the origin server's certificate — it only sees gatekeeper's generated certificate.

## Per-Host Certificate Generation
## Per-host certificate generation

When gatekeeper intercepts a CONNECT tunnel for a host, `CA.GenerateCert` creates a certificate on the fly:

- The certificate's `CommonName` and SAN (Subject Alternative Name) match the target host.
- The certificate's `CommonName` and Subject Alternative Name (SAN) match the target host.
- IP addresses are added as IP SANs; hostnames as DNS SANs.
- Each certificate is signed by gatekeeper's CA private key.
- Generated certificates are cached in memory by hostname to avoid repeated key generation.
- Leaf certificates are valid for one year. The CA certificate is valid for ten years.

The CA supports RSA, EC, and Ed25519 private keys via PKCS1, PKCS8, and SEC 1 formats.

## ALPN Negotiation
## ALPN negotiation

The client-facing TLS handshake advertises `h2` before `http/1.1` in its ALPN `NextProtos` list, so gatekeeper prefers HTTP/2 with clients that support it and falls back to HTTP/1.1 otherwise. The negotiated protocol determines which transport gatekeeper uses to reach the upstream server: when the client negotiated `h2`, gatekeeper forwards over an `http2.Transport`, since an h2 request cannot be round-tripped through an HTTP/1.1 transport without framing errors; otherwise it forwards over a standard `http.Transport`. This matters for gRPC clients, which require h2 end-to-end — gatekeeper's `http2.Transport` never falls back to HTTP/1.1, so if the upstream only speaks HTTP/1.1, a connection from an h2 client to that upstream fails.

WebSocket upgrades follow a related but separate path through TLS interception — see [WebSockets](../guides/10-websockets.md).

## Why the Client Must Trust the CA
## Why the client must trust the CA

The dynamically generated certificates are not signed by a public CA. Clients reject them unless they explicitly trust gatekeeper's CA certificate. In container environments, the CA certificate is mounted into the container's trust store (e.g., `/etc/ssl/certs/`). Without this, every HTTPS request through the proxy fails with a certificate verification error.

> **Note:** Applications with certificate pinning will fail even with the CA trusted. This is expected — interception requires replacing the origin certificate.

## Non-CONNECT Relay Path
## Non-CONNECT relay path

Plain HTTP requests (no TLS) bypass the interception flow entirely. Gatekeeper reads the request directly, injects credentials, and forwards it using a standard `http.Transport`. No certificate generation occurs.

Expand Down
14 changes: 7 additions & 7 deletions docs/content/concepts/02-credential-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ description: "How Gatekeeper matches hostnames, injects authentication headers,
keywords: ["gatekeeper", "credential injection", "host matching", "authorization headers"]
---

# Credential Injection
# Credential injection

Gatekeeper injects authentication headers into proxied HTTP requests based on hostname matching. Clients never handle raw credentials — they send requests through the proxy, which resolves the correct credential and sets the appropriate header before forwarding to the upstream server.

## Host Matching
## Host matching

Each credential is configured with a `host` pattern. When gatekeeper intercepts a request, it looks up credentials for the target hostname, stripping any port from the request host unconditionally before comparing — there is no notion of a default or matched port in credential lookup.

Expand All @@ -23,7 +23,7 @@ A `host` pattern cannot contain a port — `credentials[].host` is validated and

Host comparison is case-insensitive. `API.GitHub.com` matches `api.github.com`.

## Header Injection
## Header injection

The default injection header is `Authorization`. Override it with the `header` field:

Expand All @@ -42,7 +42,7 @@ Gatekeeper injects credentials in two modes:

2. **Auto-injection.** If the client sends no matching header, gatekeeper injects the credential unconditionally. When multiple credentials share the same header name for a host, the `claude` grant is deprioritized — it is only injected when the client explicitly sends a placeholder.

## Grant Names
## Grant names

The `grant` field is an optional label that identifies a credential for logging and MCP relay matching. Grant names appear in canonical log lines and OpenTelemetry span attributes.

Expand All @@ -57,7 +57,7 @@ credentials:

Built-in grant names (`github`, `anthropic`, `openai`, `aws`, and others) map to predefined host patterns. These mappings are used by network policy to auto-allow hosts for configured grants.

## Prefix and Format
## Prefix and format

For `Authorization` headers, gatekeeper ensures the value includes an auth scheme prefix. The behavior depends on configuration:

Expand All @@ -77,7 +77,7 @@ credentials:
var: GITHUB_TOKEN
```

## Multiple Credentials Per Host
## Multiple credentials per host

A host can have multiple credential entries with different header names. All matching credentials are injected:

Expand All @@ -98,6 +98,6 @@ credentials:

When multiple credentials share the same header name, placeholder replacement takes priority. If no placeholder matched, auto-injection picks the non-`claude` grant to avoid overriding explicit OAuth flows.

## Credential Stripping
## Credential stripping

Gatekeeper removes `Proxy-Authorization` and `Proxy-Connection` headers from all forwarded requests. These are hop-by-hop headers used between the client and the proxy — they must never reach the upstream server. Injected credential headers (like `Authorization`) are also redacted in log output to prevent credential leakage.
14 changes: 7 additions & 7 deletions docs/content/concepts/03-credential-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ description: "How Gatekeeper resolves credentials from pluggable backends includ
keywords: ["gatekeeper", "credential sources", "background refresh", "credential resolver"]
---

# Credential Sources
# Credential sources

Gatekeeper resolves credentials from pluggable backends called **credential sources**. Each source implements a single method — `Fetch` — that returns a credential value. Sources range from simple (read an environment variable) to complex (exchange tokens with an external STS).

## The Source Interface
## The source interface

All credential sources implement `CredentialSource`:

Expand All @@ -21,7 +21,7 @@ type CredentialSource interface {

`Fetch` retrieves the current credential value. It accepts a context for cancellation and timeout — gatekeeper enforces a 10-second timeout on all startup fetches. `Type` returns a string identifier for logging (e.g., `"env"`, `"aws-secretsmanager"`).

## Static vs Dynamic Sources
## Static vs dynamic sources

**Static sources** return the same value on every call. They are fetched once at startup and cached:

Expand All @@ -40,7 +40,7 @@ type CredentialSource interface {
| `gcp-service-account` | key location, `scopes` | Mints GCP OAuth2 access tokens from a service account key |
| `github-app` | `app_id`, `installation_id`, private key | Generates GitHub App installation tokens |

## RefreshingSource and Background Refresh
## RefreshingSource and background refresh

Sources whose credentials expire implement `RefreshingSource`:

Expand All @@ -59,7 +59,7 @@ type RefreshingSource interface {

The `github-app` and `gcp-service-account` sources are `RefreshingSource`s. Both produce tokens that expire after one hour, so gatekeeper refreshes them every 45 minutes. The `process` source is also a `RefreshingSource`: when its command output is AWS `credential_process`-format JSON, TTL is the time until the embedded `Expiration`; otherwise it reports a fixed interval (`ttl` in the source config, default 5 minutes).

## Source Deduplication
## Source deduplication

When multiple credential entries share the same `SourceConfig` (identical `type`, `var`, `secret`, etc.), gatekeeper fetches the credential once and applies it to all matching hosts. A single background refresh goroutine updates every host that shares the source.

Expand All @@ -85,7 +85,7 @@ credentials:

Both entries share the same `github-app` source. Gatekeeper makes one API call to GitHub, generates one installation token, and applies it to both `api.github.com` (as `Bearer`) and `github.com` (as `Basic x-access-token:token`).

## CredentialResolver for Dynamic Resolution
## CredentialResolver for dynamic resolution

Some credential flows require per-request context — for example, RFC 8693 token exchange, where the proxy exchanges a caller's identity token for a scoped access token. These flows use `CredentialResolver` instead of `CredentialSource`:

Expand All @@ -97,7 +97,7 @@ Unlike static sources (fetched once at startup), resolvers are called on every r

The `token-exchange` source type creates a `CredentialResolver`. All other source types create a `CredentialSource`.

## Error Handling
## Error handling

Credential source errors at startup are fatal — gatekeeper refuses to start if any `Fetch` call fails. This fail-fast behavior prevents the proxy from running without required credentials.

Expand Down
Loading
Loading