Skip to content

feat(desktop): tenant-wide SAML signing-certificate expiry board - #219

Merged
tiredithumans merged 2 commits into
mainfrom
feat/saml-cert-expiry-audit
Aug 12, 2026
Merged

feat(desktop): tenant-wide SAML signing-certificate expiry board#219
tiredithumans merged 2 commits into
mainfrom
feat/saml-cert-expiry-audit

Conversation

@tiredithumans

Copy link
Copy Markdown
Owner

Summary

Follow-up to #218. The audit's credential rules read an application's keyCredentials, but a SAML signing certificate lives on the service principal — so expiring signing certificates were invisible in-app entirely. The first anyone heard of one was Microsoft's 60-day email, which goes to that app's notificationEmailAddresses and, by default, only to the admin who originally added the app. This is the list that makes rotation schedulable instead of reactive.

Security → "SSO certificates" lists every SAML app's signing certificate, soonest first, on the same AuditDashboard scaffold as the credential-expiry board (same filters, same badges, same CSV export).

  • One server-side filtered scan, not a fan-out. preferredSingleSignOnMode supports $filter eq on the default query surface (no ConsistencyLevel, no $count), and only SAML apps have a signing certificate — anything wider is waste. Capped at SP_INDEX_MAX over that filtered subset, which no real tenant reaches.
  • Rows come from the same build_rollover the SSO tab uses, so the board and the per-app panel can't disagree about whether a replacement is staged.
  • Two columns carry what the expiry date doesn't: whether a replacement is already staged (the difference between clicking Activate and starting a rollover), and whether anyone at all is on Entra's 60/30/7-day warnings. The "≤ 30 days, no replacement staged" facet is the actual work queue, and the banner counts rows genuinely at risk rather than every row with a date coming up.
  • An expired certificate that still has a valid staged replacement reads "Auto-promoted" — Entra has already switched, whether or not anyone pressed Activate.

Deliberately not a risk-score input

An expiring certificate is an availability risk — sign-in stops on a known date — while risk_score ranks exposure. Adding points would push apps up a ranking operators read as "most over-permissioned" because they're due for routine maintenance. It reuses CredentialStatus and EXPIRY_WARNING_DAYS so "Expiring Soon" means the same number of days on both boards, and an unreadable expiry is Unknown, never Active.

Cache busting is wider than it looks

invalidate_sso_cert_board fires on Ok from every certificate mutation plus set_notification_emails (it flips the "nobody is warned" column) and set_sso_mode (it decides whether the app is on the board at all). Missing either of those last two leaves the board contradicting the SSO tab for up to the TTL.

The build-profile commit (3deaa47) — please read

Adding one more view exposed a latent blocker: the frontend dev wasm had already reached 2.19 GiB, and rust-lld can't write a DWARF custom section that large. cargo check passes and the link dies with SIGBUS, which looks nothing like the size problem it is. The tree was sitting just under the limit, so whichever view got added next would have appeared to be the thing that broke the build.

[profile.dev] debug = "line-tables-only" takes it to 0.31 GiB (verified linking, and dev rebuilds get faster). Line tables are the part that survives into a browser panic trace anyway; what's given up is variable inspection in a DWARF-aware wasm debugger. Deliberately not strip = "debuginfo" (what profile.test uses) — that drops line tables too.

This supersedes the existing "scoped to test so profile.dev keeps its debuginfo" note, which is updated in place rather than left to contradict the new setting. Flagging prominently because it reverses a documented deliberate choice.

