Skip to content

About

An API-Gateway focused on Security and a leightweight approach. It incorporates a BFF Token handling as well, FAPI 2.0

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

API-Sheriff

Status

Build & Quality

Java CI with Maven Integration Tests

Last Build License Maven Central

Quality Gate Status Lines of Code Coverage

Container Image

Container Image Release

Benchmarks

Integration Performance Integration Trend

What is it?

A security-focused API Gateway with a lightweight approach, built with Quarkus.

Request pipeline: edge checks
Important

0.2.4 is an ALPHA release. The configuration surface and the APIs may change without a migration path. Per this project’s pre-1.0 rules, breaking changes are made freely: keys are renamed or removed, values change meaning, and no deprecation shim, compatibility alias or upgrade tooling is provided. A gateway.yaml that boots today may fail to boot on the next release, deliberately and loudly.

What alpha does not mean. It does not mean the code is untested or the security posture is provisional — the gateway is boot-validated fail-closed, covered by unit and containerised integration suites, and gated on a blocking quality gate. It means the surface is not frozen.

Intended use: evaluation, integration and feedback. Try it, build against it, and report what breaks. Do not deploy it where an unannounced breaking change between releases would be costly.

See Known Limitations for what is known to be missing or constrained at this cut.

Maven Coordinates

<dependency>
    <groupId>de.cuioss.sheriff.gateway</groupId>
    <artifactId>api-sheriff</artifactId>
</dependency>

Modules

The reactor declared in the root pom.xml:

Module Content

api-sheriff

The deployable Quarkus application: core library, CDI producers, REST endpoints and native executable target.

integration-tests

Integration-test coordinator: Docker infrastructure, native container builds and the HTTPS IT suites.

benchmarks

k6 HTTP load-testing benchmarks against a containerised stack.

demo-client

Demo SPA and Playwright end-to-end suite (no Java; npm work runs only in the e2e-demo profile).

deployment

Production-shaped Docker Compose sample (no Java; started only by -Pcompose-sample).

build-parent

Published build parent for downstream deployers (no Java; builds no artifact).

Deployment

Production Docker Image

Build the native executable and production Docker image:

# Build native executable
./mvnw clean install -Pnative -pl api-sheriff -am -DskipTests

# Build production Docker image
docker build -f api-sheriff/src/main/docker/Dockerfile.native -t api-sheriff:latest api-sheriff/

Run with TLS certificates mounted at runtime:

docker run -p 8443:8443 -p 127.0.0.1:9000:9000 \
  -v /path/to/certs:/certs:ro \
  -e QUARKUS_HTTP_SSL_CERTIFICATE_FILES=/certs/server.crt \
  -e QUARKUS_HTTP_SSL_CERTIFICATE_KEY_FILES=/certs/server.key \
  -e QUARKUS_MANAGEMENT_SSL_CERTIFICATE_FILES=/certs/server.crt \
  -e QUARKUS_MANAGEMENT_SSL_CERTIFICATE_KEY_FILES=/certs/server.key \
  api-sheriff:latest
  • Port 8443 (HTTPS) — the data-plane edge. Routes are deny-by-default: only the paths declared by the mounted gateway.yaml anchors and endpoints/*.yaml are served, and every other request is rejected with 404. The image ships no built-in application endpoints of its own

  • Port 9000 (HTTPS) — management endpoints (health and metrics), served beneath the image’s management context path. The management interface has exactly one port, so supplying QUARKUS_MANAGEMENT_SSL_CERTIFICATE_* above converts 9000 itself to HTTPS; there is no simultaneous plain-HTTP listener. It is additionally bound to 127.0.0.1 (loopback only) so the unauthenticated management interface is not exposed on all host interfaces, matching the security posture documented in doc/architecture/observability-and-errors.adoc

Both context paths — the data-plane one and the management one — are fixed when the image is built, so the matching QUARKUS_*_ROOT_PATH environment variables are routing-inert against a published image — they move nothing, though a divergent value may draw a startup build-time-mismatch warning. Moving either takes a rebuild through the Maven seam; see The Context Path.

  • Rebuilding under your own context path without cloning this repository is the supported customisation path: inherit the published build parent de.cuioss.sheriff.gateway:api-sheriff-build-parent, set one property, and build — see Rebuilding Under Your Own Context Path

Known Limitations

Verified as still open at the 0.2.4 cut. Each is a deliberate boundary or a known gap, not a surprise waiting to be discovered.

A route with no forward block forwards every header and query parameter

The forward block is a three-mode policy: headers_allow / query_allow is a positive-list, headers_deny / query_deny is a negative-list, and declaring neither means everything crosses. A route that omits the block, or one of its two dimensions, forwards that dimension in full.

The bound. The policy decides what crosses within a route the operator exposed; it never decides which routes are reachable. Route matching stays deny-by-default — a request matching no route still reaches no upstream. Nor does it leak credentials: no forward mode copies the browser Cookie — it crosses only as the route’s cookies block admits it, and the gateway’s own cookies never do — an inbound Authorization, like a client-sent provenance or URL-override header (X-Original-URL, X-Forwarded-Server, …​), is re-admitted only where a route declares a positive-list naming it, and the hop-by-hop, framing and regenerated forwarding headers the gateway owns are withheld regardless (see the never-forward set). Note also that a declared-empty list is not the absent state: headers_allow: [] is a positive-list naming nothing, and keeps forwarding nothing.

Mitigation, one line: declare a positive-list (headers_allow / query_allow) on any route that must withhold client input. See Architecture — Forward Policy and ADR-0036.

No cookie crosses a route by default

The cookies block decides both directions per route. A route that resolves no block runs allow: none: its upstream receives no browser cookie, and no upstream Set-Cookie reaches the browser. The gateway’s own cookies cross under no block in either direction.

Mitigation, one line: declare cookies: { allow: [<the names>] } on every route whose upstream relies on its own cookies. See Configuration — cookies and ADR-0037.

Upstream response headers are relayed except for gateway-owned names

Both directions carry a gateway-owned never-relay set — the connection-specific and framing names, plus, on the request side, the credentials and provenance headers the gateway owns. The response side also strips the upstream’s identity headers, Server and X-Powered-By, by default — on the HTTP relay, the gRPC relay and an asset route with source: upstream. A route keeps them only by setting keep_identity_headers: true (see Configuration — the identity set). Beyond those sets the two directions differ: a route can narrow what crosses on the request side (headers_deny / headers_allow), while on the HTTP and gRPC relays every other upstream response header — a framework header outside the identity set, such as X-AspNet-Version — reaches the client unchanged. Two places do change origin headers: an asset route with source: upstream answers through the asset envelope, which sets its own Content-Type, X-Content-Type-Options and, for an authenticated asset, Cache-Control: no-store, and drops Set-Cookie; and a security_headers entry in set mode overwrites the origin’s value. Only a few named route keys act on a response header: keep_identity_headers (above), the cookie policy for Set-Cookie (see above), not_modified: false, which strips ETag and Last-Modified, and upstream.rewrite_location: true, which rewrites Location. There is no response-header allowlist or denylist. Strip such headers at the upstream, or in front of the gateway, if they matter to you.

The TLS and HTTP/2 fingerprint of the listener is a separate residual that no gateway key disguises (TLS and HTTP/2 fingerprints).

The server-mode session store is single-node and in-memory

InMemorySessionStore is the only implementation of the SessionStore seam, and session.store accepts exactly one value, memory. There is no distributed or persistent option. A server-mode BFF deployment is therefore single-node, or several replicas behind sticky sessions in the orchestration layer, and a replica that restarts or fails over ends the sessions it held. A session ends at its absolute lifetime (oidc.session.ttl_seconds) and, earlier, at its idle timeout (oidc.session.idle_timeout_seconds). An expired session is removed by a periodic sweep within a minute of expiring, so it does not hold a slot until the store fills. Size oidc.session.max_sessions against expected peak concurrency — at the ceiling a new login is refused fail-closed, and that happens only when that many sessions are still live. See Running more than one replica. Cookie mode is the stateless alternative by design.

Cookie-mode refresh fits the cookie budget for a measured claim set, not for any claim set

A cookie-mode session that refreshes seals a live access, refresh and ID token into one cookie. SealedSessionCookieCodec deflates before it seals, and SealedSessionCookieCodecTest measures a three-token session shaped like the integration realm’s at roughly 2,700 bytes against the 4019-byte browser-safe value budget (BROWSER_SAFE_COOKIE_VALUE_BUDGET) — a margin of roughly 1,300 bytes. That margin belongs to one realm’s claim set: an identity provider minting larger tokens spends it first, and a seal past the budget is refused (ApiSheriff-114). Two things stay unmeasured on a pull request: the browser lane that would observe a dropped cookie gates nothing, and no Chromium storage threshold has been recorded. See Route 4 — package and compress and ADR-0043.

Integration-test coverage has recorded gaps

Three contributor documents enumerate them rather than leaving them implicit: Declared-Limit Assertion Coverage records which declared gateway limits have a boundary assertion and which do not, Test-Corpus Integrity records the assertion shapes that cannot support the behaviour a test’s name claims and the methods still to strengthen, and Cookie-Deliverability Blindness records the properties the integration lane is structurally incapable of observing at all, however strong its assertions. Read all three before treating a green suite as proof of a specific bound.

The FAPI 2.0 controls hold in key mode only, and no conformance-suite run exists

In key mode the BFF implements the FAPI 2.0 controls (DPoP route). It pushes every authorization request (PAR), binds every access token it obtains to one DPoP key, refuses a token response that is not bound to that key, and authenticates to the identity provider with private_key_jwt. Two limits belong to that statement:

  • No conformance-suite run exists. The controls are implemented and evidenced; no conformance claim is made.

  • It holds in key mode only. With oidc.client_secret configured the client-authentication requirement is not met. Pushed requests and the DPoP sender constraint stay in effect in that mode.

    How a BFF is configured. Key-based client authentication is the default: an oidc block that declares no client_secret authenticates with private_key_jwt, signed with the key oidc.client_authentication.key_file names or with a key generated at startup. A client secret remains an operator choice, selected by configuring oidc.client_secret. Client-secret authentication is not FAPI 2.0 conformant and may weaken security, and the gateway says so at every boot (ApiSheriff-133).

    What every BFF deployment needs from its identity provider. A pushed-authorization-request endpoint and DPoP-bound access tokens, in both modes; without them every login is refused. In key mode the identity provider additionally fetches the gateway’s client key from its JWKS endpoint. See FAPI 2.0 Status (including its open gaps) and the BFF setup guide.

Per-route rate limiting is planned, not implemented

The per-route rate_limit field is parsed and bound into RateLimitConfig, but nothing on the request path reads it: it applies no limit and causes no 429. Every route that declares the key is named at boot by the WARN ApiSheriff-140, so a configuration that relies on it does not read as a control in force. The gateway’s 429 answers all come from enumeration_detection, the per-source detection substrate: the opt-in block action, and, under the opt-in delay action, a tripped source whose share of the held-answer sub-budget is used up — see Per-Source Detection Substrate.

From the first two-platform release on, the published image serves linux/amd64 and linux/arm64, each at GraalVM’s default CPU baseline

From the first release the two-platform lane cuts, ghcr.io/cuioss/api-sheriff is one image index over two images, linux/amd64 and linux/arm64, each built natively on its own architecture and run through the full integration-test suite before it is published. Tags up to and including 0.2.4 are linux/amd64 only. Every tag from that release on resolves to the image for the pulling host’s platform, so no --platform flag is needed and an arm64 host — AWS Graviton, Apple Silicon — runs the arm64 image natively rather than under emulation. The native executable in each image is compiled for GraalVM’s default target — quarkus.native.additional-build-args carries no -march:

  • linux/amd64 requires the x86-64-v3 instruction set (AVX2, BMI2, FMA);

  • linux/arm64 requires GraalVM’s AArch64 default, which AWS Graviton 2 and every later Graviton, and Apple Silicon, implement.

    A CPU below either baseline — an x86-64 CPU without AVX2, or an older AArch64 core such as Graviton 1 or the Raspberry Pi 4 — cannot start the published image, and neither can an amd64 image forced onto an arm64 host under emulation: the process exits at once with "The current machine does not support all of the following CPU features that are required by the image". A default local build does not serve a CPU below either baseline: it compiles for the same GraalVM default target as the published image, not for the host it runs on. An arm64 host that would otherwise force an amd64 image, and anyone running local changes, can build the image locally — see Route B — a locally built image in the compose sample. The decision and the undecided -march=compatibility alternative are recorded in ADR-0070.

About

An API-Gateway focused on Security and a leightweight approach. It incorporates a BFF Token handling as well, FAPI 2.0

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages