Skip to content

Feat: add static-inject AuthBridge plugin for static credential injection#655

Merged
pdettori merged 4 commits into
rossoctl:mainfrom
pdettori:feat/static-inject-plugin
Jul 13, 2026
Merged

Feat: add static-inject AuthBridge plugin for static credential injection#655
pdettori merged 4 commits into
rossoctl:mainfrom
pdettori:feat/static-inject-plugin

Conversation

@pdettori

@pdettori pdettori commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Adds a new static-inject outbound AuthBridge plugin that swaps a placeholder credential on the
outbound Authorization header for a real static credential, resolved from a mounted secret — so a
model-influenced workload never holds the real secret.

This is the credential-injection mechanism for the serverless-harness RC1 AuthBridge egress
control-plane PoC
(single-tenant, Kind-first). It is a self-contained plugin on the existing
pipeline.Plugin/Configurable contract — no shared-interface change — modeled on the in-repo
token-broker/ibac plugins.

What it does

  • Config (strict decode): source (secret_dir | mappings), secret_dir, mappings (tests only),
    key_by (host | static), key, placeholder.
  • OnRequest (outbound): reads the bearer; fail-closed deny on missing bearer, placeholder mismatch,
    unresolved key, or an unsafe resolved value; otherwise rewrites Authorization: Bearer <real>.
  • FileResolver is path-contained (rejects ../absolute/separator keys); SafeSetHeader rejects
    CR/LF/NUL (CWE-113). Configure commits state only after full validation (a failed config = deny-state).
  • Registered as static-inject via a build-tagged cmd/authbridge-proxy/plugins_staticinject.go
    (droppable with -tags exclude_plugin_staticinject), matching the sibling plugin pattern.

Testing

  • go test ./plugins/staticinject/... — 19 tests (incl. subtests), all pass; TDD (RED→GREEN).
  • go build ./authlib/... ./cmd/authbridge-proxy/..., gofmt, go vet — clean.
  • Built the authbridge-proxy image from this branch and verified static-inject is registered.

Docs: a static-inject entry added to authbridge/docs/plugin-reference.md.

Assisted-By: Claude Code

Summary by CodeRabbit

  • New Features
    • Added the static-inject outbound auth plugin to replace an approved bearer placeholder with a resolved static credential (including configurable header injection).
    • Improved reverse-proxy handling for streaming: forwards header mutations and preserves SSE event: lines.
  • Security
    • Fail-closed credential injection with strict checks for missing/unmatched placeholders, unresolved/empty credentials, and unsafe header values.
    • Prevents unsafe credential values from being forwarded via header-splitting characters and blocks path traversal for file-backed lookups.
  • Documentation
    • Added plugin reference for static-inject.
  • Tests
    • Expanded test coverage for plugin behavior, security checks, resolvers, and reverse-proxy/SSE streaming behavior.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pdettori, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 450ffc37-2662-4db2-8e1f-69dad0b7cb93

📥 Commits

Reviewing files that changed from the base of the PR and between 452f314 and adf5d2a.

📒 Files selected for processing (6)
  • authbridge/authlib/plugins/staticinject/plugin.go
  • authbridge/authlib/plugins/staticinject/plugin_test.go
  • authbridge/authlib/plugins/staticinject/resolver.go
  • authbridge/authlib/plugins/staticinject/resolver_test.go
  • authbridge/cmd/authbridge-proxy/plugins_staticinject.go
  • authbridge/docs/plugin-reference.md
📝 Walkthrough

Walkthrough

Adds the static-inject authbridge plugin with filesystem or inline credential resolution, fail-closed header injection, build registration, tests, and documentation. Also updates reverse-proxy header forwarding, backend Host handling, compression behavior, and SSE event preservation.

Changes

Static credential injection

Layer / File(s) Summary
Credential resolution and header safety
authbridge/authlib/plugins/staticinject/resolver.go, authbridge/authlib/plugins/staticinject/resolver_test.go
Adds map and secret-directory resolvers, traversal protection, credential trimming, and CR/LF/NUL-safe header helpers with tests.
Plugin configuration and resolver construction
authbridge/authlib/plugins/staticinject/plugin.go, authbridge/authlib/plugins/staticinject/plugin_test.go
Adds configuration defaults and validation, resolver selection, plugin metadata, registration, and preservation of the last valid configuration after errors.
Outbound request injection flow
authbridge/authlib/plugins/staticinject/plugin.go, authbridge/authlib/plugins/staticinject/plugin_test.go
Validates bearer input, resolves credentials by host or static key, injects configured headers, and rejects missing or unsafe credentials.
Build activation and plugin reference
authbridge/cmd/authbridge-proxy/plugins_staticinject.go, authbridge/docs/plugin-reference.md
Registers the plugin through a conditional side-effect import and documents its configuration and denial behavior.

Reverse-proxy and SSE handling

Layer / File(s) Summary
Backend request rewriting and header propagation
authbridge/authlib/listener/reverseproxy/server.go, authbridge/authlib/listener/reverseproxy/server_test.go
Rewrites backend Host values, strips Accept-Encoding, and forwards pipeline header additions, changes, and deletions with regression coverage.
SSE event capture and reframing
authbridge/authlib/listener/internal/sseframe/*, authbridge/authlib/listener/reverseproxy/server.go, authbridge/authlib/listener/reverseproxy/streaming_test.go
Captures SSE event fields and preserves them when reframing streaming responses, with tests for event reset and compression handling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ReverseProxy
  participant Pipeline
  participant Backend
  Client->>ReverseProxy: Request with headers and Host
  ReverseProxy->>Pipeline: Run inbound plugins
  Pipeline-->>ReverseProxy: Mutated header state
  ReverseProxy->>Backend: Forward rewritten Host and synchronized headers
  Backend-->>ReverseProxy: SSE response with event and data fields
  ReverseProxy-->>Client: Reframed SSE response preserving event fields
Loading

Possibly related issues

  • kagenti/kagenti-extensions#656 — Covers the reverse-proxy SSE, gzip, header propagation, and backend Host changes.
  • kagenti/kagenti-extensions#530 — Proposes the static credential injection behavior implemented here.

Possibly related PRs

Suggested reviewers: kellyaa, huang195, akram

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding the static-inject AuthBridge plugin for static credential injection.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
authbridge/authlib/plugins/staticinject/plugin_test.go (1)

12-147: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a test for the static-inject.unsafe-value deny path.

The OnRequest method denies with static-inject.unsafe-value when the resolved credential contains CR/LF/NUL, but no plugin-level test covers this security-critical path. While SafeHeaderValue/SafeSetHeader are tested directly in resolver_test.go, the integration through OnRequest is untested.

🧪 Suggested test for unsafe-value deny path
func TestDenyOnUnsafeCredentialValue(t *testing.T) {
	p := New()
	cfg := `{
		"source": "mappings",
		"mappings": {"api.example.com": "evil\r\nX-Injected: 1"},
		"key_by": "host"
	}`
	if err := p.Configure(json.RawMessage(cfg)); err != nil {
		t.Fatalf("Configure() error = %v", err)
	}

	pctx := &pipeline.Context{
		Host: "api.example.com",
		Headers: http.Header{
			"Authorization": []string{"Bearer PLACEHOLDER"},
		},
	}

	action := p.OnRequest(context.Background(), pctx)

	if action.Type != pipeline.Reject {
		t.Fatalf("OnRequest() action.Type = %v, want %v (Reject for unsafe value)", action.Type, pipeline.Reject)
	}
	if got := pctx.Headers.Get("Authorization"); got != "Bearer PLACEHOLDER" {
		t.Errorf("Authorization header = %q, want unmodified %q", got, "Bearer PLACEHOLDER")
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/authlib/plugins/staticinject/plugin_test.go` around lines 12 -
147, Add a plugin-level test named TestDenyOnUnsafeCredentialValue alongside the
existing OnRequest tests. Configure a mapping whose resolved credential contains
CR/LF characters, invoke OnRequest with the placeholder Authorization header,
and assert it returns pipeline.Reject while leaving the Authorization header
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@authbridge/authlib/plugins/staticinject/plugin_test.go`:
- Around line 186-208: Update TestConfigure_FailureLeavesZeroDenyState to
perform the invalid reconfiguration on the already-valid plugin p instead of
creating and testing a fresh p2. After the failed Configure call, invoke
p.OnRequest with the existing request context and assert it still returns the
previously configured behavior, verifying Configure atomically preserves valid
state on failure.

---

Nitpick comments:
In `@authbridge/authlib/plugins/staticinject/plugin_test.go`:
- Around line 12-147: Add a plugin-level test named
TestDenyOnUnsafeCredentialValue alongside the existing OnRequest tests.
Configure a mapping whose resolved credential contains CR/LF characters, invoke
OnRequest with the placeholder Authorization header, and assert it returns
pipeline.Reject while leaving the Authorization header unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5477f62f-de47-45f3-84b3-69166284a8aa

📥 Commits

Reviewing files that changed from the base of the PR and between 7831168 and 8621750.

📒 Files selected for processing (6)
  • authbridge/authlib/plugins/staticinject/plugin.go
  • authbridge/authlib/plugins/staticinject/plugin_test.go
  • authbridge/authlib/plugins/staticinject/resolver.go
  • authbridge/authlib/plugins/staticinject/resolver_test.go
  • authbridge/cmd/authbridge-proxy/plugins_staticinject.go
  • authbridge/docs/plugin-reference.md

Comment thread authbridge/authlib/plugins/staticinject/plugin_test.go Outdated
@pdettori

Copy link
Copy Markdown
Member Author

Thanks for the review — addressed in 43debf2 (test-only):

  • Added TestDenyOnUnsafeCredentialValue — drives a CR/LF-poisoned resolved credential through OnRequest and asserts Reject plus that the Authorization header is left unmodified (the poisoned value never lands on the header). Verified via a RED/GREEN round-trip (temporarily bypassing the SafeSetHeader guard makes it fail).
  • Fixed the atomic-commit test (renamed TestConfigure_FailurePreservesPreviousState): it now configures a valid resolver, then reconfigures the same plugin with an invalid payload and asserts it still resolves with the previous config (Continue + Bearer REAL) — testing the real commit-after-validate guarantee rather than a fresh unconfigured instance.
  • TestConfigure_Invalid now asserts strings.Contains(err.Error(), wantErr) per subcase, so a case can no longer pass for the wrong validation reason.

Assisted-By: Claude Code

@huang195 huang195 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Bellissimo — this is a clean, self-contained plugin done right: fail-closed zero value, strict DisallowUnknownFields decode, Configure commits state only after full validation, and genuinely careful security helpers. The FileResolver path-containment (filepath.Dir(join) != Clean(Dir)) correctly rejects .., absolute, and nested keys, and SafeSetHeader closes the CWE-113 header-splitting door — both with tests. 19 tests, TDD RED→GREEN, and a thorough plugin-reference.md entry. Chef's kiss on the docs. 👨‍🍳

One non-blocking fail-closed edge as the single inline suggestion. Minor doc nit (not inline): the config table could state whether the key_by: host lookup key includes the port — pctx.Host is a bare field, so operators need to know whether the secret file should be named api.example.com or api.example.com:443.

Maintainer author, green CI, DCO signed. Approving — bravo! 🇮🇹

Assisted-By: Claude Code

Comment thread authbridge/authlib/plugins/staticinject/plugin.go Outdated
pdettori added a commit to pdettori/kagenti-extensions that referenced this pull request Jul 13, 2026
ReadCredentialFile trims a whitespace-only secret file to "" and returns
ok, and an inline mapping value may be "". The OnRequest guard only
checked !ok, so an empty credential was forwarded as "Bearer " (or an
empty raw header) instead of denying. Add a value == "" guard alongside
!ok and lock it in with a test covering both an empty inline mapping and a
whitespace-only secret file.

Addresses review feedback from @huang195 on PR rossoctl#655.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
pdettori added 4 commits July 13, 2026 13:37
…tion

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
…t, wantErr asserts)

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
Add inject_header config option to staticInjectConfig so credentials
can be written to a header other than Authorization (e.g. x-api-key
for direct api.anthropic.com auth). Default remains 'Authorization',
preserving byte-identical legacy behavior (Bearer scheme, Authorization
not deleted). Any other configured header writes the raw credential
value and removes the inbound Authorization header so a stale
placeholder bearer never reaches the backend.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
ReadCredentialFile trims a whitespace-only secret file to "" and returns
ok, and an inline mapping value may be "". The OnRequest guard only
checked !ok, so an empty credential was forwarded as "Bearer " (or an
empty raw header) instead of denying. Add a value == "" guard alongside
!ok and lock it in with a test covering both an empty inline mapping and a
whitespace-only secret file.

Addresses review feedback from @huang195 on PR rossoctl#655.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
@pdettori
pdettori force-pushed the feat/static-inject-plugin branch from 452f314 to adf5d2a Compare July 13, 2026 17:44
@pdettori
pdettori merged commit 9c131ee into rossoctl:main Jul 13, 2026
19 checks passed
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Kagenti Issue Prioritization Jul 13, 2026
@pdettori
pdettori deleted the feat/static-inject-plugin branch July 13, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants