Skip to content

Fix: Run the TLS bridge without SPIRE (need-driven SPIFFE provider; drop in-process trust self-check)#523

Merged
huang195 merged 3 commits into
rossoctl:mainfrom
huang195:fix/tlsbridge-spire-decouple
Jun 19, 2026
Merged

Fix: Run the TLS bridge without SPIRE (need-driven SPIFFE provider; drop in-process trust self-check)#523
huang195 merged 3 commits into
rossoctl:mainfrom
huang195:fix/tlsbridge-spire-decouple

Conversation

@huang195

@huang195 huang195 commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

Two boot-path fixes so the AuthBridge TLS bridge (proxy-sidecar) runs on clusters without SPIRE. Follow-up to #522.

1. Build the SPIFFE provider only when something consumes it

The platform base config ships an empty spiffe: {} for every agent. cmd/authbridge-proxy built the SPIFFE provider on mere presence of that block — but spiffe.NewProviderworkloadapi.NewX509Source blocks until the SPIRE Workload API returns the first SVID, with no timeout. A proxy-sidecar agent that only runs the TLS bridge mints leaves from a cert-manager CA and never touches an SVID, so on a SPIRE-less cluster it would hang forever before binding any listener (pod Running but every port closed, no error).

spiffeProviderNeeded now gates construction on a real consumer:

  • top-level mTLS (needs the X509Source), or
  • a plugin whose identity is spiffe-based (identity.type=spiffe — today only token-exchange).

An unused spiffe: {} logs skipping SPIRE provider and is skipped. mTLS without a spiffe block still fails loud via the existing guard.

2. Remove RunTrustSelfCheck

It read the sidecar's own SSL_CERT_FILE / NODE_EXTRA_CA_CERTS / REQUESTS_CA_BUNDLE, but the operator sets those on the agent container — so it logged a false agent will not trust minted leaves and egress will safely tunnel WARN on every real deployment (even when the agent trusts the CA fine). An in-process check can't validate another container's trust store.

Test Plan

  • go build + go vet (authlib, cmd/authbridge-proxy)
  • go test — added spiffeProviderNeeded unit coverage (mtls→true, spiffe-identity→true, client-secret→false, empty→false); tlsbridge package tests pass
  • End-to-end on Kind with the SPIRE socket unmounted: sidecar logs skipping SPIRE provider + tls-bridge enabled, binds :8081, forges a leaf from the cert-manager CA (issuer=authbridge-tls-bridge-ca-<workload>), and the agent's HTTPS to example.com is decrypted (HTTP/2 404) — no hang, no permission denied, no trust self-check WARN, no SPIRE volume mounted. (Requires the companion operator fix that sets fsGroup for the CA mount.)

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Summary by CodeRabbit

  • Improvements
    • Optimized SPIFFE provider initialization to only build when enabled by mTLS or when plugin identity configuration requires it.
    • Added log messaging when SPIFFE configuration is present but not used.
  • Bug Fixes / Behavior Change
    • Removed TLS bridge “trust self-check” warnings during engine setup.
  • Tests
    • Added unit tests covering SPIFFE activation and provider-consumer coverage rules, including malformed configuration handling.
  • Documentation
    • Updated the TLS bridge phase plan to reflect the removal of the trust self-check step.

…ss trust self-check

Two boot-path fixes so the TLS bridge runs without SPIRE.

1) Need-driven SPIFFE provider. The platform base config ships an empty
   'spiffe: {}' for every agent, and main built the provider on mere presence of
   the block — but NewProvider blocks until the SPIRE Workload API returns an
   SVID. A proxy-sidecar agent that only runs the TLS bridge (cert-manager CA,
   no SVID) on a SPIRE-less cluster would hang forever before binding any
   listener. spiffeProviderNeeded now gates construction on actual consumers:
   top-level mTLS, or a plugin with identity.type=spiffe (today only
   token-exchange). An unused 'spiffe: {}' logs and is skipped.

2) Remove RunTrustSelfCheck. It inspected the sidecar process's own
   SSL_CERT_FILE/NODE_EXTRA_CA_CERTS/REQUESTS_CA_BUNDLE, but the operator sets
   those on the agent container, not the sidecar — so it logged a false
   'agent will not trust minted leaves' WARN on every real deployment. An
   in-process check can't validate another container's trust store.

Verified end-to-end: bridge boots with the SPIRE socket unmounted, forges a
leaf from the cert-manager CA, and the agent's HTTPS is decrypted.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@huang195
huang195 requested a review from a team as a code owner June 18, 2026 20:30
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 34bada84-13eb-4afe-86c4-9263ce62611e

📥 Commits

Reviewing files that changed from the base of the PR and between 0c9e75a and 6f9650e.

📒 Files selected for processing (4)
  • authbridge/authlib/plugins/registry.go
  • authbridge/cmd/authbridge-proxy/main.go
  • authbridge/cmd/authbridge-proxy/main_test.go
  • authbridge/docs/superpowers/plans/2026-06-17-authbridge-tlsbridge-phase1.md
