Skip to content

PAM - Rotation daemon - #1232

Draft
Hinton wants to merge 18 commits into
mainfrom
pam/rotation-daemon
Draft

PAM - Rotation daemon#1232
Hinton wants to merge 18 commits into
mainfrom
pam/rotation-daemon

Conversation

@Hinton

@Hinton Hinton commented Jul 7, 2026

Copy link
Copy Markdown
Member

Generated from the server pam/rotation branch api.json (swagger internal
group) via support/generate-api-bindings-ci.sh. Adds the rotation daemon,
jobs, attempts, configs, daemons and target-systems API groups and their
models.## 🎟️ Tracking

📔 Objective

🚨 Breaking Changes

Hinton added 11 commits July 6, 2026 14:51
Generated from the server pam/rotation branch api.json (swagger internal
group) via support/generate-api-bindings-ci.sh. Adds the rotation daemon,
jobs, attempts, configs, daemons and target-systems API groups and their
models.
New commercial crate bitwarden_license/bitwarden-rotation-daemon
(bw-rotation-daemon binary). Implements the daemon token parser
(0.daemon.<id>.<secret>:<key>), org-key unwrap + cipher password
re-encryption on a standalone KeyStore, password-policy mapping onto
bitwarden-generators (password() made pub), the bounded failure
taxonomy with SafeDetail, and the IdentityClient + SessionManager
session state machine per rotation-daemon.allium.
…ation, executor, CLI

- api/: DaemonAuthMiddleware (bearer + single 401 refresh-retry, hard-fail
  on session loss) over the generated bitwarden-api-api rotation clients;
  error classification incl. daemon-route-404 -> NotEligible.
- resolver/: CredentialResolver trait + env resolver (per-kind required vars).
- integrations/: Integration trait/registry + custom-script runner (secrets
  over stdin only, stdout/stderr nulled, exit-code contract, timeout kill,
  script-root containment).
- executor/: transient retry budget (gated variant re-checks the
  step-boundary gate before every try), 5-arm gate (execute_by lease,
  session phase, connectivity pause), 7-step rotation flow with
  Verified/CipherWritten proof tokens, poll loop with single-flight claim
  + heartbeat task, refresh-probe on NotEligible.
- cli/config/main: clap surface with no token argv slot (env/file XOR),
  signal-wired cancellation, exit codes 0/1/2/3.

170 crate tests passing.
…lper

- integrations/entra.rs: Microsoft Graph rotate (PATCH passwordProfile),
  directory-confirmation verify (lastPasswordChangeDateTime + optional ROPC
  probe), revokeSignInSessions; SP client-credentials auth (admin reset,
  never the rotated credential); account_identity encoded as a single URL
  path segment; detail carries HTTP status + Graph error.code only.
- examples/register.rs: TEST-ONLY admin-side wrap emulator (org key from
  env/stdin, never argv) producing the register-request payload + token
  template for e2e.

191 crate tests passing.
Security: strip BWRD_TOKEN from the environment after read and .env_clear()
custom-script children so the credential can't be inherited (no_leak.sh now
asserts it); validate Graph error.code charset before it enters a failure
report; redirect::Policy::none() on the identity/api/entra HTTP clients so a
cross-host redirect can't leak the bearer.

Spec: make VerifiedBeforeSuccess compile-time enforced via report_success_inner
taking the Verified/CipherWritten proof tokens by value; report target_updated
on unexpected step-5 cipher errors instead of silently leaving the attempt
unreported.

Deps: move anyhow to the workspace. 196 crate tests passing.
- Clear all crate clippy findings incl. the config.rs unwrap_used deny
  (is_some()+unwrap -> if let) and a test holding ENV_LOCK across await.
- tests/daemon_flow.rs: 5 wiremock-driven end-to-end scenarios (happy path
  claim->cipher writeback->success, transient failure-budget exhaustion,
  claim 409 race, startup credential-refused, terminate-failed still succeeds)
  via a #[doc(hidden)] DaemonConfig::new_for_test hook.

201 tests passing (196 unit + 5 integration).
…d bindings

- Make the crate pass the full CI lint suite: fmt, clippy (0 warnings incl.
  print-macro allow on the register example), cargo-sort, udeps, and
  rustdoc -D warnings (resolve/de-link intra-doc links to internal items).
- Add anyhow to the workspace deps + renovate owner group (dep-ownership).
- Reformat README (prettier).
- bitwarden-importers: set the new access_rule_id: None on
  CollectionWithIdRequestModel (field added to the regenerated bindings from
  the server branch; None preserves prior behavior).
… flags

Settings (URLs, intervals, retry tuning, script root/timeout, Entra
probe) move to a TOML config file (--config <PATH> / BWRD_CONFIG);
built-in defaults live in impl Default for FileConfig and fill missing
keys via #[serde(default)]. deny_unknown_fields rejects a token key in
the file: the daemon token is env-only (BWRD_TOKEN, stripped from the
environment after read, empty treated as missing) and --token-file is
removed. BWRD_API_URL / BWRD_IDENTITY_URL override the file's URLs
(env > file > built-in defaults).

Tests gain a process-wide TEST_ENV_LOCK so config and custom-script
modules serialise all env mutation. README rewritten around the config
file; renovate now pins toml.
…-level URL keys

Server URLs now live in an [environment] TOML table: base derives
{base}/api and {base}/identity, with optional explicit api/identity
overrides. Precedence per URL: BWRD_API_URL/BWRD_IDENTITY_URL env vars
> explicit keys > base derivation > startup error. The old top-level
api_url/identity_url keys are rejected via deny_unknown_fields.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔍 SDK Breaking Change Detection

SDK Version: pam/rotation-daemon (17d0ebc)

⚠️ If breaking changes are detected, a corresponding pull request addressing them must be ready for merge in the affected client repository.

Client Status Details
typescript ✅ No breaking changes detected Compilation passed with new SDK version - View Details
android ✅ No breaking changes detected Compilation passed with new SDK version - View Details

Breaking change detection uses the build of the SDK from this branch, including any incompatibities pre-existing on or merged into this branch. Check the workflow logs to confirm.
Results update as workflows complete.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.63370% with 572 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.56%. Comparing base (365a7eb) to head (c091a3b).

Files with missing lines Patch % Lines
...bitwarden-rotation-daemon/src/executor/rotation.rs 73.10% 181 Missing ⚠️
...itwarden-rotation-daemon/src/integrations/entra.rs 88.47% 96 Missing ⚠️
...ense/bitwarden-rotation-daemon/src/auth/session.rs 86.21% 71 Missing ⚠️
...ense/bitwarden-rotation-daemon/src/executor/mod.rs 84.63% 61 Missing ⚠️
...rden_license/bitwarden-rotation-daemon/src/main.rs 0.00% 61 Missing ⚠️
...n_license/bitwarden-rotation-daemon/src/api/mod.rs 96.13% 29 Missing ⚠️
...-rotation-daemon/src/integrations/custom_script.rs 94.44% 21 Missing ⚠️
...en_license/bitwarden-rotation-daemon/src/config.rs 98.37% 13 Missing ⚠️
...se/bitwarden-rotation-daemon/src/executor/retry.rs 96.47% 10 Missing ⚠️
...icense/bitwarden-rotation-daemon/src/api/models.rs 93.38% 9 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1232      +/-   ##
==========================================
+ Coverage   85.09%   85.56%   +0.46%     
==========================================
  Files         476      497      +21     
  Lines       65899    72012    +6113     
==========================================
+ Hits        56080    61614    +5534     
- Misses       9819    10398     +579     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Hinton added 7 commits July 7, 2026 14:45
…ging

Session renewals log success/retry/terminal transitions; every rotation
step logs an info milestone; all reported failures emit one warn with
attempt_id, failure code, sync state, and safe detail (failure report
sites now route through report_failure_absorb). No secret material is
ever logged.
…redentials

New optional [targets.<uuid>] TOML table supplying per-target
credentials: script (CustomScript), tenant_id/client_id (Entra).
Per-key precedence: config file wins, env vars fill gaps.
client_secret is env-only and rejected in the file, same policy as
the daemon token. Also avoids the POSIX shell limitation that env
var names derived from digit-leading UUIDs cannot be exported.

Includes the executor's claim/heartbeat/startup log lines and the
startup configured_targets count, plus README docs for logging and
the [targets] section.
CONTRACT C2 verified end-to-end against the dev server: the cipher
data blob is flat PascalCase, but the server serializer omits null
fields, so a login cipher that never had a password has no Password
key at all. The write is now insert-or-replace; CipherDataShape is
only returned when the blob root is not a JSON object. Fixes
CipherEncryptFailed on first rotation of a fresh cipher.
…pagation

Microsoft Graph is eventually consistent: lastPasswordChangeDateTime lags an
administrative passwordProfile reset by seconds-to-minutes. The single-shot
verify read ran ~0.5s after rotation and got the previous timestamp, falsely
failing StalePasswordChangeTimestamp and leaving the target updated but the
vault unwritten.

verify now polls the directory for a fresh timestamp up to VERIFY_MAX_WAIT
(60s) at VERIFY_POLL_INTERVAL (5s); timing is injectable so tests stay
single-shot (zero) and a poll test can inject tiny values. The ROPC probe now
runs first (authoritative, no replication lag). HTTP and body-parse errors
still return immediately with their existing classification; only a 200 with a
stale/missing timestamp is retried. All verify error paths keep effect=Applied.
The tracing setup fed `option_env!("RUST_LOG")` to `with_default_directive`,
which had two problems: option_env! captures RUST_LOG at compile time (not
runtime), and with_default_directive parses a single Directive, so a
comma-separated RUST_LOG (e.g. "info,bitwarden_rotation_daemon=debug") baked in
at build time panicked the daemon at startup.

Use a static INFO default directive instead; from_env_lossy already reads
RUST_LOG at runtime and parses full comma-separated filter strings leniently.
Conflicts were confined to Cargo.lock and generated bitwarden-api-api
files. Resolved by taking main's side, then regenerating the API
bindings from the server pam/rotation branch (69d020730, which includes
main's binding source e899a9c9) via artifacts/api.json +
support/generate-api-bindings-ci.sh. Regenerated output matches the
merged tree exactly: main's bindings plus the PAM rotation endpoints.

cargo check, full test suite (2035 passed, 0 failed) and clippy green.
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