Problema
Qualche tempo fa un cliente ci chiese come fosse possibile resettare le password degli account di Active Directory (AD) in maniera automatica e trasparente. La richiesta riguardava dei service account “storici”, che per varie ragioni non era possibile convertire a Group Managed Service Account. Questi account mancavano di una qualsiasi politica di rotazione e di messa in sicurezza delle password, e per evitare rischi si voleva un qualcosa che fosse automaticamente in grado di eseguire il reset della password ad intervalli pianificati, e che le mettesse a disposizione degli utenti che ne dovessero avere bisogno per le relative configurazioni applicative.
Disegno architetturale
Sulla base del requisito, ci siamo posti alcune domande.
- Come potevamo mantenere le password in sicurezza, evitando di farle viaggiare in chiaro nella pipeline del processo?
- Come potevamo permettere agli utenti di accedere a queste password in maniera sicura e registrata?
- Dove avremmo dovuto eseguire il processo? Avremmo dovuto usare obbligatoriamente un prodotto onprem o ci saremmo potuti avvalere, per qualche elemento del processo, di strumenti cloud?
Poiché il cliente aveva già a disposizione una sottoscrizione Azure, la risposta naturale ai primi due punti è stata Azure Key Vault. Questa risorsa permette non solo di mantenere dei Secrets in sicurezza, ma attraverso l’RBAC di Azure permette di definire chi può accedere a questi Secrets, ed attraverso il registro delle Azure Activities permette di controllare chi accede ai Secrets e quando.
Stabilito di usare Key Vault, il terzo punto è diventato una mera questione accademica: tra scrivere del codice onprem che usasse le API di Key Vault, oppure usare del codice in Azure, abbiamo scelto la seconda strada. A quel punto abbiamo discusso se usare Azure Functions oppure Azure Automation, ed abbiamo scelto quest’ultima perché più adatta al tipo di operazione pianificata richiesta dal cliente.
Siamo così arrivati al seguente disegno architetturale:
- La pianificazione “triggera” l’esecuzione di un primo runbook PowerShell che genera la password per un certo utente e la scrive nel Key Vault con una certa naming convention.
- Viene quindi inviata una notifica al referente applicativo nominato per la password, in modo che possa avviare le operazioni non automatizzabili legate al cambio password in atto.
- Al suo termine, il primo runbook passa l’utente in perimetro come parametro ad un secondo runbook, eseguito onprem attraverso un Hybrid Runbook Worker
- L’Hybrid Worker esegue effettivamente il reset in AD.
Implementazione
Per non annoiarvi con una pletora di dettagli poco interessanti su come sono state realizzate le diverse componenti, mi limiterò ad elencare alcune scelte che dovranno caratterizzare lo specifico delle vostre implementazioni.
Quanti Key Vault?
La prima questione pratica da affrontare riguarda il numero di Key Vault da creare. Allo stato attuale è possibile governare i permessi di accesso ai Secrets in maniera granulare, quindi posso potenzialmente usare un unico Key Vault per tutte le mie password, e definire poi i singoli utenti o gruppi che possono leggere ogni singola password. Ma questo genererà anche un unico registro delle Azure Activities, ed anche un unico centro di costo su cui verrà addebitata questa risorsa.
Il nostro cliente ha scelto l’opzione Key Vault unico per tutte le password, ma qui non c’è una soluzione migliore o peggiore, bisognerà valutare caso per caso.
Come gestire lo storico delle password?
Volevamo dare al cliente una via di fuga in caso di problemi nel meccanismo di reset della password, e per questo volevamo mantenere uno storico delle password usate.
Key Vault fornisce questa funzionalità di default, quindi ci siamo fregati le mani per la soddisfazione e siamo passati oltre.
Come generare la password?
Abbiamo già anticipato che per i Runbook si è scelto di usare PowerShell, scelta comunque consigliata visto che poi avremo bisogno di interagire con l’AD onprem.
Per il codice di generazione password si è scelto di non usare il classico [System.Web.Security.Membership]::GeneratePassword(), ma piuttosto di usare un generatore randomico basato su una serie di caratteri permessi. Questo perché in potenza questa password potrebbe dover arrivare su sistemi che non rispettano le stesse convenzioni di Windows in termini di caratteri validi. Su internet si trovano svariati esempi tutti ugualmente validi, questo è il nostro:
function CreateRandomPassword() { $allowedChars = ""; $allowedChars = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z," $allowedChars += "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z," $allowedChars += "1,2,3,4,5,6,7,8,9,0,!,@,#,$,%,&,?" $sep = ',' $arr = $allowedChars.Split($sep) $passwordString = "" $temp = "" for ($i = 0; $i -lt 16; $i++) { $rand = Get-Random -Minimum 0 -Maximum ($arr.Length - 1) $temp = $arr[$rand] $passwordString += $temp } return $passwordString; }
Quali credenziali usare per l’Hybrid Worker?
Ovviamente non volevamo avere dei set di credenziali da fare viaggiare in chiaro, e nemmeno eravamo entusiasti all’idea di aggiungere ancora un altro set di credenziali che comunque sarebbero anche loro state da gestire, portandoci in un loop infinito.
Di concerto con il cliente, abbiamo deciso che il contesto di sicurezza del runbook eseguito localmente e che avrebbe effettivamente eseguito il cambio password, sarebbe stato quello di default dell’hybrid worker, cioè il computer account stesso. A questo punto, non abbiamo fatto altro che assegnare specificamente la delega di cambio password sulle OU AD contenenti gli account da trattare, et voilà, task eseguito con il seguente semplice script:
param ( [Parameter (Mandatory=$true)] [String] $AutomationAccountName ,[Parameter (Mandatory=$true)] [String] $KeyVaultName ,[Parameter (Mandatory=$true)] [String] $ResourceGroupName ,[Parameter (Mandatory=$true)] [String] $UserName ) function AppendLog ([string]$Message) { $script:CurrentAction = $Message $script:TraceLog += ((Get-Date).ToString() + "`t" + $Message + " `r`n") } $script:CurrentAction = "" $script:TraceLog = "" $ErrorActionPreference = 'Stop' $SecretName = $UserName + "-password" AppendLog 'Starting operations...' try { # Connect with the Run As Account and error out if there's no account to use $connectionAssetName = "AzureRunAsConnection" AppendLog ("Getting automation connection...") $conn = Get-AutomationConnection -Name $ConnectionAssetName if ($null -eq $conn) { throw "Could not retrieve $connectionAssetName connection asset. Check that this asset exists in the automation account." } Add-AzAccount -ServicePrincipal -Tenant $conn.TenantID -ApplicationId $conn.ApplicationId -CertificateThumbprint $conn.CertificateThumbprint | Write-Verbose AppendLog ("Setting context to " + $conn.SubscriptionId + "...") Set-AzContext -Subscription $conn.SubscriptionId -ErrorAction Stop | Write-Verbose # Get the key vault and error out if we cannot get access to the resource AppendLog ("Getting key vault " + $KeyVaultName + "...") $keyVault = Get-AzKeyVault -VaultName $KeyVaultName -ResourceGroupName $ResourceGroupName if ($null -eq $keyVault) { throw "Could not retrieve key vault $KeyVaultName. Check that a key vault with this name exists in the resource group $ResourceGroupName." } AppendLog ("Getting new " + $UserName + " password from key vault...") $NewPWD = ConvertTo-SecureString (Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $SecretName -AsPlainText) -AsPlainText -Force # Set the new password on the account AppendLog ("Setting " + $UserName + " password in AD...") $User = Get-ADUser -Filter {SamAccountName -eq $Username} Get-ADUser -Filter {SamAccountName -eq $Username} | Set-ADAccountPassword -NewPassword $NewPWD -Reset AppendLog ('Successfully changed password for user ' + $UserName) $PWDResetFlag = $true } catch { AppendLog ('Exception ' + $_.Exception + ". Error Details: " + $_.ErrorDetails) $script:TraceLog $PSCmdlet.ThrowTerminatingError($_) }
Come inviare la mail di notifica?
Per l’invio della notifica abbiamo scelto di usare un Sendgrid Account come SMTP relay. Abbiamo usato il Key Vault come contenitore per la APIKey sempre per evitare di avere password in chiaro:
$APIKey = Get-AzKeyVaultSecret -VaultName $KeyVaultName -ResourceGroupName $ResourceGroupName -Name "SGAPIKEY-001" -AsPlainText $APIUri = "https://api.sendgrid.com/v3/mail/send" $Header = @{} $Header.Add("Authorization",("Bearer " + $APIKey)) $MessageBody = "The password for the user $UserName has been successfully changed. The new password is available as $SecretName under the Secrets of the $KeyVaultName Azure Key Vault." $Body = @{"subject"=("Executing password change for "+$UserName);"personalizations"=@(@{"to"=@(@{"email"=$NotificationAddress;"name"=$NotificationAddress})});"content"=@(@{"type"="text/plain";"value"=$MessageBody});"from"=@{"email"=$AuthenticatedSender;"name"=$AuthenticatedSender}} | ConvertTo-Json -Depth 5 $ReturnData = Invoke-WebRequest -Method Post -Uri $APIUri -Body $Body -Headers $Header -ContentType "application/json" -UseBasicParsing
Come potete notare, la notifica avverte l’Application Manager del cambio password, ma non contiene la password stessa; sarà poi il suo ruolo RBAC a permettergli di visualizzare la password o meno.
Conclusioni
Grazie ad Azure Automation, abbiamo un prodotto funzionale e sufficientemente resiliente agli errori da poter essere portato in produzione. Ovviamente è molto essenziale e quindi migliorabile, ma soddisfa il requisito iniziale ad un costo veramente competitivo. E l’Hybrid Worker permetterà di integrarlo con tutti quei sistemi che supportano cmdlet PowerShell. Devo aggiornare un campo di una tabella di un DB con la nuova password? Devo scrivere la nuova password in un file di configurazione? Devo chiamare una API di un web service per aggiornare una configurazione applicativa con la nuova password? Tutti scenari che possono essere soddisfatti grazie alla combo Azure Automation-PowerShell.
Riferimenti
Provide access to Key Vault keys, certificates, and secrets with an Azure role-based access control