Loosely Decoupled
GuideAWS

Test AccessDenied in AWS PowerShell with a Role That Has No Permissions

Watch your script handle a real AccessDenied, raised with a throwaway role’s temporary keys, without changing your own permissions or writing any data.

A faceless figure in a plain coat tries the handles of a row of shut doors in a dim corridor while a man in a hoodie watches calmly with a coffee. A small tag on the coat is cyan.
On this page

TL;DR

Create a role with no permissions that your account can assume, switch the session to its temporary keys, and run the script. Every call it makes that needs a permission is denied for real:

PowerShell
$identity  = Get-STSCallerIdentity
$partition = $identity.Arn.Split(':')[1]
$account   = $identity.Account
$trust = '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:' + $partition + ':iam::' + $account + ':root"},"Action":"sts:AssumeRole"}]}'
New-IAMRole -RoleName denytest -AssumeRolePolicyDocument $trust
$previous = Get-AWSCredential
try {
    # A new role took about eight seconds to become assumable in testing.
    Start-Sleep -Seconds 8
    $delay = 1
    foreach ($attempt in 1..6) {
        try {
            $keys = (Use-STSRole -RoleArn "arn:${partition}:iam::${account}:role/denytest" -RoleSessionName denytest -ErrorAction Stop).Credentials
            break
        }
        catch {
            # Retry only the denial a new role gives; rethrow anything else, and the last denial.
            if ($_.Exception.InnerException.ErrorCode -ne 'AccessDenied' -or $attempt -eq 6) { throw }
            Start-Sleep -Seconds $delay
            $delay *= 2
        }
    }
    Set-AWSCredential -Credential $keys
    ./Your-Script.ps1
}
finally {
    # Restore your own credentials first; deleting the role needs them.
    if ($previous) { Set-AWSCredential -Credential $previous } else { Clear-AWSCredential }
    Remove-IAMRole -RoleName denytest -Force
}

AWS.Tools wraps the AWS error in an InvalidOperationException; the AccessDenied code is on $_.Exception.InnerException, not on $_.Exception.

A script that calls AWS will one day run with credentials that can’t do what it asks: a new role missing one permission, a scheduled task under the wrong profile. What it does then is usually untested, because the identity the script was written with can do everything.

I hit this while writing How to Publish a Custom CloudWatch Metric from PowerShell. Its common failures needed the exact message Write-CWMetricData prints when the caller isn’t allowed to publish. Removing a permission from my account to see one error was out, and so was publishing a real metric to watch it fail. This is the trick I used instead, and the exception details in this article come from those Write-CWMetricData runs.

A role with no permissions is a second identity that IAM denies for every action that needs a permission. A call made with its keys comes back with the genuine AccessDenied, and nothing is written or changed. The IAM Policy Simulator says whether a call would be denied; it doesn’t produce the exception a catch block has to handle.

Install the IAM and STS modules

New-IAMRole and Remove-IAMRole are in AWS.Tools.IdentityManagement; Get-STSCallerIdentity and Use-STSRole are in AWS.Tools.SecurityToken.

PowerShell
Install-Module -Name AWS.Tools.IdentityManagement, AWS.Tools.SecurityToken -Scope CurrentUser -Force
Import-Module AWS.Tools.IdentityManagement, AWS.Tools.SecurityToken

Create a role with no permissions

The trust policy decides who can assume the role. The partition and account come from your own ARN, so the same lines work in the standard, China and GovCloud partitions. Naming your account’s root lets any identity in the account with sts:AssumeRole permission assume the role. Yours can if its policy allows sts:AssumeRole on this role; an administrator’s does. No permission policy is attached, so the role is denied everything except calls that need no permission, such as Get-STSCallerIdentity, and resources whose own policy grants access to anyone.

PowerShell
$identity  = Get-STSCallerIdentity
$partition = $identity.Arn.Split(':')[1]
$account   = $identity.Account
$trust = '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:' + $partition + ':iam::' + $account + ':root"},"Action":"sts:AssumeRole"}]}'
New-IAMRole -RoleName denytest -AssumeRolePolicyDocument $trust

Assume the role for temporary keys

Use-STSRole returns temporary credentials for the role; keep the Credentials property. A new role isn’t assumable straight away: in my test the first attempt was denied and the role became assumable about eight seconds later. Wait that long before the first call, which would otherwise be denied for certain, then retry with a backoff to cover a slower day. The loop waits 1, 2, 4, 8 and 16 seconds between attempts, retries only AccessDenied, and rethrows after the sixth attempt so a role you can’t assume fails instead of looping:

