Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

go-gateway

A simple API gateway template: a single Go entrypoint that reverse-proxies requests to your backend services by path prefix, with security, traffic control, and resilience features built in and configurable per route.

                    ┌──────────────────┐
                    │      client       │
                    └─────────┬────────┘
                              │
                    ┌─────────▼────────┐
                    │      gateway      │  (this repo)
                    │  request ID, log, │
                    │  security headers,│
                    │  Keycloak auth    │
                    └──┬─────┬──────┬───┘
           /api/auth/* │     │      │ /* (catch-all)
                       │     │/api/db/*
                 ┌─────▼┐ ┌──▼───┐ ┌▼──────────┐
                 │ auth │ │  db  │ │ SvelteKit │
                 │ svc  │ │ svc  │ │ frontend  │
                 └──────┘ └──────┘ └───────────┘

Requests to /api/db/* are validated against a Keycloak realm's JWKS endpoint before being proxied, and can additionally require specific Keycloak roles, be rate-limited, CORS-restricted, deadline-bounded, and resilience-wrapped (retries + circuit breaking) — all per route, all config-driven. /api/auth/* and everything else pass through untouched unless configured otherwise.

Requirements

  • Go 1.24+
  • (optional, for the full local walkthrough) Docker, to run a local Keycloak

Project layout

cmd/gateway/main.go     entrypoint: flags, logging, config, server, TLS, hot-reload, shutdown
internal/config/        Config struct, Load()/Watch() (Viper), defaults, validation
internal/logging/       structured (slog) logger constructor
internal/middleware/    AuthFunc extension point, security headers, CORS, rate limiting, roles, request logging
internal/auth/          KeycloakValidator: the default AuthFunc implementation
internal/proxy/         per-route httputil.ReverseProxy construction, retry + circuit-breaker transport
internal/server/        wires config + middleware + proxies into a hot-reloadable *http.Server
config/config.yaml      sample config (auth, db, frontend routes; db shows every optional feature)

Dependencies

This template intentionally uses a handful of small, well-known libraries instead of staying 100% standard library:

Library Why
spf13/viper YAML config with comments, automatic env-var overrides (GATEWAY_*), and built-in file-watching for hot-reload
go-chi/chi Mount() is a natural fit for "hand off everything under this prefix" reverse-proxy routing, plus reusable RequestID/RealIP middleware
lestrrat-go/jwx JWKS fetching/caching and JWT parsing/validation for the Keycloak auth hook
sony/gobreaker Circuit breaker state machine for the per-route resilience wrapper
golang.org/x/time/rate Token-bucket rate limiting (Go team maintained, effectively an stdlib extension)

Everything else — proxy construction, the middleware chain, structured logging, graceful shutdown — is standard library only.

Configuration

All fields live in config/config.yaml (path overridable via -config). Every field can also be set via env var: GATEWAY_<SECTION>_<FIELD>, e.g. GATEWAY_SERVER_ADDR or GATEWAY_AUTH_JWKS_URL.

server

Field Default Description
addr :8080 Listen address
read_timeout_seconds 10 http.Server.ReadTimeout
write_timeout_seconds 30 http.Server.WriteTimeout; also the fallback per-route request timeout
idle_timeout_seconds 120 http.Server.IdleTimeout
shutdown_timeout_seconds 15 Max time to drain in-flight requests on shutdown
hsts_enabled false Send Strict-Transport-Security even without local TLS — set this when TLS is terminated upstream (e.g. behind a Cloudflare Tunnel)

tls

Optional. Leave both empty (the default) to serve plain HTTP — the normal case behind a TLS-terminating proxy or tunnel. Set both to have the gateway terminate TLS itself.

Field Default Description
cert_file "" PEM certificate file
key_file "" PEM private key file

auth

Required only if at least one route has require_auth: true — config loading fails fast otherwise.

Field Default Description
jwks_url — Keycloak realm's JWKS endpoint, e.g. .../realms/<realm>/protocol/openid-connect/certs
issuer — Expected iss claim, e.g. http://localhost:8180/realms/<realm>
audience — Optional; if set, the expected aud claim
jwks_refresh_seconds 300 How often to refresh cached signing keys

routes

Field Description
name Human-readable, used in logs
path_prefix Must end in /, e.g. /api/db/
target Backend base URL, e.g. http://localhost:8082
strip_prefix If true, strips path_prefix before forwarding
require_auth If true, requests must carry a valid Bearer token
required_roles Optional list; if set, the token's Keycloak realm roles must include at least one. Requires require_auth: true
timeout_seconds Optional; bounds this route's full handling time (auth, rate limiting, proxying incl. retries). Falls back to server.write_timeout_seconds when 0
cors Optional block, see below
rate_limit Optional block, see below
circuit_breaker Optional block, see below
retry Optional block, see below

Routes are matched by longest-prefix, so registration order in the file doesn't matter — /api/db/ always wins over the catch-all / regardless of where each appears in the list.

routes[].cors

Field Default Description
enabled false
allowed_origins [] Exact origin strings, e.g. https://app.example.com
allowed_methods GET,POST,PUT,PATCH,DELETE,OPTIONS
allowed_headers Content-Type,Authorization
allow_credentials false Cannot be true together with "*" in allowed_origins — config loading rejects that combination, since browsers do too
max_age_seconds — Access-Control-Max-Age on preflight responses

routes[].rate_limit

Per-client-IP token bucket.

Field Default Description
enabled false
requests_per_second — Required (> 0) when enabled
burst max(1, requests_per_second)

routes[].circuit_breaker

Wraps the outbound call to this route's backend.

Field Default Description
enabled false
max_failures 5 Consecutive failed requests before the breaker opens
open_timeout_seconds 30 How long the breaker stays open before allowing a trial request

A "failure" here is a connection error, or a response status of 502/503/504 — counted per client request (after any retries for that request are exhausted), not per individual retry attempt.

routes[].retry

Retries idempotent requests (GET/HEAD/OPTIONS/PUT/DELETE — never POST/PATCH) on connection errors or a 502/503/504 response, with exponential backoff.

Field Default Description
enabled false
max_attempts 2 Total attempts, i.e. 2 = one retry
backoff_base_ms 100 Backoff doubles each attempt: base, base*2, base*4, ...

Request bodies up to 2 MiB are buffered in memory to support replaying them across attempts; larger or unknown-length bodies are sent once, without retries, to bound memory use.

Running locally

  1. Start stand-in backends for auth and db (anything that listens and responds works for testing routing — SvelteKit's own dev server covers the frontend):

    // stand-in.go — go run stand-in.go, then again on a different port
    package main
    
    import ("fmt"; "net/http"; "os")
    
    func main() {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintf(w, "hit %s %s\n", r.Method, r.URL.Path)
        })
        http.ListenAndServe(":"+os.Args[1], nil)
    }
    go run stand-in.go 8081 &   # auth
    go run stand-in.go 8082 &   # db
    cd your-sveltekit-app && npm run dev -- --port 5173 &
  2. (Only needed if you want /api/db/* to actually require a token/role) run a local Keycloak and import a realm:

    docker run -p 8180:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \
      quay.io/keycloak/keycloak:latest start-dev

    Create a realm gateway-demo, a confidential client, and a user with realm role api-user (the sample config.yaml's db route requires it via required_roles); then obtain a token (client-credentials or password grant) via Keycloak's token endpoint for use with curl below.

  3. Run the gateway:

    go run ./cmd/gateway
    # or, to hot-reload config.yaml without restarting:
    go run ./cmd/gateway -watch-config
  4. Try it:

    curl localhost:8080/healthz
    curl -i localhost:8080/api/auth/login       # open route
    curl -i localhost:8080/                     # frontend catch-all
    curl -i localhost:8080/api/db/widgets       # 401 — no token
    curl -i localhost:8080/api/db/widgets \
      -H "Authorization: Bearer $TOKEN"         # 200 if the token has the api-user role, else 403

    Every response carries an X-Request-Id header and baseline security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Strict-Transport-Security when server.hsts_enabled or tls is set). Requests that pass Keycloak validation are forwarded with X-User-Id (and X-User-Roles, if the token has a realm_access.roles claim) so backends don't need to re-validate the token themselves.

Adding a new route

Add an entry to routes: in config.yaml and restart (or, with -watch-config running, just save the file) — no code changes needed.

Config hot-reload

Run with -watch-config to have the gateway watch config.yaml and rebuild its routing table (and re-fetch JWKS if auth changed) on every save, without dropping connections or restarting the process. A save that fails to load or validate is logged and ignored — the gateway keeps serving the last good config. Off by default; enable it deliberately, since it means the process trusts whatever the config file says at any point in its lifetime, not just at startup.

Plugging in a different identity provider

internal/middleware.AuthFunc is the extension point:

type AuthFunc func(w http.ResponseWriter, r *http.Request) (*http.Request, bool)

internal/auth.KeycloakValidator.Authenticate is the shipped implementation. To use a different IdP, write a type with the same method shape and swap the wiring in cmd/gateway/main.go's buildAuthFn — nothing else in the middleware chain or route configuration needs to change.

Logging

JSON logs via log/slog, one line per request plus lifecycle events. Level is controlled by LOG_LEVEL (debug, info, warn, error; defaults to info).

Health check

GET /healthz returns {"status":"ok"}. It only reports the gateway process itself — it has no dependencies of its own to check.

Graceful shutdown

On SIGINT/SIGTERM, the gateway stops accepting new connections and waits (up to shutdown_timeout_seconds) for in-flight requests to complete before exiting. To see it in action, start a slow request and hit Ctrl+C on the gateway — the request should finish before the process exits, and the log should show shutdown signal received followed by gateway stopped cleanly.

Testing

go test ./...

Docker

docker build -t go-gateway .
docker run -p 8080:8080 go-gateway

Mount a custom config with -v ./my-config.yaml:/etc/gateway/config.yaml. If you enable tls in that config, also mount the cert/key files at the paths it references (e.g. -v ./certs:/etc/gateway/certs).

Out of scope

This is a template, not a production-hardened gateway. Deliberately not included:

  • Load balancing across multiple instances of the same backend — a straightforward future extension would be changing Route.Target to Route.Targets []string with round-robin selection
  • Full OIDC discovery / multi-IdP support — this template targets a single Keycloak realm's static JWKS URL, not .well-known discovery
  • Observability (metrics, distributed tracing) — planned for a later phase
  • ACME/Let's Encrypt automatic certificates for the optional tls block — bring your own cert/key, or terminate TLS upstream instead

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages