Public/New-Keytab.ps1
|
<#
SPDX-License-Identifier: Apache-2.0 Copyright (c) 2025 Stefan Ploch #> function New-Keytab { <# .SYNOPSIS Create a keytab for an AD user, computer, or krbtgt using replication-safe key extraction. .DESCRIPTION Front-door cmdlet that discovers principal type and extracts Kerberos keys via directory replication. Defaults to AES-only encryption types. Deterministic output is available when -FixedTimestampUtc is provided. Supports JSON summaries and PassThru. krbtgt extractions are gated and require -AcknowledgeRisk with a documented justification. .PARAMETER SamAccountName The account's sAMAccountName (user, computer$, or krbtgt). .PARAMETER Type Principal type. Auto infers from name; User or Computer can be forced. krbtgt is detected automatically. .PARAMETER Domain Domain NetBIOS or FQDN. When omitted, attempts discovery. .PARAMETER IncludeEtype Encryption type IDs to include. Default: 18,17 (AES-256, AES-128). RC4 (23) is not included by default and must be explicitly opted-in when legacy compatibility is required. .PARAMETER ExcludeEtype Encryption type IDs to exclude. .PARAMETER IncludeLegacyRC4 Includes the RC4 encryption type (23). .PARAMETER AllowDeadCiphers Allow the use of deprecated or weak encryption types (other than 17,18,19,20,23). No support guaranteed. .PARAMETER AESOnly Restrict to AES encryption types only (18,17). .PARAMETER ModernCrypto Include modern AES-SHA2 encryption types (19,20) in addition to defaults. Requires newer Kerberos implementations. .PARAMETER OutputPath Path to write the keytab file. .PARAMETER Server Domain Controller to target for replication (optional). .PARAMETER Justification Free-text justification string for auditing high-risk operations. .PARAMETER Credential Alternate credentials to access AD/replication. .PARAMETER EnvFile Optional .env file to load credentials from. .PARAMETER RestrictAcl Apply a user-only ACL to outputs. .PARAMETER Force Overwrite existing OutputPath. .PARAMETER PassThru Return a small object summary in addition to writing files. .PARAMETER Summary Write a JSON summary file. .PARAMETER SummaryPath Optional path to write a JSON summary. Defaults next to OutputPath when summaries are requested. .PARAMETER VerboseDiagnostics Emit additional diagnostics during extraction. .PARAMETER SuppressWarnings Suppress risk warnings. .PARAMETER FixedTimestampUtc Use a fixed timestamp for deterministic output. Determinism is opt-in and not auto-populated. .PARAMETER IncludeShortHost For computer accounts, include HOST/shortname SPN. .PARAMETER AdditionalSpn Additional SPNs (service/host) to include for computer accounts. .INPUTS System.String (SamAccountName) via property name. .OUTPUTS System.String (OutputPath) or summary object when -PassThru. .EXAMPLE New-Keytab -SamAccountName web01$ -Type Computer -OutputPath .\web01.keytab -IncludeShortHost -Summary Create a computer keytab including short HOST/ SPNs and write a summary JSON. .EXAMPLE New-Keytab -SamAccountName user1 -IncludeEtype 18,17 -ExcludeEtype 23 -OutputPath .\user1.keytab -FixedTimestampUtc (Get-Date '2020-01-01Z') Create a deterministic user keytab with AES types only. .EXAMPLE New-Keytab -SamAccountName web01$ -Type Computer -ModernCrypto -OutputPath .\web01-modern.keytab -Summary Create a computer keytab with modern AES-SHA2 encryption types and write a summary JSON. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')] param( # Common [Parameter(Mandatory, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [string]$SamAccountName, [ValidateSet('Auto','User','Computer')] [string]$Type = 'Auto', [Parameter(ValueFromPipelineByPropertyName)][string]$Domain, [Parameter(ValueFromPipelineByPropertyName)][object[]]$IncludeEtype = @(18,17), [Parameter(ValueFromPipelineByPropertyName)][object[]]$ExcludeEtype, [Parameter(ValueFromPipelineByPropertyName)][ValidateNotNullOrEmpty()][string]$OutputPath, [Parameter(ValueFromPipelineByPropertyName)][Alias('JsonSummaryPath')][string]$SummaryPath, [Parameter(ValueFromPipelineByPropertyName)][string]$Server, [Parameter(ValueFromPipelineByPropertyName)][string]$Justification, [Parameter(ValueFromPipelineByPropertyName)][pscredential]$Credential, [Parameter(ValueFromPipelineByPropertyName)][string]$EnvFile, [switch]$RestrictAcl, [switch]$Force, [switch]$PassThru, [switch]$Summary, [switch]$AcknowledgeRisk, [switch]$VerboseDiagnostics, [switch]$SuppressWarnings, [datetime]$FixedTimestampUtc, # Computer-only extras [switch]$IncludeShortHost, [string[]]$AdditionalSpn, # Quick settings [switch]$IncludeLegacyRC4, [switch]$AESOnly, [switch]$AllowDeadCiphers, [switch]$ModernCrypto # Include AES-SHA2 types (19,20) ) begin { # Firstly, check if the parameters specified make sense if (($AESOnly.IsPresent) -and ($IncludeLegacyRC4.IsPresent -or $AllowDeadCiphers.IsPresent)) { throw "-AESOnly cannot be defined with -IncludeLegacyRC4 or -AllowDeadCiphers." } # Handle modern crypto convenience parameter if ($ModernCrypto.IsPresent -and -not $PSBoundParameters.ContainsKey('IncludeEtype')) { $IncludeEtype = @(17,18,19,20) # All AES types } # Then, compose policy intent for replication path; orchestration can use it to resolve final etypes (BigBrother) try { $script:__nk_policy = Get-PolicyIntent -IncludeEtype $IncludeEtype -ExcludeEtype $ExcludeEtype -AESOnly:$AESOnly ` -IncludeLegacyRC4:$IncludeLegacyRC4 -AllowDeadCiphers:$AllowDeadCiphers -PathKind 'Replication' } catch { Write-Verbose ("Policy composition failed: {0}" -f $_.Exception.Message) } # Surface unknown include/exclude early for better UX (availability warnings are handled later) if ($script:__nk_policy) { if ($script:__nk_policy.UnknownInclude -and $script:__nk_policy.UnknownInclude.Count -gt 0) { Write-Warning ("Unknown IncludeEtype: {0}" -f ($script:__nk_policy.UnknownInclude -join ', ')) } if ($script:__nk_policy.UnknownExclude -and $script:__nk_policy.UnknownExclude.Count -gt 0) { Write-Warning ("Unknown ExcludeEtype: {0}" -f ($script:__nk_policy.UnknownExclude -join ', ')) } } if ($Server) { $dcCmd = Get-Command -Name Get-ADDomainController -ErrorAction SilentlyContinue if ($dcCmd) { try { $dc = Get-ADDomainController -Server $Server -ErrorAction Stop if ($dc.IsReadOnly) { Write-Warning ("Target DC '{0}' is read-only (RODC); replication-based extraction may fail." -f $Server) } } catch { if (-not $SuppressWarnings.IsPresent) { Write-Warning ("Unable to query domain controller '{0}': {1}" -f $Server, $_.Exception.Message) } } } else { Write-Verbose ("-Server specified ('{0}'); ensure it is a writable DC." -f $Server) } } if ($OutputPath) { $out = Resolve-PathUniversal -Path $OutputPath -Purpose Output } else { $out = Resolve-OutputPath -Directory (Get-Location).Path -BaseName ($SamAccountName.TrimEnd('$')) -Extension '.keytab' -CreateDirectory } $type = $Type if ($type -eq 'Auto') { # Auto-detect based on trailing '$' (computer accounts) if ($SamAccountName -match '\$$') { $type = 'Computer' } else { $type = 'User' } } } process { switch ($type) { 'Computer' { $domainFqdn = Resolve-DomainContext -Domain $Domain $realm = $domainFqdn.ToUpperInvariant() $compName = $SamAccountName.TrimEnd('$') $desc = Get-ComputerPrincipalDescriptors -ComputerName $compName -DomainFqdn $domainFqdn -IncludeShortHost:$IncludeShortHost if ($AdditionalSpn) { foreach ($p in $AdditionalSpn) { if ($p -notmatch '/') { continue } $parts = $p.Split('/',2) $svc = $parts[0]; $ihost = $parts[1] $desc += ,(New-PrincipalDescriptor -Components @($svc.ToLowerInvariant(),$ihost.ToLowerInvariant()) -Realm $realm -NameType $script:NameTypes.KRB_NT_SRV_HST -Tags @('Explicit')) } } if ($desc.Count -eq 0) { throw "No SPN principals resolved for computer '$compName'." } if ($PSCmdlet.ShouldProcess($compName,"Create computer keytab")) { $extra = @{} if ($PSBoundParameters.ContainsKey('FixedTimestampUtc') -and $FixedTimestampUtc) { $extra.FixedTimestampUtc = $FixedTimestampUtc } $inc = if ($script:__nk_policy) { $script:__nk_policy.IncludeIds } else { $IncludeEtype } $exc = if ($script:__nk_policy) { $script:__nk_policy.ExcludeIds } else { $ExcludeEtype } $result = New-PrincipalKeytabInternal -SamAccountName ("{0}$" -f $compName) -Domain $domainFqdn -Server $Server -Credential $Credential ` -OutputPath $out -IncludeEtype $inc -ExcludeEtype $exc -Policy $script:__nk_policy -RestrictAcl:$RestrictAcl -Force:$Force ` -JsonSummaryPath $SummaryPath -PassThru:$PassThru -Summary:$Summary -Justification $Justification -AcknowledgeRisk:$AcknowledgeRisk ` -PrincipalDescriptorsOverride $desc -VerboseDiagnostics:$VerboseDiagnostics @extra return $result } } 'User' { $userName = $SamAccountName if ($PSCmdlet.ShouldProcess($userName,"Create user keytab")) { $extra = @{} if ($PSBoundParameters.ContainsKey('FixedTimestampUtc') -and $FixedTimestampUtc) { $extra.FixedTimestampUtc = $FixedTimestampUtc } $inc = if ($script:__nk_policy) { $script:__nk_policy.IncludeIds } else { $IncludeEtype } $exc = if ($script:__nk_policy) { $script:__nk_policy.ExcludeIds } else { $ExcludeEtype } $result = New-PrincipalKeytabInternal -SamAccountName $userName -Domain $Domain -Server $Server -Credential $Credential ` -OutputPath $out -IncludeEtype $inc -ExcludeEtype $exc -Policy $script:__nk_policy ` -RestrictAcl:$RestrictAcl -Force:$Force -JsonSummaryPath $SummaryPath -PassThru:$PassThru -AcknowledgeRisk:$AcknowledgeRisk ` -Summary:$Summary -Justification $Justification -VerboseDiagnostics:$VerboseDiagnostics @extra return $result } } } } } |