Back to Checks
aws

Amazon Bedrock agent execution role follows least privilege

bedrock_agent_role_least_privilege
Service: bedrockSeverity: high
OfficialBuilt-inVerified
Categories:gen-ai
Resource Type:Other

Bedrock Agent execution roles (agentResourceRoleArn) should grant only the minimum permissions the agent needs. The evaluation FAILs when the role has an AWS-managed *FullAccess policy attached, has an inline statement allowing broad actions on Resource: "*", or has no permissions boundary configured.

Risk

An overly permissive Bedrock Agent execution role turns a successful prompt injection into AWS privilege escalation. A model coerced into calling tools can invoke any API the role allows — reading secrets, modifying IAM, exfiltrating data from S3, or pivoting laterally. Least privilege plus a permissions boundary keeps the blast radius bounded even when guardrails fail.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks bedrock_agent_role_least_privilege

Recommendation

Apply least privilege to every Bedrock Agent execution role: scope Action and Resource to exactly what the agent needs, avoid AWS-managed *FullAccess policies, and always attach a permissions boundary so that future policy edits cannot exceed an approved ceiling. Treat agent roles as high-risk because prompt injection can weaponize any granted permission.

Remediation

CLI

aws iam put-role-permissions-boundary --role-name <execution_role_name> --permissions-boundary <boundary_policy_arn>

Terraform

resource "aws_iam_role" "bedrock_agent" {
  name                 = "<execution_role_name>"
  assume_role_policy   = data.aws_iam_policy_document.trust.json
  permissions_boundary = aws_iam_policy.bedrock_agent_boundary.arn  # CRITICAL: caps maximum privileges
}

resource "aws_iam_role_policy" "bedrock_agent_inline" {
  role   = aws_iam_role.bedrock_agent.name
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect   = "Allow",
      Action   = ["s3:GetObject"],            # CRITICAL: narrow action
      Resource = ["arn:aws:s3:::my-rag-bucket/*"]  # CRITICAL: narrow resource
    }]
  })
}

Other

1Identify the Bedrock Agent's execution role (agentResourceRoleArn) in the IAM console
2Detach any AWS-managed *FullAccess policies (e.g. AmazonBedrockFullAccess, AdministratorAccess)
3Replace inline policies that use Resource: "*" with statements scoped to specific resource ARNs and minimal action sets
4Attach a permissions boundary that caps what the role can ever do, even if a future policy is added
5Re-run Prowler to confirm the check passes

Check Code

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.bedrock.bedrock_agent_client import (
    bedrock_agent_client,
)
from prowler.providers.aws.services.iam.iam_client import iam_client
from prowler.providers.aws.services.iam.lib.policy import check_admin_access
from prowler.providers.aws.services.iam.lib.privilege_escalation import (
    check_privilege_escalation,
)


class bedrock_agent_role_least_privilege(Check):
    """Ensure Bedrock Agent execution roles follow least privilege.

    A Bedrock Agent's execution role is evaluated against three criteria:
    - No AWS-managed ``*FullAccess`` policy attached.
    - No attached or inline policy granting administrative access or known
      privilege escalation combinations.
    - A permissions boundary is configured on the role.
    """

    def execute(self) -> list[Check_Report_AWS]:
        """Run the least-privilege evaluation across all Bedrock Agents.

        Returns:
            A list of ``Check_Report_AWS`` with one entry per agent. The
            status is ``FAIL`` when any of the criteria above is violated and
            ``MANUAL`` when the execution role cannot be resolved in IAM. When
            the IAM role inventory itself could not be listed, a single
            account-level ``MANUAL`` report is returned instead.
        """
        findings = []

        if iam_client.roles is None and bedrock_agent_client.agents:
            # iam:ListRoles was denied: this is an account-wide condition, so
            # emit one account-level MANUAL instead of one per agent.
            report = Check_Report_AWS(metadata=self.metadata(), resource={})
            report.region = iam_client.region
            report.resource_id = iam_client.audited_account
            report.resource_arn = iam_client.audited_account_arn
            report.status = "MANUAL"
            report.status_extended = "Cannot evaluate Bedrock Agent execution roles: the IAM roles could not be listed. Verify that the scanning credentials are allowed to call iam:ListRoles."
            findings.append(report)
            return findings

        roles_by_arn = {role.arn: role for role in (iam_client.roles or [])}

        for agent in bedrock_agent_client.agents.values():
            report = Check_Report_AWS(metadata=self.metadata(), resource=agent)
            report.status = "PASS"
            report.status_extended = (
                f"Bedrock Agent {agent.name} execution role follows least privilege."
            )

            role = roles_by_arn.get(agent.role_arn) if agent.role_arn else None
            if role is None:
                report.status = "MANUAL"
                report.status_extended = (
                    f"Bedrock Agent {agent.name} execution role could not be "
                    f"resolved in IAM and cannot be evaluated for least privilege; "
                    f"verify the role manually."
                )
                findings.append(report)
                continue

            violations = []

            for policy in role.attached_policies:
                policy_arn = policy.get("PolicyArn", "")
                policy_name = policy.get("PolicyName") or policy_arn
                if policy_arn.startswith(
                    "arn:aws:iam::aws:policy/"
                ) and policy_arn.endswith("FullAccess"):
                    violations.append(
                        f"managed policy {policy_name} grants full access"
                    )
                    continue
                policy_obj = iam_client.policies.get(policy_arn)
                if policy_obj is None or not policy_obj.document:
                    continue
                document = policy_obj.document
                if check_admin_access(document):
                    violations.append(
                        f"managed policy {policy_name} grants administrative access"
                    )
                elif check_privilege_escalation(document):
                    violations.append(
                        f"managed policy {policy_name} allows privilege escalation"
                    )

            for inline_name in role.inline_policies:
                policy_obj = iam_client.policies.get(f"{role.arn}:policy/{inline_name}")
                if policy_obj is None or not policy_obj.document:
                    continue
                document = policy_obj.document
                if check_admin_access(document):
                    violations.append(
                        f"inline policy {inline_name} grants administrative access"
                    )
                elif check_privilege_escalation(document):
                    violations.append(
                        f"inline policy {inline_name} allows privilege escalation"
                    )

            if not role.permissions_boundary:
                violations.append("no permissions boundary configured")

            if violations:
                report.status = "FAIL"
                report.status_extended = (
                    f"Bedrock Agent {agent.name} execution role violates least "
                    f"privilege: {'; '.join(violations)}."
                )

            findings.append(report)

        return findings