Initializing Enclave...

Fixing AccessDenied on secretsmanager:GetSecretValue Caused by aws:PrincipalOrgID Condition Mismatch

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10–20 mins

TL;DR

  • What broke: A Secrets Manager resource policy has an aws:PrincipalOrgID condition that doesn't match the AWS Organization ID of the calling principal, causing a hard AccessDenied regardless of identity-based policies.
  • How to fix it: Verify the exact Organization ID (o-xxxxxxxxxx) attached to the secret's resource policy matches the org of the calling account; correct the value or scope the condition to the right OU.
  • Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your resource policy and get the corrected condition block without leaking ARNs to a third-party server.

The Incident (What Does the Error Mean?)

Raw error output from CLI or CloudWatch Logs:

An error occurred (AccessDenied) when calling the GetSecretValue operation:
User: arn:aws:iam::123456789012:role/my-app-role is not authorized to perform:
secretsmanager:GetSecretValue on resource: arn:aws:secretsmanager:us-east-1:987654321098:secret:prod/db-credentials
because no resource-based policy allows the GetSecretValue action

Immediate consequence: The calling Lambda, ECS task, or EC2 instance role receives a hard deny. There is no fallback. The application cannot retrieve the secret. If this is a database credential or API key fetch at startup, the service fails to initialize entirely — pods crash-loop, Lambda invocations return 500s, and on-call gets paged.

The deceptive part: the IAM identity policy on the role may be perfectly correct. The block is happening at the resource policy evaluation layer, specifically at the Condition block. AWS evaluates resource policy conditions as an explicit deny gate when the principal is cross-account.


The Attack Vector / Blast Radius

aws:PrincipalOrgID is a powerful guardrail — it restricts secret access to principals that belong to your AWS Organization. When misconfigured, the blast radius splits two ways:

Scenario A — Value Typo or Stale Org ID: The resource policy was copy-pasted from another secret or another region. The aws:PrincipalOrgID value references an old or incorrect org ID (e.g., from a pre-merger org structure or a sandbox org). Every principal — including production workloads — is locked out. Downtime is immediate and total.

Scenario B — Overly Permissive Fallback Temptation: Engineers under pressure remove the aws:PrincipalOrgID condition entirely to restore service. This is the dangerous fix. The secret is now accessible to any authenticated AWS principal with the right identity policy, including principals in external accounts if the Principal block is set to "*" or "AWS": "*". This is a credential exfiltration vector — an attacker with any foothold in an account that has secretsmanager:GetSecretValue in an identity policy can now pull your production DB password.

Blast radius of a fully open resource policy on a secret:

  • Database credentials exposed cross-account
  • API keys for third-party services accessible outside your trust boundary
  • Audit trail becomes meaningless — you can't distinguish legitimate from malicious access by org membership

How to Fix It (The Solution)

Step 1 — Confirm Your Actual Org ID

aws organizations describe-organization --query 'Organization.Id' --output text
# Returns: o-ab12cd34ef

Also confirm the calling principal's account is actually IN that org:

aws organizations describe-account --account-id 123456789012

If it returns an error or a different master account, the calling account is not in the org the secret expects.


Basic Fix — Correct the Org ID Value

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowOrgAccess",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
-         "aws:PrincipalOrgID": "o-xxxxWRONGxxxx"
+         "aws:PrincipalOrgID": "o-ab12cd34ef"
        }
      }
    }
  ]
}

Enterprise Best Practice — Scope to OU + Specific Actions + Explicit Principal

Never use "Principal": "*" in a Secrets Manager resource policy in production. Combine aws:PrincipalOrgID with aws:PrincipalOrgPaths for OU-level scoping and lock down the Principal to specific roles.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LeastPrivilegeOrgAccess",
      "Effect": "Allow",
-     "Principal": "*",
+     "Principal": {
+       "AWS": "arn:aws:iam::123456789012:role/my-app-role"
+     },
      "Action": [
-       "secretsmanager:*"
+       "secretsmanager:GetSecretValue",
+       "secretsmanager:DescribeSecret"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
-         "aws:PrincipalOrgID": "o-xxxxWRONGxxxx"
+         "aws:PrincipalOrgID": "o-ab12cd34ef"
+       },
+       "ForAnyValue:StringLike": {
+         "aws:PrincipalOrgPaths": "o-ab12cd34ef/r-xxxx/ou-xxxx-yyyyyyyy/*"
        }
      }
    }
  ]
}

Key hardening points:

  • Explicit Principal ARN — eliminates the wildcard blast radius
  • aws:PrincipalOrgPaths — restricts to a specific OU subtree, not the entire org
  • Minimal actionsGetSecretValue + DescribeSecret only; never secretsmanager:*

💡 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

1. Checkov — Catch Wildcard Principals at PR Time

Add to your pipeline:

checkov -d . --check CKV_AWS_149
# CKV_AWS_149: Ensure that Secrets Manager secret is not publicly accessible

Checkov will flag any resource policy with "Principal": "*" lacking a restrictive condition.

2. OPA / Conftest Policy for Org ID Validation

If you manage secrets via Terraform, enforce the org ID at plan time:

# policy/secrets_manager.rego
package aws.secretsmanager

violation[msg] {
  res := input.resource_changes[_]
  res.type == "aws_secretsmanager_secret_policy"
  policy := json.unmarshal(res.change.after.policy)
  stmt := policy.Statement[_]
  condition := stmt.Condition.StringEquals
  not startswith(condition["aws:PrincipalOrgID"], "o-")
  msg := sprintf("Secret policy for %v has invalid or missing PrincipalOrgID", [res.address])
}

3. Terraform — Parameterize the Org ID, Never Hardcode

variable "org_id" {
  description = "AWS Organization ID"
  type        = string
  sensitive   = false
}

data "aws_iam_policy_document" "secret_policy" {
  statement {
    effect    = "Allow"
    actions   = ["secretsmanager:GetSecretValue"]
    principals {
      type        = "AWS"
      identifiers = [var.app_role_arn]
    }
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "aws:PrincipalOrgID"
      values   = [var.org_id]
    }
  }
}

Feed org_id from a remote state output or SSM Parameter Store — never from a hardcoded string in the repo.

4. AWS Config Rule — Continuous Compliance

Enable the managed rule secretsmanager-secret-unused and pair it with a custom Config rule that validates aws:PrincipalOrgID is present and matches your known org ID pattern (o-[a-z0-9]{10,32}). Alert via SNS to your security channel on any drift.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →