Initializing Enclave...

How to Fix AWS SCP Deny on iam:CreateAccessKey Blocking Power Users

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15 mins

TL;DR

  • What broke: An SCP with Effect: Deny on iam:CreateAccessKey has no condition scoping, so it fires on every principal in the OU — including your power users and CI/CD roles that legitimately need programmatic credentials.
  • How to fix it: Add a StringNotLike or ArnNotLike condition to the Deny statement so it exempts tagged or explicitly named power-user principals from the blanket block.
  • Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your SCP JSON and get a scoped, least-privilege version back in seconds without leaking your ARNs.

The Incident (What Does the Error Mean?)

When a power user or CI role calls iam:CreateAccessKey, AWS evaluates all SCPs in the hierarchy before evaluating identity-based policies. An explicit SCP Deny cannot be overridden by any IAM policy, including AdministratorAccess. The call is dead on arrival.

Raw error returned to the caller:

An error occurred (AccessDenied) when calling the CreateAccessKey operation:
  User: arn:aws:iam::123456789012:assumed-role/PowerUserRole/session
  is not authorized to perform: iam:CreateAccessKey
  with an explicit deny in a service control policy

Immediate consequences:

  • Terraform/Pulumi apply pipelines fail when trying to rotate or bootstrap service account keys.
  • Developer onboarding scripts break silently — aws configure completes but no key is actually written.
  • Incident responders cannot generate break-glass credentials during an active outage.

The Attack Vector / Blast Radius

This is a dual-edged misconfiguration — it hurts defenders and, if misconfigured in the opposite direction, helps attackers.

Why the blanket Deny is dangerous to operations: SCPs apply to the entire OU. A single poorly scoped statement cascades across every account in that OU. If your PowerUserRole is used by 40 engineers and 12 CI/CD pipelines across 8 accounts, every single one of those principals is now locked out of programmatic credential creation. There is no IAM Allow that can dig you out — SCPs are evaluated before identity policy evaluation in the IAM authorization logic.

Why removing the Deny entirely is dangerous to security: If you simply delete the SCP statement in a panic, you re-expose the attack surface it was designed to close:

  • A compromised developer account can call iam:CreateAccessKey on any other IAM user, including high-privilege service accounts, creating a lateral movement path.
  • Long-lived access keys are a top-3 initial access vector in AWS breach post-mortems (Pacu, CloudFox, and similar tooling enumerate them in seconds).
  • Attackers with iam:CreateAccessKey on a dormant admin user can create a persistent backdoor key that survives password resets and MFA changes.

The correct posture: Deny iam:CreateAccessKey for everyone except a tightly scoped set of principals (break-glass roles, specific CI/CD roles) identified by ARN or principal tag.


How to Fix It (The Solution)

Basic Fix — Exempt a Specific Role ARN

Scope the Deny away from your power user role using ArnNotLike.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyCreateAccessKey",
      "Effect": "Deny",
      "Action": "iam:CreateAccessKey",
      "Resource": "*",
-     "Condition": {}
+     "Condition": {
+       "ArnNotLike": {
+         "aws:PrincipalArn": [
+           "arn:aws:iam::*:role/PowerUserRole",
+           "arn:aws:iam::*:role/CICDDeployRole"
+         ]
+       }
+     }
    }
  ]
}

⚠️ Wildcard account ID (*) in the ARN is intentional here — this SCP is attached at the OU level and must match the role across all member accounts.


Enterprise Best Practice — Tag-Based Condition (Scalable)

Hardcoding role ARNs in SCPs is a maintenance nightmare at scale. Use aws:PrincipalTag to exempt any principal carrying an authorized tag. This integrates cleanly with your IdP's SAML attribute mapping.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyCreateAccessKeyUnlessAuthorized",
      "Effect": "Deny",
      "Action": [
        "iam:CreateAccessKey",
+       "iam:UpdateAccessKey",
+       "iam:DeleteAccessKey"
      ],
      "Resource": "*",
      "Condition": {
-       "StringEquals": {
-         "aws:RequestedRegion": "us-east-1"
-       }
+       "StringNotEqualsIfExists": {
+         "aws:PrincipalTag/AllowAccessKeyManagement": "true"
+       }
      }
    }
  ]
}

Tag your power user role in each account:

aws iam tag-role \
  --role-name PowerUserRole \
  --tags Key=AllowAccessKeyManagement,Value=true \
  --region us-east-1

Why StringNotEqualsIfExists instead of StringNotEquals: StringNotEquals evaluates to true (and therefore triggers the Deny) when the tag key is absent. StringNotEqualsIfExists skips the condition entirely if the tag doesn't exist on the principal, which is the behavior you want for service roles that don't carry this tag at all.


💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

This class of misconfiguration — overly broad SCP Deny without condition scoping — is entirely preventable with policy-as-code gates.

1. Checkov — Scan SCPs Before aws organizations update-policy

# Install
pip install checkov

# Scan your SCP JSON file
checkov -d ./scps/ --framework cloudformation
# Or for raw JSON policy files:
checkov --file ./deny_access_key_scp.json --check CKV_AWS_274

CKV_AWS_274 flags IAM policies (and can be extended for SCPs) that allow or deny iam:CreateAccessKey without condition constraints.

2. OPA/Rego — Custom Policy Gate in Your Pipeline

# opa/scp_deny_scope_check.rego
package aws.scp

violation[msg] {
  stmt := input.Statement[_]
  stmt.Effect == "Deny"
  stmt.Action[_] == "iam:CreateAccessKey"
  not stmt.Condition
  msg := sprintf(
    "SCP statement '%v' denies iam:CreateAccessKey with no condition scoping. Add ArnNotLike or PrincipalTag condition.",
    [stmt.Sid]
  )
}
# Evaluate before applying
opa eval --data opa/scp_deny_scope_check.rego \
         --input deny_access_key_scp.json \
         "data.aws.scp.violation"

3. Terraform aws_organizations_policy — Enforce via lifecycle Precondition

resource "aws_organizations_policy" "deny_create_access_key" {
  name    = "DenyCreateAccessKey"
  content = data.aws_iam_policy_document.deny_access_key.json

  lifecycle {
    precondition {
      condition = can(
        jsondecode(data.aws_iam_policy_document.deny_access_key.json).Statement[0].Condition
      )
      error_message = "SCP Deny on iam:CreateAccessKey MUST include a Condition block to scope exemptions."
    }
  }
}

4. Git Pre-Commit Hook — Last-Mile Defense

# .git/hooks/pre-commit
#!/bin/bash
for f in $(git diff --cached --name-only | grep 'scp.*\.json'); do
  if jq -e '.Statement[] | select(.Effect=="Deny" and (.Action=="iam:CreateAccessKey" or (.Action[]?=="iam:CreateAccessKey"))) | select(.Condition == null or .Condition == {})' "$f" > /dev/null 2>&1; then
    echo "ERROR: $f contains an unscoped SCP Deny on iam:CreateAccessKey. Add a Condition block."
    exit 1
  fi
done

Key takeaway: Every SCP Deny on a sensitive IAM action must ship with a condition. Unconditional Denies are operational landmines. Tag-based exemptions scale; ARN lists don't.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →