Cracks in the Bedrock: Bypassing SCP Enforcement with Long-Lived API Keys

9 mins to read

Introduction

Following the release of Amazon Bedrock Powered by AWS Mantle, I discovered a mechanism to bypass Service Control Policy (SCP) statements limiting the use of bedrock-mantle IAM permissions. By leveraging long-lived API keys backed by Service Specific Credentials, I was able to successfully leverage bedrock-mantle:CreateInference despite an SCP statement denying that action.

SCPs are one of AWS’s prime centralized access enforcement mechanisms. As such, any mechanism to circumvent their controls is of importance. This issue was reported to AWS, who promptly adjusted SCP enforcement logic to close this gap. As such, the issue detailed in this article is no longer present in any AWS customer environments.

This article details the work I’d completed to identify, investigate, and fully scope the gap in SCP enforcement logic.

Background

Service Specific Credentials

AWS Service Specific Credentials are a hodgepodge of distinct authentication mechanisms for specific AWS services. As the name implies, these are credentials designed to be used for a single service rather than some service-agnostic credential like IAM User Access Keys. They’re tied directly to IAM Users, and depending on the service, the credential itself can take different shapes. Currently, there are three services that support service specific credentials:

ServiceService Specific Credential Type
Amazon BedrockAPI Key
AWS CodeCommitUsername / Password
Amazon Keyspaces (for Apache Cassandra)Username / Password

In the case of Amazon Bedrock, requests can then be made to bedrock API endpoints (like InvokeModel) by providing the API key rather than the usual SigV4 request signing. The permissions associated with the API key are inherited from the permissions of the IAM user the credential is tied to.

Bedrock API Keys

In July 2025, AWS released Bedrock API Keys. What Amazon calls “API Keys” in the Bedrock console are actually a combination of two distinct things: short-term keys and long-term keys

Short-term keys last up to 12 hours, inherit the permissions of the identity creating them, and are effectively just pre-signed URLs using SigV4 for authentication. They can be easily generated using the bedrock console, or programmatically using the aws-bedrock-token-generator client library.

Long-term keys last 1+ days and are implemented using Service Specific Credentials. The one-click option to create long-term keys in the bedrock console returns an API key not tied to the calling identity. Rather, under the hood, the following occurs:

  • An IAM User with the naming convention BedrockAPIKey-XXXX is created
  • The AmazonBedrockLimitedAccess managed policy is attached to this new user
  • A Service Specific Credential is created for this new user, and is returned in the console

The identity creating the long-term key does need all the requisite permissions for each of these steps (iam:CreateUser, iam:AttachUserPolicy and iam:CreateServiceSpecificCredential). It’s made clear in the console when creating keys this way that the permissions associated with the backing IAM User are directly tied to the permissions of the long-term key. 

Alternatively, long-term keys can be created for existing IAM users by simply creating a Bedrock Service Specific Credential for them. As with the bedrock console-created API keys, the service specific credential’s user’s IAM permissions will control the capabilities of the API key.

SCPs and Bedrock API Keys

Both short-term and long-term keys are affected by service control policies (SCPs) when performing bedrock API actions:

  • SCPs are evaluated against the key-creator when using a short-term key
  • SCPs are evaluated against the IAM User owning the service specific credential when using a long-term key

For example, an identity attempting to use bedrock:InvokeModel using a short-term key when it is explicitly denied using an SCP will run into the following error:

~ $ SHORT_TERM_TOKEN="$(python3 -c 'from aws_bedrock_token_generator import provide_token; print(provide_token())')"
~ $ MODEL="amazon.nova-lite-v1:0"
~ $ URL="https://bedrock-runtime.us-east-1.amazonaws.com/model/$MODEL/invoke"
~ $ curl -sS -X POST $URL \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -H "Authorization: Bearer $SHORT_TERM_TOKEN" \
    -d '{"messages": [{"role": "user", "content": [{"text": "Say hello"}]}]}'

{"Message":"User: arn:aws:iam::<account>:user/<user> is not authorized to perform: bedrock:InvokeModel on resource: arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0 with an explicit deny in a service control policy: arn:aws:organizations::<organization>:policy/o-<org_id>/service_control_policy/p-<policy_id>"}

Similarly, using a long-term key produces the same results:

~ $ USER=$(aws sts get-caller-identity \
      --query Arn \
      --output text \
      | awk -F '/' '{print $NF}')
~ $ LONG_TERM_TOKEN=$(aws iam create-service-specific-credential \
      --user-name $USER \
      --service-name bedrock.amazonaws.com \
      --query ServiceSpecificCredential.ServiceCredentialSecret \
      --output text)
~ $ MODEL="amazon.nova-lite-v1:0"
~ $ URL="https://bedrock-runtime.us-east-1.amazonaws.com/model/$MODEL/invoke"
~ $ curl -sS -X POST $URL \
      -H "Content-Type: application/json" \
      -H "Accept: application/json" \
      -H "Authorization: Bearer $LONG_TERM_TOKEN" \
      -d '{"messages": [{"role": "user", "content": [{"text": "Say hello"}]}]}'

{"Message":"User: arn:aws:iam::<account>:user/<user> is not authorized to perform: bedrock:InvokeModel on resource: arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0 with an explicit deny in a service control policy: arn:aws:organizations::<organization>:policy/o-<org_id>/service_control_policy/p-<policy_id>"}

Initial Discovery & Investigation

Sonrai Security continually assesses new AWS services and features for risk – with a distinct emphasis placed on the potential for abuse of individual IAM permissions. When working through the vast quantity of new capabilities announced at re:Invent 2025, I noted that AWS had released support for the Responses API on OpenAI API-compatible models available in bedrock. 

Inferencing models in this manner requires permissions in the bedrock-mantle IAM namespace rather than the traditional bedrock IAM namespace used for other bedrock actions. The full service name is “Amazon Bedrock Powered by AWS Mantle”, but it will subsequently be referred to as Bedrock Mantle for brevity.

Around the time of this release, the AmazonBedrockLimitedAccess managed policy (which you may recall is the default policy for Bedrock’s console-created long-term keys) was updated to include Bedrock Mantle actions like bedrock-mantle:CreateInference. AWS’s own documentation indicates that the same bedrock API keys used for existing bedrock actions can be used for Bedrock Mantle actions as well.

Sonrai Security’s Cloud Permissions Firewall relies in part on Service Control Policies (SCPs) to implement strong privileged access controls. Recalling AWS’s assertion that both IAM policies and SCPs are effective in enforcing model restrictions, I wanted to see whether that feature was available for Bedrock Mantle-based model invocation as well. 

Before worrying about specific condition keys, I went to quickly verify that my SCPs applied to my test IAM User. I denied bedrock-mantle:* using an SCP statement and signed in as an IAM User with AmazonBedrockLimitedAccess. I then tested my permissions via all three available access mechanisms:

IAM Test (Curl request signed with SigV4):

~ $ eval "$(aws configure export-credentials --format env)"
~ $ curl --request POST https://bedrock-mantle.us-east-1.api.aws/v1/responses \
      --user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \
      --aws-sigv4 "aws:amz:${AWS_REGION}:bedrock" \
      -H "Content-Type: application/json" \
      -H "X-Amz-Security-Token: ${AWS_SESSION_TOKEN}" \
      -d '{
        "model": "openai.gpt-oss-120b",
        "input": [
          {"role": "user", "content": "Hello! How can you help me today?"}
        ]
      }'

{"code":"access_denied","message":"User: arn:aws:iam::<account>:user/<user> is not authorized to perform: bedrock-mantle:CreateInference on resource: arn:aws:bedrock-mantle:us-east-1:<account>:project/default with an explicit deny in a service control policy","param":null,"type":"permission_denied_error"}

Short-Term Key Test:

~ $ export SHORT_TERM_TOKEN="$(python3 -c 'from aws_bedrock_token_generator \
      import provide_token; print(provide_token())')"
~ $ curl -X POST https://bedrock-mantle.us-east-1.api.aws/v1/responses \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer $SHORT_TERM_TOKEN" \
     -d '{
       "model": "openai.gpt-oss-120b",
       "input": [
         {"role": "user", "content": "Hello! How can you help me today?"}
       ]
     }'

{"code":"access_denied","message":"User: arn:aws:iam::<account>:user/<user> is not authorized to perform: bedrock-mantle:CreateInference on resource: arn:aws:bedrock-mantle:us-east-1:<account>:project/default with an explicit deny in a service control policy","param":null,"type":"permission_denied_error"}

Long-Term Key Test:

~ $ export USER=$(aws sts get-caller-identity \
      --query Arn \
      --output text \
      | awk -F '/' '{print $NF}')
~ $ export LONG_TERM_TOKEN=$(aws iam create-service-specific-credential \
      --user-name $USER \
      --service-name bedrock.amazonaws.com \
      --query ServiceSpecificCredential.ServiceCredentialSecret \
      --output text)
