Public/Core/Initialize-IDBridgeADServiceAccount.ps1

<#
.SYNOPSIS
Bootstrap the gMSA IDBridge runs as: create it, delegate its AD rights, grant it the
secrets certificate.

.DESCRIPTION
One-command AD-side setup for unattended runs — the Active Directory counterpart to
Initialize-IDBridgeGoogleServiceAccount. Run it once, elevated, on the domain-joined
machine that runs IDBridge (the account creating the gMSA and writing the OU ACL needs
the rights to do so, e.g. a domain admin). Each step is idempotent, so re-running is safe.

Steps:
  1. Creates the group Managed Service Account (default 'gMSA-IDBridge') with this
     computer's account in PrincipalsAllowedToRetrieveManagedPassword — no password is
     ever generated, stored, or typed; Windows retrieves it from AD. Requires a usable
     KDS root key in the domain (a fresh 'Add-KdsRootKey' takes ~10 hours to become
     usable; the function explains rather than creating one silently). An existing gMSA
     is kept and any missing computer principals are added.
  2. Delegates least-privilege rights on the managed root OU (AD.userRootOU by default):
       - create organizational units (CreateChild of class organizationalUnit only —
         no OU delete or modify);
       - create user objects, and full control over descendant user objects (the whole
         lifecycle IDBridge drives: update, rename, disable, move to trash);
       - modify group membership (WriteProperty on the 'member' attribute of descendant
         group objects only — no group create or delete).
     Nothing outside the target OU is touched.
  3. Grants the gMSA private-key read on the Cms secrets certificate (via
     Grant-IDBridgeCertificatePrivateKeyAccess) so scheduled runs can decrypt the vault.
     The certificate comes from -CertThumbprint, or is auto-resolved the same way
     Set-IDBridgeSecret finds it. Sites on the DpapiNG or AzKeyVault provider need no
     certificate — pass -SkipCertificateAccess (a missing certificate is otherwise a
     warning, not a failure).

Full control over user objects includes Delete — Move-ADObject (OU moves, deactivate to
trash) requires Delete on the object being moved, so "manage but never delete" cannot be
expressed in the ACL (IDBridge itself never calls Remove-ADUser). The delegation is only
as safe as the OU's population: NEVER place admin or otherwise privileged accounts under
the managed root OU (protected accounts would resist the ACL via AdminSDHolder, but the
rule stands regardless).

After this, run Register-IDBridgeScheduledTask on the IDBridge host to install the
account there and schedule the nightly run. Requires an initialized session
(Initialize-IDBridge) and the ActiveDirectory RSAT module.

.PARAMETER AccountName
gMSA name without the trailing $ (max 15 characters, a sAMAccountName limit). Defaults
to 'gMSA-IDBridge' — the name used throughout the docs.

.PARAMETER TargetOU
Distinguished name of the OU to delegate on. Defaults to AD.userRootOU from
IDBridgeConfig.psd1 — the managed root OU. Must exist.

.PARAMETER ComputerName
Computer account(s) allowed to retrieve the gMSA's password. Defaults to this computer.
Add the new host here (re-run) when IDBridge moves machines.

.PARAMETER CertThumbprint
Thumbprint of the Cms certificate in Cert:\LocalMachine\My to grant private-key read on.
When omitted, the certificate is auto-resolved (Secrets.Cms.Thumbprint, else the single
'CN=IDBridge Secrets' certificate).

.PARAMETER SkipCertificateAccess
Skip the certificate grant entirely — for sites on the DpapiNG or AzKeyVault secrets
provider, where no Cms certificate exists.

.EXAMPLE
Initialize-IDBridgeADServiceAccount

.EXAMPLE
Initialize-IDBridgeADServiceAccount -TargetOU 'OU=YourDistrict,DC=yourdomain,DC=local'

.EXAMPLE
Initialize-IDBridgeADServiceAccount -SkipCertificateAccess # DpapiNG/AzKeyVault site

.NOTES
   Created by: Sam Cattanach
   Modified: 2026-08-27
#>

function Initialize-IDBridgeADServiceAccount {
    [CmdletBinding()]
    [OutputType('Microsoft.ActiveDirectory.Management.ADServiceAccount')]
    param (
        [Parameter()]
        [ValidateLength(1, 15)]
        [string]$AccountName = 'gMSA-IDBridge',

        [Parameter()]
        [string]$TargetOU,

        [Parameter()]
        [string[]]$ComputerName = @($env:COMPUTERNAME),

        [Parameter()]
        [string]$CertThumbprint,

        [Parameter()]
        [switch]$SkipCertificateAccess
    )

    $IDConfig = Get-IDBridgeConfig

    if (-not $TargetOU) { $TargetOU = $IDConfig.AD.userRootOU }
    if ([string]::IsNullOrWhiteSpace($TargetOU)) {
        Throw "No -TargetOU was provided and AD.userRootOU is not set in IDBridgeConfig.psd1."
    }

    try { Import-Module -Name ActiveDirectory -ErrorAction Stop }
    catch { Throw "The ActiveDirectory PowerShell module (RSAT) is required: $($_)" }

    $domain = Get-ADDomain
    $gmsaIdentity = "$($domain.NetBIOSName)\$($AccountName)$"

    try { $null = Get-ADOrganizationalUnit -Identity $TargetOU -ErrorAction Stop }
    catch { Throw "The delegation target OU '$TargetOU' was not found: $($_)" }

    #region Create the gMSA (or add missing computer principals to an existing one)
    $computers = foreach ($computer in $ComputerName) {
        try { Get-ADComputer -Identity $computer -ErrorAction Stop }
        catch { Throw "Computer account '$computer' was not found in AD: $($_)" }
    }

    $gmsa = Get-ADServiceAccount -Filter "Name -eq '$AccountName'" -Properties PrincipalsAllowedToRetrieveManagedPassword -ErrorAction SilentlyContinue
    if (-not $gmsa) {
        #A gMSA needs a usable KDS root key (EffectiveTime in the past). Checked here so the
        #failure explains itself; skipped quietly where the Kds cmdlets aren't installed —
        #New-ADServiceAccount raises its own key error in that case.
        if (Get-Command -Name Get-KdsRootKey -ErrorAction SilentlyContinue) {
            $usableKeys = @(Get-KdsRootKey -ErrorAction SilentlyContinue | Where-Object { $_.EffectiveTime -le (Get-Date) })
            if ($usableKeys.Count -eq 0) {
                Throw "No usable KDS root key exists in the domain, so a gMSA cannot be created. Create one with 'Add-KdsRootKey' (run by a domain admin; it becomes usable ~10 hours later, once every DC can replicate it) and re-run."
            }
        }

        try {
            New-ADServiceAccount -Name $AccountName -DNSHostName "$($AccountName.ToLower()).$($domain.DNSRoot)" -PrincipalsAllowedToRetrieveManagedPassword $computers -Enabled $true -ErrorAction Stop
            Write-Log -Message "ADBootstrap: Created gMSA '$gmsaIdentity' (password retrievable by: $($ComputerName -join ', '))."
        }
        catch { Throw "Error creating the gMSA '$AccountName': $($_)" }
        $gmsa = Get-ADServiceAccount -Identity $AccountName -Properties PrincipalsAllowedToRetrieveManagedPassword
    } else {
        $currentPrincipals = @($gmsa.PrincipalsAllowedToRetrieveManagedPassword)
        $missing = @($computers | Where-Object { $currentPrincipals -notcontains $_.DistinguishedName })
        if ($missing) {
            try { Set-ADServiceAccount -Identity $AccountName -PrincipalsAllowedToRetrieveManagedPassword ($currentPrincipals + $missing.DistinguishedName) -ErrorAction Stop }
            catch { Throw "Error adding computer principals to the existing gMSA '$AccountName': $($_)" }
            Write-Log -Message "ADBootstrap: gMSA '$gmsaIdentity' already exists; added password retrieval for: $($missing.Name -join ', ')."
        } else {
            Write-Log -Message "ADBootstrap: gMSA '$gmsaIdentity' already exists and covers $($ComputerName -join ', '). <No Action Taken>"
        }
    }
    #endregion Create the gMSA (or add missing computer principals to an existing one)

    #region Delegate rights on the target OU
    #Schema GUIDs are constant across every AD forest.
    $ouClassGuid    = [guid]'bf967aa5-0de6-11d0-a285-00aa003049e2'   #organizationalUnit class
    $userClassGuid  = [guid]'bf967aba-0de6-11d0-a285-00aa003049e2'   #user class
    $groupClassGuid = [guid]'bf967a9c-0de6-11d0-a285-00aa003049e2'   #group class
    $memberAttrGuid = [guid]'bf9679c0-0de6-11d0-a285-00aa003049e2'   #member attribute

    $gmsaSid = [System.Security.Principal.SecurityIdentifier]$gmsa.SID
    $allow = [System.Security.AccessControl.AccessControlType]::Allow
    $inheritAll = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
    $inheritDescendents = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::Descendents

    $delegations = @(
        @{  Description = 'create organizational units (no delete/modify)'
            Rule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($gmsaSid, [System.DirectoryServices.ActiveDirectoryRights]::CreateChild, $allow, $ouClassGuid, $inheritAll) }
        @{  Description = 'create user objects'
            Rule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($gmsaSid, [System.DirectoryServices.ActiveDirectoryRights]::CreateChild, $allow, $userClassGuid, $inheritAll) }
        @{  Description = 'full control over user objects (includes Delete - required for OU moves)'
            Rule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($gmsaSid, [System.DirectoryServices.ActiveDirectoryRights]::GenericAll, $allow, $inheritDescendents, $userClassGuid) }
        @{  Description = 'modify group membership (member attribute only)'
            Rule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($gmsaSid, [System.DirectoryServices.ActiveDirectoryRights]::WriteProperty, $allow, $memberAttrGuid, $inheritDescendents, $groupClassGuid) }
    )

    try {
        $aclPath = "AD:\" + $TargetOU
        $acl = Get-Acl -Path $aclPath
        $addedCount = 0
        foreach ($delegation in $delegations) {
            $rule = $delegation.Rule
            $existing = $acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | Where-Object {
                $_.IdentityReference -eq $gmsaSid -and
                $_.ActiveDirectoryRights -eq $rule.ActiveDirectoryRights -and
                $_.AccessControlType -eq $rule.AccessControlType -and
                $_.ObjectType -eq $rule.ObjectType -and
                $_.InheritedObjectType -eq $rule.InheritedObjectType -and
                $_.InheritanceType -eq $rule.InheritanceType
            }
            if ($existing) {
                Write-Log -Message "ADBootstrap: ACE already present on '$TargetOU': $($delegation.Description). <No Action Taken>"
            } else {
                $acl.AddAccessRule($rule)
                Write-Log -Message "ADBootstrap: Adding ACE on '$TargetOU' for '$gmsaIdentity': $($delegation.Description)."
                $addedCount++
            }
        }
        if ($addedCount -gt 0) { Set-Acl -Path $aclPath -AclObject $acl }
    }
    catch { Throw "Error delegating rights on '$TargetOU' to '$gmsaIdentity': $($_)" }
    #endregion Delegate rights on the target OU

    #region Grant private-key read on the Cms certificate
    if ($SkipCertificateAccess) {
        Write-Log -Message "ADBootstrap: Certificate private-key access skipped (-SkipCertificateAccess)."
    } else {
        $cert = $null
        if ($CertThumbprint) {
            $cert = Get-Item -Path "Cert:\LocalMachine\My\$CertThumbprint" -ErrorAction SilentlyContinue
            if (-not $cert) { Throw "No certificate with thumbprint '$CertThumbprint' found in Cert:\LocalMachine\My." }
        } else {
            try { $cert = Resolve-IDBridgeCmsCertificate }
            catch { Write-Log -Message "ADBootstrap: No Cms certificate to grant ($($_.Exception.Message)) - grant later with Grant-IDBridgeCertificatePrivateKeyAccess, or use -SkipCertificateAccess on a DpapiNG/AzKeyVault site." -Level Warn }
        }
        if ($cert -and $cert.PSParentPath -notlike '*LocalMachine*') {
            Write-Log -Message "ADBootstrap: The resolved Cms certificate (thumbprint $($cert.Thumbprint)) is in the CurrentUser store - a scheduled run needs a LocalMachine certificate. Skipping the grant." -Level Warn
            $cert = $null
        }
        if ($cert) {
            try { Grant-IDBridgeCertificatePrivateKeyAccess -Thumbprint $cert.Thumbprint -Identity $gmsaIdentity }
            catch { Throw "The gMSA and OU delegation are in place, but granting private-key read failed: $($_)" }
        }
    }
    #endregion Grant private-key read on the Cms certificate

    Write-Host "gMSA '$gmsaIdentity' is ready. Delegated on '$TargetOU': create OUs, create/manage users, modify group membership." -ForegroundColor Green
    Write-Host "Next: run Register-IDBridgeScheduledTask on the IDBridge host to install the account and schedule the run."
    return $gmsa
}