PowerShell credentials en 1Password

PowerShell en credentials: veilig werken met 1Password

Iedere beheerder kent het moment: je schrijft een handig PowerShell-script en vervolgens heeft dat script een wachtwoord nodig. Een SQL-server, een API of een Microsoft 365-tenant vraagt immers om inloggegevens. De verleiding is groot om dat wachtwoord gewoon in het script te zetten. Toch doe je jezelf daarmee geen plezier, want dat script belandt vroeg of laat in een Git-repository, een gedeelde map of een e-mail aan een collega. Vandaar deze blog post die dit keer gaat over PowerShell en credentials en 1Password.

In deze blog loop ik daarom stap voor stap door alles wat met PowerShell en credentials te maken heeft. Eerst bekijken we de ingebouwde mogelijkheden van PowerShell. Daarna bespreken we de valkuilen. Vervolgens koppelen we PowerShell aan 1Password, zodat je scripts hun geheimen ophalen zonder dat ze die ooit opslaan. Tot slot sluiten we af met concrete best practices.

De basis: PSCredential, Get-Credential en SecureString

PowerShell werkt met een vast object voor inloggegevens: het PSCredential-object. Dit object combineert een gebruikersnaam met een wachtwoord als SecureString. Vrijwel alle cmdlets die authenticatie ondersteunen, zoals Invoke-Command, New-PSSession en Connect-ExchangeOnline, accepteren zo’n object via de parameter -Credential.

De eenvoudigste manier om een credential te maken, is Get-Credential. Deze cmdlet toont een prompt en stopt de invoer direct in een PSCredential-object:

$cred = Get-Credential -UserName 'CORP\svc-backup' -Message 'Wachtwoord voor de back-upaccount'
Invoke-Command -ComputerName SRV01 -Credential $cred -ScriptBlock { Get-Service }

Dat werkt prima zolang jij achter het toetsenbord zit. Een geplande taak of een CI/CD-pipeline kan echter niet typen. Daarom grijpen veel beheerders naar een alternatief, en precies daar gaat het vaak mis.

Bovendien is het goed om te weten wat SecureString wel en niet doet. Op Windows versleutelt .NET de inhoud in het geheugen. Op Linux en macOS gebeurt dat echter niet. Microsoft raadt SecureString voor nieuwe ontwikkeling zelfs af. Zie het dus vooral als een manier om te voorkomen dat een wachtwoord per ongeluk in logs of op het scherm verschijnt, niet als echte beveiliging.

Valkuilen: platte tekst, Export-Clixml en DPAPI

De grootste fout is natuurlijk het wachtwoord letterlijk in het script zetten:

# Doe dit niet
$pass = ConvertTo-SecureString 'Zomer2026!' -AsPlainText -Force

Iedereen die het script opent, leest het wachtwoord mee. Bovendien blijft het in de Git-geschiedenis staan, ook nadat je het later verwijdert.

Een populair alternatief is Export-Clixml. Je exporteert eenmalig een credential naar een XML-bestand en leest het later weer in met Import-Clixml. Op Windows versleutelt PowerShell het wachtwoord daarbij met DPAPI. Dat klinkt veilig, maar het heeft duidelijke nadelen:

  • Alleen dezelfde gebruiker op dezelfde machine kan het bestand ontsleutelen. Verhuist je script naar een andere server, dan moet je alles opnieuw exporteren.
  • Op Linux en macOS versleutelt Export-Clixml het wachtwoord niet echt.
  • Je hebt geen centraal overzicht. Wisselt een wachtwoord, dan zoek je alle XML-bestanden handmatig op.
  • Rotatie, rechtenbeheer en audit logging ontbreken volledig.

Kortom: deze methoden verplaatsen het probleem, maar lossen het niet op. Je wilt juist één centrale, versleutelde bron waar je scripts hun geheimen op het moment zelf ophalen.

SecretManagement en SecretStore als tussenstap

Microsoft biedt zelf een nette oplossing: de module Microsoft.PowerShell.SecretManagement. Deze module geeft je één set cmdlets, zoals Get-Secret, Set-Secret en Get-SecretInfo, ongeacht waar je geheimen werkelijk staan. Daarnaast levert Microsoft met Microsoft.PowerShell.SecretStore een lokale, versleutelde kluis mee.

Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore
Register-SecretVault -Name LokaleKluis -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
Set-Secret -Name ApiKey -Secret 'abc123'
$key = Get-Secret -Name ApiKey -AsPlainText

De kracht zit vooral in de abstractie. Je script vraagt simpelweg om Get-Secret -Name ApiKey, terwijl een extensiemodule bepaalt waar dat geheim vandaan komt. Er bestaan extensies voor onder meer Azure Key Vault, KeePass en ook 1Password. De 1Password-extensie is een communitymodule, dus test die eerst grondig voordat je erop bouwt.

Toch blijft een lokale SecretStore per machine beperkt. Werk je in een team, dan wil je dat iedereen dezelfde, actuele geheimen gebruikt. Daarom kijken we nu naar de officiële 1Password CLI.

1Password CLI installeren en koppelen aan PowerShell

1Password levert een eigen commandlinetool: op. Deze tool werkt op Windows, macOS en Linux en laat zich dus moeiteloos vanuit PowerShell aanroepen. Op Windows installeer je hem het snelst met winget:

winget install AgileBits.1Password.CLI
op --version

Vervolgens koppel je de CLI aan de 1Password-desktopapp. Open daarvoor in de app de instellingen, ga naar Developer en zet de integratie met de 1Password CLI aan. Het grote voordeel: je ontgrendelt de CLI voortaan met Windows Hello, Touch ID of je systeemwachtwoord. Je hoeft je hoofdwachtwoord dus nooit in een terminal te typen.

Controleer daarna of alles werkt:

op signin
op whoami
op vault list

Zie je je kluizen staan? Dan is de basis klaar en kun je geheimen gaan ophalen.

Secrets ophalen: op item get, op read en secret references

De CLI biedt twee hoofdroutes om een geheim op te vragen. Allereerst haal je met op item get een compleet item op. Combineer dat met --format json, want PowerShell zet JSON met ConvertFrom-Json direct om naar objecten:

$item = op item get 'SQL-prod' --vault 'Infra' --format json | ConvertFrom-Json
$item.fields | Select-Object label, type

Daarnaast, en dat is vaak handiger, gebruik je op read met een zogenoemde secret reference. Zo’n verwijzing heeft de vorm op://<kluis>/<item>/<veld>. Het mooie daarvan: de verwijzing zelf bevat geen geheim, dus je mag hem gerust in je script of in Git opnemen.

Met twee regels bouw je zo een volwaardig PSCredential-object:

$user = op read 'op://Infra/SQL-prod/username'
$pass = op read 'op://Infra/SQL-prod/password' | ConvertTo-SecureString -AsPlainText -Force
$cred = [pscredential]::new($user, $pass)

Het wachtwoord bestaat nu alleen nog kort in het geheugen van je sessie. Gebruik je dit patroon vaker? Verpak het dan in een eigen functie, bijvoorbeeld:

function Get-OPCredential {
    param([Parameter(Mandatory)][string]$Vault, [Parameter(Mandatory)][string]$Item)
    $user = op read "op://$Vault/$Item/username"
    $pass = op read "op://$Vault/$Item/password" | ConvertTo-SecureString -AsPlainText -Force
    [pscredential]::new($user, $pass)
}

$cred = Get-OPCredential -Vault Infra -Item SQL-prod

Let wel op: controleer na elke op-aanroep $LASTEXITCODE. Een vergrendelde kluis of een typfout in de verwijzing levert anders stilletjes een lege waarde op, terwijl je script gewoon doorloopt.

op run en op inject: geheimen zonder sporen

Soms wil je een bestaand script helemaal niet aanpassen. In dat geval biedt op run uitkomst. Je zet secret references in omgevingsvariabelen of in een .env-bestand, en op run vervangt die verwijzingen pas op het moment dat het script start:

# .env
# DB_USER=op://Infra/SQL-prod/username
# DB_PASS=op://Infra/SQL-prod/password

op run --env-file .env -- pwsh -File .\Backup-Database.ps1

Binnen het script lees je de waarden simpelweg uit met $env:DB_USER en $env:DB_PASS. Zodra het proces stopt, verdwijnen ze weer. Bovendien maskeert op run geheimen standaard in de uitvoer, zodat ze niet per ongeluk in een log terechtkomen.

Daarnaast bestaat op inject. Deze opdracht vult een sjabloonbestand met echte waarden, bijvoorbeeld een configuratiebestand voor een applicatie:

op inject -i appsettings.template.json -o appsettings.json

Hier geldt echter een duidelijke waarschuwing: het resultaat staat wél als platte tekst op schijf. Gebruik op inject daarom alleen als een applicatie echt een bestand verwacht, zet het uitvoerbestand in .gitignore en ruim het na gebruik op.

Automatisering met service accounts

De desktopintegratie werkt uitstekend op je eigen werkplek. Een geplande taak om drie uur ‘s nachts kan echter geen vingerafdruk scannen. Daarvoor gebruik je een 1Password-service account.

Een service account maak je aan in de beheeromgeving van 1Password. Vervolgens geef je het account alleen toegang tot de kluizen die het werkelijk nodig heeft, liefst met alleen leesrechten. Je ontvangt daarna een token. Zodra de omgevingsvariabele OP_SERVICE_ACCOUNT_TOKEN gevuld is, gebruikt de CLI automatisch dat account:

$env:OP_SERVICE_ACCOUNT_TOKEN = '<token uit je CI-secret of beveiligde opslag>'
$cred = Get-OPCredential -Vault Automation -Item SQL-backup

Dat token is uiteraard zelf ook een geheim. Zet het dus nooit in het script. Sla het in plaats daarvan op in de secret store van je pipeline, zoals GitHub Actions secrets of Azure DevOps variable groups. Op een Windows-server kun je het bijvoorbeeld via SecretManagement ophalen. Zo hoef je maar één geheim te beheren, terwijl al het andere centraal in 1Password blijft.

Tot slot houdt 1Password bij welk service account welk item heeft opgevraagd. Daardoor krijg je eindelijk zicht op wie wat gebruikt, iets wat bij XML-bestanden onmogelijk is.

Let op: wat kan een script eigenlijk zien?

Zodra jij de CLI ontgrendelt, heeft die terminalsessie dezelfde toegang als jij in de app. Een simpele lus over op item list en op item get trekt dus je complete kluis leeg. Dat is geen lek, want de CLI is gewoon een andere ingang naar je eigen account. Toch moet je je daar bewust van zijn.

1Password beperkt dat risico wel. Elk nieuw terminalvenster vraagt namelijk opnieuw om biometrische goedkeuring. Bovendien verloopt de autorisatie na 10 minuten inactiviteit en uiterlijk na 12 uur. Vergrendel je de app, dan vervalt alle toegang direct (bron). Daarnaast sluit je zelf af met op signout.

Het echte gevaar zit daarom in wat je zelf binnen dat venster draait. Een onbekend script uit een forum kan in een ontgrendelde terminal alles uitlezen. Draai zulke scripts dus altijd in een schoon venster.

Voor automatisering kies je bovendien nooit je persoonlijke account. Service accounts kunnen namelijk niet bij je Personal-kluis. Maak daarom een aparte kluis, bijvoorbeeld Automation, met alleen de items die je scripts echt nodig hebben:

op vault create Automation

Koppel daar vervolgens een service account aan met alleen leesrechten. Zo ziet een script nooit meer dan die ene kluis, ook niet als er iets misgaat.

Best practices en conclusie

Wil je dit alles in de praktijk brengen? Houd dan de volgende vuistregels aan:

  1. Zet nooit een wachtwoord, token of API-sleutel in een script, ook niet tijdelijk.
  2. Gebruik secret references (op://…) in plaats van geheimen; die mag je gewoon in Git opnemen.
  3. Kies op run als je een bestaand script ongewijzigd wilt laten, en op read als je zelf een PSCredential bouwt.
  4. Geef service accounts alleen toegang tot de kluizen die ze nodig hebben, bij voorkeur alleen-lezen.
  5. Controleer altijd $LASTEXITCODE na een op-aanroep, zodat een mislukte opvraging niet ongemerkt blijft.
  6. Vermijd Write-Host $pass en Start-Transcript rond gevoelige code, want transcripts leggen alles vast.
  7. Roteer wachtwoorden regelmatig; dankzij 1Password pas je ze op één plek aan en pakken al je scripts de nieuwe waarde direct op.

Kortom: PowerShell geeft je met PSCredential en SecretManagement een solide basis, maar pas in combinatie met een centrale wachtwoordkluis wordt het echt veilig. De 1Password CLI vult precies dat gat. Je scripts bevatten voortaan alleen nog verwijzingen, je team deelt één actuele bron van waarheid en je ziet precies wie welk geheim gebruikt. Begin dus vandaag met één script, vervang het hardgecodeerde wachtwoord door op read en breid het daarna stap voor stap uit.

0 Shares: