DEV Community

Bala Paranj
Bala Paranj

Posted on

Zero Permissions on Secrets Manager. Full Access to Your Secrets.

✓ Human-authored analysis; AI used for formatting and proofreading.

A 2025 CloudGoat walkthrough by VirajMathpati documents a privilege escalation that no permission-based scanner catches. The principal with a synthetic IAM user named cg-sns-user-cgidxi93qpes3g has zero permissions on Secrets Manager. Run their identity through IAM Access Analyzer; it confirms no access. Run it through PMapper; same answer. Run it through any policy-based review; the IAM user cannot read any secret in the account.

The user retrieves a secret in seven API calls.

The trick: the user's permissions don't connect to Secrets Manager directly. The connection is a credential-value flow through four services. SNS publishes an API key as a message payload. The user subscribes to the topic and receives the key in their inbox. The key authenticates to an API Gateway. The gateway's Lambda integration reads from Secrets Manager. The secret comes back as the HTTP response body.

No policy connects SNS to Secrets Manager. The connection is the credential value itself. Every step is individually authorized. The compound chain is invisible to any tool that checks permissions service-by-service.

The four services and the four configurations

[1] IAM user policy
    Allow:  sns:Subscribe, sns:Receive, sns:ListTopics, …,
            apigateway:GET            (Resource: *)
    Deny:   apigateway:GET on 7 specific paths

[2] SNS topic
    Topic ARN:    public-topic-cgidxi93qpes3g
    Topic policy: NONE (defaults to IAM-only gate)
    Publishes:    a message containing an API Gateway key

[3] API Gateway
    Auth:         API key only (no IAM, no Cognito, no Lambda authorizer)
    Integration:  Lambda

[4] Lambda
    Reads:        the target secret in Secrets Manager
Enter fullscreen mode Exit fullscreen mode

Each one is fine by per-service checklist criteria. The user's IAM policy is broad but doesn't grant Secrets Manager. The SNS topic has no resource policy — the standard "topic owner only" default. The API Gateway uses key-only auth, which is a documented authentication option. The Lambda accesses Secrets Manager via its execution role, the standard pattern.

The bug is the connection.

Four Z3 queries

The Z3 prover in stave/examples/sns-secrets-compound-chain/z3prove/ runs four queries against the writeup configuration and four against a remediated version.

Finding 1: the topic is subscribable + publishes credentials

The user's IAM policy admits sns:Subscribe on Resource: "*". The topic has no resource policy. In AWS, the IAM identity policy is the only gate when no topic policy is configured. The user wins.

--- Finding 1: SNS topic subscribable, publishes credential ---
  subscribable + sensitive topics: 1 / 1
  verdict: SAT — witness: arn:aws:sns:us-east-1:676206926638:public-topic-cgidxi93qpes3g
           (publishes api_key targeting arn:aws:apigateway:us-east-1::/restapis/x93anl9mj7)
Enter fullscreen mode Exit fullscreen mode

The data-flow annotation publishes_credential_type=api_key on the topic asset makes this a security concern rather than a subscription pattern concern. Stave's existing broad-subscribe control checks the topic's resource policy. This issue lives in the IAM identity policy plus the absence of a topic policy plus the data-flow fact about what the topic publishes.

Finding 2: the API Gateway deny coverage gap

The user's policy grants apigateway:GET on Resource: "*" and denies seven specific patterns:

{
  "Effect": "Deny",
  "Action": "apigateway:GET",
  "Resource": [
    "/apikeys",
    "/apikeys/*",
    "/restapis/*/resources/*/methods/GET",
    "/restapis/*/methods/GET",
    "/restapis/*/resources/*/integration",
    "/restapis/*/integration",
    "/restapis/*/resources/*/methods/*/integration"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Seven patterns. The author was thinking carefully. They identified specific high-sensitivity paths (API keys, method bodies, integration internals) and denied them. By any reasonable security review, this deny is well-considered.

Z3 walks 24 known API Gateway management paths and finds 21 of them reachable:

--- API Gateway Deny coverage analysis ---
  OPEN      : /restapis
  OPEN      : /restapis/{id}
  OPEN      : /restapis/{id}/resources
  OPEN      : /restapis/{id}/resources/{resource_id}
  OPEN      : /restapis/{id}/resources/{resource_id}/methods/{method}
  BLOCKED   : /restapis/{id}/resources/{resource_id}/methods/{method}/integration
  OPEN      : /restapis/{id}/stages
  OPEN      : /restapis/{id}/stages/{stage}
  OPEN      : /restapis/{id}/deployments
  OPEN      : /restapis/{id}/deployments/{deployment_id}
  OPEN      : /restapis/{id}/models
  OPEN      : /restapis/{id}/authorizers
  ... (more)
Enter fullscreen mode Exit fullscreen mode

The author blocked the API key endpoint. Reasonable. But the principal can enumerate the rest: /restapis lists every API ID. /restapis/{id}/stages lists each API's stages. /restapis/{id}/resources lists the resource paths. With those three calls, the attacker has the full URL. They don't need /methods/GET (denied) or /integration (denied). They have the API key from Finding 1. They invoke the URL directly.

The deny was correct for the things the author thought to protect. AWS API Gateway has more management endpoints than any individual reviewer holds in their head. The deny-list approach loses the race to the service surface area, just like the autoscaling iteration.

Finding 3: the 5-hop data-flow chain

The most novel modeling in this article. The Z3 program computes five booleans:

hop 1: principal can subscribe to topic                       true
hop 2: topic publishes credential                             true
hop 3: API Gateway accepts credential, no IAM auth required   true
hop 4: API Gateway integrates with downstream function        true
hop 5: function reads Secrets Manager                         true
Enter fullscreen mode Exit fullscreen mode

Each hop is a fact read from a different service's observation. The conjunction is the data-flow chain. Z3's And of five booleans tells us the chain is satisfiable.

This is the iteration's distinguishing feature. The five facts come from four different observation assets: the IAM user, the SNS topic, the API Gateway, the Lambda + one cross-asset claim ("function reads Secrets Manager"). No single CEL predicate can express this. The composition is necessarily multi-asset.

--- Finding 3: data-flow credential chain (5 hops) ---
  hop 1 (principal can subscribe to topic):                 true
  hop 2 (topic publishes credential):                       true
  hop 3 (API Gateway accepts credential, no IAM auth):      true
  hop 4 (API Gateway integrates with downstream function):  true
  hop 5 (function reads Secrets Manager):                   true
  verdict:  SAT — full credential-flow chain is reachable
            (zero direct Secrets Manager permissions; access via
             credential value flowing through SNS → API GW → Lambda)
Enter fullscreen mode Exit fullscreen mode

Finding 4: the compound chain

F1 ∧ F2 ∧ F3. The complete escalation:

sns:ListTopics                  → discover the topic
sns:Subscribe                   → subscribe with attacker email
(receive SNS message)           → extract the API key
apigateway:GET /restapis        → enumerate API IDs
apigateway:GET /restapis/.../stages    → enumerate stages
apigateway:GET /restapis/.../resources → enumerate paths
curl https://<api>.execute-api.../user-data
    -H "x-api-key: <leaked-key>"  → invoke API
                                  → API GW invokes Lambda
                                  → Lambda reads Secrets Manager
                                  → secret returned in HTTP body
Enter fullscreen mode Exit fullscreen mode

Seven calls. Three services touched directly. A fourth (Secrets Manager) reached transitively. Zero direct permissions on the target.

Why permission analysis misses this

A permission-based reasoner such as IAM Access Analyzer, Zelkova, PMapper or any policy resolver works by walking the policy graph: "principal P has policy that grants action A on resource R." The graph is complete for permission edges. But:

  • There is no permission edge from SNS to Secrets Manager. SNS doesn't have permission to read secrets. SNS publishes messages. The "edge" is the credential value that travels in a message payload.
  • There is no permission edge from the IAM user to the API key. The user has sns:Subscribe. The key is a string in a message body. Receiving the key isn't a permission grant; it's receipt of data.

A permission graph cannot represent "principal P receives credential C via mechanism M, and C authenticates to service S." That isn't a permission relationship. It's a data-flow relationship. The information is in the configuration of what flows where, not in who is allowed to do what.

This iteration extends the model to include data-flow facts as observation annotations. The Z3 program reasons over both permission facts and data-flow facts uniformly. The chain reaches a service the user has no permission on, but the user reaches the service's output.

The remediation

Three changes, each closing one finding:

 // 1. Scope sns:Subscribe to specific topic prefixes.
 {
-  "Action": "sns:Subscribe",
-  "Resource": "*"
+  "Action": "sns:Subscribe",
+  "Resource": "arn:aws:sns:us-east-1:676206926638:ops-alerts-*"
 }

 // 2. Remove apigateway:GET. The user's data-science
 //    role doesn't need it. If a future use case
 //    arises, scope to the specific API ARN.

 // 3. Stop publishing credentials through SNS. The
 //    correct architecture is to put the API key in
 //    Secrets Manager, grant the consumer's IAM
 //    role secretsmanager:GetSecretValue on the
 //    specific secret ARN, and let the consumer
 //    retrieve it via the SDK at runtime. Messaging
 //    services are not credential transport.
Enter fullscreen mode Exit fullscreen mode

Plus changing the API Gateway authorization to AWS_IAM so the resource policy and IAM identity policy both gate access, adding a defense in depth even if a future credential leak occurs.

After remediation, all four Z3 queries return UNSAT. The chain is broken at hop 1 (the user can no longer subscribe to the credential-publishing topic).

The architectural lesson

The published configuration is a teaching exercise. CloudGoat is intentionally vulnerable. But the shape of the bug is real. Production AWS environments do this: a CI/CD pipeline publishes a deployment notification to SNS. The notification includes a deployment URL. The URL is a presigned S3 URL that grants read access to a build artifact. Anyone subscribed to the topic can fetch the artifact. Credential value flowing through messaging that subscribers harvest.

The architectural rule: messaging services are not credential transport. If a credential needs to flow between services, put it in Secrets Manager (or SSM Parameter Store) and grant the consuming service secretsmanager:GetSecretValue on the specific secret ARN. The credential never traverses a notification path; the consumer retrieves it via SDK calls authenticated by IAM.

Two reasons:

  1. SNS subscription is a public surface. Adding a subscriber doesn't require any permission on the thing being notified about. It only requires sns:Subscribe. Anyone with that permission joins the recipient list.
  2. Subscriber list management is loose. Operators add subscribers more freely than they add IAM permissions. The "everyone gets dev-team alerts" pattern is comfortable; "everyone gets production secrets" should not be.

When the credential is in Secrets Manager and the consumer's IAM grants secretsmanager:GetSecretValue on a specific ARN, the access pattern is:

  • Authentication: SigV4 with the consumer's identity.
  • Authorization: explicit IAM policy.
  • Audit: every retrieval is a CloudTrail event with the principal recorded.
  • Rotation: Secrets Manager handles it; consumers fetch the latest value.

None of these properties hold for a credential delivered via SNS message. The audit trail shows a generic "publish" event; the principal that received it is a subscriber identifier, not an IAM principal; rotation requires re-publishing.

What pattern-matching tools see

Run an IAM policy review on cg-sns-user. The output is "this user has SNS read access and API Gateway read access. No access to Secrets Manager." Correct.

Run a posture scan on the SNS topic. The output is "topic has no resource policy; access governed by IAM." Correct.

Run an API Gateway scan. The output is "API uses key-only auth; consider IAM authorization for defense in depth." Useful but not foundational.

Each per-service tool is correct for what it checks. None of them composes the four answers into "this principal can read your secrets."

The Z3 prover does. The five hops conjunct into one boolean, and the boolean returns SAT.

Checklist

  • No SNS topic publishes credential values, API keys, or any secret material in message bodies
  • If a credential needs to flow between services, it lives in Secrets Manager (or SSM) with IAM-scoped retrieval, not in a notification stream
  • sns:Subscribe in IAM identity policies is scoped to specific topic ARN patterns, not Resource: "*"
  • API Gateway methods that read Secrets Manager use AWS_IAM authorization, not API_KEY only
  • CI runs cross-service data-flow analysis on pre-merge observation snapshots; the example shipped with this article is the template

The IAM user has no Secrets Manager permission. The secret comes back anyway. That's not a bug in any single service. It's a bug in the composition. The permission graph is silent. The credential graph talks. Z3 reads both.

How this relates to existing compliance mods

A framework benchmark in turbot/steampipe-mod-aws-compliance will correctly report the IAM user as having zero Secrets Manager permissions. That's what the IAM policy says, and the framework controls are accurately reading the IAM API. The verdict isn't wrong; it's just on the wrong question. The compositional finding ("the secret comes back because a Lambda subscribed to an SNS topic the user can publish to") isn't representable in per-resource framework controls because no framework section names "subscribe to a topic whose subscriber holds a permission you don't." That's the class of finding Stave's compound + chain catalog covers. Frameworks catch what the IAM policy permits; Stave catches what the cross-service composition delivers. Both run against the same snapshot; both render in Powerpipe. Comparison: aws-compliance-mod.


The example is at stave/examples/sns-secrets-compound-chain/. Two binaries side by side: a CEL evaluation via pkg/stave.Apply (the existing CTL.SNS.POLICY.SUBSCRIBE.BROAD.001 per-topic control reports clean on both fixtures because the risk lives in the IAM identity policy and absence of topic policy, not in a topic-side broad subscribe) and a Z3 SAT prover that runs the four queries from this article and prints the API Gateway management deny coverage table. The Z3 binary lives in a sibling Go module so its libz3 link stays out of Stave's main vendored tree. Stave detects this pattern and 31 other H1-grounded scenarios from local AWS configuration snapshots, without cloud credentials.

Top comments (0)