Skip to content

Tighten the password pin step after the schema 165 upgrade - #5466

Merged
erikdarlingdata merged 6 commits into
devfrom
fix/5458-legacy-pin-candidates
Oct 7, 2026
Merged

erikdarlingdata merged 6 commits into
devfrom
fix/5458-legacy-pin-candidates

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

After the schema upgrade, the service takes one-time pins for saved passwords in the old format so they keep working. The upgrade now records which old-format values are in the store, with the hash of each value and the connection it is saved for, and the pin step works from that record. A value is pinned only when its text and its connection both still match the record and this machine can open it. The step no longer keeps an opened password in memory, and a Windows host takes over a step a Linux host left open.

SECTION: Fixed

ENTRY: - The one-time password pin step after the schema 165 upgrade is tightened ([#5466]) - a pin step that fails at start is tried again on the next sweep instead of waiting for a restart.

Details

What changes (all under Darling/):

  • New rung V167 legacy-pin-candidates (PerformanceMonitor.Darling.Storage/LegacyPinCandidateTables.cs, PgMigrations.cs, StorageVersion.cs = 167). It creates config.legacy_secret_pin_candidate (row-level security on, no policy, owner-only row and truncate triggers made before the capture) and records the old-format values present while the pin marker is pending. A store whose step is done or skipped records nothing. V165 is not changed. The first number was 166 and dev took 166 for the PagerDuty auto-resolve column, so this rung is 167.
  • DarlingPasswordKeyStore.SnapshotLegacyPinsAsync (PerformanceMonitor.Darling.Service/DarlingPasswordKeyStore.cs): checks every owner-only trigger first (the candidate table's two included); locks the marker (Windows may take over skipped, Linux only turns pending into skipped); matches a live value only when both the stored-text hash and the connection hash equal the record (fixed-time compare); only matches are offered to canOpen. done clears the record and raises config_version in the same transaction as the pins. Nothing matching any more is done with no pins and no bump. Matches but none opened is a rollback and not-here.
  • DarlingSecrets.CanUnprotect(string): answers whether this machine opens a value; the opened bytes are zeroed and no string is built.
  • Provisioning (DarlingManagedRoles.cs and tools/provision-roles.sql): the new table has its own revoke, and is in the to_regclass lists and the trigger allow list. In the script the four V165 tables keep their guard and revoke, and the candidate table is revoked in its own to_regclass branch.
  • Viewer probe: new last arm and parameter hasLegacyPinCandidates, GetBoolean(142), returns 167.

Tests (local PostgreSQL in UTC; Total / Failed):

  • *PasswordKey* 170 / 0 (12 skipped, platform-specific), *LegacyPin* 8 / 0, *Rung* 350 / 0, *Grant* 45 / 0, *TablesLive* 6 / 0, *CaggGroupIndexDrop* 9 / 0, *SchemaVersion* 35 / 0, *StoreUpgrade* 269 / 0 (13 skipped), *Secrets* 58 / 0
  • *Census* 440 / 0, *Ratchet* 15 / 0, *Scrub* 120 / 0, *Guard* 244 / 0, *Hygiene* 90 / 0 (*Scrub* ran before the merge with dev; the rest ran after it)
  • New: LegacyPinCandidateRungTests (5, includes the live AStoreAt165_CapturesItsValues_ADoneStoreNone), DarlingSecretsCanUnprotectTests (2), ThePinStep_NeverBuildsThePlaintext, and live ARowWrittenAfterTheUpgrade_IsNeverPinned_AndIsRefused, ARowMovedToAnotherHost_IsNotPinned, ALinuxHostLeavesTheStepToWindows, AHostThatOpensNone_LeavesTheStepOpen, WhenNothingStillMatches_TheStepIsDone, ADroppedCandidateTrigger_RefusesAndPinsNothing
  • Build: 0 warnings, 0 errors
  • Full suites (CI runs them)

Updated pins: PasswordKeyRungTests, PagerDutyAutoResolveRungTests (no longer the top rung, the arm sits below V167's), PasswordKeyGrantParityTests, PasswordKeyTablesLiveTests, CaggGroupIndexDropLiveTests, the fixtures in DarlingPasswordKeyStoreLiveTests and PasswordKeySeamLiveTests.

Follow-up changes

  • LegacyPinCandidateTables.cs:107: the record is taken only while the marker is pending; a store whose marker is skipped or done records nothing, and a Windows host then finds no match and sets the step done. Tests: LegacyPinCandidateRungTests.AStoreAt165_CapturesItsValues_ADoneStoreNone (also covers skipped) and DarlingPasswordKeyStoreLiveTests.AStoreWhoseStepWasSkippedBeforeTheRecord_RecordsNothing_AndAWindowsHostSetsItDone.
  • tools/provision-roles.sql:638-646: the four-table guard and revoke for the V165 tables are as before, so a store at V165 or V166 still gets them; the candidate table has its own branch. Comments in the script and DarlingManagedRoles.cs:1161 say V167. Tests: PasswordKeyGrantParityTests.TheScriptsGuard_NamesTheFourV165Tables_AndTheCandidateTableHasItsOwnBranch and the updated parity test; the script also ran clean in ComposeStoreRolesLiveTests (9 / 0).

Tests after these changes (local PostgreSQL in UTC; Total / Failed): LegacyPinCandidateRungTests + DarlingPasswordKeyStoreLiveTests + PasswordKeyGrantParityTests 33 / 0; *PasswordKey*, *LegacyPin*, *Rung*, *Grant*, *Provision*, *Secrets* together 649 / 0 (12 skipped); *Census* 440 / 0, *Ratchet* 15 / 0, *Scrub* 120 / 0, *Guard* 244 / 0, *Hygiene* 90 / 0; build 0 warnings.

Part B (start-up retry)

The one-time pin step now runs after the password key step on every path, including the one where the key step itself fails, and a step that could not run is tried again on each sweep.

What changes (all under Darling/PerformanceMonitor.Darling.Service/):

  • DarlingPasswordKeyRuntime.cs: RunPinStepAsync (:515) is called after the key step on the normal path (:170), the key-step failure path (:183) and the unresolved-directory path (:121). Only a cancellation escapes it. A failure, or a key table whose protection is incomplete, sets PinsWaiting (:100) and logs one Warning ("Old-format saved passwords wait for the one-time pin step, which could not run ... The service tries it again on every sweep."), then Debug lines. A step that matches values but opens none here (not-here), and a step a Linux host left open, are not waiting; not-here logs one Warning that the step stays open for the machine that saved them.
  • SweepCheckAsync (:222) retries first while the step is waiting, before it returns for a runtime that holds no key. The earlier one-shot snapshot call in StartAsync is gone.
  • StoreConfigProvider.cs: LegacyPinsWaiting (:71), set by the worker right after the key step and before the first view (DarlingWorker.cs:2940); ReenterLine(count, waiting) (:1830) is the old re-enter line extracted, with a "wait for the one-time pin step, do not enter them again yet" text while waiting. The line is logged again when the pair (count, waiting) changes. The saved-password text in DarlingSecrets is unchanged.
  • Reload: a retry that pins values raises config_version in the same transaction as the pins (part A), so every host's reload beacon picks it up on its next pass; at start the bump precedes the first view.

Tests (local PostgreSQL in UTC; Total / Failed):

  • *PasswordKey* 172 / 0 (12 skipped, platform-specific), *LegacyPin* 8 / 0, *ReenterLine* 4 / 0, *Secrets* 58 / 0, *StoreConfig* 43 / 0
  • *Census*, *Ratchet*, *Scrub*, *Guard*, *Hygiene*, *Rung*, *Grant*, *TablesLive*, *CaggGroupIndexDrop*, *SchemaVersion* together: 1255 / 0
  • New or replaced: ReenterLineTests (4), DarlingPasswordKeyRuntimeTests (the start-up order test, bounded to RunCollectionLoopAsync), and in DarlingPasswordKeyStoreLiveTests the two retry tests plus a not-waiting check in the existing "host that opens none" test
  • Build: 0 warnings, 0 errors
  • Full suites (CI runs them)

The upgrade records which old-format saved passwords are in the store, with the hash of each value and the connection it is saved for. The one-time pin step works from that record: a value is pinned only when its text and its connection both still match, and only when this machine opens it. The opened password is never held. A pin step that fails at start is tried again, and a Windows host takes over a step a Linux host left open.
The pin step runs after the key step on every path, and a step that could not run is tried again on each sweep. The re-enter line says to wait while that is the case.
The candidate record is taken only while the pin marker is pending. A store whose step is set aside or done records nothing, and a Windows host then finds no match and sets the step done.

Provisioning script: keep the schema 165 revoke when the newer table is missing. The four-table guard and revoke are as before, the candidate table has its own branch, and the rung numbers in the comments say V167.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review October 7, 2026 14:52
@erikdarlingdata
erikdarlingdata merged commit 19dc906 into dev Oct 7, 2026
44 of 48 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/5458-legacy-pin-candidates branch October 7, 2026 15:55
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