Test plan

  • just verify — fmt, clippy, test, web-fmt-check, web-clippy, web-test, web-build all green
  • New backend tests: unreadable expiry classifies Unknown (never Active), thresholds match the audit's credential rules exactly, and the CSV export guards formula-leading display names while preserving the staged/notified columns
  • Demo fixtures cover the three states worth seeing — expiring with nothing staged and nobody notified, expiring with a replacement ready, and healthy
  • Wasm link verified with the dashboard mounted (this was the blocker; 2.19 GiB → 0.31 GiB)
  • just web-itest did NOT run — no chromedriver on this machine. The new dashboard has no behavioural coverage, same gap as feat(desktop): stage SAML signing certificates instead of rotating in place #218
  • Not exercised against a live tenant — the $filter shape is covered by unit tests and Microsoft's advanced-query docs, not an integration test

Follow-ups

  • The staged-rollover panel and this board both still lack GUI tests; one shard covering the SSO tab plus this dashboard would close both.
  • Bulk stage ("stage certs for everything on the ≤30-day-unprepared list") is the natural next step now that the work queue is a filter.

🤖 Generated with Claude Code

https://claude.ai/code/session_017BSW4ReFWuL64BfTkEP74S

tiredithumans and others added 2 commits August 12, 2026 09:59
The dev wasm had reached 2.19 GiB, and `rust-lld` cannot write a DWARF
custom section that large: `cargo check` passes and the link dies with
SIGBUS, which looks nothing like the size problem it is. The tree had been
sitting just under the limit, so whichever view got added next would appear
to be the thing that broke the build.

`debug = "line-tables-only"` takes it to 0.31 GiB. Line tables are the part
that survives into a browser panic trace anyway; what's given up is variable
inspection in a DWARF-aware wasm debugger. Dev rebuilds also get faster.

Deliberately not `strip = "debuginfo"` (what profile.test uses) — that drops
the line tables too, and a dev panic pointing at no source line is a bad
trade for the last ~0.1 GiB. Supersedes the "profile.dev keeps its
debuginfo" note, which is updated in place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BSW4ReFWuL64BfTkEP74S
The audit's credential rules read an *application's* keyCredentials, but a
SAML signing certificate lives on the service principal — so expiring signing
certificates were invisible in-app entirely. The first anyone heard of one was
Microsoft's 60-day email, which goes to that app's notificationEmailAddresses
and, by default, only to the admin who first added it.

Security -> "SSO certificates" lists every SAML app's signing certificate,
soonest first, on the same AuditDashboard scaffold as the credential board.

- `list_saml_sso_service_principals` is ONE server-side filtered scan:
  `preferredSingleSignOnMode` supports `$filter eq` on the default query
  surface, and only SAML apps have a signing certificate, so anything wider
  is waste. Capped at SP_INDEX_MAX over that filtered subset, which no real
  tenant reaches.
- Rows are built by the same `build_rollover` the SSO tab uses, so the board
  and the per-app panel cannot disagree about whether a replacement is staged.
- Two columns carry what the date alone doesn't: whether a replacement is
  staged (click Activate vs start a rollover), and whether anyone at all is on
  Entra's 60/30/7-day warnings. The "no replacement staged" facet is the work
  queue, and the banner counts rows genuinely at risk rather than every row
  with a date coming up. An expired certificate that still has a staged
  replacement reads "Auto-promoted" — Entra has already switched.

Deliberately NOT a risk-score input. An expiring certificate is an
availability risk; risk_score ranks exposure, and points here would push apps
up a ranking operators read as "most over-permissioned" for being due routine
maintenance. It reuses CredentialStatus + EXPIRY_WARNING_DAYS so "Expiring
Soon" means the same on both boards, and an unreadable expiry is Unknown,
never Active.

Cache busting is wider than it looks: `invalidate_sso_cert_board` fires on Ok
from every certificate mutation plus `set_notification_emails` (it flips the
"nobody is warned" column) and `set_sso_mode` (it decides board membership).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BSW4ReFWuL64BfTkEP74S
@tiredithumans
tiredithumans merged commit 06af5cc into main Aug 12, 2026
9 checks passed
@tiredithumans
tiredithumans deleted the feat/saml-cert-expiry-audit branch August 12, 2026 15:20
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