From d5015733bfc5fa41c8e0a4dad597f0f5cbd3de95 Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Fri, 15 May 2026 21:34:03 +0000 Subject: [PATCH 1/9] docs: add documentation site content Add structured documentation matching moat/keep's four-section taxonomy (Getting Started, Concepts, Guides, Reference) with 25 content pages, a style guide, and README. Getting Started: introduction, installation, quick start Concepts: TLS interception, credential injection, credential sources, network policy, MCP relay, observability, host gateway Guides: CA setup, env/AWS/GCP/GitHub App/token-exchange credentials, network lockdown, OpenTelemetry, Go library usage, WebSocket support Reference: CLI, config file schema, credential sources, environment variables, LLM policy Co-Authored-By: Claude Opus 4.6 --- docs/README.md | 84 ++++ docs/STYLE-GUIDE.md | 284 ++++++++++++++ docs/content/concepts/01-tls-interception.md | 77 ++++ .../concepts/02-credential-injection.md | 108 +++++ .../content/concepts/03-credential-sources.md | 102 +++++ docs/content/concepts/04-network-policy.md | 74 ++++ docs/content/concepts/05-mcp-relay.md | 71 ++++ docs/content/concepts/06-observability.md | 96 +++++ docs/content/concepts/07-host-gateway.md | 61 +++ .../getting-started/01-introduction.md | 47 +++ .../getting-started/02-installation.md | 54 +++ .../content/getting-started/03-quick-start.md | 100 +++++ docs/content/guides/01-ca-setup.md | 100 +++++ .../guides/02-environment-credentials.md | 79 ++++ docs/content/guides/03-aws-secrets-manager.md | 93 +++++ docs/content/guides/04-gcp-secret-manager.md | 96 +++++ docs/content/guides/05-github-app-tokens.md | 114 ++++++ docs/content/guides/06-token-exchange.md | 226 +++++++++++ docs/content/guides/07-network-lockdown.md | 108 +++++ docs/content/guides/08-opentelemetry.md | 97 +++++ docs/content/guides/09-go-library.md | 123 ++++++ docs/content/guides/10-websockets.md | 76 ++++ docs/content/reference/01-cli.md | 72 ++++ docs/content/reference/02-config-file.md | 370 ++++++++++++++++++ .../reference/03-credential-sources.md | 316 +++++++++++++++ docs/content/reference/04-environment.md | 107 +++++ docs/content/reference/05-llm-policy.md | 76 ++++ 27 files changed, 3211 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/STYLE-GUIDE.md create mode 100644 docs/content/concepts/01-tls-interception.md create mode 100644 docs/content/concepts/02-credential-injection.md create mode 100644 docs/content/concepts/03-credential-sources.md create mode 100644 docs/content/concepts/04-network-policy.md create mode 100644 docs/content/concepts/05-mcp-relay.md create mode 100644 docs/content/concepts/06-observability.md create mode 100644 docs/content/concepts/07-host-gateway.md create mode 100644 docs/content/getting-started/01-introduction.md create mode 100644 docs/content/getting-started/02-installation.md create mode 100644 docs/content/getting-started/03-quick-start.md create mode 100644 docs/content/guides/01-ca-setup.md create mode 100644 docs/content/guides/02-environment-credentials.md create mode 100644 docs/content/guides/03-aws-secrets-manager.md create mode 100644 docs/content/guides/04-gcp-secret-manager.md create mode 100644 docs/content/guides/05-github-app-tokens.md create mode 100644 docs/content/guides/06-token-exchange.md create mode 100644 docs/content/guides/07-network-lockdown.md create mode 100644 docs/content/guides/08-opentelemetry.md create mode 100644 docs/content/guides/09-go-library.md create mode 100644 docs/content/guides/10-websockets.md create mode 100644 docs/content/reference/01-cli.md create mode 100644 docs/content/reference/02-config-file.md create mode 100644 docs/content/reference/03-credential-sources.md create mode 100644 docs/content/reference/04-environment.md create mode 100644 docs/content/reference/05-llm-policy.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..fcb935f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,84 @@ +# Gatekeeper Documentation + +## Contents + +### Getting Started + +- [Introduction](./content/getting-started/01-introduction.md) — What Gatekeeper does, key capabilities, the CONNECT flow +- [Installation](./content/getting-started/02-installation.md) — Install via go install, build from source, or Docker +- [Quick Start](./content/getting-started/03-quick-start.md) — Inject your first credential in five minutes + +### Concepts + +- [TLS Interception](./content/concepts/01-tls-interception.md) — Why MITM is necessary, per-host certificate generation, CA trust +- [Credential Injection](./content/concepts/02-credential-injection.md) — Host matching, header injection, grant names, prefix/format options +- [Credential Sources](./content/concepts/03-credential-sources.md) — Source interface, static vs dynamic, background refresh, deduplication +- [Network Policy](./content/concepts/04-network-policy.md) — Permissive vs strict modes, allow lists, blocked response format +- [MCP Relay](./content/concepts/05-mcp-relay.md) — MCP request proxying, SSE streaming, tool credential injection +- [Observability](./content/concepts/06-observability.md) — OTel instrumentation, canonical log lines, request ID tracking +- [Host Gateway](./content/concepts/07-host-gateway.md) — Synthetic hostname mapping, loopback equivalence, port-based access control + +### Guides + +- [CA Setup](./content/guides/01-ca-setup.md) — Generate a CA certificate and trust it on your system +- [Environment Credentials](./content/guides/02-environment-credentials.md) — Inject credentials from environment variables +- [AWS Secrets Manager](./content/guides/03-aws-secrets-manager.md) — Fetch credentials from AWS Secrets Manager +- [GCP Secret Manager](./content/guides/04-gcp-secret-manager.md) — Fetch credentials from Google Cloud Secret Manager +- [GitHub App Tokens](./content/guides/05-github-app-tokens.md) — Auto-refreshing short-lived GitHub installation tokens +- [Token Exchange](./content/guides/06-token-exchange.md) — Per-user credential resolution via RFC 8693 +- [Network Lockdown](./content/guides/07-network-lockdown.md) — Restrict proxy traffic to specific hosts +- [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 + +### Reference + +- [CLI](./content/reference/01-cli.md) — Command-line flags, exit codes, signals, health endpoint +- [Config File](./content/reference/02-config-file.md) — Complete gatekeeper.yaml schema reference +- [Credential Sources](./content/reference/03-credential-sources.md) — Per-source-type field reference +- [Environment Variables](./content/reference/04-environment.md) — OTEL_*, AWS, GCP, and proxy environment variables +- [LLM Policy](./content/reference/05-llm-policy.md) — Keep integration for Anthropic API response evaluation + +--- + +## Directory Structure + +``` +docs/ + README.md # This file + STYLE-GUIDE.md # Writing guidelines + content/ # User-facing documentation + getting-started/ + concepts/ + guides/ + reference/ +``` + +## Frontmatter Schema + +Each documentation file includes YAML frontmatter: + +```yaml +--- +title: "Page Title" +description: "Brief description for SEO and previews" +keywords: ["gatekeeper", "keyword1", "keyword2"] +--- +``` + +The following are inferred from the file path: +- **slug** — From filename (e.g., `01-introduction.md` → `introduction`) +- **section** — From parent directory (e.g., `getting-started/`) +- **order** — From numeric prefix (e.g., `01-`, `02-`) +- **prev/next** — From adjacent files in the same directory + +## Writing Guidelines + +See [STYLE-GUIDE.md](./STYLE-GUIDE.md) for voice, tone, and formatting conventions. + +Summary: +1. **Be objective** — State facts, avoid hyperbole +2. **Be respectful** — Don't disparage other tools +3. **Be factual** — Make specific, verifiable claims +4. **Be practical** — Lead with examples, explain after +5. **Test examples** — All code examples should work as written diff --git a/docs/STYLE-GUIDE.md b/docs/STYLE-GUIDE.md new file mode 100644 index 0000000..2744666 --- /dev/null +++ b/docs/STYLE-GUIDE.md @@ -0,0 +1,284 @@ +# Documentation Style Guide + +This guide establishes the voice, tone, and conventions for Gatekeeper documentation. Follow these guidelines to ensure consistency across all pages. + +## Voice and Tone + +### Be Objective +State facts. Avoid hyperbole, marketing language, and subjective claims. + +| Avoid | Prefer | +|-------|--------| +| "Gatekeeper makes credential management incredibly easy" | "Gatekeeper injects credentials at the network layer" | +| "The blazingly fast proxy" | "The proxy adds ~2ms latency per request" | +| "Finally, a solution that actually works" | (Just describe what it does) | +| "Unlike other tools that get this wrong..." | (Describe Gatekeeper's approach without comparison) | + +Don't use words like: revolutionary, game-changing, seamless, effortless, simple (as a claim), easy (as a claim), powerful, robust, elegant, beautiful, magic/magical. + +### Be Respectful +Acknowledge that other tools exist and serve their purposes. Avoid dismissive comparisons. + +When comparing approaches, describe what Gatekeeper does and let readers draw their own conclusions. Don't tell them what's wrong with their current workflow. + +### Be Factual +Make specific, verifiable claims. Avoid generalizations and euphemisms. + +| Avoid | Prefer | +|-------|--------| +| "Credentials are kept secure" | "Credentials are resolved at the network layer and never stored in container environment variables" | +| "Full visibility into what happened" | "Canonical log lines record method, host, path, status, duration, and credential injection details per request" | +| "Enterprise-grade security" | (Describe the specific security properties) | + +If you can't point to a specific mechanism, the claim is too vague. + +### Be Direct +Write in active voice. State what things do, not what they "can" or "may" do. + +| Avoid | Prefer | +|-------|--------| +| "You can use the `host` field to match requests" | "The `host` field matches requests" | +| "Gatekeeper may automatically detect the token prefix" | "Gatekeeper detects the token prefix automatically" | +| "It is possible to configure multiple credential sources" | "Configure multiple credential sources" | + +### Be Concise +Eliminate filler words. Every sentence should convey information. + +| Avoid | Prefer | +|-------|--------| +| "In order to start the proxy, you need to..." | "To start the proxy..." | +| "It's important to note that tokens are never..." | "Tokens are never..." | +| "Basically, what happens is that the proxy..." | "The proxy..." | + +### Be Precise +Use specific terms consistently. Avoid synonyms that create ambiguity. + +| Term | Definition | Don't use | +|------|------------|-----------| +| **credential source** | A backend that provides a credential value | provider, backend, fetcher | +| **grant** | A named label for a credential, used in logging and network policy | permission, access | +| **inject** | Add credentials at the network layer | pass, provide, supply | +| **intercept** | Terminate and re-establish TLS to read plaintext requests | decrypt, unwrap | + +### Be Practical +Lead with what users need to do, not theory. Show working examples first, explain after. + +```markdown + +Gatekeeper uses a TLS-intercepting proxy to inject credentials. The proxy +terminates the client's TLS, reads the plaintext request, and adds +Authorization headers. To use this feature: + + +Configure a credential source in gatekeeper.yaml: + + credentials: + - host: api.github.com + source: + type: env + var: GITHUB_TOKEN + +The token is injected at the network layer—it never appears in the +client command. +``` + +### Be Honest About Limitations +Document what Gatekeeper doesn't do, edge cases, and known issues. Users trust documentation that acknowledges limitations. + +```markdown + +> **Note:** Gatekeeper fetches the secret once at startup. To pick up +> a rotated secret, restart the proxy. + + +Applications with certificate pinning will fail even with the CA +trusted. This is expected—interception requires replacing the origin +certificate. +``` + +## Formatting Conventions + +### Headings +- Use sentence case: "Getting started" not "Getting Started" +- Keep headings short (under 6 words when possible) +- Don't skip levels (h2 → h4) + +### Code Blocks +Always specify the language for syntax highlighting: + +````markdown +```bash +gatekeeper --config gatekeeper.yaml +``` + +```yaml +credentials: + - host: api.github.com + source: + type: env + var: GITHUB_TOKEN +``` + +```go +ca, _ := proxy.LoadCA(certPEM, keyPEM) +``` +```` + +Use `text` for log output and ASCII diagrams. + +### Inline Code +Use backticks for: +- Commands: `gatekeeper` +- Flags: `--config` +- File names: `gatekeeper.yaml` +- Environment variables: `OTEL_EXPORTER_OTLP_ENDPOINT` +- Field names: `source.type`, `host` +- Values: `"strict"`, `"permissive"` +- Header names: `Authorization` + +Don't use backticks for: +- Product names: Gatekeeper, Docker, GitHub +- General concepts: credential injection, TLS interception + +### File Paths +- Use relative paths when referring to project files: `./gatekeeper.yaml` +- Use absolute paths only when necessary for system paths + +### Lists +Use bullet lists for unordered items. Use numbered lists only for sequential steps. + +### Tables +Use tables for structured comparisons and field definitions. Keep cells concise. + +### Admonitions +Use blockquotes with bold labels for callouts: + +```markdown +> **Note:** Additional context that's helpful but not critical. + +> **Warning:** Something that could cause problems if ignored. +``` + +## Content Guidelines + +### Show Real Output +When documenting commands, use realistic output that matches what users will see. Test commands before documenting them. + +### Explain the "Why" +Don't just show what to do—briefly explain why it matters. + +### Link to Related Content +Cross-reference related pages. Use relative links: + +```markdown +See [Credential Sources](../concepts/03-credential-sources.md) for details +on how the refresh lifecycle works. +``` + +### Credential Safety +Never log or display real credential values. Use placeholders: +- `ghp_xxxx` for GitHub tokens +- `sk-xxxx` for API keys +- `Bearer ghp_xxxx` for Authorization headers +- `my-secret-token` for generic placeholders + +### Error Messages +When documenting errors, show the full error message and explain how to resolve it. + +## Section Definitions + +The documentation has four sections. Each serves a distinct purpose. + +### Getting Started + +**Purpose:** Onboard new users from install to first successful proxy run. + +**Audience:** Someone who has never used Gatekeeper. + +**Contains:** Installation instructions, a guided walkthrough, and orientation material. Pages are sequential—each builds on the previous one. + +**Does not contain:** Deep explanations, exhaustive configuration options, or advanced workflows. + +### Concepts + +**Purpose:** Explain *how things work* and *why they are designed that way*. Build mental models. + +**Audience:** Someone who wants to understand the system, not accomplish a specific task. + +**Contains:** Architecture, design decisions, trade-offs, data flow descriptions. Describes mechanisms and explains rationale. + +**Does not contain:** Step-by-step instructions or exhaustive configuration tables. Link to guides for "how" and reference for "all options." + +**Test:** If you removed all code blocks and the page still makes sense, it's a concept page. + +### Guides + +**Purpose:** Help users accomplish specific tasks. Answer "how do I do X?" + +**Audience:** Someone who has a goal and needs steps to reach it. + +**Contains:** Prerequisites, step-by-step instructions, working examples, verification steps. May include brief context (3-5 sentences) to orient the reader, but the bulk is procedural. + +**Does not contain:** Deep architectural explanations or exhaustive option tables. + +**Test:** The page should read as a recipe. A reader should be able to follow it start-to-finish and achieve a result. + +### Reference + +**Purpose:** Provide complete, structured specifications. Answer "what are all the options?" + +**Audience:** Someone who knows what they want to do and needs exact syntax, fields, or values. + +**Contains:** Configuration schemas with all fields, environment variable tables, format specifications. Every option documented with type, default, and description. + +**Does not contain:** Extended explanations or guided workflows. + +**Test:** The page should work as a lookup table. A reader should be able to find any option in under 10 seconds. + +## Frontmatter Template + +Every documentation page should start with this frontmatter: + +```yaml +--- +title: "Page Title" +description: "One sentence description for SEO and link previews." +keywords: ["gatekeeper", "relevant", "keywords"] +--- +``` + +The following are inferred from the file path and don't need to be specified: +- `slug` — From filename (e.g., `01-introduction.md` → `introduction`) +- `section` — From parent directory +- `order` — From numeric prefix +- `prev`/`next` — From adjacent files + +## Terminology + +### Capitalize +- Gatekeeper (the product) +- Docker +- GitHub, GitLab +- macOS, Linux, Windows + +### Don't Capitalize +- container, proxy +- credential, token, grant +- network policy +- audit log, trace + +### Abbreviations +Spell out on first use, then use abbreviation: + +- TLS (Transport Layer Security) +- CLI (command-line interface) +- API (application programming interface) +- CA (Certificate Authority) +- MCP (Model Context Protocol) +- STS (Security Token Service) +- OTel (OpenTelemetry) + +Common abbreviations that don't need expansion: +- URL, HTTP, HTTPS +- JSON, YAML +- ID (identifier) diff --git a/docs/content/concepts/01-tls-interception.md b/docs/content/concepts/01-tls-interception.md new file mode 100644 index 0000000..10c37e4 --- /dev/null +++ b/docs/content/concepts/01-tls-interception.md @@ -0,0 +1,77 @@ +--- +title: "TLS Interception" +description: "How Gatekeeper terminates TLS connections, generates per-host certificates, and enables credential injection into HTTPS requests." +keywords: ["gatekeeper", "TLS interception", "MITM proxy", "certificate generation"] +--- + +# 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 + +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 + +When a client sends an HTTPS request through gatekeeper, the flow has five stages: + +1. **CONNECT request.** The client sends `CONNECT api.github.com:443 HTTP/1.1` to the proxy. +2. **Network policy check.** Gatekeeper evaluates the target host against allow/deny rules. If denied, a `407` response is returned immediately. +3. **Tunnel establishment.** Gatekeeper responds with `HTTP/1.1 200 Connection Established` and hijacks the raw TCP connection. +4. **TLS handshake with the client.** Gatekeeper generates a certificate for `api.github.com` signed by its CA, then performs a TLS handshake as the server. The client validates the certificate against the CA it trusts. +5. **Request interception.** Gatekeeper reads the plaintext HTTP request, injects credential headers, opens a separate TLS connection to the real `api.github.com`, and forwards the request. + +```text +Client Gatekeeper api.github.com + |--- CONNECT :443 ------->| | + |<-- 200 Connected --------| | + |--- TLS handshake ------->| (CA-signed cert) | + |--- GET /repos (plain) -->| | + | |-- inject Authorization ------>| + | |--- TLS handshake ------------>| + | |--- GET /repos (encrypted) --->| + | |<-- 200 OK --------------------| + |<-- 200 OK ---------------| | +``` + +## Two Separate TLS Connections + +The proxy maintains two independent TLS sessions per intercepted request: + +| Connection | Endpoint | Certificate | +|---|---|---| +| Client-side | Client to gatekeeper | Dynamically generated, signed by gatekeeper's CA | +| Server-side | Gatekeeper to origin | Origin server's real certificate, verified against system roots | + +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 + +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. +- 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. + +## 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 + +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. + +The relay path (`/relay/{name}/{path}`) handles a special case: when the target host is in `NO_PROXY` (e.g., a host-side service reachable at the same address as the proxy), direct connections bypass the proxy. The relay endpoint accepts direct HTTP requests, injects credentials, and forwards to the configured target URL. + +## Without a CA + +When no CA is configured, gatekeeper cannot perform TLS interception. CONNECT tunnels pass through as opaque TCP streams — the proxy relays bytes without reading them. Credential injection is impossible for HTTPS traffic in this mode. Per-path network rules also cannot be enforced, since the proxy cannot see the HTTP request inside the encrypted tunnel. diff --git a/docs/content/concepts/02-credential-injection.md b/docs/content/concepts/02-credential-injection.md new file mode 100644 index 0000000..a3dd6d4 --- /dev/null +++ b/docs/content/concepts/02-credential-injection.md @@ -0,0 +1,108 @@ +--- +title: "Credential Injection" +description: "How Gatekeeper matches hostnames, injects authentication headers, and handles multiple credentials per host." +keywords: ["gatekeeper", "credential injection", "host matching", "authorization headers"] +--- + +# 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 + +Each credential is configured with a `host` pattern. When gatekeeper intercepts a request, it matches the target hostname against configured patterns to determine which credentials to inject. + +Matching rules: + +| Pattern | Matches | Does Not Match | +|---|---|---| +| `api.github.com` | `api.github.com` | `github.com`, `foo.api.github.com` | +| `*.github.com` | `api.github.com`, `foo.bar.github.com` | `github.com` | +| `api.example.com:8080` | `api.example.com:8080` | `api.example.com:443` | + +Port handling: + +- Patterns without an explicit port match only ports 80 and 443 (the standard HTTP/HTTPS ports). +- Patterns with an explicit port match that port exactly. +- Port numbers are stripped from the request host before hostname comparison — `api.github.com:443` matches a pattern for `api.github.com`. + +Host comparison is case-insensitive. `API.GitHub.com` matches `api.github.com`. + +## Header Injection + +The default injection header is `Authorization`. Override it with the `header` field: + +```yaml +credentials: + - host: api.example.com + header: x-api-key + source: + type: env + var: EXAMPLE_API_KEY +``` + +Gatekeeper injects credentials in two modes: + +1. **Placeholder replacement.** If the client sends a request with the target header already set (e.g., a stub `Authorization` value), gatekeeper replaces it with the real credential. This lets the client choose which credential to use when multiple grants target the same host. + +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 + +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. + +```yaml +credentials: + - host: api.github.com + grant: github + source: + type: env + var: GITHUB_TOKEN +``` + +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 + +For `Authorization` headers, gatekeeper ensures the value includes an auth scheme prefix. The behavior depends on configuration: + +- **No prefix, no format.** Gatekeeper auto-detects the scheme from known token prefixes. GitHub `ghp_` and `ghs_` tokens get `token` scheme. GitHub `gho_` and `github_pat_` tokens get `Bearer`. Everything else defaults to `Bearer`. +- **Explicit prefix.** The `prefix` value is prepended with a space: `prefix: "token"` produces `token sk-xxxx`. +- **Basic format.** Set `format: basic` to produce HTTP Basic authentication. The `prefix` field becomes the username: `Basic base64(prefix:value)`. + +```yaml +# HTTP Basic auth for git smart HTTP +credentials: + - host: github.com + format: basic + prefix: x-access-token + grant: github + source: + type: env + var: GITHUB_TOKEN +``` + +## Multiple Credentials Per Host + +A host can have multiple credential entries with different header names. All matching credentials are injected: + +```yaml +credentials: + - host: api.anthropic.com + header: x-api-key + grant: anthropic + source: + type: env + var: ANTHROPIC_API_KEY + - host: api.anthropic.com + header: anthropic-beta + source: + type: static + value: "prompt-caching-2024-07-31" +``` + +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 + +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. diff --git a/docs/content/concepts/03-credential-sources.md b/docs/content/concepts/03-credential-sources.md new file mode 100644 index 0000000..149adbb --- /dev/null +++ b/docs/content/concepts/03-credential-sources.md @@ -0,0 +1,102 @@ +--- +title: "Credential Sources" +description: "How Gatekeeper resolves credentials from pluggable backends including environment variables, secret managers, and token exchange." +keywords: ["gatekeeper", "credential sources", "background refresh", "credential resolver"] +--- + +# 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 + +All credential sources implement `CredentialSource`: + +```go +type CredentialSource interface { + Fetch(ctx context.Context) (string, error) + Type() string +} +``` + +`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 sources** return the same value on every call. They are fetched once at startup and cached: + +| Source | Config | Behavior | +|---|---|---| +| `env` | `var: GITHUB_TOKEN` | Reads the environment variable at startup | +| `static` | `value: "sk-xxxx"` | Returns the literal value | + +**Dynamic sources** fetch from external systems and may return different values over time: + +| Source | Config | Behavior | +|---|---|---| +| `aws-secretsmanager` | `secret: my-secret`, `region: us-east-1` | Fetches from AWS Secrets Manager | +| `gcp-secretmanager` | `secret: my-secret`, `project: my-project` | Fetches from GCP Secret Manager | +| `github-app` | `app_id`, `installation_id`, private key | Generates GitHub App installation tokens | + +## RefreshingSource and Background Refresh + +Sources whose credentials expire implement `RefreshingSource`: + +```go +type RefreshingSource interface { + CredentialSource + TTL() time.Duration +} +``` + +`TTL` returns the duration until the most recently fetched credential expires. Gatekeeper uses this to schedule background refresh: + +- **Refresh interval.** 75% of TTL, with a floor of 30 seconds. A token with a 60-minute TTL refreshes every 45 minutes. +- **Failure backoff.** On fetch failure, gatekeeper retries with exponential backoff starting at 1 second, doubling each attempt, capped at 60 seconds. A random jitter (up to 25% of the backoff) is added to prevent thundering herds. +- **Hot-swap.** Refreshed credentials are applied to the proxy immediately via `SetCredentialWithGrant`. In-flight requests use the previous value; subsequent requests use the new one. + +The `github-app` source is a `RefreshingSource`. GitHub App installation tokens expire after one hour, so gatekeeper refreshes them every 45 minutes. + +## 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. + +```yaml +credentials: + - host: api.github.com + grant: github + source: + type: github-app + app_id: "12345" + installation_id: "67890" + private_key_path: ./key.pem + - host: github.com + grant: github + format: basic + prefix: x-access-token + source: + type: github-app + app_id: "12345" + installation_id: "67890" + private_key_path: ./key.pem +``` + +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 + +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`: + +```go +type CredentialResolver func(ctx context.Context, proxyReq, innerReq *http.Request, host string) ([]credentialHeader, error) +``` + +Unlike static sources (fetched once at startup), resolvers are called on every request. They receive both the proxy-level request (`proxyReq`, carrying `Proxy-Authorization`) and the application-level request (`innerReq`, which the resolver may inspect and modify). This enables patterns like extracting a subject identity header from the request, exchanging it for an access token, and stripping the identity header before forwarding. + +The `token-exchange` source type creates a `CredentialResolver`. All other source types create a `CredentialSource`. + +## 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. + +During background refresh, errors are logged and retried with backoff. The previous credential value remains in use until a successful refresh replaces it. diff --git a/docs/content/concepts/04-network-policy.md b/docs/content/concepts/04-network-policy.md new file mode 100644 index 0000000..f98f653 --- /dev/null +++ b/docs/content/concepts/04-network-policy.md @@ -0,0 +1,74 @@ +--- +title: "Network Policy" +description: "How Gatekeeper enforces network access control with permissive and strict modes, allow lists, and per-path rules." +keywords: ["gatekeeper", "network policy", "allow list", "strict mode"] +--- + +# Network Policy + +Gatekeeper enforces network policy to control which hosts a client can reach through the proxy. Policy evaluation happens before credential injection — blocked requests never receive credentials. + +## Permissive vs Strict + +The `policy` field controls the default behavior: + +| Mode | Behavior | +|---|---| +| `permissive` | All hosts are allowed. This is the default. | +| `strict` | Only hosts in the allow list are reachable. All other requests are denied with a `407` response. | + +```yaml +network: + policy: strict + allow: + - api.github.com + - "*.anthropic.com" +``` + +In permissive mode, the allow list is ignored. All traffic passes through. + +## Allow List Mechanics + +The allow list is a set of host patterns. Each pattern follows the same matching rules as credential host patterns: + +- **Exact match.** `api.github.com` matches only `api.github.com`. +- **Wildcard.** `*.github.com` matches any subdomain: `api.github.com`, `raw.githubusercontent.com` does not match (different base domain), but `foo.bar.github.com` does. +- **Port-specific.** `api.example.com:8080` matches only that port. Patterns without a port match only ports 80 and 443. + +When a client sends a CONNECT request, gatekeeper extracts the host and port and checks them against the allow list. If no pattern matches, the tunnel is refused. + +## Grant Hosts Are Auto-Allowed + +When callers pass grant names to `SetNetworkPolicy`, gatekeeper expands each grant to its known host patterns and adds them to the allow list automatically. This is used by moat's daemon layer, which passes per-run grants when registering runs. + +For example, the `github` grant expands to: + +- `github.com` +- `api.github.com` +- `*.githubusercontent.com` +- `*.github.com` + +In standalone mode (`gatekeeper.yaml`), grant expansion does not apply — only the explicit `allow` list is used. Add credential hosts to the allow list manually when using strict mode. + +## Interaction with Credential Injection + +Network policy and credential injection are independent checks that run in sequence: + +1. **Network policy** runs first. If the host is denied, the request is blocked with a `407` response. No credential lookup occurs. +2. **Credential injection** runs second, only for allowed requests. The proxy matches the host against credential patterns and injects headers. + +This ordering has a security property: credentials are never sent to unauthorized hosts. Even if a credential pattern matches a host that is blocked by network policy, the credential is never injected because the request never reaches the injection step. + +## Per-Path Rules + +When gatekeeper has path-level rules (configured via `RequestChecker`), it evaluates them on the inner HTTP request after TLS interception — not on the CONNECT tunnel. The CONNECT request only carries the host, not the path. Gatekeeper intercepts the tunnel, reads the plaintext request, and then checks `method` and `path` against the rules. + +> **Note:** Per-path rules require TLS interception (a CA must be configured). Without interception, only host-level allow/deny applies. Gatekeeper logs a warning if path rules are configured but the CA is missing. + +## Host Gateway Policy + +When a request targets a host gateway address (synthetic hostname or loopback), gatekeeper applies a separate check: the destination port must be in the run's `AllowedHostPorts` list. This prevents containers from reaching arbitrary services on the host machine. See [Host Gateway](./07-host-gateway.md) for details. + +## Blocked Response Format + +Blocked requests receive a `407 Proxy Authentication Required` response with a `Proxy-Authenticate: Moat-Policy` header and a plaintext body explaining which host was denied. The `X-Moat-Blocked` header indicates the denial reason (`request-rule` for network policy, `host-service` for host gateway blocks). diff --git a/docs/content/concepts/05-mcp-relay.md b/docs/content/concepts/05-mcp-relay.md new file mode 100644 index 0000000..2de62be --- /dev/null +++ b/docs/content/concepts/05-mcp-relay.md @@ -0,0 +1,71 @@ +--- +title: "MCP Relay" +description: "How Gatekeeper relays Model Context Protocol requests to remote MCP servers with credential injection and SSE streaming." +keywords: ["gatekeeper", "MCP relay", "model context protocol", "SSE streaming"] +--- + +# MCP Relay + +Gatekeeper relays Model Context Protocol (MCP) requests to remote MCP servers with credential injection. MCP clients that cannot route traffic through an HTTP proxy connect to gatekeeper's relay endpoint directly, and gatekeeper forwards requests to the real MCP server with authentication headers attached. + +## What MCP Relay Does + +MCP servers often require authentication — an API key, OAuth token, or other credential. The MCP relay solves two problems: + +1. **Credential injection for MCP.** The client sends requests to gatekeeper without credentials. Gatekeeper looks up the configured grant for the target MCP server and injects the real credential before forwarding. +2. **Proxy bypass.** Some MCP clients do not respect `HTTP_PROXY` settings. The relay endpoint (`/mcp/{server-name}`) accepts direct HTTP connections, eliminating the need for proxy-aware clients. + +## Request Flow + +A relay request follows this path: + +1. The client sends a request to `http://proxy-host:port/mcp/{server-name}[/path]`. +2. Gatekeeper matches `{server-name}` against configured `MCPServerConfig` entries. +3. Gatekeeper builds the target URL from the server's configured `URL` field, preserving any sub-path and query string from the original request. +4. If the server has an `Auth` config, gatekeeper resolves the credential by grant name and injects it into the forwarded request header. +5. Gatekeeper forwards the request to the real MCP server and streams the response back to the client. + +```yaml +# MCP server configuration (set via MCPServerConfig) +# name: context7 +# url: https://mcp.context7.com/mcp +# auth: +# grant: mcp-context7 +# header: Authorization +``` + +A request to `/mcp/context7/v1/endpoint` forwards to `https://mcp.context7.com/mcp/v1/endpoint` with the `Authorization` header set to the resolved credential. + +## SSE Streaming + +MCP uses Server-Sent Events (SSE) for streaming responses. Gatekeeper supports this by flushing response data incrementally: + +- After writing response headers, gatekeeper calls `Flush()` on the `http.ResponseWriter` if it implements `http.Flusher`. +- The response body is streamed with `io.Copy`, delivering events to the client as they arrive from the upstream server. + +The relay HTTP client has no client-level timeout — MCP SSE streams are long-lived connections that may remain open indefinitely. + +## Credential Injection Modes + +MCP credential injection works in two modes: + +**Relay mode.** Requests to `/mcp/{server-name}` are proxied directly. Gatekeeper resolves the credential by grant name from `RunContextData.Credentials` (daemon mode) or the `CredentialStore` (standalone mode) and sets the header on the outgoing request. + +**Stub replacement mode.** When an MCP client sends a request through the CONNECT proxy (not the relay endpoint) to an MCP server URL, gatekeeper checks if the authentication header contains a stub value (`moat-stub-{grant}`). If it matches, the stub is replaced with the real credential. Non-stub values are left unchanged — the client may already have a valid credential. + +For OAuth grants (grant names starting with `oauth:`), the credential value is automatically prefixed with `Bearer `. + +## Keep Policy Evaluation + +When a Keep policy engine is configured for an MCP server (keyed as `mcp-{server-name}`), gatekeeper evaluates `tools/call` requests before forwarding: + +- The request body is parsed as JSON to extract the method, tool name, and arguments. +- If the Keep engine returns `Deny`, the request is blocked with a `403` response. +- If the engine returns `Redact`, tool arguments are mutated according to the policy's mutation rules, and the modified request is forwarded. +- Non-JSON request bodies are denied (fail-closed) when a policy is configured. + +## Error Handling + +- Unknown server name: `404` with a message listing available server count. +- Credential resolution failure: `500` with the grant name and a suggested `moat grant` command. +- Upstream connection failure: `502` with the target URL and error details. diff --git a/docs/content/concepts/06-observability.md b/docs/content/concepts/06-observability.md new file mode 100644 index 0000000..d188eb8 --- /dev/null +++ b/docs/content/concepts/06-observability.md @@ -0,0 +1,96 @@ +--- +title: "Observability" +description: "How Gatekeeper produces structured logs, distributed traces, and request metrics via OpenTelemetry." +keywords: ["gatekeeper", "observability", "OpenTelemetry", "metrics", "logging"] +--- + +# Observability + +Gatekeeper produces structured logs, distributed traces, and request metrics via OpenTelemetry. The proxy core has no direct OTel dependency — instrumentation is layered on externally through callbacks and HTTP middleware. + +## Callback-Based Architecture + +The `proxy` package defines two callback types for instrumentation: + +- **`RequestLogger`** — called once per completed request with a `RequestLogData` struct containing method, host, path, status code, duration, injected headers, grant names, denial info, and request context. +- **`PolicyLogger`** — called on each policy denial with scope, operation, rule, and message. + +The `gatekeeper` package (standalone server wiring) sets these callbacks at startup. The callbacks write canonical log lines, enrich OTel spans, and record metrics. This design keeps `proxy/proxy.go` free of OTel imports. + +## OTelHandler Middleware + +`proxy.OTelHandler` wraps the proxy's `http.Handler` with OpenTelemetry tracing and metrics: + +```go +s.proxyServer = &http.Server{ + Handler: proxy.OTelHandler(&healthHandler{next: s.proxy}), +} +``` + +For each request, the handler: + +1. Classifies the request type: `connect`, `mcp`, `relay`, or `http`. +2. Starts a root span with `SpanKindServer` and the span name `proxy.request`, `proxy.mcp`, `proxy.relay`, or `proxy.http`. +3. Sets span attributes: `http.request.method`, `server.address`, `proxy.request.type`. +4. Wraps the `ResponseWriter` with a `statusRecorder` that captures the HTTP status code. +5. After the handler returns, records `proxy.request.duration` (histogram) and `proxy.request.count` (counter) with method, server address, request type, and status code as attributes. + +The `statusRecorder` implements `http.Hijacker` by delegating to the underlying writer. This is critical — CONNECT requests call `Hijack()` to take over the raw connection, and the OTel wrapper must not break this. + +## Canonical Log Lines + +Gatekeeper emits one wide structured log entry per request at completion. Each log line contains all request context in a single record: + +| Field | Description | +|---|---| +| `request_id` | Unique identifier (TypeID with `req` prefix) | +| `http_method` | Request method | +| `http_host` | Target hostname | +| `http_path` | Request path | +| `http_status` | Response status code | +| `duration_ms` | Request duration in milliseconds | +| `proxy_type` | Request classification (`http`, `connect`, `mcp`, `relay`) | +| `credential_injected` | Whether any credential was injected | +| `injected_headers` | Comma-separated list of injected header names | +| `grants` | Comma-separated list of grant names used | +| `denied` | Whether the request was denied by policy | +| `deny_reason` | Denial reason (e.g., `Host not in allow list: example.com`) | +| `run_id` | Per-run identifier (daemon mode) | +| `user_id` | User ID from proxy auth username | + +Log level is determined by outcome: `ERROR` for server errors or transport failures, `WARN` for policy denials or client errors, `INFO` for successful requests. + +## Request ID Tracking + +Every request receives a unique identifier. Gatekeeper checks for an `X-Request-Id` header from the caller. If present, it is reused. Otherwise, gatekeeper generates a TypeID with a `req` prefix (e.g., `req_01h455vb4pex5vsknk084sn02q`). + +The request ID is: + +- Set on the response via `X-Request-Id` header. +- Propagated to upstream servers via `X-Request-Id` on the forwarded request. +- Stored in the request context for extraction by loggers and span enrichment. +- Included in canonical log lines and OTel span events. + +## slog-to-OTel Bridge + +Gatekeeper uses a `multiHandler` to fan out every slog record to two destinations: + +1. The configured slog handler (JSON or text, writing to stderr/stdout/file). +2. An `otelslog.NewHandler("gatekeeper")` that converts slog records to OTel log records, correlating them with the active trace context. + +This ensures that all structured logs — not just request logs — appear in the OTel log pipeline with correct trace and span IDs. + +## Metrics + +Four metrics instruments are registered under the `gatekeeper` meter: + +| Metric | Type | Description | +|---|---|---| +| `proxy.request.duration` | Float64 Histogram (seconds) | Duration of proxy requests | +| `proxy.request.count` | Int64 Counter | Total number of proxy requests | +| `proxy.credential.injections` | Int64 Counter | Credential injections by host and header | +| `proxy.policy.denials` | Int64 Counter | Policy denials by scope and rule | + +## Configuration + +OTel is configured entirely via standard `OTEL_*` environment variables. There are no YAML knobs for tracing, metrics, or logs. The CLI entry point (`cmd/gatekeeper/main.go`) creates OTLP HTTP exporters for traces, metrics, and logs and registers them as global providers. When no `OTEL_*` variables are set, the no-op provider is used and instrumentation has zero overhead. diff --git a/docs/content/concepts/07-host-gateway.md b/docs/content/concepts/07-host-gateway.md new file mode 100644 index 0000000..fa3cb5c --- /dev/null +++ b/docs/content/concepts/07-host-gateway.md @@ -0,0 +1,61 @@ +--- +title: "Host Gateway" +description: "How Gatekeeper maps synthetic hostnames to host machine IPs, enabling containers to reach host services with credential injection." +keywords: ["gatekeeper", "host gateway", "container networking", "loopback equivalence"] +--- + +# Host Gateway + +Gatekeeper's host gateway maps a synthetic hostname (used inside containers) to the host machine's IP address. This enables containers to reach services running on the host while maintaining credential injection and network policy enforcement. + +## What Host Gateway Solves + +Containers cannot reliably address the host machine. Docker provides `host.docker.internal`, but this is not universal — it resolves differently across platforms and may not exist in all runtimes. The host gateway gives each run a consistent hostname that resolves to the host machine's actual IP, allowing gatekeeper to intercept, authorize, and forward the traffic. + +The `RunContextData` struct carries two fields for this: + +| Field | Purpose | +|---|---| +| `HostGateway` | The synthetic hostname the container uses (e.g., `moat-host-gateway`) | +| `HostGatewayIP` | The actual IP address to forward traffic to | + +When a CONNECT request targets the gateway hostname, gatekeeper rewrites the dial address from the synthetic hostname to `HostGatewayIP` before establishing the upstream connection. + +## Synthetic Hostname Mapping + +The container's `/etc/hosts` file maps the synthetic hostname to the proxy's IP. When the container connects to `moat-host-gateway:8080`, the request routes to gatekeeper. Gatekeeper recognizes the hostname as a gateway address, applies network policy, and dials the real host IP. + +```bash +# Inside the container +curl http://moat-host-gateway:8080/api/data +``` + +Gatekeeper intercepts this as a CONNECT (for HTTPS) or plain HTTP request, checks that port 8080 is in `AllowedHostPorts`, and forwards to `{HostGatewayIP}:8080`. + +## Loopback Equivalence + +When `HostGatewayIP` resolves to a loopback address (`127.0.0.1`, `::1`), gatekeeper treats `localhost`, `127.0.0.1`, and `::1` as equivalent to the gateway hostname. This prevents a bypass: without this equivalence, a container can connect directly to `localhost` or `127.0.0.1` to skip network policy that only checks the gateway hostname. + +The equivalence check follows this logic: + +1. If `HostGatewayIP` is set, parse it and check `IsLoopback()`. +2. If `HostGatewayIP` is empty, check whether `HostGateway` itself is a loopback IP. +3. If `HostGateway` is a non-IP hostname (synthetic), assume loopback — synthetic hostnames are injected into container `/etc/hosts` pointing at the host, which is loopback from the proxy's perspective. + +When loopback equivalence is active, credentials configured for the gateway hostname also match requests to `localhost`, `127.0.0.1`, and `::1`. + +## Port-Based Access Control + +Host gateway traffic is not governed by the standard allow/deny list. Instead, each destination port must be explicitly listed in `AllowedHostPorts`. A request to `moat-host-gateway:3000` is allowed only if port 3000 appears in the run's allowed ports. + +This is a security boundary. Without port restrictions, a container can reach any service on the host — databases, admin interfaces, other proxies. The port allowlist limits exposure to explicitly configured services. + +The `AllowedHostPorts` field in `RunContextData` lists the permitted ports. This is configured programmatically — not through `gatekeeper.yaml`. Moat sets this field when registering runs. + +Requests to unlisted ports receive a `407` response with an `X-Moat-Blocked: host-service` header and a message indicating which port was denied and how to allow it. + +## Credential Matching Across Gateway and Loopback + +Credentials configured for the gateway hostname apply to all equivalent addresses when loopback equivalence is active. If a credential targets `moat-host-gateway` and the gateway routes to loopback, that credential also applies to requests targeting `localhost`, `127.0.0.1`, or `::1` on an allowed port. + +The proxy resolves credentials by host after rewriting the dial address but before forwarding. The hostname used for credential lookup is the original hostname from the client's request (the gateway hostname or loopback alias), not the rewritten `HostGatewayIP`. diff --git a/docs/content/getting-started/01-introduction.md b/docs/content/getting-started/01-introduction.md new file mode 100644 index 0000000..77f6eb0 --- /dev/null +++ b/docs/content/getting-started/01-introduction.md @@ -0,0 +1,47 @@ +--- +title: "Introduction" +description: "Overview of Gatekeeper, a standalone credential-injecting TLS-intercepting proxy that transparently injects authentication headers into HTTPS requests." +keywords: ["gatekeeper", "proxy", "credential injection", "TLS interception"] +--- + +# Introduction + +Gatekeeper is a standalone credential-injecting TLS-intercepting proxy. It sits between HTTP clients and upstream servers, transparently injecting authentication headers into proxied HTTPS requests based on hostname matching. Clients route traffic through the proxy and never handle raw credentials directly. + +## Key capabilities + +- **Credential injection** — Resolve credentials from environment variables, static values, AWS Secrets Manager, GCP Secret Manager, or GitHub App tokens, then inject them as HTTP headers for matching hosts. +- **TLS interception** — Man-in-the-middle proxy with per-host certificate generation from a configured CA. The proxy terminates TLS, reads plaintext requests, injects credentials, and forwards to the real server. +- **Multiple credential sources** — Pluggable backend system. Environment variables and static values for development. AWS Secrets Manager, GCP Secret Manager, and GitHub App tokens for production. RFC 8693 token exchange for multi-user deployments. +- **Network policy** — Allow or deny traffic by host pattern. `permissive` mode allows all traffic. `strict` mode denies all traffic except explicitly allowed hosts. +- **MCP relay** — Forward Model Context Protocol requests to upstream servers with credential injection and SSE streaming. +- **Observability** — OpenTelemetry traces, metrics, and logs. Canonical log lines per request. Configured entirely via standard `OTEL_*` environment variables. + +## How it works + +Gatekeeper operates as an HTTP CONNECT proxy. The credential injection flow has five steps: + +1. Client sends `CONNECT host:443` through the proxy (typically via the `HTTP_PROXY` environment variable). +2. Proxy establishes TLS with the client using a dynamically-generated certificate for that host, signed by the configured CA. +3. Proxy reads the plaintext HTTP request from the client. +4. If a credential matches the request host, the proxy injects the configured header (default: `Authorization`). +5. Proxy forwards the request to the real server over a separate TLS connection and streams the response back to the client. + +The client must trust the proxy's CA certificate. Generate one with the included `examples/gen-ca.sh` script or provide an existing CA. + +## Credential source types + +| Source | Type value | Use case | +|---|---|---| +| Environment variable | `env` | Local development, CI | +| Static value | `static` | Fixed API keys | +| AWS Secrets Manager | `aws-secretsmanager` | AWS-hosted credentials | +| GCP Secret Manager | `gcp-secretmanager` | GCP-hosted credentials | +| GitHub App | `github-app` | Short-lived installation tokens with auto-refresh | +| Token exchange | `token-exchange` | Multi-user OAuth via RFC 8693 STS | + +## Relationship to Moat + +Gatekeeper is a general-purpose proxy with no knowledge of Moat. It was extracted from Moat's internal proxy package into a standalone Go module (`github.com/majorcontext/gatekeeper`). + +Moat imports Gatekeeper as a library dependency and adds a daemon layer on top: per-run registration, token-scoped credential contexts, and Unix socket management. Gatekeeper handles the proxy mechanics. Moat handles the multi-tenant orchestration. diff --git a/docs/content/getting-started/02-installation.md b/docs/content/getting-started/02-installation.md new file mode 100644 index 0000000..339f949 --- /dev/null +++ b/docs/content/getting-started/02-installation.md @@ -0,0 +1,54 @@ +--- +title: "Installation" +description: "Install Gatekeeper via go install, build from source, or pull the Docker image." +keywords: ["gatekeeper", "installation", "go install", "docker"] +--- + +# Installation + +## Requirements + +- Go 1.25 or later + +## go install + +```bash +go install github.com/majorcontext/gatekeeper/cmd/gatekeeper@latest +``` + +This places the `gatekeeper` binary in `$GOPATH/bin`. + +## Build from source + +```bash +git clone https://github.com/majorcontext/gatekeeper.git +cd gatekeeper +go build -o gatekeeper ./cmd/gatekeeper/ +``` + +Set the version at build time with linker flags: + +```bash +go build -ldflags "-X main.version=v0.10.0" -o gatekeeper ./cmd/gatekeeper/ +``` + +## Docker + +```bash +docker pull ghcr.io/majorcontext/gatekeeper:latest +``` + +Run with a config file mounted: + +```bash +docker run --rm -v ./gatekeeper.yaml:/etc/gatekeeper/gatekeeper.yaml \ + ghcr.io/majorcontext/gatekeeper --config /etc/gatekeeper/gatekeeper.yaml +``` + +## Verify + +```bash +gatekeeper --config /dev/null +``` + +The binary starts and exits with a config error, confirming it is installed correctly. diff --git a/docs/content/getting-started/03-quick-start.md b/docs/content/getting-started/03-quick-start.md new file mode 100644 index 0000000..0d9d108 --- /dev/null +++ b/docs/content/getting-started/03-quick-start.md @@ -0,0 +1,100 @@ +--- +title: "Quick start" +description: "Start a credential-injecting proxy in under five minutes with a minimal configuration." +keywords: ["gatekeeper", "quick start", "getting started", "proxy setup"] +--- + +# Quick start + +Start a credential-injecting proxy in under five minutes. + +## Prerequisites + +- Go 1.25+ installed +- `gatekeeper` binary on `$PATH` (see [Installation](./02-installation.md)) +- `openssl` available (for CA generation) + +## Step 1: Generate a CA certificate + +The proxy needs a CA to sign per-host TLS certificates. Use the included script: + +```bash +cd examples && ./gen-ca.sh +``` + +This creates `ca.crt` and `ca.key` in the `examples/` directory. + +## Step 2: Write a minimal config + +Create `gatekeeper.yaml`: + +```yaml +proxy: + host: 127.0.0.1 + port: 9080 + +tls: + ca_cert: examples/ca.crt + ca_key: examples/ca.key + +credentials: + - host: api.example.com + header: Authorization + grant: example-api + source: + type: env + var: EXAMPLE_API_TOKEN + +network: + policy: permissive + +log: + level: info + format: text +``` + +This configures the proxy to inject the value of the `EXAMPLE_API_TOKEN` environment variable as an `Authorization` header on all requests to `api.example.com`. + +## Step 3: Start the proxy + +Set the credential and start gatekeeper: + +```bash +export EXAMPLE_API_TOKEN="sk-xxxx" +gatekeeper --config gatekeeper.yaml +``` + +The proxy logs a startup message: + +```text +level=INFO msg="gatekeeper listening" addr=127.0.0.1:9080 version=dev +``` + +## Step 4: Make a request through the proxy + +In a separate terminal, send a request through the proxy: + +```bash +curl --proxy http://127.0.0.1:9080 --cacert examples/ca.crt \ + https://api.example.com/v1/resource +``` + +The `--proxy` flag routes the request through gatekeeper. The `--cacert` flag trusts the generated CA so curl accepts the intercepted TLS certificate. + +Gatekeeper intercepts the connection, injects the `Authorization: Bearer sk-xxxx` header, and forwards the request to `api.example.com`. The credential never appears in the curl command or the client environment of the calling process. + +## Step 5: Verify credential injection + +The proxy logs each request with credential injection details: + +```text +level=INFO msg=request http_method=GET http_host=api.example.com http_path=/v1/resource http_status=200 duration_ms=142 credential_injected=true injected_headers=Authorization grants=example-api +``` + +The `credential_injected=true` and `grants=example-api` fields confirm the proxy injected the credential. + +## Next steps + +- Configure [network policy](../guides/07-network-lockdown.md) to restrict which hosts the proxy forwards to +- Add credentials from [AWS Secrets Manager](../guides/03-aws-secrets-manager.md) or [GCP Secret Manager](../guides/04-gcp-secret-manager.md) for production deployments +- Enable [OpenTelemetry](../guides/08-opentelemetry.md) for distributed tracing and metrics diff --git a/docs/content/guides/01-ca-setup.md b/docs/content/guides/01-ca-setup.md new file mode 100644 index 0000000..6da8e89 --- /dev/null +++ b/docs/content/guides/01-ca-setup.md @@ -0,0 +1,100 @@ +--- +title: "CA Certificate Setup" +description: "Generate a Certificate Authority for TLS interception and configure trust on macOS, Linux, and per-tool environments." +keywords: ["gatekeeper", "CA certificate", "TLS setup", "certificate trust"] +--- + +# CA Certificate Setup + +Generate a Certificate Authority for TLS interception and trust it on your system. Gatekeeper uses this CA to sign per-host certificates dynamically, enabling credential injection into HTTPS requests. + +## Prerequisites + +- OpenSSL installed +- Gatekeeper repository cloned + +## Generate the CA + +Run the included script from the `examples/` directory: + +```bash +cd examples && ./gen-ca.sh +``` + +This creates two files: + +- `ca.crt` — the CA certificate (distribute to clients) +- `ca.key` — the CA private key (keep private, permissions set to `0600`) + +The generated CA uses an EC P-256 key, valid for 365 days, with `CA:TRUE` and `keyCertSign` constraints. + +## Trust the CA + +### macOS + +Add the CA to the system keychain: + +```bash +sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt +``` + +### Linux (Debian/Ubuntu) + +Copy the certificate and update the trust store: + +```bash +sudo cp ca.crt /usr/local/share/ca-certificates/gatekeeper-ca.crt +sudo update-ca-certificates +``` + +### Linux (RHEL/Fedora) + +```bash +sudo cp ca.crt /etc/pki/ca-trust/source/anchors/gatekeeper-ca.crt +sudo update-ca-trust +``` + +## Per-Tool Trust + +Some tools require explicit CA configuration instead of using the system store. + +### curl + +```bash +curl --cacert ca.crt --proxy http://127.0.0.1:9080 https://api.github.com/user +``` + +### Node.js + +```bash +export NODE_EXTRA_CA_CERTS=/path/to/ca.crt +node app.js +``` + +### Python (requests) + +```bash +export REQUESTS_CA_BUNDLE=/path/to/ca.crt +python script.py +``` + +### Go + +```bash +export SSL_CERT_FILE=/path/to/ca.crt +go run main.go +``` + +## Verification + +Confirm the proxy can intercept and re-sign a TLS connection: + +```bash +curl --cacert ca.crt --proxy http://127.0.0.1:9080 -v https://example.com 2>&1 | grep "issuer" +``` + +The output should show the CN of your CA certificate as the issuer. + +## Next Steps + +- [Environment Credentials](./02-environment-credentials.md) — inject your first credential through the proxy diff --git a/docs/content/guides/02-environment-credentials.md b/docs/content/guides/02-environment-credentials.md new file mode 100644 index 0000000..261e581 --- /dev/null +++ b/docs/content/guides/02-environment-credentials.md @@ -0,0 +1,79 @@ +--- +title: "Environment Variable Credentials" +description: "Read a credential from an environment variable and inject it into HTTPS requests through Gatekeeper." +keywords: ["gatekeeper", "environment variables", "credential injection", "env source"] +--- + +# Environment Variable Credentials + +Read a credential from an environment variable and inject it into HTTPS requests. This is the simplest credential source. + +## Prerequisites + +- CA certificate generated ([CA Setup](./01-ca-setup.md)) +- Gatekeeper binary built (`go build -o gatekeeper ./cmd/gatekeeper/`) + +## Configuration + +Create `gatekeeper.yaml`: + +```yaml +proxy: + host: 127.0.0.1 + port: 9080 + +tls: + ca_cert: ca.crt + ca_key: ca.key + +credentials: + - host: api.github.com + header: Authorization + grant: github + source: + type: env + var: GITHUB_TOKEN + +network: + policy: permissive + +log: + level: info + format: text +``` + +The `env` source reads the credential from the environment variable named in `var`. The variable must be set when the proxy starts. + +## Start the Proxy + +Set the token and start gatekeeper: + +```bash +export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx" +gatekeeper --config gatekeeper.yaml +``` + +Gatekeeper resolves the credential at startup. For `Authorization` headers, the auth scheme is auto-detected from the token prefix (`ghp_` maps to `token`, `github_pat_` to `Bearer`). Override with the `prefix` field if needed. + +## Make a Request + +In another terminal: + +```bash +curl --cacert ca.crt --proxy http://127.0.0.1:9080 https://api.github.com/user +``` + +The proxy intercepts the TLS connection, injects the `Authorization` header, and forwards the request. The proxy log shows `credential_injected=true`. + +## Verification + +Check the proxy log output. A successful injection produces a line like: + +```text +level=INFO msg=request http_method=GET http_host=api.github.com http_status=200 credential_injected=true injected_headers=Authorization grants=github +``` + +## Next Steps + +- [AWS Secrets Manager](./03-aws-secrets-manager.md) — fetch credentials from AWS instead of environment variables +- [Network Lockdown](./07-network-lockdown.md) — restrict which hosts the proxy can reach diff --git a/docs/content/guides/03-aws-secrets-manager.md b/docs/content/guides/03-aws-secrets-manager.md new file mode 100644 index 0000000..b948bb2 --- /dev/null +++ b/docs/content/guides/03-aws-secrets-manager.md @@ -0,0 +1,93 @@ +--- +title: "AWS Secrets Manager Credentials" +description: "Fetch a credential from AWS Secrets Manager at proxy startup and inject it into HTTPS requests." +keywords: ["gatekeeper", "AWS Secrets Manager", "credential source", "cloud secrets"] +--- + +# AWS Secrets Manager Credentials + +Fetch a credential from AWS Secrets Manager at proxy startup and inject it into HTTPS requests. + +## Prerequisites + +- CA certificate generated ([CA Setup](./01-ca-setup.md)) +- AWS credentials configured (environment variables, IAM role, or `~/.aws/credentials`) +- A secret stored in AWS Secrets Manager containing the credential value as a plaintext string + +## IAM Permissions + +The IAM principal running gatekeeper needs: + +```json +{ + "Effect": "Allow", + "Action": "secretsmanager:GetSecretValue", + "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/github-token-*" +} +``` + +Scope the `Resource` to the specific secret ARN. + +## Configuration + +Add an `aws-secretsmanager` credential source to `gatekeeper.yaml`: + +```yaml +proxy: + host: 127.0.0.1 + port: 9080 + +tls: + ca_cert: ca.crt + ca_key: ca.key + +credentials: + - host: api.github.com + header: Authorization + grant: github + source: + type: aws-secretsmanager + secret: prod/github-token + region: us-east-1 + +network: + policy: permissive + +log: + level: info + format: text +``` + +| Field | Required | Description | +|----------|----------|--------------------------------------------------| +| `secret` | Yes | Secret name or ARN in AWS Secrets Manager | +| `region` | No | AWS region. Falls back to SDK default if omitted | + +The secret value must be a plaintext string (not binary). Gatekeeper fetches it once at startup with a 10-second timeout. + +## Start the Proxy + +```bash +gatekeeper --config gatekeeper.yaml +``` + +If AWS credentials are missing or the secret does not exist, gatekeeper exits with an error at startup. + +## Verification + +```bash +curl --cacert ca.crt --proxy http://127.0.0.1:9080 https://api.github.com/user +``` + +The proxy log confirms credential injection: + +```text +level=INFO msg=request http_host=api.github.com credential_injected=true grants=github +``` + +> **Note:** Gatekeeper fetches the secret once at startup. To pick up a rotated secret, restart the proxy. + +## Next Steps + +- [GCP Secret Manager](./04-gcp-secret-manager.md) — use GCP instead of AWS +- [Network Lockdown](./07-network-lockdown.md) — restrict proxy traffic to specific hosts diff --git a/docs/content/guides/04-gcp-secret-manager.md b/docs/content/guides/04-gcp-secret-manager.md new file mode 100644 index 0000000..8a80ca9 --- /dev/null +++ b/docs/content/guides/04-gcp-secret-manager.md @@ -0,0 +1,96 @@ +--- +title: "GCP Secret Manager Credentials" +description: "Fetch a credential from Google Cloud Secret Manager at proxy startup and inject it into HTTPS requests." +keywords: ["gatekeeper", "GCP Secret Manager", "credential source", "cloud secrets"] +--- + +# GCP Secret Manager Credentials + +Fetch a credential from Google Cloud Secret Manager at proxy startup and inject it into HTTPS requests. + +## Prerequisites + +- CA certificate generated ([CA Setup](./01-ca-setup.md)) +- GCP Application Default Credentials configured (`gcloud auth application-default login` or a service account key) +- A secret stored in GCP Secret Manager containing the credential value + +## IAM Permissions + +The service account or principal running gatekeeper needs the `Secret Manager Secret Accessor` role (`roles/secretmanager.secretAccessor`) on the target secret. + +## Configuration + +Add a `gcp-secretmanager` credential source to `gatekeeper.yaml`: + +```yaml +proxy: + host: 127.0.0.1 + port: 9080 + +tls: + ca_cert: ca.crt + ca_key: ca.key + +credentials: + - host: api.github.com + header: Authorization + grant: github + source: + type: gcp-secretmanager + project: my-gcp-project + secret: github-token + +network: + policy: permissive + +log: + level: info + format: text +``` + +| Field | Required | Default | Description | +|-----------|----------|------------|------------------------------------------| +| `project` | Yes | -- | GCP project ID | +| `secret` | Yes | -- | Secret name in Secret Manager | +| `version` | No | `"latest"` | Secret version (e.g., `"1"`, `"latest"`) | + +Gatekeeper constructs the resource name `projects/{project}/secrets/{secret}/versions/{version}` and fetches the payload at startup with a 10-second timeout. + +## Pin a Specific Version + +To pin to a specific secret version instead of `latest`: + +```yaml +source: + type: gcp-secretmanager + project: my-gcp-project + secret: github-token + version: "3" +``` + +## Start the Proxy + +```bash +gatekeeper --config gatekeeper.yaml +``` + +If ADC is not configured or the secret does not exist, gatekeeper exits with an error at startup. + +## Verification + +```bash +curl --cacert ca.crt --proxy http://127.0.0.1:9080 https://api.github.com/user +``` + +The proxy log confirms credential injection: + +```text +level=INFO msg=request http_host=api.github.com credential_injected=true grants=github +``` + +> **Note:** Gatekeeper fetches the secret once at startup. To pick up a rotated secret, restart the proxy. + +## Next Steps + +- [GitHub App Tokens](./05-github-app-tokens.md) — auto-refreshing short-lived tokens +- [Network Lockdown](./07-network-lockdown.md) — restrict proxy traffic to specific hosts diff --git a/docs/content/guides/05-github-app-tokens.md b/docs/content/guides/05-github-app-tokens.md new file mode 100644 index 0000000..ae62747 --- /dev/null +++ b/docs/content/guides/05-github-app-tokens.md @@ -0,0 +1,114 @@ +--- +title: "GitHub App Tokens" +description: "Generate short-lived GitHub installation tokens from a GitHub App private key with automatic background refresh." +keywords: ["gatekeeper", "GitHub App", "installation tokens", "auto-refresh"] +--- + +# GitHub App Tokens + +Generate short-lived GitHub installation tokens from a GitHub App private key. Tokens refresh automatically in the background. + +## Prerequisites + +- CA certificate generated ([CA Setup](./01-ca-setup.md)) +- A GitHub App created with the required permissions +- The App's private key (PEM file), downloaded from the App settings page +- The installation ID (visible in the App's installation URL: `https://github.com/settings/installations/{id}`) + +## Configuration + +Add a `github-app` credential source to `gatekeeper.yaml`: + +```yaml +proxy: + host: 127.0.0.1 + port: 9080 + +tls: + ca_cert: ca.crt + ca_key: ca.key + +credentials: + - host: api.github.com + header: Authorization + grant: github + source: + type: github-app + app_id: "12345" + installation_id: "67890" + private_key_path: ./github-app-key.pem + +network: + policy: permissive + +log: + level: info + format: text +``` + +| Field | Required | Description | +|--------------------|----------|------------------------------------------------------| +| `app_id` | Yes | GitHub App ID (from App settings) | +| `installation_id` | Yes | Installation ID for the target org/account | +| `private_key_path` | One of | Path to the PEM private key file | +| `private_key_env` | One of | Environment variable containing the PEM private key | + +Set either `private_key_path` or `private_key_env`, not both. + +### Private Key via Environment Variable + +For environments where files are not practical (containers, CI): + +```yaml +source: + type: github-app + app_id: "12345" + installation_id: "67890" + private_key_env: GITHUB_APP_PRIVATE_KEY +``` + +```bash +export GITHUB_APP_PRIVATE_KEY="$(cat github-app-key.pem)" +``` + +## Auto-Refresh Behavior + +GitHub installation tokens expire after one hour. Gatekeeper refreshes them automatically: + +1. At startup, gatekeeper generates a JWT signed with the App private key and exchanges it for an installation token via the GitHub API. +2. A background goroutine re-fetches the token at 75% of TTL (roughly every 45 minutes). +3. If a refresh fails, gatekeeper retries with exponential backoff (1s to 60s) until it succeeds. +4. Token rotation is atomic -- requests always see either the old or new token, never a partial state. + +When multiple credential entries share the same `github-app` source (e.g., `api.github.com` and `github.com`), a single refresh goroutine updates all of them. + +## Start the Proxy + +```bash +gatekeeper --config gatekeeper.yaml +``` + +## Verification + +```bash +curl --cacert ca.crt --proxy http://127.0.0.1:9080 https://api.github.com/installation/repositories +``` + +A successful response confirms the installation token was generated and injected. The proxy log shows: + +```text +level=INFO msg=request http_host=api.github.com credential_injected=true grants=github +``` + +At debug level, refresh events appear: + +```text +level=DEBUG msg="credential refreshed" host=api.github.com grant=github ttl=1h0m0s +``` + +See [`examples/gatekeeper-github-app.yaml`](https://github.com/majorcontext/gatekeeper/blob/main/examples/gatekeeper-github-app.yaml) for a complete working example. + +## Next Steps + +- [Token Exchange](./06-token-exchange.md) — per-user credential resolution via RFC 8693 +- [Network Lockdown](./07-network-lockdown.md) — restrict proxy traffic to specific hosts diff --git a/docs/content/guides/06-token-exchange.md b/docs/content/guides/06-token-exchange.md new file mode 100644 index 0000000..47817fd --- /dev/null +++ b/docs/content/guides/06-token-exchange.md @@ -0,0 +1,226 @@ +--- +title: "Token Exchange (RFC 8693)" +description: "Resolve per-user credentials dynamically by calling an external Security Token Service using RFC 8693 token exchange." +keywords: ["gatekeeper", "token exchange", "RFC 8693", "STS", "OAuth"] +--- + +# Token Exchange (RFC 8693) + +Resolve per-user credentials dynamically by calling an external Security Token Service (STS). Multiple callers with different identities share a single gatekeeper instance. Each request triggers a token exchange -- or uses a cached token -- scoped to the caller's identity. + +This guide covers gatekeeper configuration and the STS endpoint contract. Implement the STS endpoint yourself; gatekeeper is the client. + +## Prerequisites + +- CA certificate generated ([CA Setup](./01-ca-setup.md)) +- An STS endpoint that implements RFC 8693 token exchange (see [STS Endpoint Requirements](#sts-endpoint-requirements) below) +- Client credentials (`client_id` and `client_secret`) for authenticating gatekeeper to the STS + +## How It Works + +1. A request arrives at gatekeeper with a subject identity (via header or proxy auth username). +2. Gatekeeper checks its cache for a valid token for that subject. +3. On cache miss, gatekeeper sends an RFC 8693 `POST` to the STS with the subject token, client credentials, and optional resource/actor parameters. +4. The STS returns an `access_token` (and optional `expires_in`). +5. Gatekeeper caches the token and injects it into the upstream request. + +## Subject Identity Modes + +Gatekeeper extracts the subject identity from one of two sources. The two modes are mutually exclusive. + +### Mode 1: Subject from Request Header + +The subject identity is read from a named HTTP header on each request. Gatekeeper strips the header before forwarding upstream. + +```yaml +credentials: + - host: api.github.com + grant: github + prefix: Bearer + source: + type: token-exchange + endpoint: https://sts.example.com/token + client_id: gk-client + client_secret_env: STS_CLIENT_SECRET + subject_header: X-Gatekeeper-Subject + resource: https://api.github.com +``` + +The client includes the subject in each request: + +```bash +curl --proxy http://127.0.0.1:9080 --cacert ca.crt \ + -H "X-Gatekeeper-Subject: alice@example.com" \ + https://api.github.com/user +``` + +### Mode 2: Subject from Proxy Auth + +The subject identity is extracted from the username in proxy authentication credentials. No request headers are modified. + +```yaml +credentials: + - host: api.github.com + grant: github + prefix: Bearer + source: + type: token-exchange + endpoint: https://sts.example.com/token + client_id: gk-client + client_secret_env: STS_CLIENT_SECRET + subject_from: proxy-auth + resource: https://api.github.com +``` + +The client encodes the subject in the proxy URL. Percent-encode `@` as `%40`: + +```bash +export HTTP_PROXY="http://alice%40example.com:proxypass@127.0.0.1:9080" +curl --cacert ca.crt https://api.github.com/user +``` + +## Configuration Reference + +| Field | Required | Default | Description | +|----------------------|----------------|--------------------------------------------------------|----------------------------------------------------------| +| `endpoint` | Yes | -- | STS token endpoint URL | +| `client_id` | Yes | -- | OAuth client ID for HTTP Basic auth to STS | +| `client_secret` | One of | -- | Client secret (literal value) | +| `client_secret_env` | One of | -- | Environment variable containing the client secret | +| `subject_header` | One of | -- | Request header to extract subject from (stripped before forwarding) | +| `subject_from` | One of | -- | Set to `proxy-auth` to extract subject from proxy auth username | +| `subject_token_type` | No | `urn:ietf:params:oauth:token-type:access_token` | Token type URI for the subject token | +| `resource` | No | -- | Target resource URI sent to the STS | +| `actor_token_from` | No | -- | Set to `proxy-auth-password` to forward the proxy auth password as actor token | +| `actor_token_type` | No | `urn:ietf:params:oauth:token-type:access_token` | Token type URI for the actor token | + +## Actor Token Forwarding + +By default, subject identities are self-asserted -- any caller can claim any identity. In shared environments, use **actor token forwarding** to let the STS verify caller identity. + +Configure gatekeeper to forward the proxy auth password as the RFC 8693 `actor_token`: + +```yaml +credentials: + - host: api.github.com + grant: github + prefix: Bearer + source: + type: token-exchange + endpoint: https://sts.example.com/token + client_id: gk-client + client_secret_env: STS_CLIENT_SECRET + subject_from: proxy-auth + actor_token_from: proxy-auth-password + resource: https://api.github.com +``` + +Each caller uses a unique API key as the proxy auth password: + +```bash +HTTP_PROXY=http://alice%40example.com:ak_alice_xxxxx@127.0.0.1:9080 +``` + +Gatekeeper sends both `subject_token=alice@example.com` and `actor_token=ak_alice_xxxxx` to the STS. The STS validates that the API key belongs to Alice before issuing tokens. + +When `actor_token_from` is configured on any credential, gatekeeper requires all clients to provide Basic proxy auth with a non-empty password. The password is not checked against a static value -- it is forwarded to the STS. + +## Caching Behavior + +Gatekeeper caches tokens per `(subject_token, actor_token)` pair: + +- If `expires_in` is returned by the STS, the token is cached until expiry. +- If `expires_in` is `0` or omitted, a default TTL of 5 minutes is applied. +- Concurrent requests for the same subject are coalesced into a single STS call via singleflight. +- Expired entries are evicted lazily on the next exchange. +- There is no proactive refresh. When a cached token expires, the next request triggers a new exchange. + +For high-throughput scenarios, set `expires_in` to a reasonable TTL (e.g., `3600` for one hour) to avoid per-request STS calls. + +## STS Endpoint Requirements + +Gatekeeper sends a `POST` with `Content-Type: application/x-www-form-urlencoded` and HTTP Basic authentication. + +### Request Format + +```http +POST /token HTTP/1.1 +Host: sts.example.com +Authorization: Basic base64(client_id:client_secret) +Content-Type: application/x-www-form-urlencoded + +grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&subject_token=alice%40example.com&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&resource=https%3A%2F%2Fapi.github.com +``` + +### Success Response (HTTP 200) + +```json +{ + "access_token": "gho_exchanged_abc123", + "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", + "token_type": "Bearer", + "expires_in": 3600 +} +``` + +| Field | Type | Required | Description | +|---------------------|--------|----------|------------------------------------------------------| +| `access_token` | string | Yes | The token gatekeeper injects upstream | +| `issued_token_type` | string | No | Token type URI of the issued token | +| `token_type` | string | No | Informational; gatekeeper uses its own prefix config | +| `expires_in` | int | No | TTL in seconds. Defaults to 300 if omitted | + +`access_token` must be non-empty. Gatekeeper treats an empty value as an error. + +### Error Response (non-200) + +Gatekeeper treats any non-200 status as a failure and returns HTTP 502 to the client. Use standard OAuth error format: + +```json +{ + "error": "invalid_grant", + "error_description": "Subject token is expired or revoked" +} +``` + +> **Note:** Do not echo `actor_token` in error responses. Gatekeeper logs up to 200 bytes of STS error bodies, so sensitive values would appear in proxy logs. + +## Implementation Checklist + +- [ ] Accept `POST` with `Content-Type: application/x-www-form-urlencoded` +- [ ] Validate HTTP Basic auth credentials (`client_id` / `client_secret`) +- [ ] Validate `grant_type` is exactly `urn:ietf:params:oauth:grant-type:token-exchange` +- [ ] Extract `subject_token` -- the user/caller identity +- [ ] Read `resource` if present -- the target API +- [ ] Look up or mint an access token for the given subject and resource +- [ ] Return JSON with a non-empty `access_token` +- [ ] Set `expires_in` to enable caching +- [ ] Return non-200 for invalid/expired/unknown subjects +- [ ] Handle concurrent requests (idempotency or internal locking) +- [ ] *(Optional)* Validate `actor_token` against `subject_token` to prevent impersonation + +## Testing + +Test your STS endpoint with curl: + +```bash +curl -X POST https://sts.example.com/token \ + -u "gk-client:your-secret" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=alice&subject_token_type=urn:ietf:params:oauth:token-type:access_token&resource=https://api.github.com" +``` + +Then test through the proxy: + +```bash +curl --cacert ca.crt --proxy http://127.0.0.1:9080 \ + -H "X-Gatekeeper-Subject: alice" \ + https://api.github.com/user +``` + +The proxy log shows credential injection with the grant name. + +## Next Steps + +- [Network Lockdown](./07-network-lockdown.md) — combine token exchange with strict network policy +- [OpenTelemetry](./08-opentelemetry.md) — trace token exchange calls end-to-end diff --git a/docs/content/guides/07-network-lockdown.md b/docs/content/guides/07-network-lockdown.md new file mode 100644 index 0000000..a44a8ff --- /dev/null +++ b/docs/content/guides/07-network-lockdown.md @@ -0,0 +1,108 @@ +--- +title: "Network Lockdown" +description: "Restrict which hosts the proxy forwards traffic to using strict network policy with an allow list." +keywords: ["gatekeeper", "network lockdown", "strict policy", "allow list"] +--- + +# Network Lockdown + +Restrict which hosts the proxy forwards traffic to. By default, gatekeeper operates in `permissive` mode -- it proxies requests to any host. Switch to `strict` mode to deny all traffic except explicitly allowed hosts. + +## Prerequisites + +- CA certificate generated ([CA Setup](./01-ca-setup.md)) +- A working gatekeeper configuration with at least one credential + +## Permissive Mode (Default) + +The default configuration allows traffic to all hosts: + +```yaml +network: + policy: permissive +``` + +All CONNECT and HTTP requests pass through. Credentials are injected only for matching hosts; all other traffic is forwarded without modification. + +## Strict Mode + +Switch to `strict` to deny all traffic except listed hosts: + +```yaml +network: + policy: strict + allow: + - "api.github.com" + - "*.anthropic.com" +``` + +Requests to unlisted hosts receive an HTTP `407` response with a `Proxy-Authenticate: Moat-Policy` header. + +## Glob Patterns + +The `allow` list supports glob patterns for flexible matching: + +| Pattern | Matches | +|-----------------------|----------------------------------------------| +| `api.github.com` | Exact match only | +| `*.github.com` | `api.github.com`, `raw.github.com`, etc. | +| `*.example.com` | Any subdomain of `example.com` | + +Port numbers are stripped before matching -- `api.github.com:443` matches a rule for `api.github.com`. + +## Combined Configuration + +Combine credential injection with network lockdown: + +```yaml +proxy: + host: 127.0.0.1 + port: 9080 + +tls: + ca_cert: ca.crt + ca_key: ca.key + +credentials: + - host: api.github.com + header: Authorization + grant: github + source: + type: env + var: GITHUB_TOKEN + +network: + policy: strict + allow: + - "api.github.com" + +log: + level: info + format: text +``` + +This configuration injects GitHub credentials for `api.github.com` and blocks all other outbound traffic. + +## Verification + +Start the proxy and test a denied request: + +```bash +curl --cacert ca.crt --proxy http://127.0.0.1:9080 https://example.com +``` + +The proxy returns a `407` and logs: + +```text +level=WARN msg=request http_host=example.com denied=true deny_reason="Host not in allow list: example.com" +``` + +Confirm allowed requests still work: + +```bash +curl --cacert ca.crt --proxy http://127.0.0.1:9080 https://api.github.com/user +``` + +## Next Steps + +- [OpenTelemetry](./08-opentelemetry.md) — monitor denied requests with metrics and traces diff --git a/docs/content/guides/08-opentelemetry.md b/docs/content/guides/08-opentelemetry.md new file mode 100644 index 0000000..efbcbce --- /dev/null +++ b/docs/content/guides/08-opentelemetry.md @@ -0,0 +1,97 @@ +--- +title: "OpenTelemetry" +description: "Configure Gatekeeper to emit traces, metrics, and logs via OpenTelemetry using standard OTEL environment variables." +keywords: ["gatekeeper", "OpenTelemetry", "tracing", "metrics", "observability"] +--- + +# OpenTelemetry + +Gatekeeper emits traces, metrics, and logs via OpenTelemetry. Configuration uses standard `OTEL_*` environment variables -- no YAML knobs required. + +## Prerequisites + +- A working gatekeeper configuration +- An OpenTelemetry collector or compatible backend (Jaeger, Grafana, Honeycomb, etc.) + +## Configuration + +Set OTEL environment variables before starting gatekeeper: + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" +export OTEL_SERVICE_NAME="gatekeeper" +gatekeeper --config gatekeeper.yaml +``` + +For authenticated endpoints: + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT="https://your-collector:4318" +export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer " +``` + +Gatekeeper creates OTLP HTTP exporters for traces, metrics, and logs and registers them as global providers. + +## Traces + +Every proxy request creates a span. Span names reflect the request type: + +| Request Type | Span Name | +|---------------|-----------------| +| CONNECT | `proxy.request` | +| MCP relay | `proxy.mcp` | +| HTTP relay | `proxy.relay` | +| Plain HTTP | `proxy.http` | + +Each span includes attributes: + +- `http.request.method` — request method +- `server.address` — target host +- `proxy.request.type` — one of `connect`, `mcp`, `relay`, `http` +- `http.response.status_code` — response status (not set for hijacked CONNECT connections) + +A `request.complete` event is added to each span with detailed context: `request_id`, `duration_ms`, `credential_injected`, `injected_headers`, `grants`, `denied`, and `deny_reason`. + +## Metrics + +Four instruments are registered under the `gatekeeper` meter: + +| Metric | Type | Description | Attributes | +|-------------------------------|---------------|------------------------------|------------------------------------------------------| +| `proxy.request.duration` | Histogram (s) | Request duration in seconds | `http.request.method`, `server.address`, `proxy.request.type`, `http.response.status_code` | +| `proxy.request.count` | Counter | Total proxy requests | Same as above | +| `proxy.credential.injections` | Counter | Credential injection count | `server.address`, `proxy.credential.header` | +| `proxy.policy.denials` | Counter | Policy denial count | `proxy.policy.scope`, `proxy.policy.rule` | + +## Logs + +Gatekeeper bridges `slog` output to OTel via `otelslog`. Structured log records are sent to both the configured slog handler (text/JSON to stderr) and the OTel log exporter. Log records carry trace context for correlation. + +## Local Collector Example + +Run a local OpenTelemetry Collector with Jaeger: + +```bash +docker run -d --name jaeger \ + -p 4318:4318 \ + -p 16686:16686 \ + jaegertracing/all-in-one:latest +``` + +Start gatekeeper pointing to the local collector: + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" +export OTEL_SERVICE_NAME="gatekeeper" +gatekeeper --config gatekeeper.yaml +``` + +Make a request through the proxy, then open `http://localhost:16686` to view traces. + +## Verification + +After sending requests through the proxy, confirm traces and metrics are arriving at the collector. In Jaeger, search for service `gatekeeper` and look for `proxy.request` spans with `credential_injected=true` events. + +## Next Steps + +- [Go Library](./09-go-library.md) — embed gatekeeper in a Go application with custom instrumentation diff --git a/docs/content/guides/09-go-library.md b/docs/content/guides/09-go-library.md new file mode 100644 index 0000000..f513356 --- /dev/null +++ b/docs/content/guides/09-go-library.md @@ -0,0 +1,123 @@ +--- +title: "Go Library Usage" +description: "Import Gatekeeper as a Go module to embed the credential-injecting proxy in a custom application." +keywords: ["gatekeeper", "Go library", "embedding", "proxy API"] +--- + +# Go Library Usage + +Import gatekeeper as a Go module to embed the proxy in a custom application. This is how [moat](https://github.com/majorcontext/moat) integrates gatekeeper -- importing the proxy engine and adding per-run credential scoping via a daemon layer. + +## Prerequisites + +- Go 1.25+ + +## Install + +```bash +go get github.com/majorcontext/gatekeeper/proxy +``` + +## Basic Setup + +Create a proxy, load a CA, and set credentials: + +```go +package main + +import ( + "log" + "net/http" + "os" + + "github.com/majorcontext/gatekeeper/proxy" +) + +func main() { + // Load CA for TLS interception. + certPEM, _ := os.ReadFile("ca.crt") + keyPEM, _ := os.ReadFile("ca.key") + ca, err := proxy.LoadCA(certPEM, keyPEM) + if err != nil { + log.Fatal(err) + } + + // Create proxy and configure it. + p := proxy.NewProxy() + p.SetCA(ca) + p.SetCredentialWithGrant("api.github.com", "Authorization", "Bearer ghp_xxxx", "github") + p.SetNetworkPolicy("strict", []string{"api.github.com"}, nil) + + // Start HTTP server. + log.Fatal(http.ListenAndServe("127.0.0.1:9080", p)) +} +``` + +`proxy.Proxy` implements `http.Handler`. Serve it with any `http.Server` or wrap it with middleware. + +## Key API + +| Method | Description | +|------------------------------|------------------------------------------------------| +| `NewProxy()` | Create a new proxy instance | +| `SetCA(ca)` | Set the CA for TLS interception | +| `SetCredentialWithGrant(host, header, value, grant)` | Set a static credential for a host | +| `SetCredentialResolver(host, resolver)` | Set a dynamic per-request resolver | +| `SetNetworkPolicy(policy, allows, grants)` | Configure network allow/deny | +| `SetAuthToken(token)` | Require proxy authentication | +| `SetContextResolver(fn)` | Map proxy auth tokens to per-caller contexts | + +## Custom ContextResolver + +For multi-tenant setups, use a `ContextResolver` to map proxy auth tokens to per-caller credential sets: + +```go +p := proxy.NewProxy() +p.SetCA(ca) + +p.SetContextResolver(func(token string) (*proxy.RunContextData, bool) { + // Look up caller by their proxy auth token. + run, ok := registry.Get(token) + if !ok { + return nil, false + } + return &proxy.RunContextData{ + RunID: run.ID, + Credentials: map[string][]proxy.CredentialHeader{ + "api.github.com": { + {Name: "Authorization", Value: "Bearer " + run.GitHubToken, Grant: "github"}, + }, + }, + Policy: "strict", + AllowedHosts: run.AllowedHosts, + }, true +}) +``` + +Each caller authenticates via `Proxy-Authorization` (or the username/password in `HTTP_PROXY`). The resolver returns per-caller credentials, network policy, and MCP server configuration. + +## How Moat Uses Gatekeeper + +Moat imports `github.com/majorcontext/gatekeeper/proxy` and layers a daemon on top: + +1. A management API (Unix socket) accepts run registrations with per-run credentials and policies. +2. Each registration generates a unique proxy auth token. +3. The `ContextResolver` maps tokens to `RunContextData` containing that run's scoped credentials and network policy. +4. The proxy itself is shared across all runs -- credential isolation is handled entirely by the resolver. + +Gatekeeper has no knowledge of moat. It exposes the `ContextResolver` hook and `RunContextData` struct; moat provides the implementation. + +## OTel Middleware + +Wrap the proxy with `OTelHandler` for OpenTelemetry instrumentation: + +```go +handler := proxy.OTelHandler(p) +log.Fatal(http.ListenAndServe("127.0.0.1:9080", handler)) +``` + +This adds request spans, duration histograms, and request counters. See [OpenTelemetry](./08-opentelemetry.md) for details on emitted signals. + +## Next Steps + +- [WebSocket Support](./10-websockets.md) — WebSocket upgrades through the proxy diff --git a/docs/content/guides/10-websockets.md b/docs/content/guides/10-websockets.md new file mode 100644 index 0000000..2d03c8d --- /dev/null +++ b/docs/content/guides/10-websockets.md @@ -0,0 +1,76 @@ +--- +title: "WebSocket Support" +description: "WebSocket connections work through Gatekeeper with credential injection on the HTTP upgrade request and transparent frame tunneling." +keywords: ["gatekeeper", "WebSocket", "upgrade request", "bidirectional tunneling"] +--- + +# WebSocket Support + +WebSocket connections work through gatekeeper with no special configuration. The proxy intercepts the TLS connection, injects credentials on the HTTP upgrade request, and then tunnels the bidirectional WebSocket frames transparently. + +## How It Works + +1. The client sends `CONNECT host:443` through the proxy. +2. Gatekeeper terminates TLS and reads the plaintext HTTP request. +3. The client sends an HTTP `Upgrade: websocket` request. +4. Gatekeeper injects credentials into the upgrade request headers (e.g., `Authorization`), just like any other request. +5. The upgrade request is forwarded to the upstream server via a `ReverseProxy`. +6. The Go standard library's `httputil.ReverseProxy` detects the `101 Switching Protocols` response and initiates bidirectional tunneling. +7. After the upgrade, WebSocket frames flow between client and server without further proxy intervention. + +## Configuration + +No additional configuration is needed. Any credential configured for a host applies to WebSocket upgrade requests to that host: + +```yaml +proxy: + host: 127.0.0.1 + port: 9080 + +tls: + ca_cert: ca.crt + ca_key: ca.key + +credentials: + - host: ws.example.com + header: Authorization + grant: ws-api + source: + type: env + var: WS_API_TOKEN + +network: + policy: permissive + +log: + level: info + format: text +``` + +## Verification + +Connect a WebSocket client through the proxy: + +```bash +export HTTPS_PROXY=http://127.0.0.1:9080 +wscat -c wss://ws.example.com/socket --ca ca.crt +``` + +The proxy log shows the CONNECT tunnel and credential injection on the upgrade request: + +```text +level=INFO msg=request http_method=CONNECT http_host=ws.example.com:443 credential_injected=true +``` + +After the upgrade completes, the proxy tunnels frames bidirectionally until either side closes the connection. + +## Limitations + +- Credentials are injected only on the initial HTTP upgrade request. Subsequent WebSocket frames pass through unmodified. +- The proxy does not inspect or modify WebSocket frame content. +- Connection lifetime is bounded by the proxy's idle timeout and the underlying TCP keepalive settings. + +## Next Steps + +- [Network Lockdown](./07-network-lockdown.md) — restrict which WebSocket hosts the proxy can reach +- [OpenTelemetry](./08-opentelemetry.md) — trace WebSocket CONNECT tunnels diff --git a/docs/content/reference/01-cli.md b/docs/content/reference/01-cli.md new file mode 100644 index 0000000..2560d32 --- /dev/null +++ b/docs/content/reference/01-cli.md @@ -0,0 +1,72 @@ +--- +title: "CLI" +description: "Reference for the gatekeeper command-line interface, including flags, exit codes, signals, and health check endpoint." +keywords: ["gatekeeper", "CLI", "command line", "flags", "configuration"] +--- + +# CLI + +The `gatekeeper` command starts a credential-injecting TLS-intercepting proxy. + +## Usage + +```bash +gatekeeper --config gatekeeper.yaml +``` + +## Flags + +### --config + +Path to the gatekeeper configuration file. + +```bash +gatekeeper --config /etc/gatekeeper/gatekeeper.yaml +``` + +- **Type:** `string` +- **Required:** Yes (unless `GATEKEEPER_CONFIG` is set) +- **Default:** — + +If `--config` is not provided, gatekeeper reads the `GATEKEEPER_CONFIG` environment variable. If neither is set, gatekeeper exits with an error. + +--- + +## Build version + +The binary version is set at build time via `-ldflags`: + +```bash +go build -ldflags "-X main.version=1.2.3" -o gatekeeper ./cmd/gatekeeper/ +``` + +When unset, the version defaults to `"dev"`. The version appears in the startup log line and is registered as the `service.version` OpenTelemetry resource attribute. + +--- + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Clean shutdown (SIGTERM or SIGINT received) | +| `1` | Startup error (missing config, invalid config, credential fetch failure, listener bind failure, or OTel initialization failure) | + +--- + +## Signals + +Gatekeeper listens for `SIGTERM` and `SIGINT`. On receipt, it gracefully shuts down the HTTP server (5-second timeout), cancels background credential refresh goroutines, closes credential source connections, and flushes OpenTelemetry providers. + +--- + +## Health check + +The proxy exposes a health endpoint on the proxy port: + +```bash +curl http://127.0.0.1:8080/healthz +``` + +```json +{"status":"ok"} +``` diff --git a/docs/content/reference/02-config-file.md b/docs/content/reference/02-config-file.md new file mode 100644 index 0000000..cad1288 --- /dev/null +++ b/docs/content/reference/02-config-file.md @@ -0,0 +1,370 @@ +--- +title: "Config file" +description: "Complete reference for gatekeeper.yaml fields including proxy, TLS, credentials, network policy, and logging configuration." +keywords: ["gatekeeper", "config file", "YAML", "configuration reference"] +--- + +# Config file + +gatekeeper.yaml defines proxy settings, TLS configuration, credentials, network policy, and logging. + +## Top-level structure + +```yaml +proxy: + host: 127.0.0.1 + port: 8080 + auth_token: my-secret-token + +tls: + ca_cert: ca.pem + ca_key: ca-key.pem + +credentials: + - host: api.github.com + source: + type: env + var: GITHUB_TOKEN + +network: + policy: strict + allow: + - "*.github.com" + +log: + level: info + format: json + output: stderr +``` + +| Section | Description | +|---------|-------------| +| `proxy` | Proxy listener address and authentication | +| `tls` | CA certificate for TLS interception | +| `credentials` | Credential injection rules | +| `network` | Network access policy | +| `log` | Logging configuration | + +--- + +## proxy + +Configures the proxy listener. + +### proxy.port + +TCP port the proxy listens on. + +```yaml +proxy: + port: 8080 +``` + +- **Type:** `int` +- **Required:** No +- **Default:** `0` (random available port) + +### proxy.host + +Bind address for the proxy listener. + +```yaml +proxy: + host: 0.0.0.0 +``` + +- **Type:** `string` +- **Required:** No +- **Default:** `"127.0.0.1"` + +Binding to `127.0.0.1` prevents accidental exposure on all interfaces. Set to `0.0.0.0` when the proxy must be reachable from containers via a gateway IP. + +### proxy.auth_token + +Static token clients must provide via `Proxy-Authorization` header to access the proxy. + +```yaml +proxy: + auth_token: my-secret-token +``` + +- **Type:** `string` +- **Required:** No +- **Default:** — (no authentication required) + +When set, clients authenticate by including the token in the proxy URL: + +```bash +export HTTP_PROXY=http://user:my-secret-token@127.0.0.1:8080 +``` + +The username portion is ignored. The token comparison is constant-time to prevent timing attacks. + +--- + +## tls + +Configures the CA certificate used for TLS interception. Without a CA, CONNECT tunnels pass through without credential injection. + +### tls.ca_cert + +File path to the PEM-encoded CA certificate. + +```yaml +tls: + ca_cert: /etc/gatekeeper/ca.pem +``` + +- **Type:** `string` +- **Required:** No (but required for HTTPS credential injection) +- **Default:** — + +### tls.ca_key + +File path to the PEM-encoded CA private key. + +```yaml +tls: + ca_key: /etc/gatekeeper/ca-key.pem +``` + +- **Type:** `string` +- **Required:** No (but required for HTTPS credential injection) +- **Default:** — + +Both `ca_cert` and `ca_key` must be set together. The proxy uses this CA to dynamically generate per-host certificates for TLS interception. Clients must trust this CA certificate. + +--- + +## credentials + +A list of credential injection rules. Each entry maps a hostname to a credential source and the HTTP header to inject. + +```yaml +credentials: + - host: api.github.com + header: Authorization + prefix: Bearer + grant: github + source: + type: env + var: GITHUB_TOKEN +``` + +### credentials[].host + +Hostname or glob pattern to match for credential injection. + +```yaml +credentials: + - host: api.github.com +``` + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +Supports glob patterns (`*.github.com`). Port numbers are stripped before matching — `api.github.com:443` matches a rule for `api.github.com`. + +### credentials[].header + +HTTP header name to inject the credential into. + +```yaml +credentials: + - host: api.anthropic.com + header: x-api-key +``` + +- **Type:** `string` +- **Required:** No +- **Default:** `"Authorization"` + +### credentials[].prefix + +Auth scheme prefix prepended to the credential value. + +```yaml +credentials: + - host: api.github.com + prefix: "token " +``` + +- **Type:** `string` +- **Required:** No +- **Default:** — (auto-detected for `Authorization` header) + +When the header is `Authorization` and no prefix is set, gatekeeper auto-detects the scheme from known token formats: + +| Token prefix | Scheme | +|-------------|--------| +| `ghp_`, `ghs_` | `token` | +| `gho_`, `github_pat_` | `Bearer` | +| All others | `Bearer` | + +If the credential value already contains a scheme (e.g., `Bearer xxx`), it is used as-is. + +When `format` is `"basic"`, the `prefix` field is used as the Basic auth username instead. + +### credentials[].format + +Auth format for the `Authorization` header. + +```yaml +credentials: + - host: github.com + format: basic + prefix: x-access-token +``` + +- **Type:** `string` +- **Required:** No +- **Default:** — (scheme prefix mode) +- **Valid values:** `""`, `"basic"` + +When set to `"basic"`, the credential is encoded as HTTP Basic authentication: `Authorization: Basic base64(prefix:value)`. The `prefix` field becomes the username and the credential value becomes the password. Only supported with the `Authorization` header. + +### credentials[].grant + +Label for logging and metrics. Does not affect credential injection behavior. + +```yaml +credentials: + - host: api.github.com + grant: github +``` + +- **Type:** `string` +- **Required:** No +- **Default:** — + +Grant names appear in the `grants` field of canonical request log lines and in OTel span attributes. + +### credentials[].source + +Determines where the credential value comes from. See [Credential sources](./03-credential-sources.md) for all source types and their fields. + +```yaml +credentials: + - host: api.github.com + source: + type: env + var: GITHUB_TOKEN +``` + +- **Type:** `object` +- **Required:** Yes +- **Default:** — + +The `type` field selects the source backend. Each type accepts different fields. Extraneous fields for the selected type cause a validation error. + +--- + +## network + +Configures network access policy for proxied requests. + +### network.policy + +Network policy mode. + +```yaml +network: + policy: strict +``` + +- **Type:** `string` +- **Required:** No +- **Default:** `"permissive"` +- **Valid values:** `"permissive"`, `"strict"` + +| Policy | Behavior | +|--------|----------| +| `permissive` | All hosts allowed. `allow` list is ignored. | +| `strict` | Only hosts matching `allow` patterns are permitted. All other requests are denied. | + +### network.allow + +List of hostname glob patterns permitted under `strict` policy. + +```yaml +network: + policy: strict + allow: + - api.github.com + - "*.anthropic.com" + - "registry.npmjs.org" +``` + +- **Type:** `[]string` +- **Required:** No (only meaningful with `policy: strict`) +- **Default:** `[]` + +Patterns support glob syntax. Port numbers are stripped before matching. + +--- + +## log + +Configures structured logging. + +### log.level + +Minimum log level. + +```yaml +log: + level: debug +``` + +- **Type:** `string` +- **Required:** No +- **Default:** `"info"` +- **Valid values:** `"debug"`, `"info"`, `"warn"`, `"error"` + +### log.format + +Log output format. + +```yaml +log: + format: json +``` + +- **Type:** `string` +- **Required:** No +- **Default:** `"text"` +- **Valid values:** `"json"`, `"text"` + +### log.output + +Log output destination. + +```yaml +log: + output: /var/log/gatekeeper.log +``` + +- **Type:** `string` +- **Required:** No +- **Default:** `"stderr"` +- **Valid values:** `"stderr"`, `"stdout"`, or a file path + +When set to a file path, gatekeeper opens the file in append mode (creating it if needed) and closes it on shutdown. + +### log.capture_headers + +Request headers to capture in log output and strip before forwarding to the upstream server. + +```yaml +log: + capture_headers: + - X-Request-Id + - X-Correlation-Id +``` + +- **Type:** `[]string` +- **Required:** No +- **Default:** `[]` + +Captured header values are included as structured log attributes (lowercased, hyphens replaced with underscores). Values longer than 256 characters are truncated at a valid UTF-8 boundary. The headers are removed from the request before it is forwarded upstream. diff --git a/docs/content/reference/03-credential-sources.md b/docs/content/reference/03-credential-sources.md new file mode 100644 index 0000000..76a3cdd --- /dev/null +++ b/docs/content/reference/03-credential-sources.md @@ -0,0 +1,316 @@ +--- +title: "Credential sources" +description: "Reference for all credential source types including env, static, AWS Secrets Manager, GCP Secret Manager, GitHub App, and token exchange." +keywords: ["gatekeeper", "credential sources", "source types", "configuration reference"] +--- + +# Credential sources + +Each credential entry in gatekeeper.yaml includes a `source` block that determines where the credential value comes from. + +## Source types overview + +| Type | Description | Refresh | +|------|-------------|---------| +| `env` | Read from an environment variable | No | +| `static` | Literal inline value | No | +| `aws-secretsmanager` | Fetch from AWS Secrets Manager | No | +| `gcp-secretmanager` | Fetch from GCP Secret Manager | No | +| `github-app` | Generate GitHub App installation token | Yes (auto-refresh before expiry) | +| `token-exchange` | RFC 8693 token exchange | Yes (per-request, cached with TTL) | + +Sources marked **Refresh: Yes** implement background credential refresh. Gatekeeper re-fetches the credential at 75% of the token's TTL (minimum 30 seconds) and hot-swaps it on the proxy without downtime. + +--- + +## env + +Read the credential value from an environment variable at startup. + +```yaml +credentials: + - host: api.github.com + source: + type: env + var: GITHUB_TOKEN +``` + +### var + +Name of the environment variable to read. + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +The variable must be set and non-empty at startup. If unset, gatekeeper exits with an error. + +--- + +## static + +Use a literal value defined inline in the config file. + +```yaml +credentials: + - host: api.example.com + header: x-api-key + source: + type: static + value: sk-xxxx +``` + +### value + +The credential value. + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +> **Note:** Avoid committing secrets in config files. Prefer `env` or a secret manager source for production deployments. + +--- + +## aws-secretsmanager + +Fetch the credential from AWS Secrets Manager at startup. Uses the AWS SDK default credential chain (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, IAM roles, etc.). + +```yaml +credentials: + - host: api.example.com + source: + type: aws-secretsmanager + secret: my-app/api-key + region: us-east-1 +``` + +### secret + +AWS Secrets Manager secret ID or ARN. + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +### region + +AWS region for the Secrets Manager client. + +- **Type:** `string` +- **Required:** No +- **Default:** — (uses `AWS_REGION` or `AWS_DEFAULT_REGION` from the environment) + +--- + +## gcp-secretmanager + +Fetch the credential from GCP Secret Manager at startup. Uses Application Default Credentials (`GOOGLE_APPLICATION_CREDENTIALS`, metadata server, etc.). + +```yaml +credentials: + - host: api.example.com + source: + type: gcp-secretmanager + project: my-gcp-project + secret: api-key + version: latest +``` + +### project + +GCP project ID containing the secret. + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +### secret + +Secret name within the project. + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +### version + +Secret version to access. + +- **Type:** `string` +- **Required:** No +- **Default:** `"latest"` + +The underlying gRPC connection is closed on gatekeeper shutdown. + +--- + +## github-app + +Generate short-lived GitHub App installation access tokens. Tokens refresh automatically in the background before expiry. + +```yaml +credentials: + - host: api.github.com + grant: github + source: + type: github-app + app_id: "12345" + installation_id: "67890" + private_key_path: /etc/gatekeeper/github-app.pem +``` + +### app_id + +GitHub App ID. + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +### installation_id + +GitHub App installation ID. + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +### private_key_path + +File path to the PEM-encoded RSA private key for the GitHub App. + +- **Type:** `string` +- **Required:** One of `private_key_path` or `private_key_env` is required +- **Default:** — + +Mutually exclusive with `private_key_env`. Supports both PKCS#1 (`RSA PRIVATE KEY`) and PKCS#8 (`PRIVATE KEY`) PEM formats. + +### private_key_env + +Name of an environment variable containing the PEM-encoded RSA private key. + +- **Type:** `string` +- **Required:** One of `private_key_path` or `private_key_env` is required +- **Default:** — + +Mutually exclusive with `private_key_path`. The environment variable must be set and non-empty at startup. + +When multiple credentials share the same `github-app` source config (e.g., `api.github.com` and `github.com`), gatekeeper deduplicates them into a single token fetch and a single background refresh goroutine. + +--- + +## token-exchange + +Exchange a per-request subject token for an access token via RFC 8693 (OAuth 2.0 Token Exchange). Unlike other sources, `token-exchange` resolves credentials dynamically per request rather than at startup. + +```yaml +credentials: + - host: api.github.com + grant: github + source: + type: token-exchange + endpoint: https://sts.example.com/token + client_id: gatekeeper + client_secret_env: STS_CLIENT_SECRET + subject_header: X-Subject-Token + resource: https://api.github.com +``` + +### endpoint + +STS token endpoint URL. + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +### client_id + +OAuth client ID for authenticating to the STS via HTTP Basic auth. + +- **Type:** `string` +- **Required:** Yes +- **Default:** — + +### client_secret + +OAuth client secret. Sent as the Basic auth password to the STS endpoint. + +- **Type:** `string` +- **Required:** One of `client_secret` or `client_secret_env` is required +- **Default:** — + +Mutually exclusive with `client_secret_env`. + +### client_secret_env + +Name of an environment variable containing the OAuth client secret. + +- **Type:** `string` +- **Required:** One of `client_secret` or `client_secret_env` is required +- **Default:** — + +Mutually exclusive with `client_secret`. The environment variable must be set and non-empty at startup. + +### subject_header + +HTTP request header containing the subject token. The header is stripped from the request before forwarding. + +- **Type:** `string` +- **Required:** One of `subject_header` or `subject_from` is required +- **Default:** — + +Mutually exclusive with `subject_from`. + +### subject_from + +Alternative subject token extraction method. + +- **Type:** `string` +- **Required:** One of `subject_header` or `subject_from` is required +- **Default:** — +- **Valid values:** `"proxy-auth"` + +When set to `"proxy-auth"`, the subject token is extracted from the username in the `Proxy-Authorization` Basic auth header. + +Mutually exclusive with `subject_header`. + +### subject_token_type + +OAuth token type URI for the subject token. + +- **Type:** `string` +- **Required:** No +- **Default:** `"urn:ietf:params:oauth:token-type:access_token"` + +### resource + +Target resource URI included in the token exchange request. + +- **Type:** `string` +- **Required:** No +- **Default:** — + +### actor_token_from + +Source for the optional RFC 8693 actor token. + +- **Type:** `string` +- **Required:** No +- **Default:** — (no actor token) +- **Valid values:** `"proxy-auth-password"` + +When set to `"proxy-auth-password"`, the actor token is extracted from the password in the `Proxy-Authorization` Basic auth header. Requires `subject_from: proxy-auth`. + +When `actor_token_from` is configured, gatekeeper sets delegate auth mode — the static `auth_token` check is skipped and each caller's identity is validated by the STS instead. + +### actor_token_type + +OAuth token type URI for the actor token. + +- **Type:** `string` +- **Required:** No +- **Default:** `"urn:ietf:params:oauth:token-type:access_token"` + +Exchanged tokens are cached per subject (and actor, if present) using the TTL from the STS `expires_in` response field. If the STS does not return `expires_in`, a default TTL of 5 minutes is used. Concurrent requests for the same subject are coalesced into a single STS call. diff --git a/docs/content/reference/04-environment.md b/docs/content/reference/04-environment.md new file mode 100644 index 0000000..53782e2 --- /dev/null +++ b/docs/content/reference/04-environment.md @@ -0,0 +1,107 @@ +--- +title: "Environment variables" +description: "Reference for all environment variables that Gatekeeper reads, including AWS, GCP, OpenTelemetry, and client-side proxy variables." +keywords: ["gatekeeper", "environment variables", "OTEL", "AWS", "configuration"] +--- + +# Environment variables + +Environment variables that gatekeeper reads or that affect its behavior. + +## Gatekeeper variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `GATEKEEPER_CONFIG` | Path to `gatekeeper.yaml`. Used when `--config` flag is not provided. | — | + +--- + +## Credential source variables + +These variables are referenced by credential source configs in `gatekeeper.yaml`. They are not read directly by gatekeeper itself — they are read when the corresponding source type is configured. + +| Variable | Used by | Description | +|----------|---------|-------------| +| _name from `source.var`_ | `env` source | The credential value. Must be set and non-empty. | +| _name from `source.private_key_env`_ | `github-app` source | PEM-encoded RSA private key for GitHub App authentication. | +| _name from `source.client_secret_env`_ | `token-exchange` source | OAuth client secret for the STS endpoint. | + +--- + +## AWS variables + +Used by the `aws-secretsmanager` credential source via the AWS SDK default credential chain. + +| Variable | Description | +|----------|-------------| +| `AWS_ACCESS_KEY_ID` | AWS access key ID | +| `AWS_SECRET_ACCESS_KEY` | AWS secret access key | +| `AWS_SESSION_TOKEN` | AWS session token (for temporary credentials) | +| `AWS_REGION` | Default AWS region | +| `AWS_DEFAULT_REGION` | Fallback AWS region (used if `AWS_REGION` is unset) | +| `AWS_PROFILE` | Named profile from `~/.aws/credentials` | + +The `region` field in the source config takes precedence over these environment variables. + +--- + +## GCP variables + +Used by the `gcp-secretmanager` credential source via Application Default Credentials. + +| Variable | Description | +|----------|-------------| +| `GOOGLE_APPLICATION_CREDENTIALS` | Path to a service account key JSON file | + +When unset, the GCP SDK uses the metadata server (on GCE/GKE) or gcloud application-default credentials. + +--- + +## OpenTelemetry variables + +Gatekeeper initializes OTLP HTTP exporters for traces, metrics, and logs. All configuration uses standard OpenTelemetry environment variables. When no `OTEL_EXPORTER_OTLP_ENDPOINT` is set, the exporters default to `localhost:4318` (OTLP/HTTP). + +| Variable | Description | +|----------|-------------| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base URL for the OTLP collector (e.g., `http://localhost:4318`) | +| `OTEL_EXPORTER_OTLP_HEADERS` | Headers for OTLP requests (e.g., `Authorization=Bearer token`) | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`http/protobuf` is used by default) | +| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Override endpoint for traces only | +| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Override endpoint for metrics only | +| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Override endpoint for logs only | +| `OTEL_RESOURCE_ATTRIBUTES` | Additional resource attributes (e.g., `deployment.environment=production`) | +| `OTEL_SERVICE_NAME` | Override the service name (default: `gatekeeper`) | + +Gatekeeper registers the following OTel resource attributes at startup: + +| Attribute | Value | +|-----------|-------| +| `service.name` | `gatekeeper` | +| `service.version` | Build version (from `-ldflags -X main.version`) | + +### Metrics + +| Metric | Type | Description | +|--------|------|-------------| +| `proxy.request.duration` | Histogram (seconds) | Duration of proxy requests | +| `proxy.request.count` | Counter | Total number of proxy requests | +| `proxy.credential.injections` | Counter | Total number of credential injections | +| `proxy.policy.denials` | Counter | Total number of policy denials | + +--- + +## Client-side variables + +These variables are set on the client side (inside the container), not on the gatekeeper process. They direct HTTP traffic through the proxy. + +| Variable | Description | +|----------|-------------| +| `HTTP_PROXY` | Proxy URL for HTTP requests (e.g., `http://127.0.0.1:8080`) | +| `HTTPS_PROXY` | Proxy URL for HTTPS requests (e.g., `http://127.0.0.1:8080`) | +| `NO_PROXY` | Comma-separated list of hosts that bypass the proxy | + +When `proxy.auth_token` is configured, include the token in the proxy URL: + +```bash +export HTTPS_PROXY=http://user:my-secret-token@127.0.0.1:8080 +``` diff --git a/docs/content/reference/05-llm-policy.md b/docs/content/reference/05-llm-policy.md new file mode 100644 index 0000000..5d40c29 --- /dev/null +++ b/docs/content/reference/05-llm-policy.md @@ -0,0 +1,76 @@ +--- +title: "LLM policy" +description: "Reference for Gatekeeper's LLM policy evaluation, which evaluates Anthropic API responses against Keep policy rules." +keywords: ["gatekeeper", "LLM policy", "Keep", "policy evaluation", "Anthropic"] +--- + +# LLM policy + +Gatekeeper evaluates Anthropic API responses against [Keep](https://github.com/majorcontext/keep) policy rules. Denied responses are blocked before reaching the client. + +## Overview + +LLM policy evaluation is configured through `RunContextData.KeepEngines` — a map of hostnames to Keep engine instances. When a proxied response matches a host with a Keep engine, gatekeeper buffers the response body, evaluates it against the engine's rules, and either forwards or blocks the response. + +This feature is primarily used through moat's configuration layer, which compiles Keep rule files into engines and attaches them to per-run context. Standalone gatekeeper does not expose Keep configuration in `gatekeeper.yaml`. + +--- + +## Evaluation flow + +1. The proxy intercepts an HTTPS response from a host that has a Keep engine configured (e.g., `api.anthropic.com`). +2. The response body is buffered up to 10 MB (`maxLLMResponseSize`). Responses exceeding this limit are denied (fail-closed). +3. If the response is gzip-compressed, it is decompressed for evaluation. +4. The response is evaluated based on its `Content-Type`: + - **JSON responses** — Parsed and evaluated via `llm.EvaluateResponse`. + - **SSE streaming responses** (`text/event-stream`) — SSE events are parsed, evaluated via `llm.EvaluateStream`, and the (possibly redacted) events are forwarded. +5. If the policy denies the response, a JSON error body is returned to the client. + +--- + +## Fail-closed behavior + +All evaluation errors result in denial: + +- Gzip decompression failures +- SSE parse errors +- Keep evaluation errors +- Response bodies exceeding 10 MB + +--- + +## Denied response format + +When a response is denied, the client receives an HTTP 200 with a JSON body matching the Keep LLM gateway format: + +```json +{ + "type": "error", + "error": { + "type": "policy_denied", + "message": "Policy denied: rule-name. Human-readable message." + } +} +``` + +--- + +## Observability + +Policy denials are logged at `warn` level with fields: + +| Field | Description | +|-------|-------------| +| `run_id` | Run ID from per-run context | +| `scope` | Policy scope (e.g., `"llm"`) | +| `operation` | Operation type | +| `rule` | Rule name that triggered the denial | +| `message` | Human-readable denial message | + +Policy denials are also recorded as OTel span events (`policy.denial`) and increment the `proxy.policy.denials` counter with `proxy.policy.scope` and `proxy.policy.rule` attributes. + +--- + +## Codec + +Gatekeeper uses the Anthropic codec (`anthropic.NewCodec()`) for parsing API responses. The codec is stateless and shared across all requests. From e6bad0553ba41a5f30308a839d06a1305817f811 Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Fri, 15 May 2026 15:55:19 -0700 Subject: [PATCH 2/9] Update docs/content/reference/03-credential-sources.md Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- docs/content/reference/03-credential-sources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/03-credential-sources.md b/docs/content/reference/03-credential-sources.md index 76a3cdd..c5e65a0 100644 --- a/docs/content/reference/03-credential-sources.md +++ b/docs/content/reference/03-credential-sources.md @@ -20,7 +20,7 @@ Each credential entry in gatekeeper.yaml includes a `source` block that determin | `token-exchange` | RFC 8693 token exchange | Yes (per-request, cached with TTL) | Sources marked **Refresh: Yes** implement background credential refresh. Gatekeeper re-fetches the credential at 75% of the token's TTL (minimum 30 seconds) and hot-swaps it on the proxy without downtime. - +Sources marked **Refresh: Yes** have credentials that expire. `github-app` implements background credential refresh — gatekeeper re-fetches at 75% of TTL (minimum 30 seconds) and hot-swaps without downtime. `token-exchange` uses per-request lazy caching: on cache miss, gatekeeper calls the STS and caches the result for the token's TTL. --- ## env From 941ffbd69cbdd7fe1ccb1097d27c27b3cd19df31 Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Fri, 15 May 2026 15:55:29 -0700 Subject: [PATCH 3/9] Update docs/content/concepts/03-credential-sources.md Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- docs/content/concepts/03-credential-sources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/concepts/03-credential-sources.md b/docs/content/concepts/03-credential-sources.md index 149adbb..ab6ec51 100644 --- a/docs/content/concepts/03-credential-sources.md +++ b/docs/content/concepts/03-credential-sources.md @@ -88,7 +88,7 @@ Both entries share the same `github-app` source. Gatekeeper makes one API call t 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`: ```go -type CredentialResolver func(ctx context.Context, proxyReq, innerReq *http.Request, host string) ([]credentialHeader, error) +type CredentialResolver func(ctx context.Context, proxyReq, innerReq *http.Request, host string) ([]CredentialHeader, error) ``` Unlike static sources (fetched once at startup), resolvers are called on every request. They receive both the proxy-level request (`proxyReq`, carrying `Proxy-Authorization`) and the application-level request (`innerReq`, which the resolver may inspect and modify). This enables patterns like extracting a subject identity header from the request, exchanging it for an access token, and stripping the identity header before forwarding. From d663e758ec3a6fb5e9d1b0b62d5ad9f496ebcdd0 Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Fri, 15 May 2026 15:55:40 -0700 Subject: [PATCH 4/9] Update docs/content/guides/09-go-library.md Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- docs/content/guides/09-go-library.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/content/guides/09-go-library.md b/docs/content/guides/09-go-library.md index f513356..6bc2d0e 100644 --- a/docs/content/guides/09-go-library.md +++ b/docs/content/guides/09-go-library.md @@ -35,9 +35,14 @@ import ( func main() { // Load CA for TLS interception. - certPEM, _ := os.ReadFile("ca.crt") - keyPEM, _ := os.ReadFile("ca.key") - ca, err := proxy.LoadCA(certPEM, keyPEM) + certPEM, err := os.ReadFile("ca.crt") + if err != nil { + log.Fatal(err) + } + keyPEM, err := os.ReadFile("ca.key") + if err != nil { + log.Fatal(err) + } if err != nil { log.Fatal(err) } From b29ce2c18359b81be59f17cf36377ad23ee678e9 Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Fri, 15 May 2026 15:59:24 -0700 Subject: [PATCH 5/9] Update docs/content/reference/03-credential-sources.md Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- docs/content/reference/03-credential-sources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/03-credential-sources.md b/docs/content/reference/03-credential-sources.md index c5e65a0..2daa58d 100644 --- a/docs/content/reference/03-credential-sources.md +++ b/docs/content/reference/03-credential-sources.md @@ -19,8 +19,8 @@ Each credential entry in gatekeeper.yaml includes a `source` block that determin | `github-app` | Generate GitHub App installation token | Yes (auto-refresh before expiry) | | `token-exchange` | RFC 8693 token exchange | Yes (per-request, cached with TTL) | -Sources marked **Refresh: Yes** implement background credential refresh. Gatekeeper re-fetches the credential at 75% of the token's TTL (minimum 30 seconds) and hot-swaps it on the proxy without downtime. Sources marked **Refresh: Yes** have credentials that expire. `github-app` implements background credential refresh — gatekeeper re-fetches at 75% of TTL (minimum 30 seconds) and hot-swaps without downtime. `token-exchange` uses per-request lazy caching: on cache miss, gatekeeper calls the STS and caches the result for the token's TTL. + --- ## env From a33d8cf117b4d4b4509278cdd7bca3db2812733f Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 23:01:22 +0000 Subject: [PATCH 6/9] docs(guides): fix go-library code example Restore the missing proxy.LoadCA call that was dropped in a prior edit, remove the orphaned error check, and add a comment clarifying that AllowedHosts requires []proxy.HostPattern built via proxy.ParseHostPattern. --- docs/content/guides/09-go-library.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/content/guides/09-go-library.md b/docs/content/guides/09-go-library.md index 6bc2d0e..14296f9 100644 --- a/docs/content/guides/09-go-library.md +++ b/docs/content/guides/09-go-library.md @@ -43,6 +43,7 @@ func main() { if err != nil { log.Fatal(err) } + ca, err := proxy.LoadCA(certPEM, keyPEM) if err != nil { log.Fatal(err) } @@ -94,7 +95,7 @@ p.SetContextResolver(func(token string) (*proxy.RunContextData, bool) { }, }, Policy: "strict", - AllowedHosts: run.AllowedHosts, + AllowedHosts: run.AllowedHosts, // []proxy.HostPattern; build from strings with proxy.ParseHostPattern }, true }) ``` From 291be3c39a3afce148850dea6ec55ca4fb5a2470 Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Fri, 15 May 2026 23:09:18 +0000 Subject: [PATCH 7/9] docs(guides): show explicit []proxy.HostPattern construction Demonstrate building AllowedHosts from string slices via proxy.ParseHostPattern so the example compiles as-shown. --- docs/content/guides/09-go-library.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/content/guides/09-go-library.md b/docs/content/guides/09-go-library.md index 14296f9..77b6d1e 100644 --- a/docs/content/guides/09-go-library.md +++ b/docs/content/guides/09-go-library.md @@ -87,6 +87,12 @@ p.SetContextResolver(func(token string) (*proxy.RunContextData, bool) { if !ok { return nil, false } + // AllowedHosts is []proxy.HostPattern — build it from raw strings + // via proxy.ParseHostPattern. + allowedHosts := make([]proxy.HostPattern, len(run.AllowedHosts)) + for i, h := range run.AllowedHosts { + allowedHosts[i] = proxy.ParseHostPattern(h) + } return &proxy.RunContextData{ RunID: run.ID, Credentials: map[string][]proxy.CredentialHeader{ @@ -95,7 +101,7 @@ p.SetContextResolver(func(token string) (*proxy.RunContextData, bool) { }, }, Policy: "strict", - AllowedHosts: run.AllowedHosts, // []proxy.HostPattern; build from strings with proxy.ParseHostPattern + AllowedHosts: allowedHosts, }, true }) ``` From f07429b33a8824044ed72d7e9d456be012415f6e Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Fri, 15 May 2026 23:20:33 +0000 Subject: [PATCH 8/9] docs(concepts): correct OTel default exporter behavior Gatekeeper always creates OTLP HTTP exporters; there is no no-op fallback. With no OTEL_EXPORTER_OTLP_ENDPOINT set, exporters default to localhost:4318. Align with reference/04-environment.md. --- docs/content/concepts/06-observability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/concepts/06-observability.md b/docs/content/concepts/06-observability.md index d188eb8..9030a20 100644 --- a/docs/content/concepts/06-observability.md +++ b/docs/content/concepts/06-observability.md @@ -93,4 +93,4 @@ Four metrics instruments are registered under the `gatekeeper` meter: ## Configuration -OTel is configured entirely via standard `OTEL_*` environment variables. There are no YAML knobs for tracing, metrics, or logs. The CLI entry point (`cmd/gatekeeper/main.go`) creates OTLP HTTP exporters for traces, metrics, and logs and registers them as global providers. When no `OTEL_*` variables are set, the no-op provider is used and instrumentation has zero overhead. +OTel is configured entirely via standard `OTEL_*` environment variables. There are no YAML knobs for tracing, metrics, or logs. The CLI entry point (`cmd/gatekeeper/main.go`) always creates OTLP HTTP exporters for traces, metrics, and logs and registers them as global providers. When no `OTEL_EXPORTER_OTLP_ENDPOINT` is set, exporters default to `localhost:4318` — gatekeeper will attempt to connect to a local collector even with no `OTEL_*` variables configured. From d18523cae594fb7aae82dd3cef14a369a4db0981 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 23:24:36 +0000 Subject: [PATCH 9/9] =?UTF-8?q?docs(reference):=20fix=20prefix=20example?= =?UTF-8?q?=20=E2=80=94=20trailing=20space=20causes=20double-space=20heade?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/content/reference/02-config-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/02-config-file.md b/docs/content/reference/02-config-file.md index cad1288..371141a 100644 --- a/docs/content/reference/02-config-file.md +++ b/docs/content/reference/02-config-file.md @@ -187,7 +187,7 @@ Auth scheme prefix prepended to the credential value. ```yaml credentials: - host: api.github.com - prefix: "token " + prefix: "token" ``` - **Type:** `string`