Public/Reset-AccountPasswordWithKeytab.ps1

<#
SPDX-License-Identifier: Apache-2.0
Copyright (c) 2025 Stefan Ploch
#>



function Reset-AccountPasswordWithKeytab {
   <#
    .SYNOPSIS
    Reset an AD account password and generate a corresponding keytab in one atomic operation.
 
    .DESCRIPTION
    Securely rotates an account's password to a strong random value, updates Active Directory,
    derives the corresponding Kerberos keys using the new password, and produces a keytab file.
    This ensures the keytab matches the account's actual password state without manual coordination.
    Only supports user accounts.
 
    Requires explicit risk acknowledgment due to the high-impact nature of password changes.
 
        .PARAMETER SamAccountName
        The account's sAMAccountName to reset password for.
 
        .PARAMETER Realm
        Kerberos realm name. If omitted, derives from the domain.
 
        .PARAMETER NewPassword
        Specific password to set. If omitted, generates a cryptographically strong random password.
 
        .PARAMETER Kvno
        Key version number to use in the keytab. If omitted, predicts the post-reset KVNO.
 
        .PARAMETER Compatibility
        Salt generation policy: MIT, Heimdal, or Windows (default).
 
        .PARAMETER IncludeEtype
        Encryption types to include. Default: AES256, AES128.
 
        .PARAMETER ExcludeEtype
        Encryption types to exclude.
 
        .PARAMETER OutputPath
        Path for the generated keytab file.
 
        .PARAMETER Domain
        Domain to target for AD operations.
 
        .PARAMETER Server
        Specific domain controller to use.
 
        .PARAMETER Credential
        Alternate credentials for AD operations.
 
        .PARAMETER AcknowledgeRisk
        Required acknowledgment that this operation changes the account password.
 
        .PARAMETER Justification
        Required justification for audit logging.
 
        .PARAMETER WhatIfOnly
        Show operation plan without executing changes.
 
        .PARAMETER UpdateSupportedEtypes
        Update the account's msDS-SupportedEncryptionTypes attribute.
 
        .PARAMETER AESOnly
        Restrict to AES encryption types only.
 
        .PARAMETER IncludeLegacyRC4
        Include RC4 encryption type (not applicable for password path - AES only).
 
        .PARAMETER AllowDeadCiphers
        Allow obsolete encryption types (not applicable for password path - AES only).
 
        .PARAMETER RestrictAcl
        Apply user-only ACL to output files.
 
        .PARAMETER Force
        Overwrite existing output files.
 
        .PARAMETER Summary
        Generate JSON summary file.
 
        .PARAMETER SummaryPath
        Path to write operation summary JSON. Defaults next to Output when omitted.
 
        .PARAMETER PassThru
        Return operation result object.
 
        .PARAMETER FixedTimestampUtc
        Use fixed timestamp for deterministic output.
 
        .EXAMPLE
        Reset-AccountPasswordWithKeytab -SamAccountName svc-web -AcknowledgeRisk -Justification "Quarterly rotation" -OutputPath .\svc-web.keytab
 
        Resets the password for svc-web and generates a corresponding keytab.
 
        .EXAMPLE
        Reset-AccountPasswordWithKeytab -SamAccountName svc-app -WhatIfOnly -AcknowledgeRisk -Justification "Planning rotation"
 
        Shows what would be done without making changes.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$SamAccountName,

        [string]$Realm,
        [securestring]$NewPassword,
        [int]$Kvno,
        [ValidateSet('MIT','Heimdal','Windows')]
        [string]$Compatibility = 'Windows',

        [object[]]$IncludeEtype = @(18,17),
        [object[]]$ExcludeEtype,
        [string]$OutputPath,

        # AD Integration
        [string]$Domain,
        [string]$Server,
        [pscredential]$Credential,

        # Safety & Policy
        [switch]$AcknowledgeRisk,
        [string]$Justification,

        [switch]$WhatIfOnly,
        [int[]]$UpdateSupportedEtypes,

        # BigBrother integration
        [switch]$AESOnly,
        [switch]$IncludeLegacyRC4,
        [switch]$AllowDeadCiphers,

        # Output options
        [switch]$RestrictAcl,
        [switch]$Force,
        [switch]$Summary,
        [Alias('JsonSummaryPath')]
        [string]$SummaryPath,

        [switch]$PassThru,
        [datetime]$FixedTimestampUtc,
        [switch]$SuppressWarnings
    )

    begin {
        Get-RequiredModule -Name 'ActiveDirectory'
        $ErrorActionPreference = 'Stop'

        # Validate required risk acknowledgment (explicit check for better error messages)
        if (-not $AcknowledgeRisk) {
            throw "This operation requires explicit risk acknowledgment. Use -AcknowledgeRisk to proceed."
        }
        if (-not $Justification) {
            throw "This operation requires a justification. Use -Justification to provide one."
        }

        # Compose policy for password path (enforce AES-only)
        try {
            $policy = Get-PolicyIntent -IncludeEtype $IncludeEtype -ExcludeEtype $ExcludeEtype `
                                      -AESOnly:$AESOnly -IncludeLegacyRC4:$IncludeLegacyRC4 `
                                      -AllowDeadCiphers:$AllowDeadCiphers -PathKind 'Password'
        } catch {
            throw "Policy composition failed: $($_.Exception.Message)"
        }

        # Validate password path compatibility (enforc AES-only)
        Validate-PasswordPathCompatibility -Policy $policy -SuppressWarnings:$SuppressWarnings

        # Security warning for high-impact operation
        if (-not $SuppressWarnings) {
            Write-SecurityWarning -RiskLevel 'High' -SamAccountName $SamAccountName | Out-Null
        }

        if ($OutputPath) {
            $out = Resolve-PathUniversal -Path $OutputPath -Purpose Output
        } else {
            $out = Resolve-OutputPath -Directory (Get-Location).Path -BaseName ($SamAccountName.TrimEnd('$')) -Extension '.keytab' -CreateDirectory
        }

        if ($Summary.IsPresent -and (-not $SummaryPath)) {
            $SummaryPath = "$($out)_summary_$(Get-Date -Format 'yyyyMMdd_HHmmss').json"
        }
    }

    process {
        try {
            # 1. Discover account and current state
            $domainFQDN = Resolve-DomainContext -Domain $Domain
            if (-not $Realm) {
                $Realm = $domainFQDN.ToUpperInvariant()
            }

            $getParams = @{
                Identity = $SamAccountName
                Properties = 'msDS-KeyVersionNumber', 'msDS-SupportedEncryptionTypes'
            }

            if ($Server) { $getParams.Server = $Server }
            if ($Credential) { $getParams.Credential = $Credential }

            $account = Get-ADUser @getParams
            $currentKvno = $account.'msDS-KeyVersionNumber'
            if (-not $currentKvno) {
                if ($Kvno) { $Kvno }
                else { throw "Unable to determine key version number from Active Directory and no Kvno specified. Please provide a -Kvno value." }
            } else { $currentKvno + 1 }

            $predictedKvno = if ($Kvno) { $Kvno } else { $currentKvno + 1 }

            # 2. Generate password if not provided
            if (-not $NewPassword) {
                $NewPassword = New-StrongPassword -Length 64 # tbi
            }

            # 3. build operation plan
            $etypeSelection = Resolve-EtypeSelection -AvailableIds $policy.IncludeIds -Policy $policy

            $plan = [ordered]@{
                Operation = 'Reset-AccountPasswordWithKeytab'
                SamAccountName = $SamAccountName
                Domain = $domainFqdn
                Realm = $Realm
                CurrentKvno = $currentKvno
                PredictedKvno = $predictedKvno
                SelectedEtypes = $etypeSelection.Selected
                EtypeNames = @($etypeSelection.Selected | ForEach-Object { Get-EtypeNameFromId $_ })
                OutputPath = (Resolve-Path -Path (Split-Path $out -Parent)).Path + '\' + (Split-Path $out -Leaf) # resolve to ensure path
                UpdateSupportedEtypes = $UpdateSupportedEtypes
                Justification = $Justification
                Timestamp = (Get-Date).ToUniversalTime().ToString('o')
                Rollback = @{
                    Note = 'Password reset is one-way - cannot restore original password'
                    OriginalKvno = $currentKvno
                    OriginalSupportedEtypes = $account.'msDS-SupportedEncryptionTypes'
                }
            }

            if ($WhatIfOnly) {
                Write-Host "=== Operation Plan ===" -ForeGroundColor Cyan
                $plan | Format-List
                return $plan
            }

            if ($PSCmdlet.ShouldProcess($SamAccountName, "Reset password and generate keytab")) {
                # 4. execute password reset
                 Write-Verbose "Resetting password for $SamAccountName"

                $setParams = @{
                    Identity = $account
                    NewPassword = $NewPassword
                    Reset = $true
                }
                if ($Server) { $setParams.Server = $Server }
                if ($Credential) { $setParams.Credential = $Credential }

                Set-ADAccountPassword @setParams

                # 5. Update supported encryption types if requested
                if ($UpdateSupportedEtypes) {
                    Write-Verbose "Updating msDS-SupportedEncryptionTypes"
                    $etypeSum = [int]($UpdateSupportedEtypes | Measure-Object -Sum).Sum

                    $replaceParams = @{
                        Identity = $account
                        Replace = @{'msDS-SupportedEncryptionTypes' = $etypeSum}
                    }
                    if ($Server) { $replaceParams.Server = $Server }
                    if ($Credential) { $replaceParams.Credential = $Credential }

                    Set-ADObject @replaceParams
                }

                # 6. Generate keytab from new password
                Write-Verbose "Generating keytab with new Password for $SamAccountName"

                $keytabParams = @{
                    SamAccountName  = $SamAccountName
                    Realm           = $Realm
                    Password        = $NewPassword
                    Kvno            = $predictedKvno
                    Compatibility   = $Compatibility
                    IncludeEtype    = $etypeSelection.Selected
                    OutputPath      = $out
                    RestrictAcl     = $RestrictAcl
                    Force           = $Force
                    Summary         = $Summary
                    PassThru        = $true
                }

                if ($FixedTimestampUtc) {
                    $keytabParams.FixedTimestampUtc = $FixedTimestampUtc
                }

                if ($SummaryPath) {
                    $keytabParams.SummaryPath = $SummaryPath
                }

                $keytabResult = New-KeytabFromPassword @keytabParams

                # 7. Compile final result
                $result = [ordered]@{
                    Operation = 'Reset-AccountPasswordWithKeytab'
                    SamAccountName = $SamAccountName
                    Domain = $domainFqdn
                    Realm = $Realm
                    Success = $true
                    OldKvno = $currentKvno
                    NewKvno = $predictedKvno
                    Etypes = $keytabResult.Etypes
                    EtypeNames = @($keytabResult.Etypes | ForEach-Object { Get-EtypeNameFromId $_ })
                    OutputPath = $keytabResult.OutputPath
                    SummaryPath = $keytabResult.SummaryPath
                    Justification = $Justification
                    Operator = [Environment]::UserName
                    Timestamp = (Get-Date).ToUniversalTime().ToString('o')
                }

                Write-Host "Password reset completed successfully for $SamAccountName" -ForegroundColor Green
                Write-Host "New KVNO: $predictedKvno" -ForegroundColor Green
                Write-Host "Keytab: $($keytabResult.OutputPath)" -ForegroundColor Green

                if ($PassThru) {
                    return $result
                }
            }

        } catch {
            Write-Error "Password reset operation failed for ${SamAccountName}: $($_.Exception.Message)"
            Write-Warning "The account password may have been changed. Manual verification recommended."
            throw
        }
    }
}