Sense, plan, test, execute, prove.
One lifecycle for every provider. Each stage is a separately callable function registered in providers.json; Invoke-SpnLifecycle is thin glue for a human or CLI caller. Audit-only providers stop at Test and hand off instead of executing.
Discover what is actually running.
The provider reads the service the way an administrator would if they had time: SQL Server from Win32_Service and the instance port in the registry; IIS from site bindings and app-pool identities, grouped by account rather than by site; SSRS from rsreportserver.config. No user input for standard configurations.
Sense produces candidates: the service, the host variants a client might ask for, the account that should hold the SPN.
Build the exact strings, and only those.
The plan is the full expected SPN set for the discovered configuration, on the account that should carry it. For SQL Server that is FQDN and NetBIOS, with and without port. For SSAS it never contains a port; the colon suffix is the instance name. For HTTP it never contains 80 or 443.
The plan is data. You can read it, diff it, and reject it before anything touches AD.
Check the plan against AD before anyone writes.
Test-SpnPlan runs the preflight: does the caller have write rights to the target account; does any proposed SPN already exist elsewhere in the forest; is the owner the account the service really runs as. A duplicate found here is a finding, not a surprise at 2 a.m.
Write to AD only when you asked it to. Otherwise, write the runbook.
Execution is ShouldProcess-gated: -WhatIf shows the change, -Confirm asks. Every action is logged with provenance: module version, commit, run ID, timestamp.
Where the caller has no AD write rights, or the provider is audit-only, the same plan becomes a hand-off: a setspn bundle for the AD team, or an operator runbook for the far-side host. Keytab services always take the runbook path. The safety model →
Ask the service what mechanism it saw.
The proof harness records the negotiated mechanism from an independent client before the change and after it, and requires a second oracle to agree. A run passes only if the baseline was NTLM and the result is Kerberos. A baseline that was already Kerberos yields Inconclusive.
This stage is how the product knows it worked, rather than assuming that a successful LDAP write means Kerberos is fixed.
| before | after | oracle 2 | verdict | |
|---|---|---|---|---|
| sql01 / SqlAuthScheme | NTLM | KERBEROS | agrees | PASS |
| web01 / HttpSpnego | KERBEROS | KERBEROS | agrees | INCONCLUSIVE |