top of page
Search

Provisioning Wasabi S3 Buckets, IAM Users, and Policies with PowerShell and AWS SigV4

  • Writer: Josue Valentin
    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:


  1. Create bucket only

  2. Create IAM policy

  3. Create IAM user

  4. Create folders only

  5. 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.


 
 
 

Recent Posts

See All

Comments


IT Services

Fully Managed IT Solutions
Custom IT Services
Cyber Security Services
Business Phone Systems

Contact Info

Address:
Chicopee, Massachusetts 01013
Email: sales
@technetne.com

Social

  • Facebook
  • LinkedIn
Minority Own Business

Copyright © 2025 TechNet New England. All Rights Reserved. | Terms and Conditions | Privacy Policy

bottom of page