Skip to content

Fix: Gate TLS-bridge leaf cache on wall-clock validity (survive suspend)#622

Merged
huang195 merged 2 commits into
rossoctl:mainfrom
huang195:fix/tlsbridge-leaf-cache-wallclock
Jun 25, 2026
Merged

Fix: Gate TLS-bridge leaf cache on wall-clock validity (survive suspend)#622
huang195 merged 2 commits into
rossoctl:mainfrom
huang195:fix/tlsbridge-leaf-cache-wallclock

Conversation

@huang195

@huang195 huang195 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Problem

The TLS-bridge minter caches forged leaf certs on a TTL checked with time.Now(), which carries Go's monotonic clock. Across a host suspend / VM pause the monotonic clock freezes while wall time — and the leaf's x509 NotAfter — keeps advancing. So the cache kept serving a leaf the client already rejected as certificate has expired.

The downstream effect is severe and silent: the client's rejection looks like a forged-cert failure, so the bridge auto-skips the host and falls back to undecrypted passthrough. Interception (and all inference/MCP/A2A parsing) for that host silently stops until the proxy is restarted — even though the agent still works via the blind tunnel, so nothing looks broken.

This was hit in practice on a dev laptop that slept overnight: litellm's cached leaf expired, every inference call fell through to passthrough, and abctl showed no inference events despite the inference-parser being active.

Fix

Gate cache freshness on wall-clock deadlines and the cert's real validity, not a monotonic TTL:

  • mint: populate tls.Certificate.Leaf so the cache can read the cert's actual NotAfter (also lets the TLS stack skip re-parsing each handshake). Leaf validity is now + ttl + renewBefore.
  • cache: store the renewal deadline monotonic-stripped (.Round(0)) so the freshness comparison falls back to the wall clock, plus a backstop that never serves a leaf whose real NotAfter has already passed (the single value the client verifies).
  • SkipSet: same .Round(0) treatment so auto-skip entries expire on real time across a suspend instead of lingering.

Tests

TestMinter_ReMintsWallClockExpiredLeaf ages a cached leaf past its NotAfter while the monotonic deadline still reads fresh, and asserts a re-mint. Existing minter/skipset TTL tests still pass (sub-second precision preserved by keeping a wall-clock expires rather than relying on the 1s-truncated cert NotAfter).

Scope

Primarily a dev-environment robustness fix (laptop sleep / VM pause) — production nodes don't suspend — but wall-clock gating is the correct invariant regardless, and the failure mode (silent passthrough until restart) is bad enough to warrant the guard.

Fixes #623.
Part of rossoctl/rossoctl#2071.

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

… TTL

The minter cached forged leaves on a TTL compared with time.Now(), which
carries Go's monotonic clock. Across a host suspend / VM pause the
monotonic clock freezes while wall time — and the leaf's x509 NotAfter —
keeps advancing, so the cache kept serving a leaf the client already
rejected as expired ("certificate has expired"). The bridge then read the
rejection as a forged-cert failure, auto-skipped the host, and fell back
to undecrypted passthrough — silently disabling interception (and any
inference/MCP parsing) for that host until the proxy was restarted.

- mint: populate tls.Certificate.Leaf so the cache can gate on the cert's
  real NotAfter (and the TLS stack avoids re-parsing each handshake). Leaf
  validity is now now+ttl+renewBefore.
- cache: store the renewal deadline monotonic-stripped (.Round(0)) so the
  freshness check uses the wall clock, plus a backstop that never serves a
  leaf whose real NotAfter has already passed.
- SkipSet: same .Round(0) treatment so auto-skip entries expire on real
  time across a suspend instead of lingering.

Add a regression test that ages a cached leaf past its NotAfter while the
monotonic deadline still reads fresh, and asserts a re-mint.

Primarily a dev-environment robustness fix (laptop sleep / VM pause) —
production nodes do not suspend — but wall-clock gating is the correct
invariant regardless.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@huang195, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 34 minutes and 24 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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

🚦 How do rate 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 see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b5072a01-0f41-4fc9-8e6b-042c3d7af6a5

📥 Commits

Reviewing files that changed from the base of the PR and between afc9fc3 and 17fc060.

📒 Files selected for processing (2)
  • authbridge/authlib/tlsbridge/minter.go
  • authbridge/authlib/tlsbridge/minter_test.go
📝 Walkthrough

Walkthrough

The TLS bridge now stores wall-clock expiry times, rechecks cached certificates against parsed leaf NotAfter, extends minted leaf validity with a renewal buffer, and adds a regression test for re-minting after wall-clock expiry.

Changes

TLS bridge wall-clock expiry

