Fix AWS EC2 "Not Authorized to Perform ec2:RunInstances" Error

Logeshwaran.C

"Not authorized to perform: ec2:RunInstances" almost never means what people assume it means. It rarely means you lack permission to launch EC2 instances in general — it usually means you're missing permission for one specific piece of the launch (the AMI, the subnet, the security group, or the IAM role you're attaching), or that something above your own policy — a service control policy, a permissions boundary, or a missing iam:PassRole grant — is quietly overriding an identity policy that looks completely fine. The counterintuitive part: you can have AmazonEC2FullAccess attached, see it in the console, and still get this exact error, because RunInstances checks permissions on every resource involved in the launch, not just the action itself.

⚡ Quick Answer

Step 1 → Decode the real reason with aws sts decode-authorization-message

Step 2 → Check for a Deny in a service control policy, a permissions boundary, or a missing iam:PassRole grant, not just the RunInstances action itself

If the decoded message names a resource ARN (an AMI, subnet, or security group) rather than the action, jump straight to Cause 5 — that's the one almost nobody checks first.

What this error is actually telling you

Jake runs a small phone repair and resale shop, and last spring he tried to spin up a cheap EC2 instance to test a point-of-sale app before rolling it out to his three registers. The AWS CLI threw back this wall of text:

An error occurred (UnauthorizedOperation) when calling the RunInstances operation: You are not authorized to perform this operation. Encoded authorization failure message: ...

‍♂️ Jake's Reality Check

"I have the EC2 full access policy attached. It says right there, 'Allow, ec2:*'. How is this still happening?"

Because RunInstances isn't one permission check — it's a bundle of them. Launching an instance touches the AMI, the instance itself, possibly a subnet and network interface, possibly a volume, possibly an IAM instance profile. Amazon Web Services checks your permissions against every one of those resources, and any single missing piece throws the exact same generic-sounding error.

Ethan, who's helped Jake untangle AWS problems for years now, put it a different way when Jake called him about it: "It's like your building keycard letting you into the lobby but not the server room. The card reader doesn't say 'wrong floor' — it just says 'access denied,' and you're standing there guessing which door it meant."

That's the core thing to understand before you touch a single policy: "UnauthorizedOperation" on RunInstances is a symptom, not a diagnosis. Amazon EC2's RunInstances API supports resource-level permissions — meaning a policy can grant or deny access to the AMI, the subnet, the security group, the key pair, the volume, and the resulting instance separately. On top of your own identity-based policy, AWS Organizations service control policies (SCPs), IAM permissions boundaries, and session policies can all layer a Deny on top, and any explicit Deny anywhere in that stack wins — no matter how permissive your own policy looks.

Before you change anything, get the real reason. That's what the next section is for, and it's the one step almost everybody skips because it looks intimidating the first time.

Step one: decode the message before you touch any policy

If you launched through the AWS CLI or an SDK, the error includes a long, garbled block of text labeled "Encoded authorization failure message." It looks like nonsense on purpose — it's not meant to be read by a human directly. AWS Security Token Service (STS) can turn it back into a plain-English explanation of exactly which action failed and on exactly which resource.

What STS even means here

STS is the AWS service that hands out short-lived security credentials and does identity-related housekeeping tasks like this one. You don't need to understand the whole service to use this one command from it — think of it as a translator sitting between you and AWS's internal decision log.

  1. Copy the encoded string exactly as it appears in the error, including any trailing characters. It's long — often several hundred characters — and a single dropped character breaks the decode.
  2. Run the decode command from the same IAM user or role that hit the error:
    aws sts decode-authorization-message --encoded-message "PASTE_THE_STRING_HERE"
  3. Read the decoded JSON. It names the exact action (usually still ec2:RunInstances, sometimes iam:PassRole instead), the specific resource ARN it was checked against, and whether the request failed because of an explicit Deny or because no policy allowed it at all.
  4. If the decode itself fails with an AccessDenied error mentioning sts:DecodeAuthorizationMessage, that permission is also missing from your identity — see FAQ 15 below for the workaround.

One important quirk that trips people up: you generally have to decode the message using the same principal (user or role) that received it. If a teammate with broader access tries to decode your error on your behalf using their own credentials, it can fail. Have them add the sts:DecodeAuthorizationMessage permission to your role instead, or run the decode as you.

