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.

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:
$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.
Install-Module -Name AWS.Tools.IdentityManagement, AWS.Tools.SecurityToken -Scope CurrentUser -Force
Import-Module AWS.Tools.IdentityManagement, AWS.Tools.SecurityTokenCreate 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.
$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 $trustAssume 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:
# 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:
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.
$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
Remove-IAMRole -RoleName denytest -ForceRun 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 actionThe 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/denytestSTS 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