gatekeeper

package module
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2026 License: MIT Imports: 21 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.

# 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+

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

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-..."
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).

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

This section is empty.

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, 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)
}

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
}

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", "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
	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
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