Skip to content

CI runners - Set an account lockout threshold at bootstrap so a SQL login can be locked out - #10718

Merged
potatoqualitee merged 1 commit into
developmentfrom
fix-ci-runner-lockout-threshold
Sep 19, 2026
Merged

potatoqualitee merged 1 commit into
developmentfrom
fix-ci-runner-lockout-threshold

Conversation

@andreasjordan

Copy link
Copy Markdown
Collaborator

Fixes #10529, which blocks the unlocks a login that was locked out test in #10537.

What is missing

The self-hosted runner image sets no Windows account lockout threshold. Without one, LOGINPROPERTY(login, 'IsLocked') stays 0 on a runner no matter how many logons fail, so no test can cover unlocking a login, and the Set-DbaLogin unlock test in #10537 is knowingly red on CI.

The change

bootstrap-runner.ps1 runs

$null = net accounts /lockoutthreshold:5 /lockoutduration:10 /lockoutwindow:10

next to the LocalAccountTokenFilterPolicy knob it already sets, with a warning line in the run-command output if net accounts fails. The threshold is five because the test fails exactly five logons; duration and window only decide when Windows would release the lock by itself, the test unlocks explicitly.

Why the bootstrap and not the image

The issue proposed the image build. The bootstrap is the better place:

  • It takes effect on merge. runner-scale-up.yml and runner-reconcile.yml fetch bootstrap-runner.ps1 from raw.githubusercontent.com at the default branch for every fresh VM. An image change waits for the next build-modern-image.ps1 run and a VMSS repoint.
  • No sysprep question. The issue asked whether the local security policy survives sysprep /generalize. At boot time there is nothing to survive.
  • It is where the other local policy already lives. The firewall profile and the token filter policy are set there for the same reason: they are per-VM state the tests depend on.

Scope

The setting applies to the local account database of the runner VM only. The runners are not domain joined, so no domain policy overwrites it later. The practical effect on tests is that a SQL login with CHECK_POLICY = ON locks after five bad passwords, which is what the unlock test needs; the VM is single-use and deleted after its job.

Tests

.github/runners/tests/bootstrap-runner.Tests.ps1 gains a contract test that the script sets a lockout threshold of five or lower before config.cmd registers the runner. Against development's script it fails on the first assertion, the missing net accounts line. Both editions pass the file 11/11 with the change.

Verification

What I could and could not verify:

  • The bootstrap script parses and the contract test file passes on pwsh 7.6 and PowerShell 5.1.
  • The lab hosts carry the same threshold of five, and the whole unlock chain was verified there on 2026-08-08 (a CHECK_POLICY login locks after five bad logons, Set-DbaLogin.Tests.ps1 passes 45/45). The lab hosts get the value through Group Policy, so that proves SQL Server follows the local value, not the net accounts line itself.
  • Not verified on a runner. No workflow on a PR bootstraps a VM from the PR branch, so the first real proof is the SINGLE lane of the first development push after the merge, or a rerun of Set-DbaLogin - Return the login as it is after unlocking it with a new password #10537's CI once the reconcile has cycled the fleet.

created by Claude and reviewed by Andreas Jordan

🤖 Generated with Claude Code

…ogin can be locked out

The base image sets no Windows account lockout threshold, so LOGINPROPERTY(login, 'IsLocked') stays 0 on a runner no matter how many logons fail, and the Set-DbaLogin unlock test cannot pass on CI (#10529). bootstrap-runner.ps1 now runs net accounts with a threshold of five, a duration and an observation window of ten minutes, next to the other local policy knob it already sets. SQL Server reads the local policy of the machine running the instance and the runners are not domain joined, so the local value is the one that counts. The bootstrap script is fetched from the default branch for every fresh VM, so this takes effect on merge without an image rebuild. A contract test pins the threshold at five or lower and its place before the runner takes its job.

(do Set-DbaLogin)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@potatoqualitee potatoqualitee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the complete bootstrap/test change, runner workflows, linked issues, ordering, policy semantics, and exact-head CI. The lockout values are coherent and applied before runner registration in the privileged bootstrap context; I found no concrete material defect. The added test is a source-contract check rather than live policy proof, which the PR description already discloses.

@potatoqualitee

Copy link
Copy Markdown
Member

thank you 🙏🏼

@potatoqualitee
potatoqualitee merged commit d1f5a45 into development Sep 19, 2026
22 checks passed
@potatoqualitee
potatoqualitee deleted the fix-ci-runner-lockout-threshold branch September 19, 2026 06:15
@andreasjordan

andreasjordan commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

One more step is needed before this takes effect on the runners: the fleet controller bundles its own copy of bootstrap-runner.ps1 when deploy-controller.ps1 packages it, so the merged change reaches new VMs only after the controller is redeployed. Details and the CI evidence are in #10537 (comment).

created by Claude and reviewed by Andreas Jordan

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.

Self-hosted runner image has no account lockout threshold, so login lockout cannot be tested

2 participants