~ $ curl -X POST https://bedrock-mantle.us-east-1.api.aws/v1/responses \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer $LONG_TERM_TOKEN" \
      -d '{
        "model": "openai.gpt-oss-120b",
        "input": [
          {"role": "user", "content": "Hello! How can you help me today?"}
        ]
      }'

{"background":false,"created_at":1768865894.0,"error":null,"id":"resp_GTZYCN2AOPD6YWV2V6GQ734P5ULWUGMHYWI5RBLAVIJF5ZGVHDOA","incomplete_details":null,"instructions":null,"metadata":{},"model":"openai.gpt-oss-120b","object":"response","output":[{"content":[{"text":"The user says \"Hello! How can you help me today?\" It's a friendly greeting. We should respond conversationally, offering assistance, maybe ask what they need. Also we might mention capabilities: answer questions, help with tasks, etc. Should be friendly.","type":"reasoning_text"}],"id":"msg_b42b3b7680cac93c","status":"completed","summary":[],"type":"reasoning"},{"content":[{"annotations":[],"text":"Hello! 👋 I’m here to help with pretty much anything you need—whether it’s answering a question, brainstorming ideas, polishing a piece of writing, troubleshooting a tech issue, learning a new skill, planning an event, or just having a fun chat. \n\nWhat’s on your mind today?","type":"output_text"}],"id":"msg_967ebe3a3bc2727f","role":"assistant","status":"completed","type":"message"}],"parallel_tool_calls":true,"service_tier":"default","status":"completed","temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled"}

You’ll notice the last request was successful… This confused me for the better part of an hour as I conducted additional testing to confirm I’d set everything up right. So how do you get to the bottom of this? Well, long-term keys use service specific credentials, which inherit their permissions from the user the service-specific credential belongs to. My next test was to again deny bedrock-mantle:*, but this time on an inline policy directly attached to the user that owned the long-term key.

Trying to use the long-term key again resulted in the deny I expected:

~ $ curl -X POST https://bedrock-mantle.us-east-1.api.aws/v1/responses \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer $LONG_TERM_TOKEN" \
      -d '{
        "model": "openai.gpt-oss-120b",
        "input": [
          {"role": "user", "content": "Hello! How can you help me today?"}
        ]
      }'

{"code":"access_denied","message":"User: arn:aws:iam::<account>:user/<user> is not authorized to perform: bedrock-mantle:CreateInference on resource: arn:aws:bedrock-mantle:us-east-1:<account>:project/default with an explicit deny in an identity-based policy","param":null,"type":"permission_denied_error"}

~ $ 

With these results, three things had been confirmed about bedrock-mantle:CreateInference:

  • Denying the permission anywhere prevented its use via IAM authentication or short-term keys
  • Denying the permission via IAM policy worked for long-term keys
  • Denying the permission via SCP did not work for long-term keys

Further testing of other bedrock-mantle showed this to be a consistent problem across the entire IAM namespace:

PermissionAccess MechanismWhen Denied Via IAM PolicyWhen Denied Via SCP
bedrock-mantle:CancelInferenceIAM (SigV4)BlockedBlocked
Short-Term API KeyBlockedBlocked
Long-Term API KeyBlockedAllowed
bedrock-mantle:CreateInferenceIAM (SigV4)BlockedBlocked
Short-Term API KeyBlockedBlocked
Long-Term API KeyBlockedAllowed
bedrock-mantle:DeleteInferenceIAM (SigV4)BlockedBlocked
Short-Term API KeyBlockedBlocked
Long-Term API KeyBlockedAllowed
bedrock-mantle:GetInferenceIAM (SigV4)BlockedBlocked
Short-Term API KeyBlockedBlocked
Long-Term API KeyBlockedAllowed
bedrock-mantle:GetModelIAM (SigV4)BlockedBlocked
Short-Term API KeyBlockedBlocked
Long-Term API KeyBlockedAllowed
bedrock-mantle:ListModelsIAM (SigV4)BlockedBlocked
Short-Term API KeyBlockedBlocked
Long-Term API KeyBlockedAllowed
bedrock-mantle:CallWithBearerTokenIAM (SigV4)N/AN/A
Short-Term API KeyBlockedBlocked
Long-Term API KeyBlockedAllowed

Sample Abuse Scenarios

Any scenario that breaks or bypasses a centralized access control mechanism like SCPs is concerning no matter how small. Keep in mind these are identities in organization child accounts bypassing controls implemented through the org management account. But, looking specifically at the narrow scope of actions that could bypass explicit SCP denies in bedrock-mantle, what were the real risks here?

The bedrock-mantle:Model condition key was introduced alongside the new service, and access to specific models can be restricted via SCP by extending AWS’s guide on restricting model access centrally via SCPs. Given the reasonable assumption that sensible Bedrock Mantle controls may be implemented centrally via SCP, service-specific credentials could be used to bypass those controls.

IAM Users Self-Bypassing SCP Controls

An IAM User with iam:CreateServiceSpecificCredential and bedrock-mantle:CreateInference could bypass SCP-backed model restrictions by:

  1. Creating a Bedrock service-specific credential for themselves
  2. Invoking the restricted model’s responses endpoint using the service-specific credential (rather than SigV4 signing or short term keys, which would be appropriately denied)

IAM Administrators Bypassing SCP Controls Via New Users

An IAM administrator with iam:CreateUser, iam:AttachUserPolicy, and iam:CreateServiceSpecificCredential could bypass SCP-backed model restrictions by:

  1. Creating an IAM User
  2. Attaching the AmazonBedrockLimitedAccess managed policy to the new user. Note that by default, this managed policy provides access to all models.
  3. Creating a service-specific credential to the user
  4. Invoking the restricted model’s responses endpoint using the service-specific credential (rather than SigV4 signing or short term keys, which would be appropriately denied)

Disclosure & Remediation

The SCP bypass method was reported to AWS, who promptly investigated and corrected SCP enforcement to close this gap. As such, no customer action is required to mitigate the risks highlighted in this article.

Here is the official statement from AWS:

AWS is aware of an issue where, between December 4, 2025 and January 26, 2026, certain Service Control Policies (SCPs) were not fully enforced when using long-term API keys on the bedrock-mantle endpoint. This issue has been resolved, and we did not identify any impacted customers. No customer action is required.

We appreciate Sonrai Security for reporting this issue and collaborating with AWS.


The complete disclosure and remediation timeline is as follows:

  • Jan 19, 2026 – SCP bypass identified and scoped by Sonrai Security researcher
  • Jan 20, 2026 – SCP bypass reported to AWS via their Vulnerability Disclosure Program on HackerOne
  • Jan 29, 2026 – AWS begins fully enforcing SCPs for bedrock-mantle endpoints when long-term keys are used
  • Feb 4, 2026 – AWS responds to Sonrai Security, detailing the fix
  • Feb 5, 2026 – Sonrai Security confirms the SCP bypass is no longer possible

Some Musings on Long-Term API Keys

It should also be noted that even prior to any remediations from AWS, they were clear on their recommendations to use short-term keys where possible and to limit the use of long-term keys.

While the issue posed by long-term keys identified in this article has since been resolved, the following steps can still be taken to enforce the use of short-term keys rather than long-term keys:

Restrict CallWithBearerToken Permissions 

To allow the use of short-term keys while preventing the use of long-term keys in bedrock, add the following statements to an SCP:

{
  "Sid": "DenyMantleWhenLongTermBearerTokenUsed",
  "Effect": "Deny",
  "Action": "bedrock-mantle:CallWithBearerToken",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "bedrock-mantle:BearerTokenType": "LONG_TERM"
    }
  }
},
{
  "Sid": "DenyBedrockWhenLongTermBearerTokenUsed",
  "Effect": "Deny",
  "Action": "bedrock:CallWithBearerToken",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "bedrock:BearerTokenType": "LONG_TERM"
    }
  }
}

Restrict iam:CreateServiceSpecificCredential

We can also prevent the creation of long-term keys in the first place via an SCP statement:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyServiceSpecificCredential",
      "Effect": "Deny",
      "Action": [ "iam:CreateServiceSpecificCredential" ],
      "Resource": [ "*" ]
    }
  ]
}

This is also a control state you can also easily achieve by protecting the IAM service in Sonrai Security’s Cloud Permissions Firewall. Moreover, the firewall allows for easy management of exemptions should this truly be necessary for one or two identities.

Conclusion

At the end of the day, the scope of this issue was relatively minor. Long-lived keys are still relatively uncommonly used, and there were not many IAM permissions subject to this SCP bypass mechanism. AWS, for their part, were crazy fast at remediating this issue after it was reported. I don’t expect we’ll see another SCP enforcement gap like this one again anytime soon.

What this does raise is the importance of testing your own access controls rigorously. In the past year alone, we’ve seen several new ways in which AWS API actions can be authorized. Identity and Access Management in the cloud has gotten really complicated. 

As for me, I’ll continue to play with SCP controls in my sandbox AWS organization as new features and services are released. Likely, I’ll just end up more confident in my org-level controls; but, if I’m lucky (or unlucky…), maybe I’ll find some more interesting quirks.