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: Denyoniam:CreateAccessKeyhas 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
StringNotLikeorArnNotLikecondition 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
applypipelines fail when trying to rotate or bootstrap service account keys. - Developer onboarding scripts break silently —
aws configurecompletes 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:CreateAccessKeyon 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:CreateAccessKeyon 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.