Providers/AD/Private/New-ADTestSecretVault.ps1
|
function New-ADTestSecretVault { <# .SYNOPSIS Creates or configures a SecretStore vault for AD test environment passwords .DESCRIPTION Creates a computer-level SecretStore vault for storing AD test environment passwords. Configures the vault with appropriate security settings and access controls. The SecretStore itself is configured with no authentication for ease of automation, but individual vaults are password-protected for security. .PARAMETER VaultName Name of the secret vault to create. Defaults to "ADTestEnvironment" .PARAMETER VaultPassword Password for the secret vault. If not provided, defaults to "ADTestEnvironmentPassword" .PARAMETER UseDefaultPassword Use the default password "ADTestEnvironmentPassword" instead of prompting. Defaults to $true for automation. .PARAMETER Force Force recreation of the vault if it already exists .PARAMETER AllowPlaintextVault Allow creation of a vault without password protection (less secure, for testing only) .PARAMETER GlobalVault Create vault at AllUsers scope instead of CurrentUser scope. Requires administrative privileges for optimal security. .EXAMPLE New-ADTestSecretVault Creates the default ADTestEnvironment vault with default password "ADTestEnvironmentPassword" .EXAMPLE New-ADTestSecretVault -UseDefaultPassword:$false Creates the vault but prompts for password instead of using default .EXAMPLE New-ADTestSecretVault -VaultName "ProdVault" -VaultPassword $securePass Creates a custom named vault with specified password .EXAMPLE New-ADTestSecretVault -Force Recreates the vault even if it already exists .EXAMPLE New-ADTestSecretVault -GlobalVault Creates a global vault (AllUsers scope) - requires admin privileges .OUTPUTS PSCustomObject containing vault creation results .NOTES Author: Jeffrey Stuhr Version: 1.0.0 Last Updated: 2025-08-05 Security Notes: - SecretStore is configured with no authentication for automation ease - Individual vaults are password-protected using Windows Data Protection API - Vault is created at computer level for shared access when running as admin - Requires administrative privileges for optimal security #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private helper; the caller checks $WhatIfPreference before invoking it.')] [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter()] [ValidateNotNullOrEmpty()] [string]$VaultName = "ADTestEnvironment", [Parameter()] [System.Security.SecureString]$VaultPassword, [Parameter()] [bool]$UseDefaultPassword = $true, [Parameter()] [switch]$Force, [Parameter()] [switch]$AllowPlaintextVault, [Parameter()] [switch]$GlobalVault ) begin { $correlationId = [System.Guid]::NewGuid() Write-Verbose "Starting New-ADTestSecretVault - CorrelationId: $correlationId" $result = @{ CorrelationId = $correlationId VaultName = $VaultName VaultCreated = $false VaultExists = $false VaultConfigured = $false IsDefault = $false Errors = @() Warnings = @() } # Ensure SecretStore modules are available try { Import-Module Microsoft.PowerShell.SecretManagement -Force -ErrorAction Stop Import-Module Microsoft.PowerShell.SecretStore -Force -ErrorAction Stop Write-Verbose "SecretStore modules imported successfully" } catch { throw ("SecretStore modules not available. Run Test-ADTestSecretStorePrerequisite first. Error: " + "$($_.Exception.Message)") } # SecretStore configuration is per USER, not per vault, and it is shared with every # other module that registers a vault against it - OktaTestEnvironment and # EntraTestEnvironment both do, and both need Authentication = Password. # # This module prefers a passwordless store, but it must NOT impose that on a store it # did not configure: doing so re-encrypts the shared store and breaks the credentials # those modules keep in it. So the preference applies only when nothing has configured # the store yet, and otherwise whatever is found is adapted to. $storeConfigured = $false $needsConfiguration = $false $storeAuthentication = $null try { # Try to get configuration to see if SecretStore is set up $storeConfig = Get-SecretStoreConfiguration -ErrorAction Stop $storeAuthentication = [string]$storeConfig.Authentication # Check if store is accessible try { Get-SecretVault -ErrorAction Stop | Out-Null Write-Verbose "SecretStore is configured and accessible" $storeConfigured = $true if ($storeAuthentication -ne 'None') { Write-Verbose ("SecretStore is configured with Authentication '$storeAuthentication' by " + "this user or another module. Adapting to it rather than reconfiguring, because the " + "store is shared.") } } catch { Write-Verbose "SecretStore configured but not accessible: $($_.Exception.Message)" $needsConfiguration = $true } } catch { # Get-SecretStoreConfiguration THROWS when the store is LOCKED, with "A valid # password is required to access the Microsoft.PowerShell.SecretStore vault". That # error is proof a password IS configured, not evidence that nothing is. Treating # it as "not configured" is what previously led straight into reconfiguring a store # another module owns. if ($_.Exception.Message -match 'valid password is required') { Write-Verbose ("SecretStore is configured with a password and is currently locked. Adapting " + "to it rather than reconfiguring, because the store is shared per user.") $storeConfigured = $true $storeAuthentication = 'Password' } else { Write-Verbose "SecretStore not configured: $($_.Exception.Message)" $needsConfiguration = $true } } # Configured only when nothing has configured it yet. See the note above: this store is # shared, so a preference is not a licence to overwrite somebody else's setting. if ($needsConfiguration -and -not $storeConfigured) { try { Write-Verbose "Configuring SecretStore with no authentication (passwordless)..." $setSecretStoreConfigurationArgs1 = @{ Authentication = 'None' Interaction = 'None' Scope = 'CurrentUser' Confirm = $false ErrorAction = 'Stop' } Set-SecretStoreConfiguration @setSecretStoreConfigurationArgs1 Write-Verbose ("Configured SecretStore with no authentication - individual vaults can still be " + "password protected") $storeConfigured = $true } catch { $result.Warnings += ("Could not configure SecretStore automatically: $($_.Exception.Message). " + "May prompt for password during vault creation.") Write-Warning ("Could not configure SecretStore automatically: $($_.Exception.Message). May " + "prompt for password during vault creation.") } } # A store this module did not configure may require a password, in which case it has to # be unlocked before any secret can be written. Failing here is fatal rather than a # warning: every Set-Secret afterwards would otherwise fail one at a time with a message # about the secret rather than about the locked store. if ($storeAuthentication -eq 'Password') { $unlockPassword = $VaultPassword if (-not $unlockPassword -and $UseDefaultPassword) { $unlockPassword = Get-ADTestDefaultVaultPassword } if (-not $unlockPassword) { throw ("SecretStore on this machine requires a password and none was supplied. The store is " + "shared per user with OktaTestEnvironment and EntraTestEnvironment; if one of those " + "configured it, pass -VaultPassword with that password.") } try { Unlock-SecretStore -Password $unlockPassword -ErrorAction Stop Write-Verbose "Unlocked the shared SecretStore." } catch { throw ("Could not unlock SecretStore: $($_.Exception.Message). The store is configured with a " + "password that is not this module's default - most likely because OktaTestEnvironment or " + "EntraTestEnvironment configured it first, since all three share one per-user store. Pass " + "-VaultPassword with the existing password.") } } } process { try { # Check if vault already exists $existingVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue if ($existingVault) { $result.VaultExists = $true Write-Verbose "Vault '$VaultName' already exists" if (-not $Force) { Write-Verbose "Vault exists and Force not specified - skipping creation" $result.VaultConfigured = $true $result.Warnings += "Vault '$VaultName' already exists. Use -Force to recreate." return [PSCustomObject]$result } else { Write-Verbose "Force specified - removing existing vault" try { Unregister-SecretVault -Name $VaultName -ErrorAction Stop Write-Verbose "Successfully removed existing vault: $VaultName" } catch { $errorMsg = "Failed to remove existing vault '$VaultName': $($_.Exception.Message)" $result.Errors += $errorMsg throw $errorMsg } } } # Prepare vault parameters - always use password for vault even though store has none $vaultParams = @{} if ($VaultPassword) { # Use provided password $vaultParams.Password = $VaultPassword Write-Verbose "Using provided vault password" } elseif ($UseDefaultPassword) { # Use default password for the vault. Held in one place rather than as a # literal here - see Get-ADTestDefaultVaultPassword for what it is and why # publishing it is acceptable for a test module. $vaultParams.Password = Get-ADTestDefaultVaultPassword Write-Verbose "Using the module's documented default vault password" } elseif ($AllowPlaintextVault) { # Create vault without password (less secure) $vaultParams.Authentication = 'None' $result.Warnings += "Vault created without password protection (less secure)" Write-Warning "Creating vault without password protection - this is less secure" } else { # Default to using our standard password if nothing specified $vaultParams.Password = Get-ADTestDefaultVaultPassword Write-Verbose "No password specified - using the documented default vault password" } # Configure vault scope based on GlobalVault parameter if ($GlobalVault) { # Shared with Remove-ADTestSecretVault. This check used to be inlined here # and absent there, which is how the remove side's -GlobalVault ended up # declared but never read. if (Test-ADTestAdministrator) { Write-Verbose "Creating global vault (AllUsers scope) - running as administrator" $vaultParams.Scope = 'AllUsers' } else { Write-Warning ("GlobalVault requested but not running as administrator - using CurrentUser " + "scope instead") $vaultParams.Scope = 'CurrentUser' $result.Warnings += ("GlobalVault requested but not running as administrator - vault " + "created for current user only") } } else { Write-Verbose "Creating user-specific vault (CurrentUser scope)" $vaultParams.Scope = 'CurrentUser' } # Create the vault Write-Verbose "Creating SecretStore vault: $VaultName" try { if ($vaultParams.Count -gt 0) { $registerSecretVaultArgs2 = @{ Name = $VaultName ModuleName = 'Microsoft.PowerShell.SecretStore' VaultParameters = $vaultParams ErrorAction = 'Stop' } Register-SecretVault @registerSecretVaultArgs2 } else { $registerSecretVaultArgs3 = @{ Name = $VaultName ModuleName = 'Microsoft.PowerShell.SecretStore' ErrorAction = 'Stop' } Register-SecretVault @registerSecretVaultArgs3 } $result.VaultCreated = $true $result.VaultConfigured = $true Write-Verbose "Successfully created SecretStore vault: $VaultName" } catch { $errorMsg = "Failed to create SecretStore vault '$VaultName': $($_.Exception.Message)" $result.Errors += $errorMsg throw $errorMsg } # Verify vault creation $newVault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue if ($newVault) { Write-Verbose "Vault creation verified successfully" # Check if this is the default vault $defaultVault = Get-SecretVault | Where-Object { $_.IsDefault -eq $true } if (-not $defaultVault -or $defaultVault.Name -eq $VaultName) { try { Set-SecretVaultDefault -Name $VaultName -ErrorAction Stop $result.IsDefault = $true Write-Verbose "Set '$VaultName' as default secret vault" } catch { $result.Warnings += "Failed to set as default vault: $($_.Exception.Message)" Write-Warning "Failed to set '$VaultName' as default vault: $($_.Exception.Message)" } } } else { $errorMsg = "Vault creation succeeded but vault verification failed" $result.Errors += $errorMsg throw $errorMsg } } catch { $errorMsg = "Failed to create AD test secret vault: $($_.Exception.Message)" $result.Errors += $errorMsg Write-Error $errorMsg -ErrorAction Stop } } end { Write-Verbose "Completed New-ADTestSecretVault - CorrelationId: $correlationId" return [PSCustomObject]$result } } |