Provisioning Wasabi S3 Buckets, IAM Users, and Policies with PowerShell and AWS SigV4
- Josue Valentin
- Jun 18
- 5 min read
If you regularly onboard new clients or handle backups in environments using S3-compatible storage like Wasabi, you have probably experienced the hassle of manually creating buckets, setting up IAM policies, and generating API credentials. It is repetitive, time-consuming, and prone to mistakes.
To streamline this, we built a PowerShell script that provisions all of these components automatically using AWS Signature Version 4 (SigV4) authentication. This guide explains what the script does, how it works, and how you can adapt it to your environment.
What the Script Does
This PowerShell script helps you automate the following tasks on a Wasabi S3-compatible platform:
Create an Object Lock-enabled S3 bucket
Generate a dedicated IAM policy with access to that bucket
Create an IAM user and attach the policy
Generate API access keys for the new user
Create folders used by backup tools such as Veeam
The script is interactive. It prompts for a customer short name, builds resource names automatically, and provides a menu to either perform a specific task or run all steps.
Secure Credentials
The script requires access credentials for a privileged account. These should not be hardcoded. Use environment variables or prompt securely. In the public version of this script, access and secret keys are placeholders and must be handled safely in production.
Folder Structure for Backups
The script creates the following folders inside the S3 bucket:
veeam_filelvl_bk/
veeam_image_bk/
These are typical folder names used for file-level and image-based backups with tools like Veeam.
Custom AWS SigV4 Authentication
Instead of relying on external modules, the script includes a self-contained function to generate SigV4 headers for REST API calls. This enables it to:
Create Object Lock-enabled buckets
Make IAM API requests for users and policies
Generate and attach access keys securely
This makes the script portable and ideal for automated provisioning where lightweight dependencies are preferred.
Menu Workflow
After entering the customer name, the script dynamically creates names for the bucket, user, and policy. You can then select one of five options:
Create bucket only
Create IAM policy
Create IAM user
Create folders only
Provision all
This allows you to run only the parts you need depending on your progress in onboarding.
Error Handling
The script includes logic to:
Detect if the bucket or user already exists
Catch and log errors from failed API calls
Synchronize system time to avoid signature errors
Before You Use It
To safely test this script:
Replace credential placeholders with secure credential management
Adjust folder names to match your backup software
Expand policy scope if your application requires more permissions
Full Script
# === PROMPT FOR CUSTOMER NAME ===
$customer = Read-Host -Prompt "Enter the customer short name (e.g. 'clientname', 'bucketname')"
$envTag = "prod"
$monthTag = (Get-Date).ToString("yyyyMM")
# === MENU SELECTION ===
Write-Host "\nSelect action to perform:" -ForegroundColor Cyan
Write-Host " 1 - Create bucket only"
Write-Host " 2 - Create policy"
Write-Host " 3 - Create user"
Write-Host " 4 - Create folders only"
Write-Host " 5 - Provision all (bucket, user, policy, access keys)"
$choice = Read-Host "Enter your selection (1-5)"
# === DYNAMICALLY BUILD NAMES ===
$bucketName = "$($customer.ToLower())-bucket"
$userName = "user-$bucketName"
$policyName = "policy-$bucketName-access"
$veeamFolder1 = "veeam_filelvl_bk/"
$veeamFolder2 = "veeam_image_bk/"
# === PREDEFINED CREDENTIALS AND SETTINGS ===
$accessKey = "ACCESSKEYHERE"
$secretKey = "SECRETKEYHERE"
$region = "us-east-1"
$s3Host = "s3.wasabisys.com"
$iamHost = "iam.wasabisys.com"
# === TIME SYNC ===
try { w32tm /resync | Out-Null; Start-Sleep -Seconds 2; Write-Host "System time synchronized." } catch { Write-Warning "Time sync failed. Run PowerShell as Administrator." }
# === DEPENDENCIES ===
Add-Type -AssemblyName System.Web
Add-Type -AssemblyName System.Security
# === SIGV4 SIGNER ===
function Get-SigV4AuthHeader {
param(
[string]$method, [string]$targetHost, [string]$uri,
[string]$region, [string]$service, [string]$payload
)
$dateTime = [DateTime]::UtcNow
$amzDate = $dateTime.ToString("yyyyMMddTHHmmssZ")
$dateStamp = $dateTime.ToString("yyyyMMdd")
$payloadHash = [System.BitConverter]::ToString((New-Object Security.Cryptography.SHA256Managed).ComputeHash([Text.Encoding]::UTF8.GetBytes($payload))).Replace("-", "").ToLower()
$canonicalHeaders = "host:$targetHost`n" + "x-amz-content-sha256:$payloadHash`n" + "x-amz-date:$amzDate`n"
$signedHeaders = "host;x-amz-content-sha256;x-amz-date"
$canonicalRequest = "$method`n$uri`n`n$canonicalHeaders`n$signedHeaders`n$payloadHash"
$hashedCanonicalRequest = [System.BitConverter]::ToString((New-Object Security.Cryptography.SHA256Managed).ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalRequest))).Replace("-", "").ToLower()
$credentialScope = "$dateStamp/$region/$service/aws4_request"
$stringToSign = "AWS4-HMAC-SHA256`n$amzDate`n$credentialScope`n$hashedCanonicalRequest"
function HmacSHA256 ($key, $msg) {
$hmac = New-Object System.Security.Cryptography.HMACSHA256
$hmac.Key = $key
return $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($msg))
}
$kDate = HmacSHA256 ([Text.Encoding]::UTF8.GetBytes("AWS4$secretKey")) $dateStamp
$kRegion = HmacSHA256 $kDate $region
$kService = HmacSHA256 $kRegion $service
$kSigning = HmacSHA256 $kService "aws4_request"
$signature = [System.BitConverter]::ToString((HmacSHA256 $kSigning $stringToSign)).Replace("-", "").ToLower()
return @{
"Authorization" = "AWS4-HMAC-SHA256 Credential=$accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature"
"x-amz-date" = $amzDate
"x-amz-content-sha256" = $payloadHash
}
}
# === ACTION LOGIC ===
if ($choice -eq "1" -or $choice -eq "5") {
$checkUri = "https://$s3Host/$bucketName"
try {
Invoke-RestMethod -Uri $checkUri -Method HEAD -ErrorAction Stop | Out-Null
Write-Host "Bucket '$bucketName' already exists. Skipping creation."
} catch {
Write-Host "Creating Object Lock-enabled bucket '$bucketName'..."
$headers = Get-SigV4AuthHeader -method "PUT" -targetHost $s3Host -uri "/$bucketName" -region $region -service "s3" -payload ""
$headers["x-amz-bucket-object-lock-enabled"] = "true"
try {
Invoke-RestMethod -Uri "https://$s3Host/$bucketName" -Method PUT -Headers $headers -Body ""
Write-Host "Bucket created with Object Lock support."
} catch {
Write-Warning "Bucket creation failed: $($_.Exception.Message)"
}
}
}
if ($choice -eq "4") {
$checkUri = "https://$s3Host/$bucketName"
try {
Invoke-RestMethod -Uri $checkUri -Method HEAD -ErrorAction Stop | Out-Null
Write-Host "Bucket '$bucketName' exists. Proceeding to create folders..."
foreach ($folder in @($veeamFolder1, $veeamFolder2)) {
$uri = "/$bucketName/$folder"
$folderUri = "https://$s3Host$uri"
$headers = Get-SigV4AuthHeader -method "PUT" -targetHost $s3Host -uri $uri -region $region -service "s3" -payload ""
try {
Invoke-RestMethod -Uri $folderUri -Method PUT -Headers $headers -Body ""
Write-Host "Folder '$folder' created."
} catch {
Write-Warning "Folder creation failed: $($_.Exception.Message)"
}
}
} catch {
Write-Warning "Bucket '$bucketName' not found. Cannot create folders."
}
}
if ($choice -eq "2" -or $choice -eq "5") {
$encodedPolicy = [System.Web.HttpUtility]::UrlEncode(@"
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::$bucketName" },
{ "Effect": "Allow", "Action": "s3:*", "Resource": ["arn:aws:s3:::$bucketName", "arn:aws:s3:::$bucketName/*"] }
]
}
"@)
$body = "Action=CreatePolicy&Version=2010-05-08&PolicyName=$policyName&PolicyDocument=$encodedPolicy"
$headers = Get-SigV4AuthHeader -method "POST" -targetHost $iamHost -uri "/" -region $region -service "iam" -payload $body
try {
Invoke-RestMethod -Uri "https://$iamHost/" -Method POST -Body $body -Headers $headers -ContentType "application/x-www-form-urlencoded"
Write-Host "Policy '$policyName' created."
} catch {
if ($_.Exception.Message -like '*EntityAlreadyExists*') {
Write-Host "Policy '$policyName' already exists. Skipping."
} else {
Write-Warning "Policy creation failed: $($_.Exception.Message)"
}
}
}
if ($choice -eq "3" -or $choice -eq "5") {
$body = "Action=CreateUser&Version=2010-05-08&UserName=$userName"
$headers = Get-SigV4AuthHeader -method "POST" -targetHost $iamHost -uri "/" -region $region -service "iam" -payload $body
try {
Invoke-RestMethod -Uri "https://$iamHost/" -Method POST -Body $body -Headers $headers -ContentType "application/x-www-form-urlencoded"
Write-Host "User '$userName' created."
} catch {
if ($_.Exception.Message -like '*EntityAlreadyExists*') {
Write-Host "User '$userName' already exists. Skipping."
} else {
Write-Warning "User creation failed: $($_.Exception.Message)"
}
}
$attachBody = "Action=AttachUserPolicy&Version=2010-05-08&UserName=$userName&PolicyArn=arn:aws:iam::wasabi:$policyName"
$headers = Get-SigV4AuthHeader -method "POST" -targetHost $iamHost -uri "/" -region $region -service "iam" -payload $attachBody
try {
Invoke-RestMethod -Uri "https://$iamHost/" -Method POST -Body $attachBody -Headers $headers -ContentType "application/x-www-form-urlencoded"
Write-Host "Policy attached to user."
} catch {
Write-Warning "Policy attachment failed: $($_.Exception.Message)"
}
$keyBody = "Action=CreateAccessKey&Version=2010-05-08&UserName=$userName"
$headers = Get-SigV4AuthHeader -method "POST" -targetHost $iamHost -uri "/" -region $region -service "iam" -payload $keyBody
try {
$resp = Invoke-RestMethod -Uri "https://$iamHost/" -Method POST -Body $keyBody -Headers $headers -ContentType "application/x-www-form-urlencoded"
Write-Host "Access Key ID: $($resp.CreateAccessKeyResponse.CreateAccessKeyResult.AccessKey.AccessKeyId)"
Write-Host "Secret Key: $($resp.CreateAccessKeyResponse.CreateAccessKeyResult.AccessKey.SecretAccessKey)"
} catch {
Write-Warning "Access key creation failed: $($_.Exception.Message)"
}
}
Final Notes
If you manage multiple clients or provision cloud storage regularly, this script will save hours of manual setup. It is especially useful for MSPs and IT administrators using Wasabi or other S3-compatible storage services.
Comments