Public/Get-NSPSecret.ps1
|
function Get-NSPSecret { <# .SYNOPSIS Retrieves a stored secret by name. Single call site for every NSP script that needs a credential, API key, or token. .DESCRIPTION Resolution order (first hit wins): 1. Environment variable NSP_SECRET_<NAME> (name upper-cased, non-alphanumerics -> _). For CI and one-off overrides. Plaintext by nature - only use where the environment itself is the secret store. 2. SecretManagement vault (default 'NSP'), if the module and vault are present. 3. DPAPI fallback file %LOCALAPPDATA%\NSP\Secrets\<name>.sec (current user + machine). If none match, throws with the exact command to fix it. Returns a [SecureString] by default. Pass -AsPlainText for a [string] (e.g. to put in an HTTP header). .PARAMETER Name The secret name, e.g. 'CW.Control.ApiKey'. .PARAMETER Vault SecretManagement vault name. Defaults to 'NSP'. .PARAMETER AsPlainText Return a plain [string] instead of a [SecureString]. .PARAMETER Source Require the value to come from exactly this source - 'EnvironmentVariable', 'Vault', or 'File' - instead of walking the normal fallback chain. Throws if that source doesn't have it, even if another source would. Use this for a sensitive scheduled job that must not silently pick up an ambient environment-variable override, or that must fail loudly rather than fall back to the weaker DPAPI file if the vault is unreachable. .EXAMPLE $key = Get-NSPSecret -Name 'CW.Control.ApiKey' -AsPlainText $headers.Add('CTRLAuthHeader', $key) .EXAMPLE $cred = [pscredential]::new('api', (Get-NSPSecret -Name 'Some.Token')) .EXAMPLE # scheduled task: require the vault specifically, so a leftover NSP_SECRET_* env var # from someone's interactive session can't silently override it. Get-NSPSecret -Name 'CW.Control.ApiKey' -Source Vault -AsPlainText #> [CmdletBinding()] [OutputType([System.Security.SecureString], [string])] param( [Parameter(Mandatory, Position = 0)][string]$Name, [string]$Vault = $script:NSPVaultName, [switch]$AsPlainText, [ValidateSet('EnvironmentVariable', 'Vault', 'File')][string]$Source ) $envName = 'NSP_SECRET_' + (($Name -replace '[^A-Za-z0-9]', '_').ToUpperInvariant()) # --- 1. Environment override ------------------------------------------------- if (-not $Source -or $Source -eq 'EnvironmentVariable') { $envVal = [Environment]::GetEnvironmentVariable($envName) if (-not [string]::IsNullOrEmpty($envVal)) { Write-Verbose "Get-NSPSecret: '$Name' resolved from env var $envName." if ($AsPlainText) { return $envVal } return (ConvertTo-SecureString -String $envVal -AsPlainText -Force) } if ($Source -eq 'EnvironmentVariable') { throw "Secret '$Name' not found in environment variable $envName (Source 'EnvironmentVariable' was required explicitly; other sources were not checked)." } } # --- 2. SecretManagement vault -------------------------------------------------- if (-not $Source -or $Source -eq 'Vault') { if (-not (Get-Module -ListAvailable -Name 'Microsoft.PowerShell.SecretManagement')) { if ($Source -eq 'Vault') { throw "Secret '$Name' not found: SecretManagement is not installed (Source 'Vault' was required explicitly)." } } else { try { Import-Module Microsoft.PowerShell.SecretManagement -ErrorAction Stop } catch { Write-Verbose "Get-NSPSecret: SecretManagement present but failed to import ($_)." } $vaultPresent = $false try { $vaultPresent = [bool](Get-SecretVault -Name $Vault -ErrorAction SilentlyContinue) } catch { $vaultPresent = $false } if (-not $vaultPresent -and $Source -eq 'Vault') { throw "Secret '$Name' not found: vault '$Vault' is not registered (Source 'Vault' was required explicitly). Run Initialize-NSPSecretStore." } if ($vaultPresent -and (Get-SecretInfo -Name $Name -Vault $Vault -ErrorAction SilentlyContinue)) { Write-Verbose "Get-NSPSecret: '$Name' resolved from vault '$Vault'." $secure = Get-Secret -Name $Name -Vault $Vault -ErrorAction Stop if ($secure -is [string]) { $secure = ConvertTo-SecureString -String $secure -AsPlainText -Force } if ($AsPlainText) { return (ConvertFrom-NSPSecureString -SecureString $secure) } return $secure } if ($vaultPresent -and $Source -eq 'Vault') { throw "Secret '$Name' not found in vault '$Vault' (Source 'Vault' was required explicitly). It may be locked - see Test-NSPSecretStore / Test-NSPSecret." } } } # --- 3. DPAPI fallback file ----------------------------------------------------- if (-not $Source -or $Source -eq 'File') { $path = Get-NSPSecretFallbackPath -Name $Name if (Test-Path -LiteralPath $path) { Write-Verbose "Get-NSPSecret: '$Name' resolved from DPAPI file $path." $secure = Get-Content -LiteralPath $path -Raw | ConvertTo-SecureString if ($AsPlainText) { return (ConvertFrom-NSPSecureString -SecureString $secure) } return $secure } if ($Source -eq 'File') { throw "Secret '$Name' not found: no DPAPI fallback file at $path (Source 'File' was required explicitly)." } } throw ("Secret '{0}' not found. Store it with: Set-NSPSecret -Name '{0}' " -f $Name) + ("(or set env var {0}). See NSP-Bootstrap\SECRETS.md." -f $envName) } |