PowerShell
# A new role took about eight seconds to become assumable in testing.
Start-Sleep -Seconds 8
$delay = 1
foreach ($attempt in 1..6) {
    try {
        $keys = (Use-STSRole -RoleArn "arn:${partition}:iam::${account}:role/denytest" -RoleSessionName denytest -ErrorAction Stop).Credentials
        break
    }
    catch {
        # Retry only the denial a new role gives; rethrow anything else, and the last denial.
        if ($_.Exception.InnerException.ErrorCode -ne 'AccessDenied' -or $attempt -eq 6) { throw }
        Start-Sleep -Seconds $delay
        $delay *= 2
    }
}

Catch AccessDenied in the script

The cmdlet throws a System.InvalidOperationException whose InnerException is the AWS exception. That inner exception is an Amazon.Runtime.AmazonServiceException carrying ErrorCode AccessDenied, StatusCode Forbidden and ErrorType Sender. Test the inner exception, handle the denial, and rethrow anything else. $datum is the metric from your own script:

PowerShell
try {
    Write-CWMetricData -Namespace 'MyApp/Queue' -MetricData $datum -ErrorAction Stop
}
catch {
    $awsError = $_.Exception.InnerException
    if ($awsError -is [Amazon.Runtime.AmazonServiceException] -and $awsError.ErrorCode -eq 'AccessDenied') {
        Write-Warning "No permission to publish the metric: $($awsError.Message)"
    }
    else {
        throw
    }
}

Run the script as the role

Set-AWSCredential -Credential makes the role’s keys the default for the session, so the script runs unmodified. Every call in it that needs a permission is denied. Get-AWSCredential with no arguments returns the session’s current credentials, so save them first and put them back after. Clear-AWSCredential alone would only remove the role’s keys, not restore what was there before, and the Remove-IAMRole that follows needs your own access. When nothing was set, clearing is right: later calls fall back to your default profile or environment. The restore sits in finally because the script is expected to fail: without it, an unhandled denial skips the restore, the session stays on the role’s keys, and the cleanup that follows is denied too. The script’s error still reaches you.

PowerShell
$previous = Get-AWSCredential
Set-AWSCredential -Credential $keys
try { ./Your-Script.ps1 }
finally { if ($previous) { Set-AWSCredential -Credential $previous } else { Clear-AWSCredential } }

To deny a single call instead, pass the keys to that cmdlet with -Credential $keys.

Delete the role

PowerShell
Remove-IAMRole -RoleName denytest -Force

Run this with your own credentials restored. In a script, put it in the same finally as the restore, after it, as the answer block at the top does. The role is then deleted even when the script under test or the assume fails, and the error still reaches you.

The temporary keys expire on their own, and the role they belonged to is gone.

Expected result

The script’s handler runs instead of an unhandled error. Without a handler, the call fails with the error an under-permissioned identity gets:

Error

Write-CWMetricData: User: arn:aws:sts::<account-id>:assumed-role/denytest/denytest is not authorized to perform: cloudwatch:PutMetricData because no identity-based policy allows the cloudwatch:PutMetricData action

The message names the assumed role and session, the refused action and the reason. Replace the account ID before you share it.

After the role is deleted, a second Remove-IAMRole confirms it’s gone:

Error

Remove-IAMRole: The role with name denytest cannot be found.

Common failures

Use-STSRole is denied

Error

User: arn:aws:iam::<account-id>:user/<user-name> is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::<account-id>:role/denytest

STS prints the same message whether the role was created seconds ago, doesn’t exist, or exists but won’t let you in, so the message alone doesn’t say which. Check them in that order: let the backoff run after New-IAMRole, confirm the role with Get-IAMRole -RoleName denytest, then confirm your own policy allows sts:AssumeRole on it. The inner exception is an Amazon.SecurityToken.AmazonSecurityTokenServiceException with ErrorCode AccessDenied, wrapped the same way as CloudWatch’s.

ErrorCode is empty in the catch block

$_.Exception is the InvalidOperationException wrapper, which carries no AWS fields. Read ErrorCode, StatusCode and Message from $_.Exception.InnerException. A typed catch [Amazon.CloudWatch.AmazonCloudWatchException] does match the wrapped error, so the type works as a filter.

Script still runs with full access

The script sets its own credentials, with -ProfileName or Set-AWSCredential inside it, which override the session default. If the script takes a credential parameter, pass $keys to it; otherwise run its calls with -Credential $keys.

Tested with

PowerShell
7.6.6
AWS.Tools
5.0.311
OS
macOS 27.0
Date
October 2026

New articles, by email.

A short note when something new is published.