Layer / File(s) Summary
SkipSet expiry rounding
authbridge/authlib/tlsbridge/decision.go
SkipSet.Add rounds stored expiry timestamps so later Contains checks use wall-clock comparisons.
Minter cache and minting
authbridge/authlib/tlsbridge/minter.go, authbridge/authlib/tlsbridge/minter_test.go
renewBefore is introduced, cache hits require both e.expires and e.cert.Leaf.NotAfter to still be valid, cached deadlines are rounded to wall-clock time, minted leaves use m.ttl + renewBefore, the returned certificate now carries a parsed Leaf, and a test mutates cached leaf expiry to assert re-minting.

Sequence Diagram(s)

sequenceDiagram
  participant GetCertificateForHost
  participant Minter
  participant "x509.ParseCertificate"
  participant "tls.Certificate"
  GetCertificateForHost->>Minter: request certificate for host
  Minter->>Minter: compare time.Now() with e.expires and Leaf.NotAfter
  Minter->>x509.ParseCertificate: parse minted DER leaf
  x509.ParseCertificate-->>Minter: parsed Leaf
  Minter->>tls.Certificate: set Leaf before returning
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • pdettori

Poem

🐰 I hop where wall clocks softly chime,
and round the edge of suspend-time.
My leaf says “fresh,” my cache says “near,”
so mint again, the path is clear.
Hoppity! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Title check ✅ Passed The title clearly reflects the main change: switching TLS-bridge leaf caching to wall-clock validity to survive suspend/pause.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ 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

🤖 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/tlsbridge/minter.go`:
- Around line 121-124: The exported MinterOpts.LeafTTL documentation is now
inaccurate because minter.go’s leaf certificate validity uses LeafTTL plus
renewBefore rather than treating LeafTTL as both validity and cache TTL. Update
the LeafTTL comment on MinterOpts to describe the new model clearly, and make
sure the wording matches the behavior in the leaf minting logic around the
Minter and its ttl/renewBefore handling.
🪄 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: b15b300b-6056-4bd5-8666-571f7766efee

📥 Commits

Reviewing files that changed from the base of the PR and between 160f917 and afc9fc3.

📒 Files selected for processing (3)
  • authbridge/authlib/tlsbridge/decision.go
  • authbridge/authlib/tlsbridge/minter.go
  • authbridge/authlib/tlsbridge/minter_test.go

Comment thread authbridge/authlib/tlsbridge/minter.go Outdated
…eadline

- gofmt: re-align the x509.Certificate{} literal. The prior edit de-aligned
  NotBefore:/NotAfter: to one space; gofmt -l flagged minter.go (CI missed it
  because go-fmt pre-commit only covers proxy-init/).
- test: add TestMinter_CacheDeadlineIsWallClock, asserting the cache freshness
  deadline carries no monotonic reading (exp == exp.Round(0)) — directly
  guarding the .Round(0) suspend fix, which the existing test did not exercise.
  Clarify that TestMinter_ReMintsWallClockExpiredLeaf covers the NotAfter
  backstop (a state normal operation cannot produce).

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>

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

Correct diagnosis and minimal fix for the monotonic-vs-wall-clock cache-staleness bug. Verified the mechanism end-to-end:

  • .Round(0) on the cache deadline (expires) and the SkipSet entry strips the monotonic reading, so time.Now().Before(...) falls back to the wall clock — Go's documented behavior when one operand lacks a monotonic reading — and the cache correctly expires across a suspend. TestMinter_CacheDeadlineIsWallClock guards this precisely via exp != exp.Round(0).
  • The Leaf.NotAfter backstop gates on the value the client actually verifies; Leaf != nil fails safe (re-mint, never serve a leaf with no parsed cert). Sub-second freshness stays on expires since x509 NotAfter is 1s-truncated — backstop, not primary.
  • Populating tls.Certificate.Leaf at mint also lets the TLS stack skip per-handshake re-parsing (parse-once). renewBefore (1h) equals the old inline value, so leaf validity always outlasts the cache deadline and re-mint precedes real expiry. No data race — the cert is effectively immutable post-mint and read under m.mu.

Two assertive regression tests cover both the wall-clock deadline and the NotAfter backstop. Nothing actionable found.

Areas reviewed: Go. Commits: 2, signed-off: yes. CI: all passing (Go CI ×3, CodeQL, CodeRabbit).

@huang195
huang195 merged commit 13eec8e into rossoctl:main Jun 25, 2026
19 checks passed
@huang195
huang195 deleted the fix/tlsbridge-leaf-cache-wallclock branch June 25, 2026 14:43
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Jun 25, 2026
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.

bug(tlsbridge): forged-leaf cache serves wall-clock-expired certs across host suspend → silent passthrough

3 participants