✅ Files skipped from review due to trivial changes (1)
  • authbridge/docs/superpowers/plans/2026-06-17-authbridge-tlsbridge-phase1.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • authbridge/cmd/authbridge-proxy/main.go

📝 Walkthrough

Walkthrough

Removes the RunTrustSelfCheck exported function from the tlsbridge engine and its call site. Adds SPIFFEConsumerPlugins() registry helper for discovering SPIFFE-consuming plugins. Introduces spiffeProviderNeeded and pluginUsesSPIFFEIdentity helpers that inspect the boot config's mTLS flag and pipeline plugin JSON configs to detect SPIFFE usage; the SPIFFE/SPIRE Workload API connection is established only when actually required. Includes comprehensive table-driven tests and documentation alignment.

Changes

Conditional SPIFFE Provider and Trust-Check Removal

Layer / File(s) Summary
Remove RunTrustSelfCheck from tlsbridge engine and call site
authbridge/authlib/tlsbridge/engine.go, authbridge/cmd/authbridge-proxy/main.go
RunTrustSelfCheck exported function and its unused imports (log/slog, os, strings) are deleted from engine.go. The corresponding call during TLS bridge engine initialization in main.go is removed.
Add SPIFFEConsumerPlugins() registry discovery
authbridge/authlib/plugins/registry.go
New exported function iterates over registered plugin factories under a read lock, instantiates each plugin, and returns the sorted list of plugin names that implement spiffe.ProviderConsumer.
Implement spiffeProviderNeeded detection and conditional provider construction
authbridge/cmd/authbridge-proxy/main.go
encoding/json import is added and tokenexchange import becomes named to access the SPIFFE identity constant. spiffeProviderNeeded inspects the MTLS flag and scans inbound/outbound pipeline plugins via pluginUsesSPIFFEIdentity, which JSON-unmarshals each plugin's config to detect identity.type == "spiffe". In main, SPIFFE provider construction is guarded: skipped with a log message when the SPIFFE block exists but spiffeProviderNeeded returns false, avoiding any Workload API connection.
Comprehensive tests for SPIFFE conditional detection
authbridge/cmd/authbridge-proxy/main_test.go
identityConfig helper builds plugin JSON configurations. TestSpiffeProviderNeeded table-driven test covers empty config, mTLS-enabled scenarios, token-exchange with client-secret vs. spiffe identity, inbound plugins with spiffe, entries without config, and malformed JSON. TestProviderConsumersCoveredByPredicate iterates through SPIFFEConsumerPlugins() and enforces every registered SPIFFE consumer is in an allowlist and evaluates correctly when configured with identity.type=spiffe.
Update documentation for RunTrustSelfCheck removal
authbridge/docs/superpowers/plans/2026-06-17-authbridge-tlsbridge-phase1.md
Plan file is updated to note that RunTrustSelfCheck was removed from engine.go and Task 10 Step 5 (which described implementing it) is cleared.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A bunny hopped through the config with glee,
"Build SPIFFE only when it's truly needed, you see!"
The trust-check is gone, detection is smart,
SPIRE connects on demand from the start.
🐇 Leaner and cleaner—a job well done!

Suggested reviewers

  • pdettori
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 directly and accurately summarizes the two main changes: fixing TLS bridge to run without SPIRE (via need-driven SPIFFE provider initialization) and removing the in-process trust self-check that caused false warnings.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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 and usage tips.

@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.

🧹 Nitpick comments (1)
authbridge/cmd/authbridge-proxy/main_test.go (1)

30-47: 💤 Low value

Consider adding a test case for malformed plugin config JSON.

The pluginUsesSPIFFEIdentity function has an error-handling path at lines 128-131 in main.go that returns false when JSON unmarshalling fails. Adding a test case with invalid JSON would verify this defensive path:

