Getting started: API estate governance (reverse proxy in front of existing APIs)
July 29, 2026 ยท View on GitHub
Last modified: 2026-07-09
What you will build
You will put SBproxy in front of a set of existing HTTP APIs as a reverse proxy, with one origin per public hostname. The gateway matches the inbound Host header, forwards the request to the right upstream, and applies a layer of governance on the way through: a bearer-token allowlist, a per-IP rate limit, and request and response header rewrites. The result is a single edge that every caller goes through, so authentication and traffic policy live in config rather than in each backend.
Prerequisites
curlfor testing requests.- A reachable upstream API. This guide uses
https://test.sbproxy.dev, the project's public HTTP echo service (request inspection, similar to httpbin), as a stand-in for your real backend. Swap in your own upstream URL when you are ready.
Install
One line installs the prebuilt binary on Linux amd64/arm64 or Apple Silicon
macOS and places it in ~/.local/bin:
curl -fsSL https://download.sbproxy.dev | sh
Homebrew, Docker, binary downloads, and source builds are in the runtime manual's installation section. Run the gateway against a config file:
sbproxy serve -f sb.yml
The proxy binds to 0.0.0.0:8080 by default.
Minimal config
Save this as sb.yml. Every key here exists in schemas/sb-config.schema.json and is drawn from the shipped examples. It governs one origin keyed on api.example.com: callers present a bearer token, requests are rate limited per IP, and headers are rewritten on the way to and from the upstream. example.com is reserved (RFC 2606), so the client-facing hostname never collides with a real domain; replace it with your own hostname in production, and replace the upstream URL with your real backend.
# yaml-language-server: $schema=https://raw.githubusercontent.com/soapbucket/sbproxy/main/schemas/sb-config.schema.json
proxy:
http_bind_port: 8080
origins:
"api.example.com":
action:
type: proxy
url: https://test.sbproxy.dev
authentication:
type: bearer
tokens:
- svc-token-alpha
- svc-token-beta
policies:
- type: rate_limiting
requests_per_second: 5
burst: 10
key: connection.remote_ip
headers:
enabled: true
include_retry_after: true
request_modifiers:
- headers:
set:
X-Forwarded-By: sbproxy
X-Trace-Id: "{{ request.id }}"
delete:
- cookie
response_modifiers:
- headers:
set:
X-Served-By: sbproxy
Cache-Control: "public, max-age=60"
The key: field is a CEL expression that names the rate-limit bucket: connection.remote_ip gives each client IP its own token bucket, and something like request.headers["x-api-key"] buckets per API key instead. Leave key: out and every caller shares one bucket. The headers block opts 429 responses into the X-RateLimit-* set and Retry-After; without it, throttled responses carry no rate-limit headers. {{ request.id }} in the X-Trace-Id value resolves to the proxy's request id, the same value the access log records as request_id. To route different paths to different backends from the same hostname, add a forward_rules block; see examples/forward-rules for path-, header-, and query-based dispatch.
Run it and expected output
Start the gateway:
sbproxy serve -f sb.yml
A request with no token is rejected before the upstream is contacted:
$ curl -i -H 'Host: api.example.com' http://127.0.0.1:8080/get
HTTP/1.1 401 Unauthorized
content-type: application/json
{"error":"unauthorized"}
A request with a valid token is forwarded, and you can see the injected request headers reflected back by the echo upstream. The echo lowercases header names, and the body below is trimmed: the hosted service sits behind a CDN that adds headers of its own.
$ curl -is -H 'Host: api.example.com' \
-H 'Authorization: Bearer svc-token-alpha' \
http://127.0.0.1:8080/get
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Content-Type: application/json; charset=utf-8
x-served-by: sbproxy
...
{"method":"GET","url":"/get","headers":{"authorization":"Bearer svc-token-alpha","host":"test.sbproxy.dev","x-forwarded-by":"sbproxy","x-trace-id":"019f487ef3e573e38ad2f4f568b5c7c3",...},"query":{},"timestamp":"..."}
Now trip the rate limit. The bucket refills at 5 tokens a second, so a sequential loop that waits on each round trip never empties it; the burst has to be concurrent:
$ seq 1 30 | xargs -P 15 -I{} curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Host: api.example.com' \
-H 'Authorization: Bearer svc-token-alpha' \
http://127.0.0.1:8080/get | sort | uniq -c
10 200
20 429
Each 429 carries the rate-limit headers the policy's headers block enabled:
HTTP/1.1 429 Too Many Requests
content-type: application/json
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2
Retry-After: 2
{"error":"rate limited"}
A Host header that matches no configured origin is rejected by the proxy itself:
$ curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Host: unknown.example.com' http://127.0.0.1:8080/get
404
You are done when
- A request with no
Authorizationheader returns401 Unauthorizedwith{"error":"unauthorized"}. - A request with
Authorization: Bearer svc-token-alphareturns200 OK. - The 200 response carries the
x-served-by: sbproxyandCache-Control: public, max-age=60headers added byresponse_modifiers. - The echoed request body shows the injected
x-forwarded-by: sbproxyheader, anx-trace-idholding the proxy's request id, and nocookieheader. - A concurrent burst from one IP drains the 10-token bucket and starts returning
429 Too Many RequestswithX-RateLimit-*andRetry-Afterheaders. - A request with an unknown
Hostreturns404.
Next steps
- docs/configuration.md - the full configuration schema and every origin field.
- docs/policy.md - the policy engine, including rate limiting and IP filtering.
- docs/headers-reference.md - the headers SBproxy reads and writes, including the forwarding headers added by default.
- docs/routing-strategies.md - host- and path-based routing across multiple backends.