If you launched from the AWS Management Console, there's no encoded message to copy — the console shows a shorter, plainer denial instead, sometimes with a "View additional details in the AWS CloudTrail console" link. That takes you to the CloudTrail event for the failed call, which records the same information the encoded message would have given you, just formatted differently.

 What the decoded message actually contains

  • The requesting principal's full ARN (which user or assumed role made the call)
  • The exact action that was denied — sometimes it's not RunInstances at all, but a dependent permission like iam:PassRole
  • The exact resource ARN the check failed against — an AMI, a subnet, an instance, a volume, or a network interface
  • Whether the denial came from an explicit Deny statement (and which policy type: identity-based, SCP, or resource-based) or from there simply being no matching Allow anywhere

Once you have that, you're not guessing anymore. Everything from here is about matching the specific cause the decoded message points to. Below are the six places this actually comes from, roughly in the order they show up in real support threads.

Cause 1: your identity-based policy simply doesn't allow ec2:RunInstances

This is the plain, boring version, and it's still the most common one. IAM denies every action by default. That means an IAM user or role only gets to do something if a policy attached to it — directly, through a group, or through a role it assumed — contains an explicit Allow statement for that exact action. No Allow means an "implicit deny," and an implicit deny produces the exact same UnauthorizedOperation error as an explicit one.

The decoded message for this case reads something close to: "User: arn:aws:iam::123456789012:user/yourname is not authorized to perform: ec2:RunInstances on resource: arn:aws:ec2:us-east-1::image/ami-0abc123 because no identity-based policy allows the ec2:RunInstances action." Notice there's no mention of a Deny — because there isn't one. There's just nothing granting it.

The minimum policy that actually works

A policy that grants only ec2:RunInstances and nothing else will still fail, because RunInstances touches several resource types in one call. AWS's own example policy for this exact situation bundles RunInstances with the actions you need to manage what you just launched:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeImages",
        "ec2:DescribeKeyPairs",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeAvailabilityZones",
        "ec2:RunInstances",
        "ec2:TerminateInstances",
        "ec2:StopInstances",
        "ec2:StartInstances"
      ],
      "Resource": "*"
    }
  ]
}

The * wildcard in the Resource field matters here for a specific reason: several of these Describe actions don't support resource-level permissions at all, meaning AWS requires * for them no matter how tightly you want to scope things. RunInstances does support resource-level permissions, though, which is what makes Cause 5 further down possible — and it's why a wide-open policy like this one is a starting point for testing, not something to leave in place long-term.

✅ Why this is the one to use for a first fix

If you just need to confirm RunInstances works at all before scoping it down, attach this policy, retest, and only then move to the tighter, resource-scoped versions later in this post. Confirming the action works before you fight over which resource it should be scoped to saves you from debugging two problems at once.

Cause 2: a service control policy is blocking it for the whole account (or org)

This is the one that makes people go slightly crazy, because it produces a very specific phrase: "with an explicit deny in a service control policy." A service control policy — SCP for short — is a guardrail set at the AWS Organizations level, above individual accounts. Think of it as a rule your company's central IT team writes once and it applies to every account underneath it, whether or not the people in those accounts even know the rule exists.

Here's the part that catches people off guard: your own IAM policy can grant full EC2 access, and it still won't matter. An SCP doesn't add permissions — it can only take them away. If an SCP has an explicit Deny on ec2:RunInstances for a given resource, region, or condition, that Deny overrides every Allow in every identity-based policy in the account, full stop. This is a very common pattern for things like restricting which AMIs are allowed (only company-approved, "golden" images), which regions are permitted, or which instance types can be launched to control cost.

⚠️ What this actually breaks

If the decoded message names "an explicit deny in a service control policy" as the reason, editing your own IAM user or role policy will do nothing — you'll keep hitting the identical error no matter how much access you add yourself, because you cannot personally override an SCP from inside your own account. Only whoever manages the AWS Organizations management account can adjust it.

A real-world example that shows up constantly: an SCP with a condition like "StringNotEquals": {"ec2:Owner": ["111122223333"]} on the image/ami-* resource — this denies RunInstances for any AMI not owned by that one approved account, silently blocking public AMIs, Marketplace AMIs, and even your own personal AMIs, while leaving everything else looking normal. If you're in a large organization and this suddenly started happening on an account that used to work fine, ask whoever owns the Organizations management account whether an SCP changed recently — that's a far shorter conversation than re-auditing your own IAM policy from scratch.

It's worth knowing what an SCP cannot do, too, because it clears up a lot of confused troubleshooting. An SCP never grants a permission by itself — even a wide-open SCP with nothing but Allow statements does nothing on its own, because the account's own identity-based policies still have to grant the action for anyone to actually do it. SCPs only ever narrow the ceiling; they are never the floor. That's a one-directional relationship worth remembering the next time someone suggests "just fix it in the SCP" as a way to grant new access — it won't work, because that isn't what SCPs are for.

Cause 3: a permissions boundary is capping what your policy can grant

Permissions boundaries are frequently confused with SCPs, but they work at a different level: a boundary is attached to an individual IAM user or role, not the whole account. It sets a ceiling on what that specific identity can ever do, no matter what other policies say it can do.

Why the math on this one is confusing

Your effective permissions are the overlap — the intersection — between your identity-based policy and your permissions boundary. Picture two circles on a Venn diagram: one circle is everything your attached policy allows, the other is everything your boundary allows. You only get what falls inside both circles. A permissions boundary never grants anything by itself; it only trims down what an identity policy is already allowed to grant.

So if someone set your boundary to cover only s3:*, cloudwatch:*, and ec2:Describe* — a fairly typical "junior developer" boundary — you can attach the fullest EC2 policy in the world and RunInstances will still be denied, because it's outside the boundary's circle entirely. This is common for contractor accounts, temporary elevated-access roles, and anyone provisioned through an automated onboarding process that defaults to a restrictive boundary "just in case."

You can check whether an identity has a boundary set at all in the IAM console under that user or role's Permissions tab — it's listed separately from attached policies, under "Permissions boundary." If one is set and it doesn't list EC2 actions, that's very likely your answer. A related, easy-to-miss detail: a permissions boundary set on a role also limits everything that role does when something else assumes it — a CI/CD pipeline, an EC2 instance's own instance profile, or an automation tool — so the boundary check applies just as much to non-human callers as it does to a person clicking around the console.

Cause 4: you're missing iam:PassRole for the instance profile

This is arguably the single most common cause once you exclude the plain "no policy at all" case above, and it's the one AWS's own troubleshooting article for this error leads with. If your RunInstances call attaches an IAM instance profile — the mechanism that lets software running on an EC2 instance itself call other AWS services, like an app on the instance reading from an S3 bucket — you need a second, completely separate permission: iam:PassRole on that specific role's ARN.

Here's why that trips people up so often: your policy might grant ec2:RunInstances with a wide-open Resource: "*", and you'll still get denied — because the missing permission isn't an EC2 permission at all. It's an IAM permission, checked as a completely separate step in the same API call. Passing a role to a service is, from AWS's perspective, handing that service the keys to act as that role, so it treats it as security-sensitive enough to require its own explicit grant.

  1. Find the exact instance profile role ARN you're attaching at launch — check your launch command, template, or the console's "IAM instance profile" field.
  2. Add an iam:PassRole statement scoped to that one role, not a wildcard covering every role in the account (a wildcard here is one of the most common privilege-escalation mistakes in AWS — see the Warning box below).
  3. Scope it to the EC2 service specifically using a condition, so the permission can only be used to pass the role to EC2 and nothing else:
{
  "Effect": "Allow",
  "Action": "iam:PassRole",
  "Resource": "arn:aws:iam::123456789012:role/EC2_instance_Profile_role",
  "Condition": {
    "StringEquals": {
      "iam:PassedToService": "ec2.amazonaws.com"
    }
  }
}

⚠️ What this actually breaks

Granting iam:PassRole with Resource: "*" to a user who can also launch EC2 instances means they can pass literally any role in the account to a new instance — including highly privileged administrative roles — then reach into that instance and effectively act as that role. This single overly broad grant is one of the most exploited privilege-escalation paths in real AWS environments, and it's the reason AWS's own managed policies for this pattern always scope PassRole to specific role ARNs with an iam:PassedToService condition attached.

A subtlety worth calling out separately: iam:PassRole isn't unique to EC2. The exact same permission, with the exact same trap, governs handing roles to Lambda functions, ECS tasks, and any other service that runs code on your behalf. If you've fixed this once for RunInstances and later see the identical "not authorized to perform: iam:PassRole" message somewhere else in your account, it's the same underlying mechanic wearing a different service's name.

Cause 5: resource-level permissions are missing on the AMI, subnet, or security group

This is the cause that surprises even people who've written IAM policies for years, and it's the one worth pinning to a mental sticky note: RunInstances is not evaluated as one action against one resource — it's evaluated against every resource type your specific launch touches. AWS added resource-level permissions for RunInstances specifically so that administrators could restrict which AMIs, subnets, security groups, and volume types a user is allowed to combine, not just whether they can launch at all.

What "resource-level" even means here

Most simple IAM actions check one thing: does this identity have permission to do this action on this one resource? RunInstances is unusual because a single call can create or reference up to half a dozen different resource types at once — depending on what you're launching into. That means a policy can allow the action generally but still deny the specific combination you asked for.

Resource type touched by RunInstances When it's involved ARN pattern to grant
image (AMI) Every launch arn:aws:ec2:*::image/*
instance Every launch (the instance being created) arn:aws:ec2:*:*:instance/*
subnet Launching into a VPC arn:aws:ec2:*:*:subnet/*
network-interface Launching into a VPC arn:aws:ec2:*:*:network-interface/*
security-group Every launch into a VPC arn:aws:ec2:*:*:security-group/*
key-pair Only if you specify one at launch arn:aws:ec2:*:*:key-pair/*
volume EBS-backed AMIs (the common case) arn:aws:ec2:*:*:volume/*
launch-template Only if launching from a template arn:aws:ec2:*:*:launch-template/*

If your policy scopes RunInstances down to only the instance resource — a common first attempt at "least privilege" — and leaves out the AMI, the subnet, or the security group ARNs, the launch fails, and the decoded authorization message will point at whichever piece got left out. This is exactly why the boring wide-open Resource: "*" policy from Cause 1 tends to "just work" while a carefully scoped one fails in a way that looks like a bug — it isn't a bug, it's the resource-level check doing exactly what it's designed to do.

‍♂️ Jake's Reality Check

"So if I lock this down to only the instance ARN, thinking that's the 'safe' minimal thing to do, I've actually broken it?"

Yes — and that's the trap. A well-meaning attempt to scope things down tightly, without listing every touched resource type, produces this exact error. Scope by resource type across the board (only these AMIs, only this subnet), not by leaving resource types out entirely.

There's a second, quieter version of this same trap: tag-based conditions on the resources themselves. If your policy allows RunInstances only for subnets carrying a specific tag, and the subnet you're launching into is missing that tag or has it spelled slightly differently, the check fails the exact same way as if the subnet ARN weren't listed at all. Tag-scoped policies are genuinely useful for organizing access by environment or team, but they move the failure point from "the ARN isn't in my policy" to "the tag on the actual resource doesn't match what my policy expects" — which is a much harder thing to spot just by reading the policy JSON, and usually means checking the resource's tags directly in the console rather than the policy alone.

Cause 6: region restrictions and condition keys you didn't know were there

The last major bucket is condition-based denials. A policy — often set by an administrator at the account or org level — can restrict EC2 actions to a specific AWS Region using the global condition key aws:RequestedRegion, or the EC2-specific ec2:Region key. If your CLI profile or SDK client defaults to a different region than the one your policy allows — a very easy mistake when you have multiple named profiles — you'll get the identical UnauthorizedOperation error, with nothing about the actual permission looking wrong at all.

Other condition-based traps that produce this same error message: a policy requiring multi-factor authentication (MFA) for the action via aws:MultiFactorAuthPresent, a policy requiring specific tags be present on the request via aws:RequestTag conditions (common in organizations that enforce cost-allocation tagging at launch time), and policies that restrict which instance types or volume types can be requested. Every one of these surfaces as the same generic denial — the specifics only show up once you decode the message.

A related trap worth flagging on its own: StringEquals conditions are case-sensitive and exact-match by default. A tag value your policy expects as "Production" won't match a resource tagged "production," and a region condition written for "us-east-1" won't match if a script accidentally passes "US-EAST-1." These look like typos in hindsight, but from inside a wall of denied-request text they read identically to a genuinely missing grant, and they're worth double-checking before you assume the policy logic itself is wrong.

Test the fix before you relaunch anything

Once you've identified and patched the actual cause, don't just relaunch and hope. AWS gives you two ways to check your work before you find out live.

The IAM policy simulator

The IAM policy simulator lets you test identity-based policies, permissions boundaries, service control policies, and resource-based policies against a specific action and resource, without actually performing the action. You can simulate policies attached to a real user, group, or role, or paste in custom policy JSON you haven't attached to anything yet. It's the closest thing to a dry run for the entire permission stack — SCPs and boundaries included — rather than just your own identity policy.

⚠️ What this actually breaks

AWS is explicit that policy simulator results can differ from what happens in your live environment, since the simulator doesn't have access to runtime context like the actual state of a resource. Always confirm against your live environment after a simulator pass — treat a passing simulation as "very likely fixed," not "guaranteed fixed."

The DryRun parameter

Nearly every EC2 API action, RunInstances included, accepts a DryRun parameter. Setting it checks whether the call would succeed — permissions and all — without actually creating anything. If you have permission, the call returns a specific "DryRunOperation" response instead of actually launching an instance. If you're still missing permission, you get the same UnauthorizedOperation error you started with, minus the risk of accidentally spinning up (and paying for) a real instance while you're still debugging.

aws ec2 run-instances --dry-run \
  --image-id ami-0abc123 \
  --instance-type t3.micro \
  --subnet-id subnet-0123abcd \
  --security-group-ids sg-0123abcd

This one habit — dry-run first, real launch second — is worth building permanently into how you test any IAM change touching RunInstances, not just this one incident.

Console, CLI, and SDK: does the error mean something different in each?

Underneath, it's the same authorization check no matter which interface you use — the difference is purely in how much of the failure they show you.

Interface What you see How to get the full reason
Management Console A short banner, sometimes with a link to view details in CloudTrail Follow the CloudTrail link, or find the failed RunInstances event manually in CloudTrail Event history
AWS CLI Full error text plus an Encoded authorization failure message Run aws sts decode-authorization-message on the encoded string
SDKs (Python, JS, Java, etc.) The exception object usually carries the same encoded message as a property Log the full exception, extract the encoded message field, decode the same way as the CLI

Tools that automate launches on your behalf — Karpenter for Kubernetes node provisioning, Terraform, CloudFormation, or your own custom automation calling RunInstances — will surface this exact same UnauthorizedOperation error, usually wrapped in whatever logging format that tool uses. The underlying cause and fix are identical; only the presentation changes. If you're chasing this down inside an infrastructure-as-code tool's logs, look specifically for the phrase "not authorized to perform" and the resource ARN right after it — that's the same information described throughout this post, just buried in a longer log line.

Terraform, CloudFormation, CDK, and Pulumi: same error, different symptoms

Infrastructure-as-code tools add a layer of indirection that makes this error more confusing, not less, because the failure surfaces inside the tool's own error formatting rather than as a raw AWS response.

  • Terraform reports the failed RunInstances call as part of the resource creation step, usually printing the full AWS error text (including the encoded authorization message) inside its own "Error: creating EC2 Instance" wrapper. Scroll past Terraform's own formatting to find the raw AWS text and decode it the same way you would from the CLI.
  • AWS CloudFormation is less forthcoming — a failed stack typically shows only "CREATE_FAILED" with a truncated reason in the Events tab. For the full detail, check AWS CloudTrail for the RunInstances event around the same timestamp, since CloudFormation itself doesn't always surface the encoded message.
  • AWS CDK deploys through CloudFormation under the hood, so it inherits the same limitation — the CDK CLI output points you to the failed stack, but the actual authorization detail still lives in CloudTrail.
  • Pulumi generally passes through the underlying AWS SDK error close to verbatim, similar to Terraform, so the encoded message is usually visible directly in the command output.

Whichever tool you're using, the credentials actually making the RunInstances call are whatever the tool is configured to run as — a local CLI profile, an assumed role in CI/CD, or an OIDC-federated role for something like GitHub Actions. It's worth confirming which identity that is before you start editing policies, since it's easy to fix the wrong principal's permissions when a pipeline and a human developer both have separate IAM identities in the same account.

Automating the check so this doesn't eat another afternoon

For anyone who hits this more than once — a platform team standing up new environments regularly, or a CI/CD pipeline that provisions test infrastructure on every run — it's worth wiring the DryRun check into the pipeline itself, ahead of the real launch step, rather than discovering a permissions gap partway through a deploy.

  1. Add a DryRun RunInstances call as an early pipeline step, using the exact same AMI, subnet, security group, and instance profile the real launch will use, so the check exercises the same resource-level permissions the live launch needs.
  2. Fail the pipeline fast on UnauthorizedOperation rather than letting it proceed into a longer provisioning step that will fail later anyway — catching it in a ten-second dry-run beats catching it after twenty minutes of other setup work.
  3. Pipe the encoded authorization message into your pipeline's logs automatically when the dry run fails, so whoever picks up the failure doesn't have to reproduce it manually to see the cause.

None of this replaces getting the underlying policy right — it just means the next time a policy drifts (a permissions boundary gets tightened, an SCP gets updated, an AMI ownership changes), you find out from a fast, isolated pipeline failure instead of a confusing mid-deploy error an hour into a larger job.

When it looks like a permission error but isn't one at all

Ethan's blunt take on this one: "Half the 'I fixed my IAM policy and it still fails' threads I read aren't IAM problems anymore. People keep editing a policy that was already right, because the second error they hit looks similar but isn't."

Two error codes get confused with UnauthorizedOperation constantly, and neither one is fixed by touching IAM at all:

  • VcpuLimitExceeded — you have permission, but launching the requested instance type would push you past your account's vCPU service quota for that instance family. This is a Service Quotas limit, not an authorization failure, and the fix is requesting a quota increase, not editing a policy.
  • InsufficientInstanceCapacity — AWS temporarily doesn't have enough spare capacity of that exact instance type in that exact Availability Zone. This has nothing to do with your account at all; it resolves itself, or you try a different Availability Zone or instance type.

The tell is in the error code itself, not just the human-readable sentence. UnauthorizedOperation or Client.UnauthorizedOperation is the only one this entire post is actually about. If your error code reads differently, everything above will look plausible but won't be the actual cause.

A least-privilege RunInstances policy you can actually use going forward

Once you've confirmed the wide-open version works, the healthy next step is narrowing it — not leaving Resource: "*" in place indefinitely. This version restricts launches to a specific approved AMI and specific subnet, while still granting every resource type RunInstances actually touches so you don't reintroduce Cause 5:

  1. List every resource type your launches actually use — check whether you launch into a VPC (almost always yes today), whether you specify a key pair, and whether you're EBS-backed (almost always yes).
  2. Scope the AMI and subnet ARNs down to the specific approved ones, rather than leaving them wide open, if your team has a defined set of golden images and networks.
  3. Leave security-group, network-interface, and volume ARNs open with a wildcard unless you also have a defined, stable list of approved ones — otherwise legitimate launches with slightly different security group combinations start failing again.
  4. Add the scoped iam:PassRole statement from Cause 4 as its own separate statement, never folded into the EC2 statement.
  5. Retest with DryRun after every change, one change at a time, so you know exactly which edit fixed or broke things.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LaunchApprovedInstances",
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": [
        "arn:aws:ec2:us-east-1::image/ami-0approved123",
        "arn:aws:ec2:us-east-1:123456789012:subnet/subnet-0approved456",
        "arn:aws:ec2:us-east-1:123456789012:network-interface/*",
        "arn:aws:ec2:us-east-1:123456789012:security-group/*",
        "arn:aws:ec2:us-east-1:123456789012:instance/*",
        "arn:aws:ec2:us-east-1:123456789012:volume/*",
        "arn:aws:ec2:us-east-1:123456789012:key-pair/*"
      ]
    },
    {
      "Sid": "PassEC2InstanceRoleOnly",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::123456789012:role/approved-instance-role",
      "Condition": {
        "StringEquals": { "iam:PassedToService": "ec2.amazonaws.com" }
      }
    }
  ]
}

✅ Why this is the one to use long-term

This shape gets you the actual security benefit of least privilege — nobody can launch an unapproved AMI or drop instances into an unapproved subnet — without recreating the exact resource-level trap from Cause 5, because every resource type RunInstances checks is still explicitly present.

Launch templates, Auto Scaling, and other edge cases that add their own wrinkle

If you're launching from a launch template rather than specifying every parameter directly in your RunInstances call, that template itself is a resource with its own ARN — arn:aws:ec2:region:account:launch-template/* — and it needs to be included in your policy's Resource list, on top of everything the template ultimately points to (the AMI, subnet, and so on baked into that template's configuration).

This shows up most often with Kubernetes autoscaling tools, where a controller role assumed by the cluster launches nodes on your behalf using a template, and where an org-level SCP with an explicit deny can silently block node scaling with no visible symptom in Kubernetes itself — just a reconciler error deep in the controller's logs mentioning RunInstances and UnauthorizedOperation. If your Kubernetes nodes suddenly stop scaling up, this exact error chain, on the controller's assumed role rather than a human user, is one of the first things worth checking.

Launching from an EC2 instance itself

A common pattern is one EC2 instance launching another — a jump box or orchestration instance calling RunInstances through the CLI or SDK, using the credentials from its own attached instance profile rather than a person's IAM user. In this case, the identity being checked is that instance profile's role, not any human identity, and the decoded authorization message will name the instance profile's role ARN as the requester. If this worked when the instance was first launched but stopped working later, check whether the instance profile role's policy — or a boundary or SCP affecting it — changed after launch, since instance profile permissions are re-evaluated on every API call, not just fixed at launch time.

Cross-account launches

If you're launching an instance in one account using an AMI owned by a different account, or a subnet shared into your account through AWS Resource Access Manager, both accounts' permissions come into play — your own identity policy in your account, plus whatever permissions the AMI or subnet owner has granted your account to use their resource. A denial here can originate from either side, and the decoded authorization message will still only describe your side of the check, since AWS doesn't expose the other account's policy details to you directly. If a cross-account AMI or shared subnet suddenly stops working with no change on your end, that's the first place to ask the resource owner about.

Launching without a VPC (EC2-Classic legacy accounts)

Very old AWS accounts created before VPC became the default networking model can, in rare cases, still reference EC2-Classic constructs. If you're on one of these legacy accounts, subnet and network-interface ARNs won't apply the same way, and the resource-level permissions you need to grant will look different from the VPC-based table earlier in this post. This is uncommon for anyone whose account is a few years old or newer, but worth ruling out if none of the standard causes above match your situation.

Why "just attach AdministratorAccess" is the wrong instinct — even temporarily

Jake's actual, costed problem here is a real one: a Saturday morning point-of-sale outage with three registers down and a line of customers waiting, and the tempting move under that pressure is always the same — slap AdministratorAccess on the role and sort out the "real" policy later. Ethan's response to that, every time: "Later never comes. And if the actual blocker is an SCP or a boundary, AdministratorAccess on your own identity policy won't even fix it — you'll have burned the emergency-fix energy on the wrong layer."

Decode the message first. It takes under a minute and tells you which of the six causes above you're actually dealing with, which means you fix the right thing on the first try instead of widening access blindly and hoping. That single habit is worth more than any policy template in this post.

The full troubleshooting order, start to finish

  1. Decode the encoded authorization failure message with STS (or pull the CloudTrail event if you launched from the console).
  2. Read whether it names an explicit Deny (check SCPs and permissions boundaries first) or an implicit deny (check your identity policy's Allow statements).
  3. Check which specific action failed — if it's iam:PassRole rather than ec2:RunInstances, go straight to the PassRole fix.
  4. Check which specific resource ARN failed — image, subnet, security-group, volume, network-interface, key-pair, or launch-template — and confirm your policy grants that resource type, not just the action.
  5. Check for condition-based denials: wrong region, missing MFA, missing required tags, or a case-sensitive mismatch in a condition value.
  6. Confirm the fix with the IAM policy simulator, then confirm again with a DryRun call before the real launch.
  7. Once it works, narrow the policy from a wide test version down to a scoped least-privilege version, retesting after each change.
Symptom in the decoded message Likely cause Who can fix it
"no identity-based policy allows"Cause 1: missing AllowYou or your admin, in your own account
"explicit deny in a service control policy"Cause 2: org-level SCPOnly the AWS Organizations management account owner
Effective permission narrower than your attached policyCause 3: permissions boundaryWhoever set the boundary on your user or role
Action is iam:PassRole, not ec2:RunInstancesCause 4: missing PassRoleYou or your admin
Resource ARN is an AMI, subnet, or security groupCause 5: resource-level gapYou or your admin
Condition key mentioned (Region, MFA, RequestTag)Cause 6: condition denialYou (check your region/tags) or your admin

Frequently asked questions

What's the fastest way to see the real reason behind "not authorized to perform ec2:RunInstances"?

Copy the encoded authorization failure message from the CLI or SDK error and run it through aws sts decode-authorization-message using the same identity that hit the error. It names the exact action and resource that failed in under a minute, which is faster than guessing your way through six possible causes.

Does having the AmazonEC2FullAccess policy guarantee RunInstances will work?

No. That policy grants the action itself broadly, but it can still be overridden by a service control policy, a permissions boundary, or a missing iam:PassRole grant if you're attaching an instance profile — none of which your own attached policy controls.

Can I fix this without asking my AWS administrator?

Only if the cause is entirely inside your own identity policy and you have permission to edit it yourself. If the decoded message points to a service control policy or a permissions boundary set by someone else, you cannot override either of those from your own account or identity — that requires whoever manages the organization or set your boundary.

What does "with an explicit deny in a service control policy" mean?

It means an AWS Organizations-level guardrail, set above your individual account, contains a rule that blocks this specific action or resource for everyone under it. An explicit Deny at this level overrides any Allow in your own identity-based policy, no exceptions.

Why does my policy simulator test pass but the real launch still fails?

AWS states directly that policy simulator results can differ from your live environment, since the simulator doesn't account for every piece of runtime context. Always confirm with a real DryRun call after a passing simulation, rather than treating the simulator result as final.

Do I need iam:PassRole even if I'm not choosing an IAM role for the instance?

No. iam:PassRole is only checked when your RunInstances call attaches an IAM instance profile. If you're launching without one, that permission never enters the picture, and this cause can be ruled out entirely.

Can a Deny statement in someone else's policy affect my account?

Yes, in one specific case: a service control policy set at the AWS Organizations level applies to every account beneath it, including yours, whether or not anyone in your account wrote or even knows about that policy.

Why does RunInstances need permission for an AMI, a subnet, AND a security group separately?

Because Amazon EC2 supports resource-level permissions for RunInstances, meaning the authorization check runs separately against every resource type the specific launch touches, not just once against the RunInstances action as a whole.

What's the difference between UnauthorizedOperation and VcpuLimitExceeded?

UnauthorizedOperation is a permissions failure — something in your policy stack is missing or denying the action. VcpuLimitExceeded is a service quota issue — you have permission, but launching that instance type would exceed your account's current vCPU limit for that instance family. They require entirely different fixes.

Can MFA requirements cause this exact error?

Yes. A policy condition using the aws:MultiFactorAuthPresent key can require MFA be active on the session for an action to succeed. If your session doesn't have MFA active, the request is denied with the same UnauthorizedOperation error as any other missing-permission cause, and the decoded message will surface the condition that failed.

Does launching from the AWS Management Console use different permissions than the CLI?

No, the underlying authorization check is identical either way. The console simply shows you less of the failure detail up front — you'll typically need to follow its link into AWS CloudTrail to see the same level of detail the CLI's encoded message gives you directly.

Why did this work yesterday and fail today with no policy changes on my end?

The most common reason is a change made outside your own account — an SCP update from your organization's central IT team, a permissions boundary adjustment, or an AMI ownership change if your policy restricted RunInstances to a specific AMI owner. Ask whoever manages your AWS Organization whether anything changed recently before re-auditing your own identity policy from scratch.

Can a launch template cause this even if my IAM policy looks fine?

Yes. A launch template is itself a resource with its own ARN, and RunInstances checks permission on that template ARN separately from the resources the template configures internally. A policy that doesn't include the launch-template ARN will fail even if every other referenced resource is correctly permitted.

What's the least-privilege policy I should actually use going forward?

One that explicitly lists every resource type your launches touch — image, instance, subnet, network-interface, security-group, volume, and key-pair if you use one — scoped to your approved AMIs and subnets where possible, plus a separate iam:PassRole statement scoped to one specific role ARN with an iam:PassedToService condition, rather than a single wide Resource wildcard left in place permanently.

How do I decode the message if I don't have sts:DecodeAuthorizationMessage permission either?

Have someone with that permission add sts:DecodeAuthorizationMessage to your own user or role, then decode it yourself using your own credentials — decoding generally has to be done by the same principal that received the original error, so a colleague with broader access decoding it on your behalf may not work.

Is it safe to just attach AdministratorAccess to get past this?

It will bypass Cause 1 immediately, but it won't fix an SCP or permissions boundary denial, since those override even administrator-level identity policies. It also leaves a far wider door open than the actual problem required, which is a real ongoing security cost for a fix that may not even address the true cause.

Revision note. Written September 2026. This will need a fresh look if AWS changes how resource-level permissions are evaluated or introduces new condition keys for RunInstances. If you've been stuck on this error for a while, you're not missing something obvious — the way this API checks permissions across several resources at once really does make the message misleading, and decoding it properly is the shortcut almost nobody shows you first.

Related