gatekeeper

package module
v0.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 20 Imported by: 0

README

Gatekeeper

A credential-injecting TLS-intercepting proxy. Route HTTPS traffic through Gatekeeper and it transparently injects authentication headers based on hostname matching. Clients never see raw credentials.

Full documentation: docs/README.md.

# Start the proxy
gatekeeper --config gatekeeper.yaml

# In another terminal — credential injected automatically
curl --proxy http://127.0.0.1:9080 --cacert ca.crt https://api.github.com/user

No GITHUB_TOKEN in the command. No secrets in environment variables. The token is resolved from the configured source and injected at the network layer.

Gatekeeper also runs an optional Postgres data plane — connect to Neon databases with only a run token, never a database password.

Installation

go install github.com/majorcontext/gatekeeper/cmd/gatekeeper@latest

Requires: Go 1.25+

Or pull the published Docker image:

docker pull ghcr.io/majorcontext/gatekeeper:latest

docker run --rm -v ./gatekeeper.yaml:/etc/gatekeeper/gatekeeper.yaml \
  ghcr.io/majorcontext/gatekeeper --config /etc/gatekeeper/gatekeeper.yaml

How it works

  1. Client sends CONNECT host:443 through the proxy
  2. Proxy terminates TLS using a dynamically-generated certificate for that host
  3. Proxy reads the plaintext request, injects the matching credential as an HTTP header
  4. Request is forwarded to the real server over a separate TLS connection
  5. Response streams back to the client

The proxy needs a CA certificate to sign per-host certificates. Generate one with the included script:

cd examples && ./gen-ca.sh

Configuration

proxy:
  host: 127.0.0.1
  port: 9080
  # proxy_protocol: true  # behind a TCP-terminating LB, recover the real client_ip

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

Credential sources

Environment variable
source:
  type: env
  var: GITHUB_TOKEN
Static value
source:
  type: static
  value: "Bearer sk-..."
Process

Runs a host command and uses its stdout as the credential. If the output is AWS credential_process-format JSON with an Expiration, the credential refreshes automatically on that expiry.

source:
  type: process
  command: "op read op://vault/example/api-key"
AWS Secrets Manager
source:
  type: aws-secretsmanager
  secret: prod/api-key
  region: us-east-1
GitHub App

Generates short-lived installation tokens from a GitHub App private key. Tokens refresh automatically in the background at 75% of TTL.

source:
  type: github-app
  app_id: "12345"
  installation_id: "67890"
  private_key_path: ./key.pem       # or use private_key_env

See examples/gatekeeper-github-app.yaml for a complete example.

Network policy

Control which hosts the proxy will forward traffic to:

network:
  policy: strict       # deny all except explicitly allowed
  allow:
    - "api.github.com"
    - "*.anthropic.com"

Policies: permissive (allow all), strict (deny all, allow listed).

PROXY protocol (client IP behind a load balancer)

Behind a TCP-terminating load balancer (e.g. GCP's global TCP Proxy LB), every connection's peer address is the load balancer's, not the real client. PROXY protocol parsing recovers the real client address, and it's configured per listener: proxy.proxy_protocol: true for the HTTP/CONNECT listener, and postgres.proxy_protocol: true for the Postgres data-plane listener. Each parses a PROXY protocol v1/v2 header from the LB and uses its advertised source as the client_ip recorded in request logs. Both are fail-open (headerless connections, like LB health checks, fall back to the raw peer address) and should only be enabled when that listener's port is reachable solely through the load balancer, since the header is otherwise forgeable by any direct client. See Deploying behind a TCP load balancer.

MCP relay

For MCP clients that can't route through HTTP_PROXY, Gatekeeper relays Model Context Protocol requests directly, injecting credentials and streaming SSE responses:

POST http://127.0.0.1:9080/mcp/context7/v1/endpoint

Gatekeeper resolves the credential configured for the context7 MCP server and injects it before forwarding to the real server. MCP servers are registered via MCPServerConfig in the Go library — not exposed in gatekeeper.yaml — which is how Moat's daemon layer wires it up per run.

LLM policy

Gatekeeper can evaluate Anthropic API responses against Keep policy rules before they reach the client, blocking a response that violates a rule instead of forwarding it. Like MCP relay, this is configured via RunContextData.KeepEngines in the Go library, not gatekeeper.yaml — it's primarily used through Moat's daemon layer.

Host gateway

A synthetic hostname used inside a sandboxed container can be mapped to the real host machine's IP, so containers reach host services without relying on host.docker.internal:

container ──▶ moat-host-gateway:8080 ──▶ gatekeeper ──▶ {HostGatewayIP}:8080

Set via RunContextData.HostGateway/HostGatewayIP in the Go library. Destination ports must be explicitly listed in AllowedHostPorts — traffic to unlisted ports is denied.

Postgres data plane

Gatekeeper can run a second listener that speaks the Postgres wire protocol, letting a sandboxed client connect to arbitrary Neon databases without any database secret ever entering the sandbox. The only credential the client holds is a run-scoped token, which is useless outside Gatekeeper.

The client connects to the real Neon hostname and presents its run token (or the proxy's auth_token) as the Postgres password. Gatekeeper terminates TLS with a CA-minted certificate, reads the target endpoint from the TLS SNI, resolves the real per-branch password from the Neon API on the fly, completes SCRAM-SHA-256 with the upstream server, and relays the connection. The run token travels as a cleartext password, but only inside Gatekeeper's own TLS tunnel — the same trust model as Proxy-Authorization on the HTTP plane.

client ──TLS(token as password)──▶ gatekeeper ──TLS(SCRAM, real password)──▶ Neon
         SNI: ep-...neon.tech                    resolved via Neon API

Routing is by SNI. The target endpoint travels in the TLS SNI field, so the embedder must arrange DNS inside the container so *.neon.tech resolves to Gatekeeper. That DNS plumbing is outside Gatekeeper's scope — it's the embedder's responsibility (e.g. Moat handles it). The v1 data plane is a blind message relay: it routes on SNI only and does no SQL-level inspection.

A Postgres listener requires a CA (tls.ca_cert and tls.ca_key) for TLS termination — Gatekeeper errors at startup otherwise.

Two resolvers are available:

  • neon — the source supplies a Neon API key; per-branch passwords are minted from the Neon API and cached with a TTL. With an account-scoped key, Gatekeeper discovers an endpoint's project automatically; with a project-scoped key (which can't list projects), set project: on the credential to its project ID.
  • static — the source supplies a fixed password directly (for non-Neon Postgres or testing).
postgres:
  host: 127.0.0.1      # optional, defaults to the proxy host
  port: 5432

tls:
  ca_cert: ca.crt
  ca_key: ca.key

credentials:
  - host: "*.neon.tech"
    postgres:
      resolver: neon
    source:
      type: env
      var: NEON_API_KEY
    grant: neon-databases

Once DNS routes *.neon.tech to Gatekeeper, connect with the run token as the password:

PGPASSWORD=<run-token> psql \
  "host=ep-cool-darkness-123456.us-east-2.aws.neon.tech dbname=neondb user=neondb_owner sslmode=require"

Moat sets PGPASSWORD in the container so the token never appears in the agent's command line. See examples/gatekeeper-postgres.yaml for a complete example.

Observability

Gatekeeper supports OpenTelemetry for traces, metrics, and logs. No YAML configuration needed — use standard OTEL_* environment variables:

export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <token>"
gatekeeper --config gatekeeper.yaml

Library usage

Gatekeeper is a Go module. Import the proxy engine directly for custom integrations:

import (
    "github.com/majorcontext/gatekeeper/proxy"
)

ca, _ := proxy.LoadCA(certPEM, keyPEM)

p := proxy.NewProxy()
p.SetCA(ca)
p.SetCredentialWithGrant("api.github.com", "Authorization", "Bearer xxx", "github")

Moat uses Gatekeeper this way — importing the proxy and adding per-run credential scoping via a daemon layer.

Development

go build ./...           # build
go test -race ./...      # test
go vet ./...             # lint

License

MIT — see LICENSE.

Documentation

Overview

Package gatekeeper provides a standalone credential-injecting TLS proxy.

Credentials are pre-configured in gatekeeper.yaml and injected for all proxied requests matching the host. Access control is via network policy (who can reach the proxy) and an optional static auth token.

For per-caller credential isolation (run registration, token-scoped credentials), use the daemon package, which provides a management API over a Unix socket.

Index

Constants

View Source
const OTelDiagnosticKey = "otel_diagnostic"

OTelDiagnosticKey marks a log record as gatekeeper's own diagnostic about the OTel pipeline itself — currently, the OTel SDK/export error records cmd/gatekeeper's logOTelError logs at DEBUG. configureLogging's otelslog bridge handler excludes any record carrying this attribute (see newOTelDiagnosticFilter): without the exclusion, a failed OTel log export produces a DEBUG diagnostic that is itself fanned out to the same OTel log-export pipeline that just failed, so it gets queued, fails on the next export attempt, produces another diagnostic, and so on indefinitely while a collector is unreachable. The console/file handler still receives every record regardless of this marker.

Variables

This section is empty.

Functions

func ResolveCredentialSource added in v0.5.0

ResolveCredentialSource creates either a static CredentialSource or a dynamic CredentialResolver from a credential config. For static sources (env, static, process, aws-secretsmanager, gcp-secretmanager, gcp-service-account, github-app), the first return is non-nil. For dynamic sources (token-exchange), the second return is non-nil.

func ResolveSource

ResolveSource creates a CredentialSource from a SourceConfig. Returns an error if the config contains fields not relevant to the selected type.

Types

type Config

type Config struct {
	Proxy       ProxyConfig        `yaml:"proxy"`
	TLS         TLSConfig          `yaml:"tls"`
	Credentials []CredentialConfig `yaml:"credentials"`
	Network     NetworkConfig      `yaml:"network"`
	Log         LogConfig          `yaml:"log"`
	Postgres    *PostgresConfig    `yaml:"postgres,omitempty"`
}

Config represents a Gate Keeper configuration file.

func LoadConfig

func LoadConfig(path string) (*Config, error)

LoadConfig reads and parses a Gate Keeper config from a file path.

func ParseConfig

func ParseConfig(data []byte) (*Config, error)

ParseConfig parses a Gate Keeper config from YAML bytes.

type CredentialConfig

type CredentialConfig struct {
	Host     string                    `yaml:"host"`             // Target host (e.g., "api.github.com")
	Header   string                    `yaml:"header,omitempty"` // Header name (default: "Authorization")
	Prefix   string                    `yaml:"prefix,omitempty"` // Auth scheme prefix (e.g., "Bearer", "token"); auto-detected if omitted
	Format   string                    `yaml:"format,omitempty"` // Auth format: "" (default scheme prefix) or "basic" (HTTP Basic)
	Source   SourceConfig              `yaml:"source"`
	Grant    string                    `yaml:"grant,omitempty"` // Optional label for logging
	Postgres *PostgresCredentialConfig `yaml:"postgres,omitempty"`
}

CredentialConfig describes a credential to resolve and inject. Host specifies which requests receive the credential. Header names the HTTP header to set (defaults to "Authorization"). Grant is an optional label used for logging.

When the header is "Authorization", the proxy needs a full header value including the auth scheme (e.g., "Bearer token123"). If the source value is a bare token without a scheme prefix, the gatekeeper auto-detects the correct scheme from known token prefixes (GitHub ghp_/gho_/etc.) or defaults to "Bearer". Set Prefix to override the auto-detected scheme.

For hosts that require HTTP Basic authentication (e.g., github.com git smart HTTP), set Format to "basic" and Prefix to the Basic auth username. The credential value becomes the password: Authorization: Basic base64(prefix:value).

type LogConfig

type LogConfig struct {
	Level          string   `yaml:"level"`                     // Log level (e.g., "debug", "info", "warn", "error")
	Format         string   `yaml:"format"`                    // Output format ("json" or "text")
	Output         string   `yaml:"output"`                    // Destination ("stderr", "stdout", or a file path; default: stderr)
	CaptureHeaders []string `yaml:"capture_headers,omitempty"` // Request headers to log and strip before forwarding
}

LogConfig configures logging.

type NetworkConfig

type NetworkConfig struct {
	Policy string   `yaml:"policy"`
	Allow  []string `yaml:"allow,omitempty"`
}

NetworkConfig configures network policy.

type PostgresConfig added in v0.12.0

type PostgresConfig struct {
	Port int    `yaml:"port"`           // listener port (e.g. 5432)
	Host string `yaml:"host,omitempty"` // bind address (default: same as proxy host)

	// ProxyProtocol enables PROXY protocol v1/v2 parsing on the Postgres
	// data-plane listener, mirroring proxy.proxy_protocol: when true, each
	// inbound connection is checked for a leading PROXY protocol header (as
	// prepended by a TCP-terminating load balancer in front of the Postgres
	// port) and, if present, the advertised source address replaces the raw
	// TCP peer address as the connection's client address for logging.
	// Connections that do not open with a PROXY header fall back to the raw
	// TCP peer address (fail-open), so load balancer health checks and
	// direct probes of the port keep working. Default false.
	//
	// Because headers are honored from any peer, a client that can reach
	// this listener directly (bypassing the load balancer) can forge the
	// logged client_ip by prepending its own PROXY header. Only enable this
	// when the port is reachable solely through the load balancer, and never
	// use client_ip for security decisions.
	ProxyProtocol bool `yaml:"proxy_protocol,omitempty"`
}

PostgresConfig configures the Postgres data-plane listener. When present, gatekeeper starts a Postgres-protocol listener that authenticates clients with their run token and injects resolved database credentials upstream.

type PostgresCredentialConfig added in v0.12.0

type PostgresCredentialConfig struct {
	Resolver string `yaml:"resolver"`
	Project  string `yaml:"project,omitempty"` // optional Neon project ID; required for project-scoped API keys
}

PostgresCredentialConfig marks a credential as a Postgres credential and selects how the upstream password is resolved. Resolver is "neon" (the Source supplies the Neon API key, passwords are minted per branch) or "static" (the Source supplies the password directly).

type ProxyConfig

type ProxyConfig struct {
	Port      int    `yaml:"port"`
	Host      string `yaml:"host"`
	AuthToken string `yaml:"auth_token,omitempty"` // Optional token clients must provide via Proxy-Authorization

	// ProxyProtocol enables PROXY protocol v1/v2 parsing on the HTTP/CONNECT
	// proxy listener. When true, each inbound connection is checked for a
	// leading PROXY protocol header (as prepended by a TCP load balancer,
	// e.g. GCP's global TCP Proxy LB) and, if present, the advertised source
	// address replaces the raw TCP peer address as the connection's client
	// address for logging (the client_ip request-log attribute). Connections
	// that do not open with a PROXY header fall back to the raw TCP peer
	// address (fail-open), so load balancer health checks and direct probes
	// of the port keep working. Default false.
	//
	// Because headers are honored from any peer, a client that can reach the
	// listener directly (bypassing the load balancer) can forge the logged
	// client_ip by prepending its own PROXY header. Only enable this when
	// the port is reachable solely through the load balancer, and never use
	// client_ip for security decisions.
	ProxyProtocol bool `yaml:"proxy_protocol,omitempty"`
}

ProxyConfig configures the proxy listener.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the Gate Keeper server. It manages a TLS-intercepting proxy with statically configured credentials.

func New

func New(ctx context.Context, cfg *Config, version string) (*Server, error)

New creates a new Gate Keeper server from the given configuration. The context is used for credential fetching (e.g., AWS Secrets Manager) and can be used to cancel startup if the process receives a signal. The version string is included in the startup log line; pass "" if unknown.

func (*Server) PostgresAddr added in v0.12.0

func (s *Server) PostgresAddr() string

PostgresAddr returns the Postgres data-plane listener's actual address (host:port). Returns empty string if no Postgres listener is configured or it has not started.

func (*Server) ProxyAddr

func (s *Server) ProxyAddr() string

ProxyAddr returns the proxy listener's actual address (host:port). Returns empty string if the proxy has not started.

func (*Server) Start

func (s *Server) Start(ctx context.Context) error

Start starts the proxy. It blocks until the context is canceled.

func (*Server) Stop

func (s *Server) Stop(ctx context.Context) error

Stop gracefully shuts down the proxy server and all background refresh goroutines.

type SourceConfig

type SourceConfig struct {
	Type    string `yaml:"type"`              // "env", "static", "process", "aws-secretsmanager", "gcp-secretmanager", "gcp-service-account", "github-app", "token-exchange"
	Var     string `yaml:"var,omitempty"`     // for env source
	Value   string `yaml:"value,omitempty"`   // for static source
	Command string `yaml:"command,omitempty"` // for process source: host command run with `sh -c`
	TTL     string `yaml:"ttl,omitempty"`     // for process source: refresh interval when output has no expiry (Go duration, default 5m)
	Secret  string `yaml:"secret,omitempty"`  // for aws-secretsmanager, gcp-secretmanager; for gcp-service-account, the secret holding the key JSON
	Region  string `yaml:"region,omitempty"`  // for aws-secretsmanager
	Project string `yaml:"project,omitempty"` // for gcp-secretmanager, gcp-service-account
	Version string `yaml:"version,omitempty"` // for gcp-secretmanager, gcp-service-account (default: "latest")

	AppID          string `yaml:"app_id,omitempty"`           // for github-app source
	InstallationID string `yaml:"installation_id,omitempty"`  // for github-app source
	PrivateKeyPath string `yaml:"private_key_path,omitempty"` // for github-app (PEM key), gcp-service-account (key JSON)
	PrivateKeyEnv  string `yaml:"private_key_env,omitempty"`  // for github-app (PEM key), gcp-service-account (key JSON)
	Scopes         string `yaml:"scopes,omitempty"`           // for gcp-service-account: space-separated OAuth scopes (default: cloud-platform)

	// token-exchange (RFC 8693) fields
	Endpoint         string `yaml:"endpoint,omitempty"`
	ClientID         string `yaml:"client_id,omitempty"`
	ClientSecret     string `yaml:"client_secret,omitempty"`
	ClientSecretEnv  string `yaml:"client_secret_env,omitempty"`
	SubjectHeader    string `yaml:"subject_header,omitempty"`
	SubjectFrom      string `yaml:"subject_from,omitempty"`
	SubjectTokenType string `yaml:"subject_token_type,omitempty"`
	Resource         string `yaml:"resource,omitempty"`
	ActorTokenFrom   string `yaml:"actor_token_from,omitempty"`
	ActorTokenType   string `yaml:"actor_token_type,omitempty"`
}

SourceConfig describes where to read a credential value from.

SourceConfig is used as a map key to deduplicate identical sources (see Server.setCredentials), so it must remain comparable: add list-valued fields as delimited strings (like Scopes), never slices, and avoid pointer fields, which would compile but silently break deduplication.

type TLSConfig

type TLSConfig struct {
	CACert string `yaml:"ca_cert"`
	CAKey  string `yaml:"ca_key"`
}

TLSConfig configures the CA certificate used for TLS interception.

Directories

Path Synopsis
cmd
gatekeeper command
skill-lint command
Command skill-lint validates Agent Skills against the open specification at https://agentskills.io/specification.
Command skill-lint validates Agent Skills against the open specification at https://agentskills.io/specification.
Package proxy provides a TLS-intercepting HTTP proxy for credential injection.
Package proxy provides a TLS-intercepting HTTP proxy for credential injection.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL