Private/30.Orchestration.New.ps1
|
<#
SPDX-License-Identifier: Apache-2.0 Copyright (c) 2025 Stefan Ploch #> # ---------------------------------------------------------------------- # # # Core Creation Orchestration # # ---------------------------------------------------------------------- # function New-PrincipalKeytabInternal { <# .SYNOPSIS Internal orchestrator for creating a keytab for a given AD account. .DESCRIPTION Resolves domain context, replicates AD account secret material, selects encryption types, builds principal descriptors, and writes the keytab (with optional deterministic timestamps). Produces an optional JSON summary and can return a pass-thru object. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$SamAccountName, [Parameter(Mandatory)][string]$Domain, [string]$Server, [pscredential]$Credential, [string]$OutputPath, [object[]]$IncludeEtype, [object[]]$ExcludeEtype, [psobject]$Policy, [switch]$RestrictAcl, [switch]$Force, [string]$JsonSummaryPath, [string]$Justification, [switch]$PassThru, [switch]$Summary, [switch]$IsKrbtgt, [switch]$AcknowledgeRisk, [switch]$IncludeOldKvno, [switch]$IncludeOlderKvno, [object[]]$PrincipalDescriptorsOverride, [switch]$VerboseDiagnostics, [switch]$SuppressWarnings, [datetime]$FixedTimestampUtc ) if (-not $OutputPath) { $base = $SamAccountName.TrimEnd('$') $OutputPath = Join-Path (Get-Location) "$base.keytab" } if ((Test-Path -LiteralPath $OutputPath) -and -not $Force) { throw "Output file '$OutputPath' already exists. Use -Force to overwrite." } $domainFQDN = Resolve-DomainContext -Domain $Domain $realm = $DomainFQDN.ToUpperInvariant() $acct = Get-ReplicatedAccount -SamAccountName $SamAccountName -DomainFQDN $domainFQDN -Server $Server -Credential $Credential # Detect krbtgt implicitly if not explicitly passed $isKrbtgtEffective = $IsKrbtgt.IsPresent -or ($SamAccountName.ToUpperInvariant() -eq 'KRBTGT') $material = Get-KerberosKeyMaterialFromAccount -Account $acct -SamAccountName $SamAccountName -Server $Server -IsKrbtgt:$isKrbtgtEffective if ($VerboseDiagnostics) { $material.Diagnostics | ForEach-Object { Write-Verbose $_ } } # Filter Kvno sets if krbtgt and old kvnos when explicitly requested $keySets = @($material.KeySets | Sort-Object -Property Kvno -Descending) if ($isKrbtgtEffective) { # Include current and up to two previous KVNOs when present $wanted = @() [int]$curr = [int]$keySets[0].Kvno $candidates = @($curr, ($curr - 1), ($curr - 2)) foreach ($keySet in $keySets) { if ($candidates -contains [int]$keySet.Kvno) { $wanted += $keySet } } $keySets = $wanted if ($keySets.Count -eq 0) { throw "No key sets selected for krbtgt after KVNO filtering" } } $allEtypes = Select-CombinedEtypes -KeySets $keySets if ($PSBoundParameters.ContainsKey('Policy') -and $Policy) { $selection = Resolve-EtypeSelection -AvailableIds ([int[]]$allEtypes) -Policy $Policy } else { $selection = Resolve-EtypeSelection -AvailableIds ([int[]]$allEtypes) -Include $IncludeEtype -Exclude $ExcludeEtype } if ($selection.UnknownInclude.Count -gt 0) { Write-Warning "Unknown IncludeEtype: $($selection.UnknownInclude -join ', ')" } if ($selection.UnknownExclude.Count -gt 0) { Write-Warning "Unknown ExcludeEtype: $($selection.UnknownExclude -join ', ')" } if ($selection.Missing.Count -gt 0) { Write-Warning "Requested Etypes not present: $($selection.Missing -join ', ')" } if ($selection.Selected.Count -eq 0) { throw "No encryption types selected." } # Principal Descriptors $principalDescriptors = if ($PrincipalDescriptorsOverride) { @($PrincipalDescriptorsOverride) } else { if ($material.PrincipalType -eq 'User') { ,(Get-UserPrincipalDescriptor -SamAccountName $SamAccountName -Realm $realm) } elseif ($material.PrincipalType -eq 'Computer') { # Build from SPNs for the computer; default includes FQDN forms @((Get-ComputerPrincipalDescriptors -ComputerName ($SamAccountName.TrimEnd('$')) -DomainFqdn $domainFQDN)) } elseif ($isKrbtgtEffective) { ,(Get-KrbtgtPrincipalDescriptor -Realm $realm) } else { throw "Unable to build principals for type '$($material.PrincipalType)'." } } $risk = $material.RiskLevel # Security banner and if so, krbtgt confirmation before writing files if (-not $SuppressWarnings) { Write-SecurityWarning -RiskLevel $risk -SamAccountName $SamAccountName | Out-Null } if ($risk -eq 'krbtgt') { # Bypass prompt when explicitly acknowledged or when confirmation is suppressed globally (CI: -Confirm:$false) if ($AcknowledgeRisk.IsPresent -or ($ConfirmPreference -eq 'None')) { # proceed silently } else { if (-not $PSCmdlet.ShouldContinue('You are about to create or handle KRBTGT key material. Proceed?', 'High Impact Operation')) { return } } } # krbtgt includes old KVNOs automatically (current, -1, -2 when available) if ($PSBoundParameters.ContainsKey('FixedTimestampUtc') -and $FixedTimestampUtc) { $finalPath = New-KeytabFile -Path $OutputPath -PrincipalDescriptors $principalDescriptors -KeySets $keySets -EtypeFilter $selection.Selected -RestrictAcl:$RestrictAcl -FixedTimestampUtc $FixedTimestampUtc } else { $finalPath = New-KeytabFile -Path $OutputPath -PrincipalDescriptors $principalDescriptors -KeySets $keySets -EtypeFilter $selection.Selected -RestrictAcl:$RestrictAcl } Write-Host "Keytab written: $finalPath" # Summary if (-not $JsonSummaryPath) { $JsonSummaryPath = [IO.Path]::ChangeExtension($finalPath,'.json') } if ($Summary -or $PassThru) { $etypeNames = @($selection.Selected | ForEach-Object { Get-EtypeNameFromId $_ }) $kvnos = @($keySets | Select-Object -ExpandProperty Kvno | Sort-Object -Unique) $generatedAt = if ($PSBoundParameters.ContainsKey('FixedTimestampUtc') -and $FixedTimestampUtc) { $FixedTimestampUtc.ToUniversalTime().ToString('o') } else { (Get-Date).ToUniversalTime().ToString('o') } $summaryObj = [ordered]@{ SamAccountName = $SamAccountName PrincipalType = $material.PrincipalType DomainFqdn = $domainFQDN Realm = $realm RiskLevel = $risk Kvnos = $kvnos Etypes = $selection.Selected EncryptionTypes = $etypeNames PrincipalCount = $principalDescriptors.Count Principals = @($principalDescriptors | ForEach-Object { $_.Display }) OutputPath = (Resolve-Path -LiteralPath $finalPath).Path GeneratedAtUtc = $generatedAt Justification = $Justification IncludeOldKvno = [bool]$IncludeOldKvno IncludeOlderKvno = [bool]$IncludeOlderKvno HighImpact = ($risk -in @('High','Critical')) } $summaryObj | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $JsonSummaryPath -Encoding UTF8 if ($RestrictAcl) { Set-UserOnlyAcl -Path $JsonSummaryPath } } if ($PassThru) { [pscustomobject]@{ SamAccountName = $SamAccountName PrincipalType = $material.PrincipalType RiskLevel = $risk OutputPath = $finalPath Etypes = $selection.Selected Kvnos = @($keySets.Kvno) PrincipalCount = $principalDescriptors.Count SummaryPath = $JsonSummaryPath } } } #endregion |