{"malformed plugin config", outbound(config.PluginEntry{
	Name: "bad-plugin", Config: json.RawMessage(`{not valid json`),
}), false},
🤖 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/cmd/authbridge-proxy/main_test.go` around lines 30 - 47, The test
suite for the pluginUsesSPIFFEIdentity function is missing a test case to verify
the error-handling path when JSON unmarshalling fails. Add a new test case to
the tests slice that includes a PluginEntry with malformed JSON in the Config
field (such as invalid JSON syntax), and verify that the function returns false
when it encounters this invalid JSON during unmarshalling, similar to how the
existing test cases use the outbound helper function with identityConfig or
other Config values.
🤖 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.

Nitpick comments:
In `@authbridge/cmd/authbridge-proxy/main_test.go`:
- Around line 30-47: The test suite for the pluginUsesSPIFFEIdentity function is
missing a test case to verify the error-handling path when JSON unmarshalling
fails. Add a new test case to the tests slice that includes a PluginEntry with
malformed JSON in the Config field (such as invalid JSON syntax), and verify
that the function returns false when it encounters this invalid JSON during
unmarshalling, similar to how the existing test cases use the outbound helper
function with identityConfig or other Config values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cb06264a-471d-47b6-88f8-5f56596f4dd0

📥 Commits

Reviewing files that changed from the base of the PR and between d6eee5c and 0c9e75a.

📒 Files selected for processing (3)
  • authbridge/authlib/tlsbridge/engine.go
  • authbridge/cmd/authbridge-proxy/main.go
  • authbridge/cmd/authbridge-proxy/main_test.go
💤 Files with no reviewable changes (1)
  • authbridge/authlib/tlsbridge/engine.go

…d by the need predicate

Review follow-up. spiffeProviderNeeded detects a plugin's need via
identity.type=spiffe, while BuildWithSPIFFE injects based on the
spiffe.ProviderConsumer interface; they agree only because token-exchange is the
lone consumer today. Deriving need purely from the interface is not possible —
token-exchange implements ProviderConsumer for BOTH identity types and injection
happens before Configure, so 'is a consumer' != 'needs the provider' (gating on
the interface would rebuild the provider for client-secret agents and reintroduce
the SPIRE-less hang). Instead add plugins.SPIFFEConsumerPlugins() and a test that
fails if a new ProviderConsumer appears that the predicate may not detect — making
the hazard enforceable.

Also note RunTrustSelfCheck's removal in the (historical) Phase 1 plan doc.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@rubambiza rubambiza added the ready-for-ai-review Request automated AI code review from clawgenti label Jun 18, 2026

@clawgenti clawgenti 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.

Need-driven SPIFFE provider construction and removal of RunTrustSelfCheck are both well-motivated and cleanly implemented; the enforcement test (TestProviderConsumersCoveredByPredicate) is a nice addition that keeps the predicate/interface agreement honest going forward.

All checks pass. Ready for human review.


Reviewed by clawgenti using github:pr-review

defer registryMu.RUnlock()
var names []string
for name, factory := range registry {
if _, ok := factory().(spiffe.ProviderConsumer); ok {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: factory() is called here purely to probe the interface — it instantiates a full plugin instance just for the type assertion. If any factory has observable side effects (spawning goroutines, opening connections, registering metrics), this enumerate call would silently trigger them. Consider adding a lighter probe path — e.g. a PluginCapabilities() or a separate RegisterConsumerTag registration step — so SPIFFEConsumerPlugins doesn't need to construct live instances. (Fine to defer until a factory with side effects actually appears.)

@cwiklik cwiklik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Clean, well-scoped fix. Gates SPIRE Provider construction on actual need (spiffeProviderNeeded: top-level mTLS, or a plugin with identity.type=spiffe) so a TLS-bridge-only proxy-sidecar boots on SPIRE-less clusters, and drops RunTrustSelfCheck (it inspected the sidecar's trust env, but the operator sets that on the agent container, so it false-WARNed on every deploy).

I verified the main risk — a consumer needing the Provider without matching the predicate — is not present: token-exchange's identity.type is required and validated, and SetSPIFFEProvider is ignored for non-spiffe identities, so a missing/other type fails fast rather than nil-derefs. TestProviderConsumersCoveredByPredicate is a nice tripwire for future consumers.

Areas reviewed: Go, Tests, Docs, Security
Commits: 2, both signed-off (DCO ✓)
CI: all green (Go CI, CodeQL, Trivy, Bandit, pre-commit)

Approving — two non-blocking notes inline.

Reviewed with Claude Code (/github:pr-review).

Comment thread authbridge/cmd/authbridge-proxy/main.go Outdated
// later with a precise error; don't force the provider on for it.
return false
}
return probe.Identity.Type == "spiffe"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit — the literal "spiffe" here duplicates the plugin's SpiffeIdentity constant. If that constant's value ever changes, this probe silently drifts → the consumer receives a nil Provider, and TestProviderConsumersCoveredByPredicate wouldn't catch it (it checks the consumer name set, not the string value). Consider referencing a shared identity-type constant instead of the literal.

// the full production set.
func TestProviderConsumersCoveredByPredicate(t *testing.T) {
// Plugins whose SPIFFE need spiffeProviderNeeded is known to detect.
covered := map[string]bool{"token-exchange": true}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

suggestioncovered is a hand-maintained set, so this asserts the consumer name is listed but not that spiffeProviderNeeded actually fires for that plugin's spiffe config. A stronger invariant: for each SPIFFEConsumerPlugins() entry, build a config with identity.type=spiffe and assert the predicate returns true. The current form is a fine tripwire — just noting it's a reminder, not a functional proof.

- pluginUsesSPIFFEIdentity: reference tokenexchange.SpiffeIdentity instead of the
  duplicated "spiffe" literal, so the probe can't drift from the plugin's own
  identity-type constant.
- TestSpiffeProviderNeeded: add a malformed-JSON case covering the unmarshal
  error path (returns false).
- TestProviderConsumersCoveredByPredicate: in addition to the name tripwire,
  assert spiffeProviderNeeded actually fires for each consumer's
  identity.type=spiffe config (functional invariant, not just membership).
- SPIFFEConsumerPlugins: document that probing via factory() is safe under the
  PluginFactory contract (cheap, side-effect-free constructor) and what to do if
  that ever changes.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ai-review Request automated AI code review from clawgenti

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants