Skip to content

Commit bd8a1f7

Browse files
committed
feat(p1): gateway hardening — bug fixes, concurrency tests, benchmarks, docs
Bug fixes: - GraphQLHandler.graphql_operation_allowed?/1 now reads policy table from Application env instead of the hardcoded :policy_rules atom. The hardcoded name broke after atomic policy reloads, which publish tables under monotonic-time-suffixed names. - Same fix applied to GRPCHandler.grpc_method_allowed?/2. P1 concurrency tests (test/concurrency_test.exs, tagged :concurrency): - Rate limiter: concurrent same-client bursts → expected 429s - Rate limiter: concurrent different-client → separate buckets - Rate limiter: internal trust bypasses limiting under load - Rate limiter: Retry-After header present on 429 - Circuit breaker: 20 concurrent failures serialize correctly through GenServer - Circuit breaker: concurrent allow? checks consistent under open circuit - Circuit breaker: success resets failure counter - Policy reload: concurrent readers never see missing table during swap - Policy reload: failed reload preserves last good policy under load P1 benchmarks (test/benchmark_test.exs, tagged :benchmark): - Rate limiter per-call latency (< 100µs) - Rate limiter throughput across unique clients - Rate limiter internal-trust short-circuit (< 10µs) - CircuitBreaker.allow? hot-path latency (ETS-only, ~nanosecond) - Circuit breaker state transition cost - Exact vs regex route lookup comparison - 405 fast-path and health endpoint throughput P1 operator docs: - Add docs/SUPPORTED-FEATURES.md as single source of truth on what is actually supported vs stubbed/planned. Covers protocols, trust sources, policy enforcement, runtime features, and explicit out-of-scope items. Includes operator quick-checklist (trusted_proxies, mTLS caveats, etc.). - Add scope warning to MULTI-PROTOCOL.md clarifying that gRPC/GraphQL handlers are stubs (aligns doc with test/research findings). - Update docs/DEPLOYMENT.md version references from v1.0.0 to v0.1.0-dev to match the reconciled version in mix.exs. https://claude.ai/code/session_01TzU2xW8y2uBCaCm2mE4NUV
1 parent bdf2505 commit bd8a1f7

7 files changed

Lines changed: 896 additions & 22 deletions

File tree

MULTI-PROTOCOL.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# Multi-Protocol API Gateway
22

3-
The HTTP Capability Gateway now supports multiple protocols: HTTP/REST, gRPC, and GraphQL.
3+
> **⚠️ SCOPE NOTICE (2026-04-16):** This document describes the vision for
4+
> multi-protocol support. Today, only **HTTP/REST** is production-ready. gRPC
5+
> and GraphQL handlers exist but are **stubs**: `GraphQLHandler.check_operation_policy/2`
6+
> always returns true, and `GRPCHandler.forward_grpc_request/5` returns a
7+
> hardcoded response with no actual forwarding. Do not route gRPC or GraphQL
8+
> traffic through this gateway in production.
9+
>
10+
> See `docs/SUPPORTED-FEATURES.md` for the authoritative status of each
11+
> protocol, and `ROADMAP.adoc` for the MVP scope.
12+
13+
The HTTP Capability Gateway aims to support multiple protocols: HTTP/REST, gRPC, and GraphQL.
14+
Only HTTP/REST is currently supported end-to-end.
415

516
## Architecture
617

docs/DEPLOYMENT.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!-- SPDX-License-Identifier: PMPL-1.0-or-later -->
22

3-
# Deployment Guide — HTTP Capability Gateway v1.0.0
3+
# Deployment Guide — HTTP Capability Gateway v0.1.0-dev
44

55
Practical guide for deploying the HTTP Capability Gateway to production environments.
66
Covers container-based deployment (Podman/Docker), bare-metal OTP releases, policy
@@ -55,10 +55,10 @@ Alpine-based runtime image (~30 MB) with no build tools or source code.
5555

5656
```bash
5757
# Build with Podman (preferred)
58-
podman build -t http-capability-gateway:1.0.0 -f Containerfile .
58+
podman build -t http-capability-gateway:0.1.0-dev -f Containerfile .
5959

6060
# Build with Docker
61-
docker build -t http-capability-gateway:1.0.0 -f Containerfile .
61+
docker build -t http-capability-gateway:0.1.0-dev -f Containerfile .
6262
```
6363

6464
The builder stage uses `hexpm/elixir:1.19.4-erlang-28.2.2-alpine-3.22.1`.
@@ -76,7 +76,7 @@ podman run -d \
7676
-e BACKEND_URL=http://backend:4000 \
7777
-e PORT=4000 \
7878
-v ./my-policy.yaml:/app/config/policy.yaml:ro \
79-
http-capability-gateway:1.0.0
79+
http-capability-gateway:0.1.0-dev
8080

8181
# Check logs
8282
podman logs -f http-capability-gateway
@@ -178,7 +178,7 @@ Create `/etc/systemd/system/http-capability-gateway.service`:
178178

179179
```ini
180180
[Unit]
181-
Description=HTTP Capability Gateway v1.0.0
181+
Description=HTTP Capability Gateway v0.1.0-dev
182182
Documentation=https://github.com/hyperpolymath/http-capability-gateway
183183
After=network.target
184184

@@ -327,7 +327,7 @@ curl -s http://localhost:4000/health | jq .
327327
{
328328
"status": "healthy",
329329
"service": "http-capability-gateway",
330-
"version": "1.0.0",
330+
"version": "0.1.0-dev",
331331
"uptime_seconds": 3600
332332
}
333333
```

docs/SUPPORTED-FEATURES.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<!-- SPDX-License-Identifier: PMPL-1.0-or-later -->
2+
3+
# Supported Features — HTTP Capability Gateway
4+
5+
**Version:** 0.1.0-dev
6+
**Last updated:** 2026-04-16
7+
**Status:** MVP verification phase (CRG grade C)
8+
9+
This document is the single authoritative reference for what the gateway
10+
actually supports today. If it is not in the **Supported** list below, it
11+
is either a stub, planned work, or out of scope — do not rely on it in
12+
production.
13+
14+
See `ROADMAP.adoc` for the formal MVP scope definition with test mappings,
15+
and `MULTI-PROTOCOL.md` for protocol-specific details (note that document
16+
describes a broader vision; the table here reflects actual runtime behaviour).
17+
18+
---
19+
20+
## Protocols
21+
22+
| Protocol | Status | Notes |
23+
|----------|--------|-------|
24+
| **HTTP/1.1** | Supported | Full verb governance, proxy forwarding, stealth mode. This is the MVP scope. |
25+
| **HTTP/2** | Supported (via Cowboy) | Inherited from Plug.Cowboy adapter. Not exercised by gateway-specific tests. |
26+
| **HTTPS / TLS** | Not supported | No TLS termination inside the gateway. Run behind a TLS-terminating proxy (nginx, Caddy, Svalinn, etc.). |
27+
| **mTLS (client certs)** | Partial | `extract_trust_level_from_cert/1` exists but is an approximation; the ASN.1 OTPCertificate record unpacking has a documented `TODO` for production use. Do not rely on mTLS trust extraction without adapting `gateway.ex:552-617`. |
28+
| **GraphQL** | Stub only | `GraphQLHandler` parses JSON bodies and does naive prefix-based operation detection. `check_operation_policy/2` always returns true. Operation-level governance is not implemented. |
29+
| **gRPC** | Stub only | `GRPCHandler` extracts service/method from path but `forward_grpc_request/5` returns a hardcoded response — no actual gRPC forwarding. |
30+
| **WebSocket** | Not supported | No implementation. |
31+
32+
## Trust Sources
33+
34+
| Source | Status | Notes |
35+
|--------|--------|-------|
36+
| **`X-Trust-Level` header** | Supported | The canonical trust source. Values: `untrusted`, `authenticated`, `internal`. Anything else is parsed as `:untrusted` (see `SafeTrust.parse_trust/1`). |
37+
| **Header stripping for untrusted sources** | Supported | `strip_untrusted_headers/2` removes `X-Trust-Level` from any request whose `remote_ip` is not in `:trusted_proxies`. Default trusted proxies: `["127.0.0.1", "::1"]`. **Operator action required**: configure `:trusted_proxies` for your deployment before exposing the gateway. |
38+
| **mTLS certificate OU** | Not recommended | Implementation exists but has a TODO (see above). Use only with caution and test thoroughly against your CA's certificate format. |
39+
| **Authorization header (JWT/Bearer)** | Not parsed | The gateway does not parse or validate tokens. The upstream that sets `X-Trust-Level` is expected to do this (e.g., indieweb2-bastion in the hyperpolymath stack). |
40+
| **IP allowlist** | Not supported | The gateway does not block by IP. Rely on upstream L4/L7 filtering. |
41+
42+
## Policy Enforcement
43+
44+
| Feature | Status | Notes |
45+
|---------|--------|-------|
46+
| **YAML policy loading** | Supported | `PolicyLoader.load_from_file/1` reads DSL v1 files. |
47+
| **Policy validation** | Supported | `PolicyValidator.validate/1` rejects malformed policies before compilation. |
48+
| **Policy compilation to ETS** | Supported | Dual-table layout: main (exact + global) and regex. |
49+
| **Exact path matching** | Supported | O(1) via ETS `{:exact, path, verb}` keys. |
50+
| **Regex path matching** | Supported | O(r) scan of dedicated regex table. |
51+
| **Global verb rules** | Supported | Fallback when no route matches. |
52+
| **Atomic policy hot-reload** | Supported | Recompiling creates new tables and swaps app env references atomically. See `test/e2e_test.exs` hot-reload tests. |
53+
| **Per-route exposure overrides** | Supported | Route `exposure` field overrides global behaviour. |
54+
| **Stealth mode (configurable status codes)** | Supported | Default 404 hides denied endpoints. |
55+
| **Default-deny on no match** | Supported | Returns 403 (or stealth code) when no rule matches. |
56+
57+
## Runtime Features
58+
59+
| Feature | Status | Notes |
60+
|---------|--------|-------|
61+
| **Rate limiting (token bucket)** | Supported | Per-(IP, trust) buckets. Defaults: `untrusted` 10 req/s, `authenticated` 100 req/s, `internal` unlimited. `X-Forwarded-For` is trusted for client IP — require a trusted reverse proxy to prevent spoofing. |
62+
| **Circuit breaker** | Supported | Three-state FSM (closed/open/half_open) per backend. 5 failures to trip, 30s before half-open probe (configurable). |
63+
| **K9 service contracts** | Partial | Trust threshold enforcement works. `rate_limit` field on contracts is declared but NOT enforced. |
64+
| **Structured JSON logs** | Supported | Every access decision logged with request_id, path, verb, trust_level, decision. |
65+
| **Telemetry events** | Supported | All major events emit `[:http_capability_gateway, ...]` telemetry. |
66+
| **Health probe** | Supported | `GET /health` returns 200 with uptime. |
67+
| **Readiness probe** | Supported | `GET /ready` returns 200 iff policy is loaded. |
68+
| **Prometheus metrics** | Supported | `GET /metrics` via `TelemetryMetricsPrometheus.Core.scrape/0`. |
69+
| **Anomaly detection (Minikaran)** | Supported | `GET /api/v1/minikaran` returns current anomalies. |
70+
| **Audit log (VeriSimDB)** | Supported | Allow/deny decisions persisted asynchronously. |
71+
72+
## Out of MVP Scope
73+
74+
The following are explicitly **not** in the v0.1.0 MVP:
75+
76+
- Multi-backend load balancing
77+
- TLS termination
78+
- Response caching
79+
- Request/response body transformation
80+
- Dynamic trust scoring / ML-based trust
81+
- Web UI / admin dashboard
82+
- Plugin system (auth, filters, custom loaders)
83+
- Distributed cluster coordination
84+
- Kubernetes operator
85+
- Helm chart
86+
87+
See `ROADMAP-v2.md` for these items; they are **aspirational** and not
88+
on the release path for v0.1.0.
89+
90+
---
91+
92+
## Operator Quick Checklist
93+
94+
Before exposing the gateway to the public internet:
95+
96+
1. **Configure `:trusted_proxies`** — add the IP addresses of your upstream
97+
TLS-terminating proxies. Without this, direct clients can forge
98+
`X-Trust-Level: internal` and bypass all governance.
99+
2. **Do NOT rely on mTLS trust extraction** until `extract_cert_subject/1`
100+
is reviewed for your certificate format.
101+
3. **Do NOT route GraphQL or gRPC traffic through this gateway**
102+
handlers are stubs. HTTP/REST only.
103+
4. **Set realistic `:rate_limits`** for your traffic. The test defaults
104+
are set very high for test predictability.
105+
5. **Provide a valid policy file at startup** — the application refuses
106+
to start without one (fail-closed).
107+
6. **Put a TLS-terminating proxy in front** — the gateway does not do TLS.
108+
7. **Monitor the `/metrics` endpoint** — especially
109+
`http_capability_gateway_circuit_breaker_*` and rate-limit counters.

lib/http_capability_gateway/graphql_handler.ex

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,24 @@ defmodule HttpCapabilityGateway.GraphQLHandler do
115115
# Check if GraphQL operation is allowed
116116
# Integrates with PolicyCompiler - GraphQL uses /graphql path
117117
defp graphql_operation_allowed?(operation_type) do
118-
# GraphQL operations are POST to /graphql
119-
# We could extend policy to include operation type restrictions
120-
case PolicyCompiler.lookup(:policy_rules, "/graphql", :POST) do
121-
{:ok, rule} ->
122-
# Additional check: some policies might restrict specific operations
123-
# For now, just check if /graphql endpoint is allowed
124-
check_operation_policy(rule, operation_type)
125-
126-
{:error, :no_match} ->
127-
false
118+
# Read the current policy table from application env so that this
119+
# handler stays correct after atomic policy reloads (see PolicyCompiler).
120+
# Hardcoding :policy_rules would miss the freshly-compiled table that
121+
# the atomic swap pattern publishes under a monotonic-time-suffixed name.
122+
policy_table = Application.get_env(:http_capability_gateway, :policy_table)
123+
124+
if is_nil(policy_table) do
125+
false
126+
else
127+
# GraphQL operations are POST to /graphql
128+
case PolicyCompiler.lookup(policy_table, "/graphql", :POST) do
129+
{:ok, rule} ->
130+
# Additional check: some policies might restrict specific operations
131+
check_operation_policy(rule, operation_type)
132+
133+
{:error, :no_match} ->
134+
false
135+
end
128136
end
129137
end
130138

lib/http_capability_gateway/grpc_handler.ex

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,20 @@ defmodule HttpCapabilityGateway.GRPCHandler do
103103
# Build gRPC path in format /Service/Method
104104
path = "/#{service}/#{method}"
105105

106-
# Check against policy rules
107-
# gRPC methods are treated as POST requests in the policy
108-
case PolicyCompiler.lookup(:policy_rules, path, :POST) do
109-
{:ok, _rule} -> true
110-
{:error, :no_match} -> false
106+
# Read the current policy table from application env so this handler
107+
# stays correct after atomic policy reloads (see PolicyCompiler).
108+
# Hardcoding :policy_rules would miss tables created by the atomic
109+
# swap pattern (which uses monotonic-time-suffixed names).
110+
policy_table = Application.get_env(:http_capability_gateway, :policy_table)
111+
112+
if is_nil(policy_table) do
113+
false
114+
else
115+
# gRPC methods are treated as POST requests in the policy
116+
case PolicyCompiler.lookup(policy_table, path, :POST) do
117+
{:ok, _rule} -> true
118+
{:error, :no_match} -> false
119+
end
111120
end
112121
end
113122

0 commit comments

Comments
 (0)