Skip to content

Clear the escrowed managed local account password on MDM re-enrollment - #50596

Merged
getvictor merged 5 commits into
mainfrom
victor-43488-clear-mla-password-on-reenrollment
Aug 6, 2026
Merged

Clear the escrowed managed local account password on MDM re-enrollment#50596
getvictor merged 5 commits into
mainfrom
victor-43488-clear-mla-password-on-reenrollment

Conversation

@getvictor

@getvictor getvictor commented Aug 5, 2026

Copy link
Copy Markdown
Member

Fixed unreleased bug where admin password was still shown on a wiped host after re-enrollment.

Add a deleted column and soft-delete the row on re-enrollment, mirroring host_recovery_key_passwords.

Related issue: Resolves #43488

Checklist for submitter

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

Database migrations

  • Checked schema for all modified table for columns that will auto-update timestamps during migration.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for safely retiring managed local account passwords during device re-enrollment.
    • Retired credentials are excluded from retrieval, status updates, command operations, and automatic rotation.
    • Successfully escrowing a password can restore a previously retired record.
  • Bug Fixes

    • Re-enrollment cleanup now clears stale password rotation state.
    • Cleanup failures stop reset or re-enrollment processing to prevent incomplete credential changes.

host_managed_local_account_passwords is keyed by host_uuid and nothing removed
it when a device re-enrolled, so a re-enrolled host inherited the previous
install's escrowed password. Fleet reported status=verified and
password_available=true for an account that no longer existed, handing an admin
a break-glass password that silently does not work. Observed on a re-imaged
Windows host that re-enrolled with the same hardware UUID.

Add a `deleted` column and soft-delete the row on re-enrollment, mirroring
host_recovery_key_passwords. Soft rather than hard delete so no lifecycle event
destroys a password; every read filters deleted = 0 and the next successful
escrow (or a device-reported failure) clears the flag and revives the row.

Hooked into both platforms, since the table is shared: Windows via
MDMWindowsDeleteEnrolledDeviceOnReenrollment, macOS via MDMResetEnrollment's
darwin case, beside the equivalent recovery lock call and after the
scepRenewalInProgress short-circuit so a SCEP renewal does not retire anything.

Deliberately not hooked into MDM turn-off, unlike recovery lock: Apple wipes the
device-side recovery lock when the profile is removed, whereas a local admin
account survives profile removal with its password still valid. Only
re-enrollment invalidates the escrow.
Copilot AI lite review requested due to automatic review settings August 5, 2026 16:54
@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dadf0016-f7c6-4445-86a0-aabd5f0e1b31

📥 Commits

Reviewing files that changed from the base of the PR and between 6787368 and 74a7ad1.

📒 Files selected for processing (2)
  • server/datastore/mysql/managed_local_account.go
  • server/datastore/mysql/managed_local_account_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/datastore/mysql/managed_local_account_test.go
  • server/datastore/mysql/managed_local_account.go

Walkthrough

This PR adds a deleted column and migration for managed local account passwords. Datastore operations revive rows on save or escrow and exclude deleted rows from reads and rotation workflows. Apple MDM reset and Windows re-enrollment cleanup soft-delete managed local account passwords. Tests cover filtering, retained encrypted data, rotation-state cleanup, and escrow revival.

Possibly related PRs

  • fleetdm/fleet#49924: Introduced related managed local account escrow and Windows cleanup flows.
  • fleetdm/fleet#50088: Added the Windows managed local account escrow flow used by this cleanup.
  • fleetdm/fleet#50675: Modifies separate frontend and error-message paths for the Windows managed local account feature.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% 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 describes the main change: clearing escrowed managed local account passwords during MDM re-enrollment.
Description check ✅ Passed The description identifies the bug, solution, linked issue, automated tests, manual QA, and relevant migration validation.
Linked Issues check ✅ Passed The changes address the linked issue's enrollment edge case by hiding stale managed local account passwords after Windows and macOS re-enrollment [#43488].
Out of Scope Changes check ✅ Passed The code, tests, migration, and schema updates are directly related to soft-deleting managed local account passwords on MDM re-enrollment.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch victor-43488-clear-mla-password-on-reenrollment

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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@server/datastore/mysql/managed_local_account.go`:
- Around line 75-76: Update the upsert handling around the managed local account
row so an escrow failure that clears deleted status also replaces or invalidates
encrypted_password before GetHostManagedLocalAccountPassword can return it,
while preserving client_error. Add a regression test covering re-enrollment,
escrow failure, and subsequent password retrieval to verify the
pre-re-enrollment password is not exposed.
- Line 326: Update the fallback checkStmt in InitiateManagedLocalAccountRotation
to include the same deleted = 0 filter as the eligibility update query, so
soft-deleted accounts are treated as absent rather than returning stale
eligibility state. Add a test covering rotation initiation after soft deletion
and assert it returns the not-found error.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d7be8300-3cb8-452f-8e8e-69e5952238b5

📥 Commits

Reviewing files that changed from the base of the PR and between 1563aeb and 4b129be.

📒 Files selected for processing (6)
  • server/datastore/mysql/apple_mdm.go
  • server/datastore/mysql/managed_local_account.go
  • server/datastore/mysql/managed_local_account_test.go
  • server/datastore/mysql/microsoft_mdm.go
  • server/datastore/mysql/migrations/tables/20260805161502_AddManagedLocalAccountDeletedColumn.go
  • server/datastore/mysql/schema.sql

Comment thread server/datastore/mysql/managed_local_account.go
Comment thread server/datastore/mysql/managed_local_account.go
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Rotation checks ignore deleted ✓ Resolved 🐞 Bug ≡ Correctness
Description
InitiateManagedLocalAccountRotation now excludes soft-deleted rows in its UPDATE (deleted=0), but
its follow-up eligibility SELECT still loads rows by host_uuid without filtering deleted=0. This can
misclassify a soft-deleted row as "not eligible" instead of "not found", producing incorrect
behavior/error handling for rotation attempts after re-enrollment.
Code

server/datastore/mysql/managed_local_account.go[R326-328]

+		  AND deleted = 0
AND encrypted_password IS NOT NULL
AND account_uuid IS NOT NULL
Evidence
The PR introduces soft-delete (deleted=1) and updates most reads to filter deleted=0, but the
rotation eligibility fallback SELECT still queries by host_uuid without deleted=0. This makes
soft-deleted rows still visible to InitiateManagedLocalAccountRotation’s diagnostic logic, even
though they are intentionally hidden elsewhere.

server/datastore/mysql/managed_local_account.go[84-102]
server/datastore/mysql/managed_local_account.go[104-147]
server/datastore/mysql/managed_local_account.go[319-372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`InitiateManagedLocalAccountRotation` guards its `UPDATE` with `deleted = 0`, but when the update affects 0 rows it runs an eligibility `SELECT` (`checkStmt`) that does **not** include `deleted = 0`. After this PR, soft-deleted rows (deleted=1) are intentionally retained but hidden from reads; the eligibility check should follow the same rule so soft-deleted rows behave like they don't exist.
### Issue Context
- Soft-delete sets `deleted = 1` on re-enrollment.
- Most read/write paths were updated to require `deleted = 0`.
- The rotation eligibility diagnostic query is now the main inconsistent path.
### Fix Focus Areas
- server/datastore/mysql/managed_local_account.go[350-358]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread server/datastore/mysql/managed_local_account.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR prevents stale escrowed managed local account passwords from being shown after MDM re-enrollment by introducing a soft-delete mechanism on host_managed_local_account_passwords, then retiring (soft-deleting) the row during re-enrollment flows for both Windows and macOS.

Changes:

  • Add deleted column to host_managed_local_account_passwords and filter all reads/updates on deleted = 0.
  • Soft-delete managed local account password rows on re-enrollment/reset-enrollment for Windows and macOS.
  • Add unit test coverage for the soft-delete behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
server/datastore/mysql/schema.sql Adds deleted column to host_managed_local_account_passwords and updates schema snapshot/migration status.
server/datastore/mysql/migrations/tables/20260805161502_AddManagedLocalAccountDeletedColumn.go Migration adding deleted column to the managed local account passwords table.
server/datastore/mysql/microsoft_mdm.go Soft-deletes managed local account password on Windows device re-enrollment cleanup.
server/datastore/mysql/managed_local_account.go Implements DB soft-delete helper and applies deleted = 0 filtering across managed local account queries.
server/datastore/mysql/managed_local_account_test.go Adds test for soft-delete + revive-on-escrow/error behavior.
server/datastore/mysql/apple_mdm.go Soft-deletes managed local account password during macOS MDMResetEnrollment cleanup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/datastore/mysql/managed_local_account.go
Comment thread server/datastore/mysql/managed_local_account_test.go
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.13208% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.28%. Comparing base (49bf85c) to head (6787368).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...60805161502_AddManagedLocalAccountDeletedColumn.go 66.66% 3 Missing and 1 partial ⚠️
server/datastore/mysql/apple_mdm.go 0.00% 1 Missing and 1 partial ⚠️
server/datastore/mysql/managed_local_account.go 94.59% 1 Missing and 1 partial ⚠️
server/datastore/mysql/microsoft_mdm.go 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main   #50596       +/-   ##
===========================================
- Coverage   68.35%   56.28%   -12.08%     
===========================================
  Files        3955     3691      -264     
  Lines      253450   228639    -24811     
  Branches    13330    13284       -46     
===========================================
- Hits       173255   128693    -44562     
- Misses      64703    87643    +22940     
+ Partials    15492    12303     -3189     
Flag Coverage Δ
backend 55.11% <81.13%> (-14.50%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

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

Copilot AI review requested due to automatic review settings August 5, 2026 18:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

server/datastore/mysql/microsoft_mdm.go:597

  • This adds the re-enrollment hook to retire managed local account passwords, but there’s no assertion in the existing re-enrollment cleanup test that the row is actually soft-deleted (deleted=1). Right now microsoft_mdm_test.go only checks that encrypted_password remains present; that would still pass even if the soft-delete call were removed later. Consider extending the existing test to assert deleted is set to 1 (and optionally that datastore reads treat it as not found).
				// Retire the escrowed managed local account password. The row is keyed by host_uuid and survives host
				// deletion on purpose, but it must not survive the enrollment that produced it: a re-enrolling device may
				// have been re-imaged, in which case the password no longer opens any account and Fleet would keep
				// reporting it as verified. Soft delete, so the secret is never destroyed by a lifecycle event; the next
				// successful escrow clears the flag and revives the row.

server/datastore/mysql/apple_mdm.go:4680

  • MDMResetEnrollment now also retires the managed local account password for darwin, but the existing MDM reset enrollment tests don’t appear to cover this new behavior. Consider extending an existing test (e.g., testMDMAppleResetEnrollment) to create a managed local account password row and assert it becomes soft-deleted on reset (and is filtered from reads).
			// Same reasoning for the managed local account password, which shares the escrow model: reaching
			// here means an unenroll/re-enroll or wipe/restore, and an ADE re-enrollment normally follows an
			// erase, so the account the password opens is usually gone. Note the deliberate difference from
			// recovery lock: we do NOT soft-delete on MDM turn-off, because a local admin account survives
			// profile removal and its password stays valid, whereas Apple wipes the device-side recovery lock.

Copilot AI review requested due to automatic review settings August 5, 2026 19:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

server/datastore/mysql/managed_local_account.go:230

  • lookupManagedLocalAccountHost is used to resolve a host by pending_command_uuid, but the table schema has no index on pending_command_uuid. With the new AND deleted = 0 predicate, this lookup can devolve into a full table scan during MDM command result processing on large fleets. Consider adding an index (ideally composite) to keep these lookups efficient.
// lookupManagedLocalAccountHost shares the join-to-hosts lookup used by both the
// AccountConfiguration ack (matches command_uuid) and the SetAutoAdminPassword ack
// (matches pending_command_uuid). The column name is interpolated, not parameterized,
// because callers pass a fixed identifier — never untrusted input.
func (ds *Datastore) lookupManagedLocalAccountHost(ctx context.Context, column, commandUUID string) (*fleet.Host, error) {
	stmt := fmt.Sprintf(`SELECT host_uuid FROM host_managed_local_account_passwords WHERE %s = ? AND deleted = 0`, column)

server/datastore/mysql/apple_mdm.go:4676

  • This comment is grammatically awkward and missing ending punctuation. Consider rephrasing to match the adjacent recovery lock comment and end with a period.
			// Same reasoning for the managed local account password, as recovery lock password, which shares the escrow model

@getvictor
getvictor marked this pull request as ready for review August 5, 2026 19:22
@getvictor
getvictor requested a review from a team as a code owner August 5, 2026 19:22
@getvictor
getvictor requested a review from sharon-fdm August 5, 2026 19:23
…a-password-on-reenrollment

# Conflicts:
#	server/datastore/mysql/schema.sql
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

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

(moved to inline comment)

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

(moved to inline comments)

Comment thread server/datastore/mysql/managed_local_account.go
func (ds *Datastore) GetHostManagedLocalAccountPassword(ctx context.Context, hostUUID string) (*fleet.HostManagedLocalAccountPassword, error) {
const stmt = `SELECT encrypted_password, updated_at FROM host_managed_local_account_passwords WHERE host_uuid = ?`
const stmt = `SELECT encrypted_password, updated_at FROM host_managed_local_account_passwords WHERE host_uuid = ? AND deleted = 0`

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.

The recovery lock has SoftDeleteRecoveryLockPasswordsForUnenrolledHosts (a cron that catches hosts where MDM was disabled without a proper CheckOut/Authenticate). There's no equivalent for MLA. If a host silently unenrolls (e.g., user manually removes MDM profile, only detected via osquery refetch), the MLA password remains deleted = 0 and visible. This may be intentionally deferred, but worth noting for completeness since the PR mirrors the recovery lock pattern in all other respects.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@sharon-fdm @coderabbitai Recovery lock and admin password have different lifetimes. If MDM is disabled or profile is removed, the admin password is still valid, unlike the recovery lock. So, we should not soft delete the admin password.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@getvictor, agreed. The managed local account password remains valid after MDM removal or disablement. It must remain available in that case. The recovery lock cleanup pattern does not apply here.

I am withdrawing the comment.

🐇


✏️ Learnings added
Learnt from: getvictor
Repo: fleetdm/fleet PR: 50596
File: server/datastore/mysql/managed_local_account.go:105-105
Timestamp: 2026-08-06T19:16:43.189Z
Learning: For Fleet managed local account passwords, MDM disablement or removal of the MDM profile does not invalidate the administrator password. Do not soft-delete `host_managed_local_account_passwords` merely because MDM is unenrolled or disabled. This differs from recovery lock passwords, which have a different lifetime and cleanup behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.


func Down_20260805161502(tx *sql.Tx) error {
return nil
}

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.

Missing corresponding _test.go file. Fleet convention requires a test for every migration (pattern: applyUpToPrev(t) -> set up data -> applyNext(t, db) -> verify). Every other migration in the directory follows this pattern.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@sharon-fdm No, we don't always add a _test.go for simple migrations like this one that just add a column.

@getvictor
getvictor requested a review from sharon-fdm August 6, 2026 19:40
@getvictor

Copy link
Copy Markdown
Member Author

@sharon-fdm I fixed or replied to your comments.

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

LGTM.

@getvictor
getvictor merged commit 3d186bd into main Aug 6, 2026
6 checks passed
@getvictor
getvictor deleted the victor-43488-clear-mla-password-on-reenrollment branch August 6, 2026 20:03
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.

Windows: Create a local admin account

3 participants