Skip to content

Lacuna + ForceField: working two-provider security middlebox example (+ findings on authorization/headers interaction)#145

Open
carlosdenner wants to merge 1 commit intoFlared:mainfrom
carlosdenner:examples/forcefield-byok
Open

Lacuna + ForceField: working two-provider security middlebox example (+ findings on authorization/headers interaction)#145
carlosdenner wants to merge 1 commit intoFlared:mainfrom
carlosdenner:examples/forcefield-byok

Conversation

@carlosdenner
Copy link
Copy Markdown

What this is

A complete, end-to-end-tested example of placing ForceField (an LLM
security gateway) behind Lacuna so a single Lacuna deployment can broker
both OpenAI and Anthropic traffic with BYOK upstream keys
and FF detector / PII / output-moderation enforcement in the middle.

The example ships:

  • examples/forcefield/lacuna-config.json — both providers, two-header
    auth pattern.
  • examples/forcefield/docker-compose.yml — runnable locally; binds host
    port via ${LACUNA_HOST_PORT:-8080} so it doesn't collide with FF's
    edge-proxy.
  • examples/forcefield/smoke_test.py — PEP-723 script with 4 cases:
    benign + jailbreak for each provider.
  • examples/forcefield/gcp-cloud-run/ — variant pointing at a hosted FF
    on Cloud Run (no Tailscale dependency).
  • examples/forcefield/claude_code.settings.json — Claude Code wired
    through Lacuna.
  • examples/forcefield/README.md + gcp-cloud-run/README.md — explain
    the data-flow and the BYOK trust model.

Verified end-to-end

Run from a fresh checkout against a live FF Cloud Run gateway, broker key
issued via FF's /v1/onboard tenant manager:

Test Result
OpenAI benign ("What is 2 plus 2?") 200, "2 plus 2 equals 4.", real OpenAI usage tokens
OpenAI jailbreak ("ignore all previous") 403 prompt_injection from FF
Anthropic with fake key 401 invalid x-api-key from Anthropic — proves Lacuna -> FF -> api.anthropic.com routing and BYOK passthrough
Anthropic jailbreak FF returns its msg_ff_block_* envelope

Two findings worth flagging to maintainers

While building this I hit two Lacuna behaviors that aren't really wrong
but cost a few hours each. Calling them out so you can decide whether to
patch, document, or leave alone. Happy to file separate issues if you'd
prefer.

1. headers + authorization interaction is the only way to do dual-key auth

Lots of security middleboxes follow a "tenant key + upstream key" model
where the proxy sees one credential and the upstream sees a different
one. Lacuna's Provider::build_request sets exactly one auth header (the
Authorization enum), then merges the static headers map.

The configuration that works is:

"authorization": "bearer",
"apikey":  "${UPSTREAM_PROVIDER_KEY}",   // upstream gets this as Bearer
"headers": { "x-api-key": "${FORCEFIELD_API_KEY}" }  // FF auth lives here

This is a perfectly fine way to express it — but it's not obvious from
the docs that headers survives alongside authorization. A one-line
note in the provider config reference would save the next person an
afternoon. (Happy to PR that note too if you want.)

2. OpenAI handlers don't extract model, so models: ["gpt-*"] denies all

In the authorizer flow, Provider::inspect_request is implemented for
the Anthropic / Bedrock / Gemini handlers (they decode the body to read
model), but the OpenAI chat-completion and responses handlers don't.
Result: request.model is None, so any non-* glob in
capability.models rejects every request to OpenAI.

The example works around it with "models": ["*"] for the OpenAI
provider and a comment in the README. A small inspect_request impl on
the OpenAI handler would be a nice follow-up — I can take a stab if it's
something you'd accept.

3. (bonus, less impactful) capabilities_header defaults to deny-all when absent

Setting capabilities_header: "Tailscale-App-Capability" with no header
present makes Capabilities::deny_all() block 100% of requests. This is
correct fail-closed behavior and totally fine inside a tailnet, but it
made the example unusable for self-hosters who don't run Tailscale, so
the example leaves it unset by default and the README explains when to
turn it back on.

Why this matters for Lacuna

Lacuna's positioning as "the small, opinionated reverse proxy in front
of every LLM call you make" pairs naturally with security gateways like
ForceField, Lakera Gateway, etc. Having a worked example in-tree that
shows the pattern (capability-filter first, security-detector second,
upstream third, BYOK end-to-end) lowers the lift for anyone composing
Lacuna with a downstream policy engine.

Out of scope

  • No changes to Lacuna's Rust source. Pure examples/ addition.
  • No CI hooks added. The smoke test is opt-in (requires FF tenant + real
    provider keys).

Reviewer questions

  • Happy to split (1) the example, (2) the dual-header docs note, and (3)
    the OpenAI inspect_request patch into separate PRs if that's easier
    to land.
  • Naming: examples/forcefield/ follows the pattern of any existing
    third-party examples — let me know if you'd prefer a different
    subdirectory or a separate cookbook/ style.

Thanks for Lacuna.

Adds a working integration example placing the ForceField LLM security gateway behind Lacuna. Both providers are BYOK -- Lacuna sends the upstream provider key as Authorization: Bearer alongside the FF tenant key as x-api-key, ForceField strips its own auth and forwards the bearer untouched to api.openai.com / api.anthropic.com.

Verified end-to-end against a live FF Cloud Run gateway: OpenAI benign returns a real completion with usage tokens; Anthropic with a fake key gets the expected 401 from api.anthropic.com (proves routing); FF blocks jailbreak prompts on both paths.

Includes local docker-compose variant and a gcp-cloud-run/ variant pointing at a hosted FF (no Tailscale dependency). PEP-723 smoke_test.py with 4 cases. README documents the two-header auth pattern and notes a couple of Lacuna config gotchas (OpenAI handlers don't extract model from body; capabilities_header without header = deny_all).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant