guide
What we actually check in an AWS assessment
The working method behind a CloudSecOps AWS security assessment: 69 checks organised by attack path rather than by service, the evidence request, read-only collection, a severity rubric, and why a benchmark-compliant account can still be three hops from account-wide admin.
This is the method CloudSecOps uses to assess an AWS estate: sixty-nine checks organised by where they sit on an attack path rather than by which service they belong to. It covers the evidence to request before day one, the first ninety minutes in an unfamiliar account, read-only collection, a severity rubric that is not CVSS, and the limits of what a configuration review can establish.
Scope, and what this is not
This is a working method, published in enough detail that a competent assessor who does not work for CloudSecOps can run the assessment from the document alone. It enumerates sixty-nine named checks. It is not a checklist in the sense that most published AWS checklists are, because the organising principle is neither the service catalogue nor a benchmark's control list.
- AWS only. A multi-account AWS Organizations estate is the assumed default. A single-account estate is handled as a degenerate case rather than ignored; most of the ORG domain becomes a set of findings instead of a set of checks. The section on where this method does not fit sets out the other architectures it strains against.
- Configuration review, evidence-led inspection, interviews, and read-only API collection. Authorised and non-destructive throughout.
- Not a penetration test. No exploitation, no privilege-escalation execution, no use of discovered credentials. Where this document describes an attack path, it describes a path to be proved on paper from policy documents. Testing whether the path executes requires separate written authorisation, a defined blast radius, a rollback position and a cleanup plan, and it is a different engagement.
- Not a compliance audit. There is no SOC 2, ISO 27001 or PCI DSS control mapping here. One section argues explicitly against treating benchmark compliance as evidence of security.
- Not application-layer testing. No SAST, no DAST, no business-logic review, except where the application's IAM posture is itself the finding, which it frequently is.
- Not a tool review. Named tooling appears only where a check has an automation hook.
- Not GCP, Azure or on-premises, even where AWS services now ingest posture data from other clouds.
Four assumptions run underneath everything below. Where they do not hold, say so in the report rather than working around them silently.
- The assessor holds an authorised, scoped, read-only role in each in-scope account, assumable from a named source principal, and the client knows the assessment is happening.
- The assessor has console access as well as API access. Console-only assessments miss policy JSON, trust policies and resource policies. API-only assessments miss the operational context that navigating the console exposes: which dashboards are stale, which alarms sit in
INSUFFICIENT_DATA, which account nobody can name an owner for. - There is at least one AWS Organizations organisation.
- The client can name an owner for each of the management account, the identity provider, detection tuning, and the CI/CD pipeline. If they cannot, that is the first finding, and it is usually a better predictor of estate health than any configuration setting.
Why service-by-service checklists fail
Almost every published AWS security checklist is one of two documents. The first walks the CIS AWS Foundations Benchmark and reports pass or fail per control. The second walks the AWS service catalogue, S3 then EC2 then RDS then Lambda, and lists the settings that matter for each. Both are easy to write and easy to automate. Both share a structural property that makes them incapable of finding the thing that actually ends an account.
Every control in a benchmark, and every item in a service walk, is a predicate over a single resource. Is this bucket blocking public access? Is this key rotating? Does this trail have log file validation enabled? Each question is answerable by looking at one object in isolation. That is precisely what makes the format automatable, and precisely what makes it blind.
Compromise does not travel through settings. It travels through edges: a trust policy that lets principal A become principal B; a resource policy that grants an external account access to a key; an iam:PassRole grant that lets a deployment role attach a policy it does not itself hold; a container runtime whose instance metadata hop limit lets a pod borrow the node's identity. An attack path is a predicate over a graph. No quantity of single-resource predicates composes into one, because the property being asserted, reachability, is not a property any single node has.
This is not a claim that benchmarks are worthless. A benchmark is a useful floor and a reasonable way to catch settings that are wrong in isolation. The claim is narrower: an account in which every applicable benchmark control passes can still contain a short path from an internet-facing load balancer to an administrative principal, and the benchmark will report a clean result the entire time. A worked demonstration appears later in this document.
The practical consequence is that services appear in this method where the path runs through them, not as chapter headings. S3 appears in the data domain, in the logging domain (because the log archive is an S3 bucket, and its retention model determines whether an attacker can erase the evidence), and in the network domain (because a VPC endpoint policy decides which buckets in which accounts are reachable from a subnet). Splitting those into an S3 chapter would put three different questions with three different threat models in one place because they share an API namespace.
The assessment model
The eleven domains below are stages on an attack path, ordered roughly by how a compromise progresses rather than by importance. Each domain has a prefix used for check IDs throughout.
Illustrative diagram: the assessment as a path.
| Domain | Position on the path | Primary evidence artifact | What it cannot tell you |
|---|---|---|---|
| ORG | The blast radius ceiling | Organizations policy documents, account status report | Whether a policy is enforced in practice for principals exempt from it |
| IDN | How humans get in | Identity Center configuration, IdP metadata, permission sets | Whether the directory reflects who actually works there |
| IAM | How privilege moves | Role and trust policy inventory across all accounts | Whether a reachable path has ever been used |
| LOG | Whether a hop is recorded | Trail configuration, selectors, Config recorder state | Whether anyone has ever read the logs |
| DET | Whether anyone would act | Detector configuration, findings, routing rules | Whether the on-call engineer knows what to do |
| DAT | What is worth reaching | Bucket and key policies, secret policies | What the data actually is |
| NET | Where the path starts | Exposure inventory, endpoint policies | Whether an exposed service is authenticated |
| WKL | Where credentials live | Instance metadata options, function policies, EKS access model | The effective runtime identity of a workload |
| PIPE | Who changes the estate | OIDC trust policies, pipeline role grants | Whether the pipeline's own supply chain is sound |
| AIX | Where new paths are appearing | Model invocation logging, guardrail enforcement | What is in the prompts |
| RSP | Whether you survive | Backup vault design, restore job records | Whether a restore works under contention |
Underneath the domains sits one organising frame that AWS supplies and that fits the path model better than any service taxonomy: the data perimeter. AWS defines it as a set of permission guardrails that help ensure only trusted identities access trusted resources from expected networks. Three perimeters, three questions, three sets of condition keys, and three different enforcement points, each with documented exemptions.
| Perimeter | Question | Principal condition keys | Enforced with |
|---|---|---|---|
| Identity | Only my identities | aws:PrincipalOrgID, aws:PrincipalOrgPaths, aws:PrincipalAccount, aws:PrincipalIsAWSService, aws:SourceOrgID, aws:SourceOrgPath, aws:SourceAccount | RCP applied to resources; VPC endpoint policy applied to the network |
| Resource | Only my resources | aws:ResourceOrgID, aws:ResourceOrgPaths, aws:ResourceAccount | SCP applied to identities; VPC endpoint policy |
| Network | Only my networks | aws:SourceIp, aws:SourceVpc, aws:SourceVpce, aws:VpceAccount, aws:VpceOrgID, aws:VpceOrgPaths, aws:ViaAWSService, aws:PrincipalIsAWSService | SCP applied to identities; RCP applied to resources |
Illustrative diagram: data perimeter enforcement points and their exemptions.
The two exemption nodes on that diagram are not footnotes. AWS states that service control policies do not affect users or roles in the management account, do not affect any service-linked role, and do not affect resource-based policies directly. Resource control policies grant nothing on their own, do not affect actions on resources in the management account, do not affect the effective permissions of any service-linked role, do not apply to AWS managed KMS keys, and do not affect kms:RetireGrant. Four consequences follow, and they shape several checks below.
- Any workload running in the management account sits outside both guardrails. That is why "what actually runs in the management account?" is check ORG-01 rather than a closing remark.
- Every service-linked role is a permanent hole through both policy types. Enumerate them and ask which service created each and why.
- An SCP cannot stop an external principal reaching an in-organisation resource through that resource's own policy. Only an RCP can. An estate with mature SCPs and no RCPs has built one half of a perimeter and usually believes it has built the whole thing.
- Because RCPs do not apply to AWS managed keys, an RCP intended to prevent cross-account KMS use has a documented gap for every
aws/*key in the estate.
Before day one: the evidence request
The evidence request is sent before access is provisioned, and it does double duty. It produces the artifacts the assessment needs, and the manner of its production is itself evidence. A client who exports the organisation's SCPs in twenty minutes from a repository is telling you something different from a client who takes nine days and produces console screenshots.
| Artifact | How the client produces it | Why it is needed | What its absence indicates |
|---|---|---|---|
| Organisation structure: OUs, accounts, account owners | organizations list-accounts, list-organizational-units-for-parent | Establishes blast radius and the units to which policy attaches | An estate where accounts are created without an owner field is one where nobody can be asked about an account later |
| All SCPs, RCPs and declarative policies, with attachment targets | organizations list-policies plus describe-policy and list-targets-for-policy | The permission ceiling, and whether it is attached where it is believed to be | Policies exported without targets are common and useless; the attachment is the control |
| Account status report | Organizations console or API | Declarative policy compliance across the organisation | Not knowing the report exists usually means declarative policies are unused |
| CloudTrail configuration for every account and Region | cloudtrail describe-trails --include-shadow-trails, get-event-selectors, get-trail-status | Whether the hops in the IAM domain would be recorded | A single-Region trail in an estate with resources in six Regions |
Log archive bucket policy, Object Lock configuration, and holders of s3:BypassGovernanceRetention | S3 API plus an IAM policy query | Whether the evidence survives the attacker who reaches admin | Retention configured in governance mode with a widely held bypass permission |
| AWS Config recorder configuration and aggregator source list, per account and Region | config describe-configuration-recorders, describe-configuration-aggregators | Whether posture data exists at all, and for which accounts | An aggregator whose source list predates the last two Region expansions |
| Full IAM export per account: roles, trust policies, attached and inline policies, permissions boundaries | iam get-account-authorization-details | The graph. The single most valuable artifact in the request | If this cannot be produced, the assessment cannot answer its central question |
| IAM credential report per account | iam generate-credential-report then get-credential-report | Long-lived keys, their age and last use | Reports that cannot be produced usually mean no account-level IAM owner |
| Identity Center configuration: instances, permission sets, assignments, IdP metadata | Identity Center API and the IdP's own export | Who can become what, and for how long | Assignments held only in the console with no source of truth in code |
| Access Analyzer analyzers per Region, and current findings | accessanalyzer list-analyzers per Region | External sharing, internal access, unused access | A single external analyzer in the home Region |
| Detector configuration: GuardDuty, Security Hub, Security Hub CSPM, Inspector, Macie | Respective describe and get calls from the delegated administrator | Whether anything is watching | Configuration that can only be read from individual member accounts |
| Network exposure inventory and VPC endpoint policies | Client's own inventory, plus ec2 describe-vpc-endpoints | Where the path starts, and what private connectivity actually permits | An inventory built from subnet route tables rather than from resources |
| Named owners for the management account, IdP, detection tuning and CI/CD | A person, by name and role | Every subsequent question has an addressee | See assumption four |
| Incident response runbooks and the last incident record | Whatever exists | Whether detection connects to action | Runbooks that reference a console layout retired two years ago |
| CI/CD federation: OIDC providers and the trust policies of pipeline roles | iam list-open-id-connect-providers plus the role trust policies | The most privileged non-human identity in most estates | Trust policies held only by whoever wrote the pipeline |
Two handling requirements go in the same email as the request, because the request asks a client to hand over documents that describe exactly how to attack them. Transfer the artifacts over an encrypted channel to a named recipient; state the retention period and the deletion trigger, which for CloudSecOps is deletion of engagement evidence at report acceptance unless the client requires otherwise in writing; and confirm that the export excludes secret material. An IAM authorization details export contains no secrets. A Terraform state file might. Ask for the former.
The first ninety minutes
The order of these queries carries the judgment. Each is chosen because its answer changes what you do next, not because it is important in isolation. Run them read-only, from the assessor role, in an account you have never seen.
Illustrative diagram: the first ninety minutes.
| Minutes | Query | The decision it feeds |
|---|---|---|
| 0–10 | organizations describe-organization, list-roots, list-policies for each policy type | Whether there is a ceiling at all. If describe-organization fails from this account, you are in a member account with no visibility and the engagement scope is wrong. Stop and fix that before continuing |
| 10–20 | cloudtrail describe-trails --include-shadow-trails and get-trail-status in the home Region and two others | Whether anything you find later can be corroborated with history. This determines whether findings can be dated |
| 20–40 | iam get-account-authorization-details, then read the trust policies before the identity policies | The graph. Trust policies first, because a permissive identity policy on an unreachable role is a lower-priority finding than a modest identity policy on a role trusted by the internet |
| 40–50 | accessanalyzer list-analyzers in every Region with resources | Whether external sharing has ever been examined, and whether a clean findings list covers the Regions the client actually uses |
| 50–60 | iam get-credential-report, iam list-users | Whether long-lived credentials exist, which changes the priority of everything in the IAM domain |
| 60–75 | guardduty get-detector, securityhub describe-hub, Security Hub CSPM standards state | Whether a detection story exists to test, or whether the detection domain is a gap analysis instead |
| 75–90 | ec2 describe-instances with a metadata-options filter, lambda list-functions, elbv2 describe-load-balancers | The first concrete starting points for the path model |
At the end of ninety minutes the assessor should be able to state, in one sentence, whether this is a governed estate with specific gaps or a hazardous estate with a governance veneer. The distinction is not about the number of findings. It is about whether the controls that exist were designed against a threat or assembled against a control list.
One branch is worth naming. If step one returns an organisation with no SCPs, no RCPs and no declarative policies, do not spend the remaining eighty minutes cataloguing what is missing. The finding writes itself. Spend the time on IAM-02, the cross-account trust graph, because in an ungoverned estate that graph is the entire security model and it will be the report's centrepiece.
ORG — organisation and tenancy boundary
Eight checks. This domain sets the ceiling on every finding below it. A permissive IAM grant in an account governed by a well-targeted SCP is a different severity from the identical grant in an account that is exempt.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| ORG-01 | Determine what workloads, roles and data live in the management account, which sits outside both SCP and RCP enforcement | Resource inventory and IAM export for the management account, compared against the AWS Security Reference Architecture's list of what belongs there | Passes when the account holds Organizations, account provisioning and delegated administrator registrations and little else. Warns on a CI/CD role, a shared services workload, a monitoring agent with a broad role, or a Terraform state bucket | Partial, via a Config aggregator query by account |
| ORG-02 | Inventory SCPs and identify every principal exempt from them | All SCPs with attachment targets; service-linked role list per account | Passes when the inventory names, per SCP, the threat it addresses and the principals it does not reach. Warns on SCPs attached to empty OUs, deny lists written against a service's current action list, and no enumeration of service-linked roles | Partial |
| ORG-03 | Test SCP effectiveness against a stated threat rather than a control name | The SCP documents plus a threat the client states aloud; policy simulation of that specific action against the effective ceiling | Warns when the client cannot state the threat any given SCP addresses. Action-list denies decay as services add APIs | Partial, via iam simulate-principal-policy |
| ORG-04 | Confirm RCPs exist and close the resource-policy bypass that SCPs structurally cannot | All RCPs with targets; the resource types holding sensitive data | Passes on RCPs enforcing aws:PrincipalOrgID over those services, with documented exceptions. Warns on no RCPs at all in an estate with mature SCPs | Partial |
| ORG-05 | Map OU structure to risk tiers and the account provisioning path | OU tree, account creation runbook, baseline stack sets; trace one recently created account from request to fully baselined | Warns when baseline controls are applied by a one-time script rather than stack sets or a landing zone, or when OU membership is assigned manually | Manual |
| ORG-06 | Establish whether declarative policies are used and what the account status report shows. There are currently eleven types, and unlike SCPs and RCPs they govern service-linked roles, which makes them the correct control for several problems teams attempt with SCPs and lose | Declarative policy documents; the account status report | Passes with declarative policies covering at least the EC2 and S3 configurations the estate cares about, and the report reviewed on a schedule. Warns when the client has not heard of declarative policies, which is common | Full, once configured |
| ORG-07 | Distinguish centralised root management being enabled from root credentials actually being deleted | Root credentials management state at organisation level; per-account root credential state via iam get-account-summary | Passes when the feature is enabled and root credentials are deleted across member accounts. Warns when the capability passes and the state fails | Partial |
| ORG-08 | Review root session usage and the scope of privileged root task policies | CloudTrail records of sts:AssumeRoot and the task policy named in each | Passes when root sessions are rare, each maps to a ticket, and each uses the narrowest applicable task policy | Full |
ORG-01. AWS's guidance is to avoid deploying workloads to the organisation's management account, and the Security Reference Architecture recommends delegating administration, including centralised root access management, to a Security Tooling account. Raise severity one band for anything internet-reachable here, and one further band if the account also holds break-glass credentials. Some estates have a genuine reason for a small footprint, Control Tower artifacts among them, so ask what created each resource rather than assuming drift. A healthy-design marker is an SCP at the organisation root denying organizations:LeaveOrganization and account:CloseAccount. Configuration cannot tell you whether the root email alias reaches more than one person, or whether recovery depends on a single individual; AWS's guidance is explicit that it must not.
ORG-04. No SCP can prevent an external principal reaching an in-organisation resource through that resource's own policy. Only an RCP can. The supported-service list is enumerated on the RCP documentation page and has expanded repeatedly since launch, so an RCP written eighteen months ago may not cover services the estate has since adopted. Read the current list rather than working from notes; S3, KMS, Secrets Manager, SQS and STS have been in scope longest. RCPs do not apply to AWS managed KMS keys, so any claim that cross-account KMS use is prevented needs a separate check for aws/* key usage.
ORG-07 and ORG-08. Whether centralised root access management is enabled and whether root credentials were actually deleted are different questions, and only the second changes the threat model. The common warning shape is the feature enabled with legacy accounts still holding root passwords and MFA devices; the second is a password recovery performed for a genuine task where the credentials were never deleted again. For ORG-08, the five AWS managed task policies are IAMAuditRootUserCredentials, IAMCreateRootUserPassword, IAMDeleteRootUserCredentials, S3UnlockBucketPolicy and SQSUnlockQueuePolicy, exposed at arn:aws:iam::aws:policy/root-task/ plus the policy name. One collection note that costs an hour if you miss it: sts:AssumeRoot is not supported on the global STS endpoint and must be sent to a Regional endpoint. Neither check can establish whether a deleted root credential was exfiltrated before deletion. That is a log question.
IDN — human identity and federation
Six checks. This domain answers how a person becomes a principal, and how long they stay one.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| IDN-01 | Establish the identity source of truth and the Identity Center deployment model, and find any second identity path nobody governs | Identity Center instance list, IdP metadata, directory export; compare the organisation instance against any account instances found. Note that the API and CLI namespaces are still sso, which is why older scripts and search results still resolve | Passes with one organisation instance, one external IdP, and no IAM users holding console passwords. Warns on account instances, which exist for isolated AWS managed applications and are invisible in every organisation-level view | Partial |
| IDN-02 | Review external IdP trust: assertion signing, certificate lifecycle, and what the IdP itself trusts | SAML or OIDC metadata, certificate expiry dates, IdP admin role membership | Passes with a named owner, tracked and alerted certificate expiry, and IdP administrative access held by a small named group with its own MFA and logging | Partial |
| IDN-03 | Inventory permission sets, session durations and the separation of privileged sets | Permission set definitions and assignments, compared against each set's intended audience | Passes with privileged sets near the one-hour minimum and no set carrying AdministratorAccess assigned to a group larger than the break-glass group. Warns when every set sits at twelve hours | Full |
| IDN-04 | Assess break-glass design and whether it has its own audit trail | Break-glass definition, credential custody process, alerting configuration; confirm alerting fires and custody is split | Passes with split custody, MFA on a device not held by the credential holder, and use alerting to a channel outside the estate being accessed. Warns when break-glass credentials share a password manager with ordinary credentials | Partial |
| IDN-05 | Test joiner, mover and leaver evidence against the actual directory | HR feed or directory export plus assignment history; sample recent leavers and movers and confirm removal with dates | Warns when movers retain old assignments alongside new ones, which is the dominant failure mode because nothing breaks when an old assignment lingers | Manual |
| IDN-06 | Determine whether end-user identity is propagated into logs or only the role session appears | Trusted identity propagation configuration; sample CloudTrail records checked for an end-user identifier | Passes when the end-user identifier appears in CloudTrail and service logs. Warns on shared permission sets assumed by many people with no propagation, particularly for data-plane services | Partial |
IDN-02. This is the highest-blast-radius finding class in the assessment and it lives outside AWS, which is exactly why configuration-only reviews miss it. A compromised or over-permissive IdP administrative role produces a total identity compromise that looks like ordinary federation in every AWS log. Essentially nothing about the IdP's posture can be concluded from AWS configuration. It has to be asked for.
IDN-03. Permission set session duration defaults to one hour, with a documented minimum of one hour and a maximum of twelve; Identity Center creates the underlying IAM roles with a twelve-hour maximum to support that range. A twelve-hour session on a read-only set in a non-production OU is not a finding worth a client's remediation budget, and saying so protects the credibility of the findings that are.
IDN-06. Attribution is an architectural property rather than a setting. Trusted identity propagation is what closes the gap between "someone using the analytics role ran this query" and knowing who, and it carries real integration cost. Raise severity where the estate has a regulatory duty to attribute data access to an individual, and present the fix as the architectural change it is.
IAM — authorization and privilege paths
Nine checks, and the centre of gravity of the whole assessment. Everything before this domain describes how a principal is obtained. Everything after describes what a principal can reach. This is where the graph is built.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| IAM-01 | Build a complete role and trust policy inventory across every in-scope account. One paginated call returns users, groups, roles, policies, inline policies and boundaries, which is why it is the most valuable artifact in the request | iam get-account-authorization-details per account; parse trust policies before identity policies, then classify each role by who can assume it | Warns on roles the client cannot attribute to a team or a service. AdministratorAccess on a role only the CI role can assume is a design decision; the same policy on a role trusted by Principal: "*" is an incident waiting for a scanner | Full |
| IAM-02 | Construct the cross-account trust graph and identify paths into privileged roles | The IAM-01 inventory for all accounts; build the directed graph of assumption edges and compute reachability into admin-equivalent roles | Warns on any path of three hops or fewer from an internet-reachable principal to an admin-equivalent role | Full, with custom tooling |
| IAM-03 | Review third-party vendor roles for external IDs and confused-deputy conditions | Vendor role trust policies and the vendor's own documentation | Passes when the external ID was generated and is controlled by the vendor. Warns on a vendor role holding ReadOnlyAccess | Partial |
| IAM-04 | Find wildcard and overly broad principals in trust policies | Trust policy JSON searched for Principal: "*", bare account roots, and conditions that do not narrow | Passes when no bare wildcard exists and any unavoidable wildcard is pinned by aws:PrincipalOrgID or an explicit account list | Full |
| IAM-05 | Assess permissions boundary coverage and the resource-policy gap | Boundary policies, the roles carrying them, and same-account resource policies | Warns when a boundary is presented as the control that makes delegated role creation safe in an account that also has resource policies naming session ARNs | Partial |
| IAM-06 | Inventory long-lived access keys with age and last use | IAM credential report per account, correlated with CloudTrail for keys marked unused | Passes when access keys exist only in a small, named, justified set with owners and rotation records. Warns on keys older than the client's own stated rotation period, and on keys last used in a Region the client does not operate in | Full |
| IAM-07 | Determine where each machine identity's credentials actually come from | Workload inventory, instance profiles, pod identity associations, OIDC providers, compared against the effective source in CloudTrail userIdentity | Warns on any divergence between intended and effective credential source | Partial |
| IAM-08 | Identify privilege-escalation primitives held by nominally non-admin roles | The IAM-01 inventory searched for the primitive action set and evaluated against PassRole targets | Passes when every PassRole grant is resource-scoped and, ideally, conditioned on iam:PassedToService | Full |
| IAM-09 | Verify Access Analyzer deployment across analyzer types and the state of finding triage | Analyzer list per Region; findings with status and age; analyzer Regions compared against Regions holding resources | Warns on a single external analyzer in the home Region and an empty findings list read as an all-clear | Partial |
IAM-02 — the cross-account trust graph. This is the check that distinguishes the method from a control walk.
Build a directed graph. Nodes are principals: roles, users, federated identities and service principals. Edges are the ways one node becomes another: sts:AssumeRole permitted by both the source's identity policy and the target's trust policy, sts:AssumeRoleWithWebIdentity from an OIDC provider, sts:AssumeRoleWithSAML from a federated identity, and sts:AssumeRoot for the root task policies. Then compute, for each admin-equivalent role in the estate, the set of principals from which it is reachable in one hop, two hops and three.
Pseudocode: the reachability computation. Requires a complete authorization-details export from every in-scope account. Incomplete input produces confidently wrong output.
# pseudocode
principals = union(get_account_authorization_details(a) for a in in_scope_accounts)
edges = []
for target in principals.roles:
for stmt in target.trust_policy.statements where stmt.Effect == "Allow":
for source in resolve_principal(stmt.Principal, principals):
# a bare account-root principal means "any principal in that account
# whose identity policy also allows sts:AssumeRole on this role"
if source.is_account_root:
sources = [p for p in principals
if p.account == source.account
and identity_policy_allows(p, "sts:AssumeRole", target.arn)]
else:
sources = [source]
for s in sources:
edges.append(Edge(s, target, conditions=stmt.Condition))
admin_equivalent = [r for r in principals.roles
if grants_admin(r) or holds_escalation_primitive(r)]
for target in admin_equivalent:
reachable_from = reverse_bfs(edges, target, max_depth=4)
report(target, reachable_from, annotate_with_entry_points(reachable_from))
Three details decide whether the output is useful or misleading.
First, resolve_principal on a bare account root. A trust policy naming arn:aws:iam::123456789012:root does not grant assumption to every principal in that account; it delegates the decision to that account's IAM, so the edge exists only where a principal there also holds sts:AssumeRole on the target in its identity policy. Tools that skip this step over-report heavily. Tools that skip it in the other direction, treating the account root as a single node, under-report just as heavily, because they lose which specific role holds the grant.
Second, conditions are part of the edge, not decoration. An edge guarded by sts:ExternalId requires a secret the attacker may not have. An edge guarded by aws:PrincipalOrgID still exists for every principal in the organisation. An edge guarded by an IP condition exists for anything running inside the estate's egress range, which usually includes the workload you are worried about. Record the condition on the edge and evaluate it against the specific starting principal rather than filtering conditioned edges out.
Third, admin_equivalent must include roles holding escalation primitives, not only roles with AdministratorAccess. See IAM-08.
The check passes when the client can produce this graph themselves, or at minimum can name the paths into their most privileged roles. Severity is determined by the entry point rather than the target, as the severity section sets out. What the graph cannot establish is whether any of these paths has ever been traversed. That question needs CloudTrail history, which is why the LOG domain is not decoration.
IAM-03. An external ID protects against the confused deputy only when the vendor generates and controls it. An external ID chosen by the client protects nobody, because the client already knows it. Where the third party is an AWS service rather than a vendor, the correct conditions are aws:SourceArn, aws:SourceAccount, aws:SourceOrgID or aws:SourceOrgPaths. The vendor-role finding that surprises clients most is ReadOnlyAccess. That policy is at version 188, last edited 2026-07-21, and it contains data-plane read actions including dynamodb:Query, dynamodb:Scan, dynamodb:PartiQLSelect, dax:GetItem, glacier:Get*, cassandra:Select and healthlake:ReadResource. A vendor whose contract describes configuration monitoring can read the contents of DynamoDB tables. Replace it with SecurityAudit plus a named, enumerated supplement, and add an explicit deny on data-plane reads. Whether the vendor has ever exercised that reach is a CloudTrail question about the role's session activity, not a policy question.
IAM-05 — permissions boundaries and their documented gap. A boundary allows only what both the identity policy and the boundary allow. The exception is documented and rarely known: within the same account, a resource-based policy that grants permissions to an IAM role session ARN grants permissions directly to the assumed role session, and permissions granted directly to a session are not limited by an implicit deny in an identity policy, a permissions boundary, or a session policy. A boundary is a ceiling on identity-based grants, not on all grants.
A second warning shape in the same area is a latent outage rather than an exposure. AWS states that a resource-based policy statement combining NotPrincipal with a Deny effect will always deny any IAM principal carrying a permissions boundary, regardless of what the NotPrincipal element says, and recommends the ArnNotEquals operator with aws:PrincipalArn instead. An estate that adopted boundaries and still has NotPrincipal denies somewhere has a fault that will be extremely confusing to debug when it fires.
IAM-06. AWS's position on long-lived keys is direct: IAM users with access keys are an account security risk. Two collection caveats belong in the report. The credential report is generated at most once every four hours and served from cache within that window, so access_key_1_last_used_date can be four hours stale. And where a key is used more than once within a fifteen-minute span, only the first use is recorded, so the field is a coarse activity signal rather than a usage count. Do not build a "this key is unused" conclusion on either resolution. Raise severity for any key attached to a user with an administrative policy, and raise again where that user has no MFA.
IAM-08 — escalation primitives. The primitive set to search for includes iam:PassRole combined with a compute-creation action, iam:CreatePolicyVersion, iam:SetDefaultPolicyVersion, iam:AttachRolePolicy, iam:PutRolePolicy, iam:UpdateAssumeRolePolicy, iam:CreateAccessKey on another user, lambda:UpdateFunctionCode on a function with a privileged execution role, glue:UpdateDevEndpoint, ssm:SendCommand against an instance with a privileged profile, and cloudformation:CreateStack with a passed role. iam:PassRole is only as dangerous as the roles it can pass. A PassRole grant scoped by Resource to three named service roles is a normal deployment pattern. A PassRole on Resource: "*" combined with lambda:CreateFunction is admin, spelled differently. Score on the pair, not on either action alone. Configuration cannot tell you whether the primitive is exercised in normal operations, and if it is, remediation will break a deployment. Knowing that in advance is the difference between a fix and an outage.
IAM-09 — Access Analyzer deployment. AWS states that for external access, Access Analyzer analyses only policies applied to resources in the same Region where the analyzer is enabled, so full coverage requires an analyzer in each Region holding supported resources. For unused access, AWS states that findings do not change based on Region and a per-Region analyzer is not required. Confirm the internal access analyzer's current Regional behaviour against the documentation before relying on a single-Region deployment of it.
Coverage matters more than Region count. The internal access analyzer covers six resource types: S3 buckets, S3 directory buckets, RDS DB snapshots, RDS cluster snapshots, DynamoDB streams and DynamoDB tables. External access covers a substantially longer list; unused access covers IAM users and roles only, excluding service-linked roles. The internal path, which matters most because most real compromise is lateral movement inside the organisation, has the thinnest tool coverage of the three. One automation hook is worth recommending regardless of deployment: custom policy checks answer whether a policy grants new access relative to a reference policy, and whether it allows a set of critical actions. Both belong in CI rather than in a quarterly review. See PIPE-04.
LOG — evidence and telemetry
Seven checks. This domain answers a single question asked repeatedly: if the hop in IAM-02 happened, would there be a record, would it still exist, and could it be trusted?
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| LOG-01 | Confirm an organisation trail exists, is multi-Region, and cannot be modified by member accounts. Only the management account or a delegated administrator can create one; members can read it but cannot stop logging or change selectors | Trail configuration and status; describe-trails --include-shadow-trails from a member account plus get-trail-status | Passes with one organisation trail delivering to a bucket in a separate log archive account. Warns on per-account trails running alongside it, which usually indicates a halted migration | Full |
| LOG-02 | Measure data event coverage against a stated investigative question | Event selectors, advanced event selectors, cost data, tested against a real question | Passes when data events cover the buckets, tables and functions holding the crown jewels, with the selection documented and the cost accepted deliberately | Partial |
| LOG-03 | Determine whether VPC endpoint activity is recorded | Network activity event configuration; check for eventCategory NetworkActivity selectors | Passes with network activity events enabled at least in denied-only mode for endpoints fronting data services | Full |
| LOG-04 | Establish CloudTrail Lake exposure and the migration plan | cloudtrail list-event-data-stores; check organisation versus account scope | Passes when Lake is unused, or used with an organisation-level event data store and a dated migration plan. Warns on account-level stores plus an account-provisioning process that adds them per account | Partial |
| LOG-05 | Verify log integrity validation is enabled and has actually been run | Trail configuration plus the output of the last validation execution | Passes with the setting enabled and a dated record of the last cloudtrail validate-logs run. Warns when nobody can say when it was last exercised | Partial |
| LOG-06 | Assess the log archive's retention model and who can defeat it | Object Lock configuration from the S3 API; an IAM query for holders of s3:BypassGovernanceRetention | Warns on a log archive in governance mode with the bypass permission inside a broadly assumed administrative role | Partial |
| LOG-07 | Review Config recording strategy, aggregator scope and recorder ownership | Recorder configuration per account and Region; aggregator source list compared against the account list and Regions holding resources | Warns when the aggregator's source list is narrower than the estate, which the aggregator itself does not report | Partial |
LOG-02. Do not ask whether data events are enabled. Ask the client's own most likely investigative question, such as whether they could tell you which objects a leaked role credential read, and determine from the selectors whether the trail could answer it. Data events are off by default, carry additional charges, and cover more than a hundred resource types including a long list of Amazon Bedrock resource types. Selector mechanics matter for a collection plan: on trails, only S3 objects in general purpose buckets, Lambda functions and DynamoDB tables can be selected with basic event selectors, and every other resource type requires advanced event selectors; event data stores support advanced selectors only. The two warning shapes are opposite and equally common: data events on everything, which usually precedes them being switched off entirely at the next cost review, and data events on nothing while the client believes S3 access is logged because server access logging exists somewhere. This is an evidence-availability finding, and it modifies the severity of other findings rather than standing alone.
LOG-03. Network activity events use eventCategory NetworkActivity, cover more than fifty services, are off by default and carry additional charges. VpceAccessDenied is currently the only supported errorCode value, so logging only denied endpoint access is the sole cost-controlled configuration available. Optional filters are eventName, vpcEndpointId and userIdentity.arn, and the last of these works on trails but not on event data stores. S3 Multi-Region Access Points are not supported. Denied-only mode is not coverage of successful access, and the report should state that limitation rather than treating it as coverage.
LOG-04 — CloudTrail Lake as a legacy path. Verified 2026-08-06: CloudTrail Lake is closed to new customers as of 2026-05-31. Existing customers can continue to use it, it receives only critical bug fixes and security updates, and AWS recommends migrating the data to Amazon CloudWatch. The nuance that makes this a check rather than a news item is scope. An organisation-level event data store continues to function as expected, including for new member accounts and expansion to additional Regions. An estate running only account-level event data stores will not get ingestion for accounts added to the organisation from now on. That is an expanding blind spot in which every newly onboarded account sits silently outside the analysis tier while the Terraform run reports success. The closure notice appears on one documentation page and the Lake cost-management page carries no mention of it, so a well-run team can reasonably not know. Raise it as information first and as a finding second.
LOG-05. Enabling log file integrity validation causes CloudTrail to deliver digest files to the bucket. In AWS's own words, it "does not validate the integrity of the files". Validation is an action someone has to take. The mechanism is SHA-256 hashing with SHA-256-with-RSA signing, hourly digest files in a folder separate from the logs, each digest carrying the signature of the previous one, with different key pairs per Region. The ordinary case is the setting enabled and nobody able to say when validation was last run.
LOG-06 — the retention model and who can defeat it. S3 Object Lock in compliance mode means a protected object version cannot be overwritten or deleted by any user including the root user, the mode cannot be changed, and the retention period cannot be shortened. Governance mode is bypassable by any principal holding s3:BypassGovernanceRetention who sends the x-amz-bypass-governance-retention:true header. Versioning is required for either. Legal holds have no expiry and are independent of retention periods. So the check is not whether retention is configured. It is which mode, and who holds the bypass, and the second half is an IAM query rather than an S3 query, which is exactly why the check belongs to a path model rather than to a service chapter.
Two trade-offs belong in the finding rather than in a footnote. AWS's log integrity page recommends S3 MFA Delete for digest protection, but MFA Delete cannot be used by an automated pipeline and requires root credentials to configure, which conflicts with centralised root access management under ORG-07. And compliance mode is not reversible: a retention period set in compliance mode cannot be shortened, and the only documented way to remove such an object before expiry is to close the AWS account. Recommending compliance mode without stating the cost and irreversibility consequences is a recommendation the client will resent in eighteen months. For most estates the tension resolves in favour of compliance mode with a deliberately chosen retention period, but the choice should be made explicitly and recorded.
LOG-07 — recording strategy, aggregator scope, and who owns the recorder. Three sub-questions sit under one ID.
The recording strategy is INCLUSION_BY_RESOURCE_TYPES, EXCLUSION_BY_RESOURCE_TYPES, all supported types, or all supported with overrides, at continuous or daily frequency. Global resource types onboarded before February 2022, namely AWS::IAM::Group, AWS::IAM::Policy, AWS::IAM::Role and AWS::IAM::User, can be recorded in all Regions where AWS Config was supported before that date, and should be recorded in one to avoid duplicate configuration items; they cannot be recorded at all in the Regions launched since, which the documentation lists explicitly. Global types onboarded later record only in their home Region, and AWS::RDS::GlobalCluster records in every Region where the customer-managed recorder is enabled unless excluded.
Aggregators are read-only and cover only the accounts and Regions they were configured with, or an organisation. A Region enabled last quarter and never added is invisible, and the aggregator does not report its own gap; it simply shows fewer resources. Compare its source list against the account list and the Regions holding resources, and treat the difference as the finding.
Recorder ownership is the newest question. Where Security Hub CSPM and Security Hub are both enabled, CSPM automatically creates and manages a service-linked configuration recorder named AWSConfigurationRecorderForSecurityHubCSPM, and does not use the customer-managed recorder when it exists. Teams that carefully tuned their own recorder are sometimes unaware of the handover. The consequence belongs in the report: where recording is on but not enabled for a resource type an enabled control checks, CSPM generates a FAILED finding for Config.1 and WARNING findings for the affected control and resource types. A WARNING does not depress the standards score, so an estate can display a high compliance percentage in which some controls were never evaluated at all. The question is not what the score is; it is what the WARNING count is and which controls are in it.
DET — detection and response readiness
Six checks. The question is not whether GuardDuty is on. It is whether, if the path in IAM-02 were traversed tonight, a named person would do something before morning.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| DET-01 | Confirm GuardDuty enablement, delegated administration and auto-enrolment for new accounts | guardduty get-detector and organisation configuration per Region, from the delegated administrator | Passes with detectors in every Region holding resources and auto-enable configured. Warns on Regions with resources and no detector, which is the ordinary shape of this finding | Full |
| DET-02 | Match protection plans to the estate's actual workload mix | Enabled protection plans compared against the workload inventory | Warns on an EKS estate with EKS Protection and no Runtime Monitoring, or Bedrock workloads with no AI Protection | Partial |
| DET-03 | Review Extended Threat Detection and audit suppression rules | Every suppression rule read individually, with the signal each removes from correlation stated | Warns on suppression rules written by resource type or finding type with no account or resource scoping | Partial |
| DET-04 | Determine which of Security Hub and Security Hub CSPM are enabled and what the January 2026 migration produced | securityhub describe-hub; standards state; check for organisation policies left in a failed state | Warns on failed organisation policies, an intact CSPM with no Security Hub, and a dashboard nobody has opened since January | Partial |
| DET-05 | Trace finding routing, ownership and time to triage | Routing configuration plus ticket records; follow one recent high-severity finding end to end | Warns on findings routed to a shared mailbox, findings with no acknowledgement record, and a backlog whose oldest item predates the current team | Manual |
| DET-06 | Confirm detection coverage exists for the specific identity paths found in IAM-02 | The IAM-02 output plus detection rules and alarms; for each named path, identify the detection that would fire | Passes when an assumption chain into a privileged role produces an alert carrying the session name and source | Manual |
DET-02. The current protection plan list, verified 2026-08-06, is AI Protection, S3 Protection, EKS Protection, Runtime Monitoring, Malware Protection for EC2, Malware Protection for S3, Malware Protection for AWS Backup, RDS Protection and Lambda Protection. One distinction is routinely collapsed: EKS Protection monitors the Kubernetes control plane audit log, while Runtime Monitoring covers EC2, ECS including Fargate, and EKS at the process level. They answer different questions and enabling one is frequently mistaken for enabling both. Separately, the foundational data sources, which are CloudTrail management events, VPC flow logs from EC2 instances and DNS logs, are ingested when GuardDuty is enabled, are not separately billed protection plans, and GuardDuty's copy is independent of whether the customer keeps their own flow logs or trail. A client who disabled their own VPC flow logs for cost has not blinded GuardDuty.
DET-03 — Extended Threat Detection and the suppression audit. Extended Threat Detection is enabled automatically for each account when GuardDuty is enabled, at no additional cost and without requiring any protection plan. It correlates signals over a 24-hour rolling window and emits attack sequence findings, including AttackSequence:EKS/CompromisedCluster, AttackSequence:ECS/CompromisedCluster and AttackSequence:EC2/CompromisedInstanceGroup. All attack sequence findings are Critical.
The check follows from one documented behaviour: when correlating events for attack sequences, Extended Threat Detection does not consider archived findings, including findings archived automatically because of suppression rules. A broad suppression rule written to quiet a noisy finding therefore degrades a correlation capability the client believes is free and automatic. Read every suppression rule and state, for each, which signal it removes from correlation. Because only the GuardDuty administrator account can create suppression rules in a multi-account environment, suppression is both centrally controlled and centrally damaging: one team's tuning decision degrades correlation for every account in the organisation. What this check cannot establish is what the correlation would have caught. It is an argument from mechanism rather than from observed loss, and it should be presented that way.
DET-04 — which Security Hub, and what January 2026 left behind. Two products carry the name, and the ambiguity is now an assessment problem. AWS Security Hub CSPM is the posture service, running the Foundational Security Best Practices, CIS, PCI DSS and NIST standards. AWS Security Hub reached general availability on 2025-12-02 and correlates and enriches signals from GuardDuty, Inspector and Security Hub CSPM, adding exposure findings, security-focused resource inventory and attack path visualisation.
The migration deadline has passed. AWS documented that an organisation not opting in to the GA experience by 15 January 2026 would have Security Hub automatically disabled organisation-wide across all accounts and Regions, with configurations removed, new findings not integrated, existing findings deleted as they age out, and organisation policies remaining in a failed state until deleted. Opt-in was by console or the EnableSecurityHubV2 API from the management or delegated administrator account. Disabling Security Hub does not affect Security Hub CSPM, which continues without disruption.
So the check is: which product is enabled, was the migration completed, and are any organisation policies sitting in a failed state? An estate that ran the preview and missed the deadline can today have no Security Hub, an intact CSPM, failed organisation policies, and a dashboard nobody has opened since January. A second sub-check follows from how exposure findings are produced: they draw on GuardDuty, Inspector, Security Hub CSPM and Macie, so Security Hub without those is an empty pane.
DET-06 — coverage for the paths you actually found. Generic detection coverage assessments produce generic answers. Take the specific paths from IAM-02 and, for each hop, ask which detection would fire and what it would say. The common warning shape is an estate whose detections are entirely GuardDuty findings, with no custom rules for its own privileged-role assumptions. A small number of CloudTrail-derived rules on assumption of named privileged roles is cheap and high-yield, and it is one of the few detection recommendations that transfers between estates.
DAT — data and keys
Seven checks. This domain is the target of most paths, and it is where scanner false positives are most expensive to the assessment's credibility.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| DAT-01 | Compute the S3 public-access posture rather than reading a flag | Account and bucket BPA settings plus bucket and access point policies, evaluated against S3's own definition of a public policy | Warns on any bucket or access point policy meeting the public definition; report access point findings with the origin type stated | Full |
| DAT-02 | Establish Object Ownership and ACL state across bucket vintages | Object Ownership configuration per bucket with creation dates; compare buckets created before and after the default changed | Warns on a recent bucket with ACLs deliberately re-enabled, which usually indicates an integration that writes with an ACL | Full |
| DAT-03 | Review bucket policies for organisation-boundary conditions and replication destinations | Bucket policies and replication configuration; resolve every destination bucket's owning account against the organisation account list | Warns on a destination account outside the organisation, and on a replication rule with no filter on a bucket holding mixed-sensitivity data | Partial |
| DAT-04 | Determine the KMS key policy design and delegation model | Key policies for customer-managed keys, classified as delegating to IAM or granting directly | Warns on a key policy granting to the account root and to a broad set of external accounts, which is the combination that matters | Full |
| DAT-05 | Assess key usage, rotation and unused keys. Since an announcement of 2026-04-27, KMS exposes the last cryptographic operation per key with a timestamp, operation type and CloudTrail event ID, replacing CloudTrail archaeology; confirm the current API name before scripting it | Key metadata, rotation state and last-usage data | Passes with automatic rotation on customer-managed keys holding long-lived data and a documented decision where it is off. Warns on keys with no recorded usage that nobody can attribute. An unused key is housekeeping unless its policy grants external access, in which case it is DAT-04 | Full |
| DAT-06 | Review Secrets Manager resource policies, rotation and cross-account key design | Secret resource policies and rotation configuration; check for secretsmanager:BlockPublicPolicy | Warns on aws:SourceIp conditions in a secret's resource policy, which break rotation Lambdas | Partial |
| DAT-07 | Find plaintext secrets in Parameter Store, environment variables and user data | SSM parameter inventory by type, Lambda environment variables, launch template and Auto Scaling user data, searched for credential-shaped values | Warns on SecureString parameters encrypted with the AWS managed key where cross-account or fine-grained control is required | Full |
DAT-01 — public is computed, not flagged. The four Block Public Access settings are BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy and RestrictPublicBuckets, and their semantics differ in ways that are routinely conflated: BlockPublicAcls rejects the call, IgnorePublicAcls accepts the call and ignores the result. S3 assumes a bucket policy is public and then attempts to prove otherwise. To be non-public, a policy must grant access only to fixed values, with no wildcards and no policy variables, of at least one of: a specific principal, aws:SourceIp at /8 or narrower for IPv4 and /32 or narrower for IPv6, aws:SourceArn, aws:SourceVpc, aws:SourceVpce, aws:SourceOwner, aws:SourceAccount, aws:userid outside the AROLEID:* pattern, s3:DataAccessPointArn, or s3:DataAccessPointAccount. aws:PrincipalOrgID also qualifies, which surprises people who assume only account or ARN pinning counts.
Three false positives are worth handling carefully because getting them wrong costs credibility. Access point BPA settings are fixed at creation and cannot be changed. An access point with a VPC network origin is treated as non-public regardless of its policy. And an access point policy using an s3:DataAccessPointArn wildcard is considered public, while the same pattern in a bucket policy with a fixed account ID is not. Defaults now work in the client's favour: new buckets, access points and objects deny public access by default.
DAT-04 — the default KMS key policy is routinely misread. The Enable IAM User Permissions statement names the account principal with kms:* on *. Reading that as "everyone in the account can use this key" is wrong. AWS's documentation is explicit that when the principal is the account principal, the statement does not give any IAM principal permission to use the key; it allows the account to use IAM policies to delegate the permissions specified. Without it, IAM policies allowing access to the key are ineffective, although IAM policies denying access remain effective.
The check must catch two opposite errors. A finding that says the key policy grants root full access is usually wrong and will cost the report credibility with any AWS-literate reader. A key policy that removes that statement and grants use directly to named principals is a different design with a real lockout risk, and it is also the only design in which the key policy is genuinely the access control. Which design you are looking at determines whether reviewing IAM policies is sufficient to know who can use the key.
DAT-06. Protect secrets with a customer-managed key where cross-account access is involved, with kms:ViaService conditioning the key policy, and enforce secretsmanager:BlockPublicPolicy when resource policies are set. Secrets Manager supports automatic rotation as often as every four hours, which makes "rotation is too disruptive" a weaker argument than it was. One specific trap is worth checking for directly: AWS cautions against aws:SourceIp conditions on a secret's policy because a rotation Lambda calls Secrets Manager from AWS-internal address space, and recommends aws:SourceVpc or aws:SourceVpce instead. An estate that added IP conditions to secrets and then quietly disabled rotation has two findings and knows about one.
DAT-07. A credential in the wrong store has no rotation, no access log granularity and no resource policy. The AWS managed key cannot carry a key policy the client controls, and resource control policies do not apply to AWS managed keys either, so a SecureString under aws/ssm is weaker than it looks in a cross-account design. This check automates well and belongs in CI rather than in an annual assessment.
NET — network and exposure
Six checks. Network findings are where severity is most often overstated, because reachability is a property of a chain and most tools report one link of it.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| NET-01 | Build an internet-exposure inventory from resources rather than from subnets | Resource inventory plus VPC Block Public Access state and its exclusion list | Warns on an exclusion list without an owner and a reason per entry | Partial |
| NET-02 | Review VPC endpoint policies against the default-allow behaviour | All VPC endpoint policies compared against the default Allow */*/* | Warns on any endpoint retaining the default policy, and on services that do not honour endpoint policies at all | Full |
| NET-03 | Trace security group and NACL paths to data services | Security group and NACL rules plus data service placement; for each data service, enumerate the ingress paths that actually reach it | Warns on security groups referencing other security groups in a chain deeper than two, which usually means the original segmentation intent has been lost | Partial |
| NET-04 | Assess DNS resolution, Resolver query logging, DNS Firewall rule groups and the VPC failure mode | Resolver configuration, query log destinations and retention, firewall rule group associations, and the per-VPC fail-open setting | Warns on query logging with a retention period too short to answer an investigative question, and on any VPC where fail-open has been enabled without a recorded reason | Partial |
| NET-05 | Review edge configuration: CloudFront, ALB, WAF rules and origin protection | Distribution and load balancer configuration; WAF web ACLs read for blocking versus counting mode | Warns on WAF rules in count mode for more than a quarter. Count mode is a legitimate tuning state and an illegitimate permanent state, and the difference is a date | Partial |
| NET-06 | Enumerate cross-VPC and cross-account connectivity and state what each path routes around; a peering connection to a vendor-managed VPC bypasses every egress control the estate has | Peering, Transit Gateway and PrivateLink configuration. What traffic actually crosses these links needs flow logs and a period of observation, not configuration | Warns on Transit Gateway route tables with a default route to a shared services VPC and no inspection. Transit Gateway now integrates with Network Firewall without a separate inspection VPC, so an estate that rejected inspection on complexity grounds two years ago may want to revisit it | Manual |
NET-01. An exposure inventory built from subnet route tables tells you which subnets could host a public resource. An inventory built from resources tells you which ones do. Enumerate instances with public IPs, load balancers with internet-facing schemes, RDS instances marked publicly accessible, API Gateway endpoints, Lambda function URLs, CloudFront distributions and any global accelerator. One control most checklists omit is VPC Block Public Access, set per account per Region, in either bidirectional mode, which blocks all traffic to and from internet gateways and egress-only internet gateways, or ingress-only mode, which blocks inbound while leaving NAT gateway and egress-only gateway outbound connections working. The interesting artifact is the exclusion list. VPC-level and subnet-level exclusions are supported, and an account-wide bidirectional block with fourteen subnet exclusions is not a perimeter; it is a list, and every entry needs an owner and a reason.
NET-02 — the endpoint policy default. The default endpoint policy allows every principal every action on every resource for that service through the endpoint. An estate that has moved to private connectivity typically has dozens of interface endpoints, each defaulting to permitting any principal to reach any resource of that service through it, including buckets in accounts that are not theirs. The network perimeter is not created by the endpoint. It is created by the endpoint policy.
Illustrative: VPC endpoint policies. Validate service action names and test in a non-production VPC before applying. An over-tight endpoint policy fails closed and breaks workloads.
{
"Comment": "Default endpoint policy - permits everything through the endpoint",
"Statement": [
{ "Effect": "Allow", "Principal": "*", "Action": "*", "Resource": "*" }
]
}
{
"Comment": "Resource perimeter - only resources in this organisation",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*",
"Condition": {
"StringEquals": { "aws:ResourceOrgID": "o-EXAMPLE" }
}
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:PrincipalOrgID": "o-EXAMPLE" }
}
}
]
}
Three traps in this area are all documented. Where an AWS service does not support endpoint policies, AWS allows full access to any endpoint for that service regardless of what a policy says, so the endpoint policy inventory must be checked against the set of services that honour it. With gateway endpoints the Principal element must be set to *, and a principal is specified using the aws:PrincipalArn condition key instead. And specifying "AWS": "123456789012" as a principal grants the account root user only, not every principal in that account, which makes a policy written that way far tighter than its author intended and produces failures that look like a service outage.
NET-04 — DNS. Exfiltration over DNS routes around most egress controls because DNS is almost always permitted. Route 53 Resolver DNS Firewall filters outbound DNS traffic for a VPC using reusable rule groups, with rule actions of ALLOW, ALERT and BLOCK, where BLOCK requires a response type of NODATA, NXDOMAIN or OVERRIDE. Four AWS managed domain lists are available and are usable as a starting deny set: AWSManagedDomainsMalwareDomainList, AWSManagedDomainsBotnetCommandandControl, AWSManagedDomainsAggregateThreatList and AWSManagedDomainsAmazonGuardDutyThreatList.
The setting most worth reading is the per-VPC fail-open configuration. AWS documents that fail-open is disabled by default, meaning that when DNS Firewall is impaired or unresponsive the VPC Resolver blocks queries it cannot get a reply for and returns SERVFAIL, favouring security over availability. Enabling fail-open reverses that. The check is therefore not only whether rule groups are associated, but whether anyone has flipped fail-open for a VPC during an incident and never flipped it back. Ask for the date and the reason on every VPC where it is enabled. Note also that the ALLOW action is not available in DNS Firewall Advanced, so an allow-list strategy and an advanced deployment are not directly interchangeable; confirm which tier is in use before recommending a rule design. Query logging with a 24-hour retention satisfies a control and answers no investigative question.
WKL — workload identity and compute
Seven checks. This domain is where the path usually enters the identity graph, because a compromised workload is a credential.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| WKL-01 | Measure IMDSv2 enforcement on instances and on account-Region defaults | describe-instances metadata options and account metadata defaults per Region, compared against the AMI's declared support | Passes with the account-Region default set to required in every Region plus a clean instance inventory and a launch template review | Full |
| WKL-02 | Assess instance profile privilege and the hop-limit interaction with containers | Instance profiles and their policies; hop limit per instance; identify hosts running containers with a hop limit above 1 | Warns on a node role holding s3:*, secretsmanager:GetSecretValue, or any iam:PassRole grant | Partial |
| WKL-03 | Establish EBS encryption-by-default state and find unencrypted legacy volumes and snapshots. The setting is per Region and has no effect on volumes or snapshots that already exist | get-ebs-encryption-by-default per Region plus a separate enumeration of existing unencrypted volumes and snapshots. This document does not state the default value for a new account because the documentation does not; verify it per account and Region | Warns on unencrypted snapshots shared outside the account, which is a materially different finding from unencrypted attached volumes | Full |
| WKL-04 | Review AMI sharing state and block public access for AMIs, including already-public AMIs | launchPermission on all owned AMIs plus the BPA state per Region, checked separately | Warns on any owned AMI with the all group in launchPermission, regardless of the BPA setting | Full |
| WKL-05 | Enumerate Lambda resource policies, function URL residue and layer policies | lambda get-policy for all functions and get-layer-version-policy for all layers, compared against current URL configuration | Passes when every wildcard principal in a Lambda policy carries aws:PrincipalOrgID, aws:SourceAccount or aws:SourceArn | Full |
| WKL-06 | Review Lambda environment variables, execution roles and VPC placement | Function configuration and execution role policies | Warns on credentials in environment variables, which are readable by anyone with lambda:GetFunctionConfiguration, and on execution roles reused across functions with different data access | Full |
| WKL-07 | Assess the EKS access model and the effective runtime identity | Cluster authentication mode, access entries, aws-auth ConfigMap, Pod Identity associations, IRSA annotations, and effective identity in CloudTrail | Warns on any disagreement between the two authorisation sources, and on an effective identity that differs from the configured association | Partial |
WKL-01 — IMDSv2 is inherited, not default. This is the check where a well-read engineer's priors are most likely to be wrong. AWS documents that when the account-level metadata default is set to no preference, the value resolves at launch from the AMI: the hop limit defaults to 2 if the AMI carries the setting ImdsSupport: v2.0 and otherwise to 1, and the same inheritance governs whether IMDSv2 is required. The practical statement is that IMDSv2 enforcement is inherited from the AMI unless the account default or the launch call sets it explicitly. An organisation that enforced IMDSv2 by setting HttpTokens: required in one Terraform module has enforced it for the instances that module launches. Instances launched from a launch template copied in 2022, from a marketplace AMI, or by a third-party appliance are governed by that AMI's declared support. The check is an inventory question plus an account-default question, and neither alone is sufficient. Whether an exception was deliberate cannot be read from configuration; some appliances genuinely require v1, and those need a documented exception rather than a silent one.
WKL-02. A hop limit of 2 on a host running a container runtime means container network namespaces can reach the instance metadata service and assume the node role, which is almost always broader than the container's own identity. The remediation carries an operational condition that must be stated: setting the hop limit to 1 on a node blocks pods from reaching IMDS at all, which breaks any workload that currently depends on the node role for credentials. Sequence the workload identity migration first, then the hop limit. This hop is one of the more reliably exploitable in the whole model and should be scored accordingly when the host runs anything internet-facing.
WKL-04 — the already-public trap. AMIs are shared through the launchPermission attribute, and adding the group all makes an AMI public. Block public access for AMIs defaults to enabled for new accounts and to disabled for existing accounts that already had one or more public AMIs when the feature arrived. The trap is that enabling block public access does not un-publish AMIs that are already public: AWS states that when it is enabled, any attempt to make an AMI public is blocked, but existing public AMIs remain publicly available. An account can therefore show block public access enabled and still be sharing an AMI with the world. Check both the setting and the current launchPermission on every owned AMI. Raise severity substantially where the AMI is a golden image, because those routinely contain baked-in agent configuration, internal hostnames and occasionally credentials.
WKL-05 — Lambda policy residue. AWS documents that deleting a function URL with auth type NONE does not delete the associated resource-based policy, and that the policy must be removed manually. The residue is a function policy carrying a public statement with no URL attached. It is invisible to anyone auditing function URLs, and it becomes live again the moment someone recreates a URL on that function. Enumerate lambda get-policy across all functions rather than list-function-url-configs.
Illustrative: detecting function URL policy residue. Read-only. Validate against your own account structure and add Region iteration and pagination before use.
# For every function in this account and Region, print any resource policy
# statement that grants to a wildcard principal, then compare against the
# set of functions that actually have a URL configured.
for fn in $(aws lambda list-functions --query 'Functions[].FunctionName' --output text); do
pol=$(aws lambda get-policy --function-name "$fn" \
--query Policy --output text 2>/dev/null) || continue
echo "$pol" | grep -q '"Principal":"\*"\|"AWS":"\*"' && echo "wildcard-principal: $fn"
done
aws lambda list-function-url-configs --function-name EXAMPLE-function
Lambda resource-based policies cover functions and layers, and a layer shared with Principal: "*" and no organisation condition is a supply-chain exposure that almost nothing checks. Read them with get-layer-version-policy.
WKL-07 — the EKS access model. Four sub-questions live under this ID, each with its own failure mode.
The authorisation source comes first. The aws-auth ConfigMap is deprecated, with access entries as the recommended method. No removal date has been published, so describe it as deprecated rather than removed. The migration hazard is documented and severe: where a cluster relied on the ConfigMap, only the access entry for the original cluster creator is created automatically when access entries are enabled, and additional roles or permissions added to the ConfigMap are not migrated. A half-migrated cluster is the normal state rather than an exceptional one, and the assessor must read both sources.
The workload identity mechanism comes second. IRSA is not deprecated. AWS recommends EKS Pod Identity where possible, and the documented reasons are practical rather than security-absolute: Pod Identity avoids per-cluster OIDC provider setup against a default quota of 100 OIDC providers per account; it avoids the 2,048-character trust policy limit, which permits roughly four trust relationships and up to about eight with a limit increase; it avoids pods calling AssumeRoleWithWebIdentity against STS quotas; and it supports role session tags for attribute-based access control, which IRSA does not. IRSA remains the only option for EKS Anywhere, Red Hat OpenShift Service on AWS, and self-managed Kubernetes on EC2. Present this as a fit decision, not as a security upgrade.
| Mechanism | Correct when | Fails at |
|---|---|---|
| Node instance profile | Nothing else is available, and the node runs one workload | Every pod on the node shares one identity; hop limit 2 makes it reachable from containers |
| IRSA | Non-EKS Kubernetes, EKS Anywhere, ROSA, or an existing working deployment | OIDC provider quota, the 2,048-character trust policy limit, no session tags |
| EKS Pod Identity | EKS clusters, including cross-account access to a target role | EKS only; requires the agent; cross-account chaining shortens the credential cache |
The effective identity comes third, and it is where the check earns its place. AWS documents that where workloads use credentials earlier in the provider chain, those credentials continue to be used even when a Pod Identity association is configured. A team can believe it has migrated while pods keep silently using the node instance profile. Verify the effective identity in CloudTrail userIdentity for that workload's actual calls rather than verifying the association and stopping there.
Cross-account target roles come fourth. Pod Identity supports assuming a target role in another account by role chaining and applies session tags carrying the cluster ARN, namespace and service account. The target role's trust policy can condition on those tag values alongside aws:PrincipalArn, which scopes the trust decision to a named service account in a named namespace in a named cluster. That is a genuinely strong design, and it is not the design most teams write first; the weak form trusts arn:aws:iam::123456789012:root with no tag conditions, because that is the version that works immediately. Where session tags are disabled, Pod Identity instead sets sts:ExternalId to a value of the form region/account-id/cluster-name/namespace/service-account-name. One false-positive note is worth recording: the credential cache lasts six hours for a Pod Identity role alone and 59 minutes when a target role is chained, so a client who says they revoked something yesterday may be describing a change that had not taken effect when the evidence was captured. Record collection timestamps on every EKS artifact.
PIPE — pipeline and infrastructure as code
Four checks. In most estates the CI/CD role is the most privileged non-human identity, and it is the one least often included in an identity review.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| PIPE-01 | Review the CI/CD federation model and the conditions on OIDC trust policies | OIDC provider list plus pipeline role trust policies; test whether the subject pattern matches more than intended | Warns on any wildcard in a subject condition, and on a trust policy with no condition beyond audience | Partial |
| PIPE-02 | Determine pipeline role privilege and blast radius | Pipeline role policies and the accounts it can reach, computed with the IAM-02 graph | Warns on one pipeline role holding AdministratorAccess in every account | Partial |
| PIPE-03 | Find infrastructure-as-code drift and out-of-band change | CloudTrail mutating events whose userIdentity is a human federated session, compared against pipeline execution and change records | Passes when console-originated infrastructure change is rare and each instance maps to an incident or an approved exception | Partial |
| PIPE-04 | Confirm policy-as-code gates exist in CI, including Access Analyzer custom policy checks | CI configuration, gate definitions and their failure behaviour | Warns on gates that warn rather than block, and on gates bypassable with a commit message flag and no record of who used it | Full |
PIPE-01 — OIDC subject conditions. This check changed under everyone on 15 July 2026 and much published guidance has not caught up.
GitHub's changelog of 2026-04-23 states that GitHub Actions OIDC tokens now include immutable identifiers in the default sub claim for new repositories; that all repositories created after 15 July 2026 automatically use the new immutable subject claim format; that repository renames and transfers after that date also adopt it; and that existing repositories are unaffected unless explicitly opted in. The new format appends numeric owner and repository IDs, of the form repo:octocat@123456/my-repo@456789:ref:refs/heads/main. The motivation is name recycling: an attacker acquiring a released organisation or repository name could previously mint tokens with an identical sub.
Illustrative: three GitHub Actions OIDC trust policies. Substitute your own account ID, organisation and repository identifiers, and verify the current sub format for the repository in question before applying.
{
"Comment": "A - works, and is wrong. The wildcard matches every repository in the org, including one created by anyone with repo-creation rights.",
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
"StringLike": { "token.actions.githubusercontent.com:sub": "repo:example-org/*:*" }
}
}
{
"Comment": "B - correct today, breaks if the repository is renamed or transferred after 2026-07-15.",
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:example-org/example-repo:ref:refs/heads/main"
}
}
}
{
"Comment": "C - CloudSecOps recommendation. Conditions on immutable claims directly, so it survives renames, transfers and the sub format change.",
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:repository_owner_id": "123456",
"token.actions.githubusercontent.com:repository_id": "456789",
"token.actions.githubusercontent.com:job_workflow_ref": "example-org/example-repo/.github/workflows/deploy.yml@refs/heads/main"
}
}
}
Version A keeps working after the migration and keeps being wrong; the wildcard is the finding, and it survives the change untouched. Version B breaks on a rename, and that outage is the good outcome, because it fails closed and someone investigates. Version C is the recommendation. GitHub's OIDC reference documents repository_id and repository_owner_id as available claims carrying the repository and organisation IDs, so a policy conditioned on them does not depend on the sub format at all. GitHub's own AWS guidance is that a trust policy must define at least one condition so that untrusted repositories cannot request access tokens, and that the audience for AWS is sts.amazonaws.com. Other claims worth conditioning on include job_workflow_ref, repository_visibility and runner_environment, the last of which distinguishes github-hosted from self-hosted runners and is worth pinning where self-hosted runners exist.
Two limits belong with this recommendation. Version C is more brittle to legitimate change than version B: the numeric IDs are opaque, and a team that migrates to a new repository must edit an AWS trust policy with no human-readable clue about which repository it refers to. Record the mapping somewhere. And no AWS-side configuration can tell you who can modify the workflow file that the trust policy trusts. A trust policy pinned to deploy.yml@refs/heads/main is only as strong as the branch protection on main and the review requirements on that file. That is a source-control question and it must be asked.
PIPE-02. Treat the pipeline role as a compromised principal and compute what it reaches using the IAM-02 graph. The common shape is one pipeline role with AdministratorAccess in every account, defended as "the pipeline needs to create anything". The fix direction is separate roles per environment with separate OIDC conditions, and a deployment role whose iam:PassRole grants are scoped by iam:PassedToService. Whether narrowing will break a deployment path used quarterly cannot be read from configuration. Ask for the release calendar before recommending a hard cut.
PIPE-04. Evaluate IAM policy changes in CI before merge, using Access Analyzer custom policy checks to answer whether a policy grants new access relative to the current one and whether it allows a set of critical actions. Amazon Inspector code security uses the Amazon Q Developer scanning engine and covers infrastructure as code alongside first-party code and third-party dependencies, so an estate already paying for Inspector may have an IaC scanning capability it has not enabled.
AIX — AI and machine learning workloads
Four checks. This is the domain moving fastest. An assessment written from twelve-month-old notes will be wrong about the mechanism here, not merely out of date on the details.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| AIX-01 | Determine which models any principal can invoke, and enumerate cross-account model access | Identity and organisation policy searched for model-ARN constraints on bedrock:InvokeModel and bedrock:Converse; AWS RAM shares enumerated | Warns when no policy constrains model invocation by model ARN, which means every serverless model in every enabled Region is invocable by any principal holding the action | Partial |
| AIX-02 | Establish Bedrock model invocation logging state and the protection of its destination | Logging configuration, then the destination bucket and log group treated as a sensitive data store: policy, encryption, retention, replication | Passes with logging enabled, a customer-managed key, a bucket policy restricted to a named analysis role, deliberate retention, and the destination inside the estate's data classification | Partial |
| AIX-03 | Verify guardrail enforcement through the Organizations Bedrock policy and IAM conditions | Bedrock policy documents plus role policies using bedrock:GuardrailIdentifier; check for a guardrail per Region | Passes with organisation-level enforcement plus a guardrail in each Region in use | Partial |
| AIX-04 | Review SageMaker AI network isolation, VPC configuration and execution role scope | Training job and model configuration checked for EnableNetworkIsolation; execution role scope read directly | Warns on an execution role with s3:* on *, which is the default shape of most SageMaker AI setups | Partial |
AIX-01 — model access is no longer something you enable. AWS documented in October 2025 that Bedrock provides automatic access to the serverless models in a Region, retiring the Model Access page along with the PutFoundationModelEntitlement IAM permission and its API call, and stating that IAM statements carrying that permission no longer have an effect. Models blocked through IAM policies or service control policies remain restricted. The standard check is therefore invalid: "which models have you enabled?" has no answer, and a client answering "none" is describing a console page that no longer exists.
The check becomes an enumeration of the identity-based and organisation-level policy that constrains bedrock:InvokeModel and bedrock:Converse by model ARN, and a determination of whether any constraint exists at all. In an estate with no such policy, the exposure appeared without anybody changing anything, which is why the date matters when a client's own notes disagree. The cross-account path runs through AWS Resource Access Manager: custom model sharing within an organisation has the receiving account copy the model into its own Region, so enumerate RAM shares and not only IAM.
AIX-02 — invocation logging, and the cost of enabling it. Model invocation logging is disabled by default. Destinations are CloudWatch Logs, S3 or both, in the same account and Region only, and logging covers Converse, ConverseStream, InvokeModel and InvokeModelWithResponseStream through the bedrock-runtime endpoint. Records carry input and output JSON bodies up to 100 KB, with larger bodies written as separate objects under a data prefix in S3 and referenced from the log entry; binary data such as images is written separately regardless of size when image logging is enabled.
Two consequences pull in opposite directions and the check must carry both. Without invocation logging there is no record of what was sent to or returned from a model, which makes prompt-injection, exfiltration-through-completion and model-abuse investigations impossible. With it, prompts and completions, which routinely contain customer personal data, credentials pasted by users, and proprietary content, land in an S3 bucket and a CloudWatch log group whose access control, encryption, retention and replication almost nobody has reviewed. Recommending invocation logging without reviewing the destination converts an observability gap into a data-protection finding. Where the estate has data residency obligations, note that the destination must be in the same Region as the inference, which constrains centralised log architectures.
AIX-03 — guardrail enforcement. Two mechanisms exist and most teams know neither. AWS Organizations has an Amazon Bedrock policy type that lets the management account reference a guardrail created in the management account and attach it at the root, an OU or an account, applying it to model inference for accounts beneath that node. Guardrails are Region-specific and a separate guardrail is required in each Region where enforcement is wanted, so an organisation policy naming one guardrail does not cover a Region that does not have one. Separately, the bedrock:GuardrailIdentifier IAM condition key denies inference requests that do not carry the configured guardrail.
Three documented caveats determine whether the control works, and all three belong in the finding. The condition key conflicts with roles that also call RetrieveAndGenerate or InvokeAgent, because those make multiple InvokeModel calls and some do not include a guardrail, producing access-denied errors even when the guardrail is specified. Guardrail input tags let a user bypass guardrail evaluation on the prompt side, although the guardrail is always applied to the response. And because guardrails do not support resource-based policies for cross-account access, the guardrail must be in the same account as the IAM role making the request. One cross-reference is worth stating because it makes guardrails load-bearing beyond safety: GuardDuty's direct prompt-injection finding type requires Amazon Bedrock Guardrails and is unavailable in Regions where Guardrails is unsupported.
AIX-04 — SageMaker AI isolation and execution role scope. A naming note matters for searching the documentation: the original service is now Amazon SageMaker AI, while bare "SageMaker" names the umbrella family that also contains SageMaker Unified Studio, SageMaker Lakehouse and SageMaker Catalog. EnableNetworkIsolation is available on CreateTrainingJob, CreateHyperParameterTuningJob and CreateModel. When enabled, containers cannot make outbound network calls to any service including S3, and no AWS credentials are made available to the container runtime; SageMaker AI still performs the S3 download and upload using the execution role, outside the container. Containers are internet-enabled by default, and the managed Chainer and SageMaker AI Reinforcement Learning containers do not support network isolation because they require S3 access.
One detection cross-reference changes how the LOG domain reads for AI workloads. GuardDuty AI Protection analyses CloudTrail data events from Bedrock, Bedrock AgentCore and SageMaker AI, and it collects them by creating a CloudTrail service-linked channel in each monitored account whose data event settings the account owner cannot modify. That coverage will not appear in describe-trails; it is visible under service-linked channels in the CloudTrail console or through the ListChannels API. An assessor who checks only trail configuration will conclude, wrongly, that there is no data-event coverage for model invocation.
RSP — resilience and operations
Five checks. Recovery is a security control, because the modern destructive event is a ransom or a wiper executed with valid credentials.
| Check | Objective and threat | Evidence and verification | Pass and warn indicators | Automation |
|---|---|---|---|---|
| RSP-01 | Assess backup coverage, vault lock and air-gapped vault design | Backup plans, vault configurations and RAM shares; protected resource types compared against the resource inventory | Warns on backup vaults in the same account as the workload with a vault access policy the account's administrative role can modify | Partial |
| RSP-02 | Obtain restore testing evidence rather than a restore testing assertion | Restore testing plan, recent restore job records with durations, Backup Audit Manager control state | Passes with dated restore jobs whose durations are compared against the stated recovery objective | Full |
| RSP-03 | Review patch management through SSM patch policies, starting with the baseline | Patch baseline definitions including classification and severity filters and auto-approval delay, read before the compliance number | Warns when the baseline is an AWS predefined baseline, which AWS provides as an example rather than as best practice | Partial |
| RSP-04 | Determine Amazon Inspector coverage across scan types and what it excludes | Inspector configuration and coverage statistics compared against the workload inventory | Warns on EC2 instances neither managed by SSM nor within agentless coverage, and on container images built and run without entering ECR | Full |
| RSP-05 | Assess incident response readiness, including access during an incident and cost-based abuse alerting | Runbooks, break-glass procedure, Cost Anomaly Detection monitors and subscriptions; walk one scenario aloud | Warns on monitors configured with no subscription, which is the same as no monitor | Manual |
RSP-01. An AWS Backup logically air-gapped vault is always encrypted, is always equipped with Vault Lock compliance mode, and can be shared through AWS Resource Access Manager with individual account IDs only, not with organisational units and not with an entire organisation. It can be combined with multi-party approval so that recovery survives loss of the owning account. The threat model to state explicitly is that backup design against hardware failure and backup design against an attacker holding administrative credentials are different designs, and only the second one is a security control.
RSP-02 — restore testing as an artifact. This check used to be an interview question and no longer needs to be. AWS Backup restore testing runs scheduled restore jobs, supports a validation window of 1 to 168 hours, tags restored resources awsbackup-restore-test and deletes them automatically afterwards, and Backup Audit Manager carries a control evaluating whether restore time meets a target. Ask for the restore testing plan, the last several restore job records with their durations, and the Audit Manager control state. "We test restores annually" and "here are twelve dated restore jobs with recorded durations" are different qualities of evidence, and the second is available for the asking.
Two false positives matter. DynamoDB, S3, SAP HANA on EC2, virtual machines and Timestream do not support tag-on-restore and are cleaned up by name instead, and S3 bucket deletion after a test can take days because lifecycle rules must remove the objects, so orphaned test resources are not automatically a process failure. What restore testing cannot establish is whether a restore works under contention, at scale, with the network partitioned. A restore test in a quiet account is evidence about the mechanism, not about the incident.
RSP-03 — the baseline before the compliance number. AWS states that its predefined patch baselines are provided as examples rather than as recommended best practices, that it does not test patches before making them available, that a node is compliant when the patches the baseline both approves and has available have been installed, and plainly that being compliant does not mean a node is secure. Linux severity ratings come from the software publisher, with no CVSS or NVD derivation. The warning indicator is therefore not compliance below some percentage. It is that the baseline is an AWS default, because a default baseline with a generous auto-approval delay can report high compliance while leaving a critical vulnerability unpatched by design. Ask for the baseline definition before looking at the compliance figure. On scope, patch policies through Quick Setup apply one configuration across Regions and accounts or across selected OUs, while maintenance windows are limited to a single account and Region, so an estate still using maintenance windows almost certainly has uneven coverage.
RSP-04. Inspector scan types are EC2 scanning, which by default activates hybrid mode combining the SSM agent with agentless EBS snapshot scanning; ECR scanning; Lambda standard scanning; Lambda code scanning; and code security using the Amazon Q Developer engine. First activation automatically enrols the account in EC2 scanning, ECR scanning and Lambda standard scanning, so the other two are deliberate additions rather than defaults. What Inspector cannot tell you is whether a reported vulnerability is reachable in the running configuration: it adjusts its own score for network reachability but does not know whether the vulnerable code path is called.
RSP-05. Walk one scenario aloud, such as the deployment role's credentials being used from an unfamiliar Region at 02:00, and record where the walk stalls. It usually stalls at access: the responder cannot reach the account, or the only path to it is the credential that is suspect. The pass condition is a documented, pre-provisioned responder role with its own alerting that does not depend on the compromised identity path. On cost as an abuse signal, Cost Anomaly Detection requires at least one monitor and at least one alert subscription per monitor. AWS-managed monitors automatically include new member accounts; customer-managed monitors track up to ten manually selected accounts and do not, which is a quiet coverage gap in a growing organisation. Cryptomining, model-invocation abuse and data egress all produce a cost signal before they produce a security signal.
Controls that look stronger than they are
Each entry pairs a documented mechanism with a judgment about why the illusion holds. The judgment is CloudSecOps's; the mechanism is sourced.
| Control | Why it looks strong | The mechanism that defeats it | What to check instead |
|---|---|---|---|
| A comprehensive SCP set | Denies appear at the top of the organisation and read as absolute | SCPs do not affect the management account, do not affect service-linked roles, and do not affect resource-based policies directly | Which principals are exempt, and whether an RCP covers the resource-policy path |
| A high Security Hub CSPM standards score | A percentage is legible to executives and trends upward | Where Config recording is incomplete, affected controls produce WARNING rather than FAILED, and a WARNING does not depress the score | The WARNING count and which controls are in it |
| Extended Threat Detection, on by default and free | It requires no configuration, so it cannot be misconfigured | Correlation does not consider archived findings, including those archived automatically by suppression rules | Every suppression rule, and which signal each removes from correlation |
| Log file integrity validation enabled | The setting name states the outcome | Enabling it delivers digest files; it does not validate the integrity of the files | The date of the last validate-logs run and what was done with the output |
| A permissions boundary on delegated admin roles | Intersection semantics are well understood and genuinely strong | Same-account resource policies granting to a role session ARN grant directly to the session, unbounded by the boundary | Resource policies naming session ARNs in the same account |
| Private connectivity through VPC endpoints | Traffic no longer traverses the internet | The default endpoint policy permits every principal every action on every resource of that service, including resources in other organisations | Every endpoint policy, and the list of services that do not honour endpoint policies at all |
| IMDSv2 enforced in the Terraform module | The module is the standard way instances are created | The account default resolves from the AMI's declared support, so anything launched outside the module inherits the AMI's setting | The per-instance inventory and the account-Region default |
| Block public access for AMIs enabled | The name describes a total control | Enabling it does not un-publish AMIs that are already public, and it defaults to disabled in accounts that already had one | Current launchPermission on every owned AMI |
| Function URLs removed | The URL is gone from the console | Deleting a function URL with auth type NONE does not delete the resource-based policy | lambda get-policy on every function, not the URL configuration list |
| An empty Access Analyzer findings list | An analyzer exists and reports nothing | The external access analyzer analyses only policies in the Region where it is enabled | Analyzers per Region, compared against the Regions holding resources |
| Object Lock configured on the log archive | Retention is a compliance-recognised control | Governance mode is bypassable by any principal holding s3:BypassGovernanceRetention | The lock mode, and the IAM query for who holds the bypass |
| A migrated EKS cluster using access entries | The ConfigMap is deprecated and the migration was performed | Only the cluster creator's access entry is created automatically; other ConfigMap mappings are not migrated | Both sources of truth, compared |
| DNS Firewall rule groups associated to the VPC | Rules exist and the association is visible | The per-VPC fail-open setting decides what happens when the firewall is unavailable, and it can be flipped during an incident and left | The fail-open state per VPC, with a date and a reason for each one enabled |
What automated scanners usually miss
Scanners are good at single-resource predicates, which is most of what this document's tables ask for, and an assessment that re-does by hand what a scanner does well is wasting the client's money. The value of a human assessment sits in five specific places.
Reachability across a graph. No scanner that evaluates resources independently can report that a public load balancer is four hops from an administrative principal, because that fact is not stored in any resource. Tools that do compute this exist, and where the client already runs one, the assessor's job shifts to validating its principal-resolution logic, particularly its handling of bare account-root principals in trust policies, which is where these tools most commonly diverge from reality in both directions.
Residue. Configuration that no longer has a corresponding object: a Lambda resource policy with no function URL, an OIDC provider for a CI system the client stopped using, a KMS grant for a decommissioned service, a cross-account role for a vendor whose contract ended. Residue is invisible to checks that iterate over live objects and ask about their settings, because residue is not attached to a live object. The way to find it is to iterate over policies and ask what each one is for.
Half-migrations. The EKS access model is the clearest example, but the pattern is general: a partial move from aws-auth to access entries, from IRSA to Pod Identity, from per-account trails to an organisation trail, from CloudTrail Lake to CloudWatch, from IAM users to Identity Center. During a half-migration both systems are live and the client's mental model describes only the destination. A scanner reports the state of each system independently and neither report is wrong; the finding is in the gap between them.
Intent. A scanner can tell you a role has iam:PassRole on *. It cannot tell you whether that role is the deployment path for a product the company depends on, in which case removing it is an outage, or a leftover from a proof of concept, in which case removing it is free. Severity, remediation sequencing and the client's willingness to act all depend on the answer, and the answer is only available by asking.
Ownership. Every finding in this document reduces to a question about who is responsible. A detection with no owner is not a detection. A guardrail nobody maintains decays into an obstacle that gets exempted. The absence of a named owner is one of the strongest predictors of estate health available to an assessor, and no scanner has a field for it.
Benchmark compliance is not proof: a constructed demonstration
This is a constructed example. It is not an anonymised engagement finding and it is not an observed result. Every individual fact used in it is verified against AWS documentation; the combination is assembled to make an argument. It has not been built and run in a live account, and a reader should treat it as a shape to look for rather than as a claim about their own estate.
Consider an account in which every applicable control of the CIS AWS Foundations Benchmark passes. Root has no access keys and has MFA. There are no IAM users with console passwords. CloudTrail is enabled in all Regions with log file validation. S3 Block Public Access is on at the account level. Every EBS volume is encrypted. Security groups do not permit 0.0.0.0/0 on port 22 or 3389. GuardDuty is enabled. AWS Config is recording.
Now trace a path.
Illustrative diagram: the constructed escalation.
Hop one. The application behind the load balancer has a server-side request forgery flaw, or any bug that yields the ability to make outbound requests with the task's credentials. Benchmark controls do not evaluate application code. Nothing fails.
Hop two. The ECS task role holds a small set of permissions plus sts:AssumeRole on the account's deployment role, because a build step needed it and the grant was never narrowed. The deployment role's trust policy names the account root, delegating the decision to IAM, and IAM allows it. No benchmark control evaluates which roles can assume which other roles. Nothing fails.
Hop three. The deployment role holds iam:PassRole on * and lambda:CreateFunction. That pair is administrative privilege spelled in two actions. The attacker creates a function whose execution role is an existing administrative role and invokes it. No benchmark control evaluates the combination of iam:PassRole with a compute-creation action. Nothing fails.
The argument is complete at three hops: full administrative control of a benchmark-compliant account, with nothing failing.
A fourth hop is where assessors most often overreach, so it is worth stating precisely what it is and is not. Security tooling accounts commonly hold an organisation-wide audit role, and the usual configuration has member-account roles trusting that audit role so the security team can read every account. That direction of trust does not extend the attacker's reach at all: reaching admin in a member account does not let you assume a role that trusts the security account. Reach extends outward only where the trust runs the other way, for example where this account's administrative role is named in another account's trust policy, or where a cross-account role in this account trusts principals here and is granted privileges elsewhere. Which direction the trust runs is exactly what IAM-02 determines, and it is a finding either way: an assessment that assumes the escalating direction is inventing a path, and an assessment that assumes the safe direction is missing one.
The reason the benchmark reports clean throughout is structural rather than a gap in any particular version. Every benchmark control is a predicate over a single resource, and an attack path is a predicate over a graph. Adding more single-resource controls does not produce a graph predicate, in the same way that no quantity of measurements of individual bricks tells you whether a wall has a door in it. The count of controls in any given benchmark version is therefore irrelevant to the argument, which is why no count appears in it.
The two fragments that close hops two and three are small.
Illustrative: the conditions that close the constructed path. Test in a non-production account. Scoping PassRole incorrectly fails closed and breaks deployments.
{
"Comment": "Closes hop three - PassRole scoped by target and by service",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": [
"arn:aws:iam::123456789012:role/service-role/app-runtime-role",
"arn:aws:iam::123456789012:role/service-role/batch-runtime-role"
],
"Condition": {
"StringEquals": { "iam:PassedToService": "lambda.amazonaws.com" }
}
}
{
"Comment": "Closes hop two - the deployment role trusts named principals, not the account root",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/ci-deploy-github"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:PrincipalTag/pipeline": "release" }
}
}
Neither fragment is a benchmark control. Both are the output of an assessment that walked the graph. That is the argument of this document, in two policy statements.
The interview set
Configuration answers what is. Interviews answer whether anyone intended it and whether anyone maintains it. Four roles, the questions that produce evidence, and what a weak answer sounds like.
| Role | Questions that produce evidence | What a weak answer sounds like |
|---|---|---|
| Cloud platform lead | How does an account get created and what does it inherit? Who decides which OU a new product team lands in? What runs in the management account and what would moving it take? What was the last SCP change and what prompted it? Which Regions are enabled but unused? | "The landing zone handles it", with no ability to name what the landing zone applies; a Region list that does not match the resource inventory; no SCP change in eighteen months, which usually means the SCPs are not maintained against a changing estate |
| IAM owner | Who can become an administrator, by what path? How would you revoke a compromised federated session right now? Who reviews a new role's trust policy? Which roles are assumable by third parties, and how do you know? | "Only the admins group", with no path analysis behind it; a revocation answer that describes disabling a user in the IdP without mentioning existing sessions or session duration; no answer at all to the third-party question, which is the common case |
| SecOps | Walk me through the last GuardDuty finding that mattered. Who tunes suppression rules and what is the review process? If the deployment role were used from an unfamiliar Region at 02:00, what happens? How long are CloudTrail logs retained, and who can delete them? | An inability to name a recent finding, which usually means findings are not read rather than that none occurred; suppression rules created by whoever was annoyed by the noise; a retention answer that does not distinguish the trail's S3 lifecycle from the analysis tier |
| Development lead | Where does your application get its AWS credentials? What does your pipeline role need that it has? When did you last change infrastructure through the console, and why? What is in your Lambda environment variables? | "Environment variables", without knowing what populates them; "everything, it's the pipeline"; a console change described as routine |
The interview is also where an assessor calibrates severity. A finding the team already knows about, has a ticket for and has scheduled is a different report entry from an identical finding nobody has seen. Say which it is.
Read-only collection and the assessor role
Two arguments here. The first is about what an assessor should be granted. The second is about what an assessor should refuse to do with it.
SecurityAudit is at version 91, last edited 2026-07-21. Its own description scopes it precisely: it grants access to read security configuration metadata and is useful for software that audits the configuration of an AWS account. It carries object-metadata reads such as s3:GetObjectAcl and s3:GetObjectTagging, plus lambda:GetPolicy, rather than object bodies. ReadOnlyAccess is at version 188, last edited 2026-07-21. It contains data-plane read actions including dynamodb:Query, dynamodb:Scan, dynamodb:PartiQLSelect, dax:GetItem, glacier:Get*, cassandra:Select and healthlake:ReadResource. Its S3 statement carries s3:Get*, which matches s3:GetObject and s3:GetObjectVersion, so it does permit reading object bodies. Its Secrets Manager statement carries secretsmanager:Describe*, secretsmanager:GetResourcePolicy and secretsmanager:List*, but not secretsmanager:GetSecretValue. The reach is uneven in ways that are not guessable from the policy name, which is the argument for reading the policy document rather than repeating what circulates about it. The DynamoDB reach alone makes the point: a vendor whose contract describes configuration monitoring can read the contents of tables.
| Capability the assessment needs | Why SecurityAudit may not cover it | Why ReadOnlyAccess overshoots | Recommended grant |
|---|---|---|---|
| Organizations policy retrieval and target enumeration | Managed policy scope changes; verify against the current version rather than assuming | Grants far more than organisation reads | organizations:Describe*, List* |
| CloudTrail channel and event data store enumeration | Newer APIs are not always reflected promptly | Not the problem here | cloudtrail:List*, Get*, Describe* |
| EKS access entry and association listing | Access entries are a recent API surface | Not the problem here | eks:List*, Describe* |
| Access Analyzer findings retrieval per Region | Usually covered; verify | Not the problem here | access-analyzer:List*, Get* |
| Bedrock and GuardDuty configuration reads | AI service coverage is the fastest-moving area | Includes model invocation where the action is classified as a read | bedrock:Get*, List* excluding inference; guardduty:Get*, List* |
| Reading customer data | Correctly absent | Present, and must not be used | Explicit Deny |
The CloudSecOps recommendation is SecurityAudit plus a named, enumerated supplement, plus an explicit deny on data-plane reads, plus a permissions boundary on the assessor role so the supplement cannot silently widen.
Illustrative: an assessor role deny statement. The action list is a starting point, not a verified-complete enumeration. Extend it for the services in your own estate and test collection against it before an engagement, because an over-broad deny will silently break checks and produce false "not configured" conclusions.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "NoDataPlaneReads",
"Effect": "Deny",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"dynamodb:GetItem",
"dynamodb:BatchGetItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:PartiQLSelect",
"dax:GetItem",
"dax:Query",
"dax:Scan",
"secretsmanager:GetSecretValue",
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
"kms:Decrypt",
"cassandra:Select",
"glacier:GetJobOutput",
"bedrock:InvokeModel",
"bedrock:Converse"
],
"Resource": "*"
}
]
}
A caveat about that statement, stated because the alternative is a false promise: a guaranteed-complete least-privilege assessor policy requires iterative testing in a live account against the specific services in scope, and no such testing underpins the list above. Treat it as a design principle with a worked starting point. Two of these denies have known collection side effects to plan for. Denying ssm:GetParameter* removes the ability to read parameter values, which is intended, but DAT-07 still needs ssm:DescribeParameters to enumerate parameters by type, so do not widen the deny to ssm:Get*. Denying kms:Decrypt is correct and has no effect on kms:DescribeKey or kms:GetKeyPolicy, which are what DAT-04 actually needs. Diff both managed policies against their current versions before each engagement; both were edited on the same day in July 2026, which is a reasonable indication of how often AWS revises them.
The second argument is about conduct. ReadOnlyAccess permits reads that an assessment has no business performing. Reading the contents of a production DynamoDB table is technically a read-only operation and is not part of a security assessment. The scope statement should say so, the role should deny it, and the assessor should behave as though the deny were not there. Collection commands used in an assessment are read-only by design: no sts:AssumeRole into discovered roles, no secretsmanager:GetSecretValue, no s3 cp, no kms decrypt. Where a finding's proof would require one of those, the finding is written from the policy document instead, and the executable proof is deferred to a separately authorised test.
Illustrative: representative read-only collection. Add Region iteration and pagination handling, and validate the assessor role's permissions against your own scope before an engagement.
# Organisation ceiling
aws organizations describe-organization
aws organizations list-policies --filter SERVICE_CONTROL_POLICY
aws organizations list-policies --filter RESOURCE_CONTROL_POLICY
aws organizations list-policies --filter DECLARATIVE_POLICY_EC2
# The graph. Run per account, from the assessor role in that account.
aws iam get-account-authorization-details > authz-123456789012.json
# Credential state
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d
# Evidence
aws cloudtrail describe-trails --include-shadow-trails
aws cloudtrail get-trail-status --name EXAMPLE-org-trail
aws cloudtrail list-event-data-stores
aws cloudtrail list-channels
aws configservice describe-configuration-recorders
aws configservice describe-configuration-aggregators
# Exposure
aws ec2 describe-instances \
--query 'Reservations[].Instances[].[InstanceId,MetadataOptions.HttpTokens,MetadataOptions.HttpPutResponseHopLimit,PublicIpAddress]' \
--output text
aws ec2 get-ebs-encryption-by-default
aws ec2 describe-vpc-endpoints --query 'VpcEndpoints[].[VpcEndpointId,ServiceName,PolicyDocument]'
# External and internal access
aws accessanalyzer list-analyzers
Record the Region and the timestamp against every artifact. Several checks above turn on timing: the credential report's four-hour cache, the Pod Identity credential cache, GuardDuty's 24-hour correlation window. An artifact without a timestamp cannot be used to settle a disagreement about when something changed.
Severity: a rubric that fits cloud configuration
Clients ask for a CVSS score. The honest answer is that CVSS is the wrong instrument for a cloud configuration finding, and the strongest evidence for that is not an argument from principle. It is that AWS itself has moved away from raw CVSS for this class of finding, twice.
Amazon Inspector produces a score in CVSS format that modifies the NVD base score. AWS documents that Inspector examines the security metrics composing the NVD base score and adjusts them according to the compute environment, giving as its own example that the score of a finding for an EC2 instance may be lowered where the vulnerability is exploitable over the network but no open network path to the internet is available from the instance. That is the platform vendor conceding that a base score alone misprices findings in a cloud environment, and correcting for reachability specifically.
AWS Security Hub exposure findings do not use CVSS at all. Severity is derived from ease of discovery, ease of exploit, likelihood of exploit, public awareness and potential impact, and findings carry two kinds of annotation: contributing traits, the factors that produced the finding, and contextual traits, factors observed but not causal, such as unused IAM permissions on an affected principal. That distinction is a useful reporting device, because it separates why the finding exists from why it is worse here than it would be elsewhere, and it is worth adopting in an assessment report regardless of whether Security Hub is in use.
The CVSS specification agrees with the conclusion in its own scope statement. CVSS v4.0, specification document version 1.2, states that factors including regulatory requirements, number of customers impacted, monetary losses due to a breach, life or property threatened, and reputational impacts are outside the scope of CVSS, and that consumers may use CVSS as an input to a process that also considers factors CVSS does not cover. A cloud misconfiguration's severity is almost entirely determined by factors on that excluded list, plus reachability. A configuration is also not a vulnerability with a CVE, has no vendor patch, and its Attack Vector and Attack Complexity metrics carry no defensible meaning for sts:AssumeRole reachability.
The rubric below is CloudSecOps's, not a standard. It is offered because a client needs a defensible ordering, not because it is authoritative.
| Factor | Levels | How to evidence the level |
|---|---|---|
| Reachability | Internet-reachable · reachable from a compromised workload · reachable only by an authenticated internal principal · not reachable | The path from IAM-02 and the exposure inventory from NET-01, stated as a specific chain |
| Blast radius | Organisation-wide · multi-account · single account · single resource | The set of accounts and resources reachable from the finding's endpoint |
| Evidence availability | Full history with integrity · management events only · partial · none | The LOG domain's output for the specific services on the path |
| Compensating controls | Enforced guardrail · detective control with an owner · detective control with no owner · none | The SCP or RCP that applies to this principal, and the DET-06 answer |
| Remediation cost | Config change · policy change with testing · architectural change · vendor dependency | The interview, not the configuration |
Illustrative diagram: severity band assignment.
Two rules keep the rubric honest. Evidence availability raises severity but never lowers it, because the absence of logging makes a finding worse and its presence does not make a reachable admin path acceptable. And a detective control with no owner earns no reduction, because the reduction is being claimed for a response that will not happen.
The rubric has a known failure mode worth stating. It is deliberately biased towards reachability, which means it will underrate findings whose harm is regulatory or contractual rather than technical: data replicated to an account in the wrong jurisdiction, or a log retention period shorter than a contractual commitment, can both score Medium here and be the most expensive item in the report. Where a client has specific regulatory exposure, add a factor for it rather than trying to express it as reachability.
A worked application. The RDS publicly accessible setting means the instance receives a public IP address in addition to its private one, and its DNS endpoint resolves to the public address from outside the VPC. AWS states that access is controlled in the end by the security group, and that the subnets in the DB subnet group must also have an internet gateway. So a publicly accessible RDS instance behind a security group permitting ingress only from one application security group is a real finding, because the flag is a latent exposure activated by a single security group edit, but it is not the same finding as one with 0.0.0.0/0 on the database port. Under the rubric, reachability is "reachable only by an authenticated internal principal" in the first case and "internet-reachable" in the second; blast radius and evidence availability are identical; the bands differ by two. A tool that reports both as a publicly accessible RDS instance at the same severity has told the client something true and useless.
The finding structure, with one worked finding
Every finding carries an identifier, a title stating the condition rather than the control name, severity with the rubric factors shown, affected scope, evidence, how it was observed, impact reasoning, remediation with a concrete diff, a verification step, and an explicit statement of what the finding does not establish. That last field is the one most reports omit and the one that most protects the client from over-reading.
AWS-2026-014 — VPC interface endpoints permit access to resources outside the organisation
Severity: High. Reachability: reachable from a compromised workload, because any principal with network access to the affected subnets can reach the endpoint. Blast radius: multi-account, because the endpoint policy places no constraint on the account owning the target resource. Evidence availability: partial, because CloudTrail network activity events are not enabled and successful access through these endpoints is therefore not recorded. Compensating controls: none identified; no RCP constrains the resource side. Remediation cost: policy change with testing.
Affected scope. Eleven interface endpoints across three VPCs in account 123456789012, including the S3, Secrets Manager and KMS endpoints in the production VPC.
Evidence. Each affected endpoint carries the default policy.
Illustrative: observed configuration, redacted. This is the default endpoint policy as returned by describe-vpc-endpoints.
{
"Statement": [
{ "Effect": "Allow", "Principal": "*", "Action": "*", "Resource": "*" }
]
}
How it was observed. aws ec2 describe-vpc-endpoints from the assessor role, read-only, on the collection date recorded in the appendix. No traffic was generated through any endpoint and no attempt was made to access a resource in another account.
Impact reasoning. A workload in these subnets that is compromised, through application vulnerability, dependency compromise or a leaked credential, can use these endpoints to write data to an S3 bucket in an account outside the organisation. The endpoint provides the network path and the policy provides no constraint on the destination account. Because network activity events are not enabled, the attempt would leave no record at the endpoint layer. This is the concrete mechanism behind the phrase "private connectivity is not a perimeter": private connectivity removes internet transit, and a resource perimeter is what removes the destination.
Remediation. Apply a resource-perimeter condition to each endpoint policy, as in the second example in NET-02, conditioning on aws:ResourceOrgID and denying principals outside the organisation. Sequence this in a non-production VPC first: an endpoint policy that is too tight fails closed, and the failure presents as an application error rather than as an access-denied message a developer will recognise. For gateway endpoints, Principal must remain * and the principal constraint is expressed with aws:PrincipalArn. Consider enabling CloudTrail network activity events in denied-only mode at the same time, so the tightening produces observable evidence rather than silence.
Verification. Re-run describe-vpc-endpoints and confirm each policy carries the condition; confirm application health after the change; confirm denied-access events appear in CloudTrail for a deliberately out-of-organisation test target in a non-production account.
What this finding does not establish. It does not establish that any principal has used these endpoints to reach an external resource; that would require network activity events, which do not exist for the period in question. It does not establish that a workload in these subnets is currently compromised. And it does not establish that the eleven endpoints are the complete set, because endpoints in Regions outside the collection scope were not examined.
What an assessment cannot tell you
The most valuable section of an assessment report is the one that states its own limits, and it is the section clients most often ask to have shortened. Resist that. A finding list read as complete is more dangerous than no finding list, because it converts an unknown into a false negative.
A read-only configuration review cannot establish:
- Whether a reachable path has ever been used. Reachability is a property of policy. Use is a property of history, and it needs CloudTrail for the whole relevant period, which frequently does not exist.
- Whether an exposed service is authenticated. The exposure inventory finds the endpoint. Whether it demands credentials is an application question and, beyond a banner check, a penetration-testing question.
- The effective runtime identity of a workload, without CloudTrail. The association, the annotation and the instance profile all describe intent. Only the API calls describe reality.
- Whether the identity provider is well administered. Everything upstream of the SAML or OIDC trust is invisible from AWS, and it is the highest-blast-radius component in the estate.
- What the data is. Bucket names, tags and encryption state describe containers. Classification requires the business.
- Whether a control has an owner. This is an interview answer, and it changes the severity of a substantial share of the findings in a typical report.
- Whether remediation will cause an outage. The dependency is usually undocumented, and the person who knows sits on the delivery team rather than the security team.
- Whether the estate you assessed is the estate that exists. Accounts outside the organisation, Regions outside the collection scope, and resources created after the collection date are all invisible. Record the collection window and the account list in the report, and state that findings are scoped to them.
Where a check above says a question cannot be concluded from configuration, that question belongs in the interview set or in a separately authorised test. An assessment that quietly guesses at those answers produces a more satisfying document and a less useful one.
Where this method does not fit
The method assumes a multi-account AWS Organizations estate with a security function, and it costs assessor days. Four architectures strain it, and one class of remediation advice in it is dangerous when applied without sequencing.
A small team on one or two accounts. Most of ORG collapses to a single finding, IDN often collapses to "there is no IdP", and the graph in IAM-02 may have twenty nodes. The method still applies but the proportions do not: running all sixty-nine checks against a three-person startup is a way to generate a report nobody will act on. Run the minimum viable action list, IAM-02, WKL-05 and LOG-06, and stop. The rest becomes useful when the second and third accounts exist.
An estate with no AWS Organizations organisation, or with several. The assessor role assumption in this document presumes one organisation and one source principal. Estates formed by acquisition frequently hold two or three organisations that trust each other through ordinary cross-account roles. That configuration is not covered by any SCP or RCP argument here, the ORG domain must be run once per organisation, and the interesting findings are almost all in the roles that span them. Say so in the scope statement rather than assessing the largest organisation and calling it the estate.
A landing-zone-managed estate. Where Control Tower or an equivalent owns the baseline, many findings are properties of the landing zone version rather than of the account, and the correct remediation is "upgrade or amend the landing zone", not "change this setting", which would be reverted at the next drift remediation. Determine who owns the landing zone before writing any remediation in the ORG, IDN or LOG domains.
Partitions and Regions with different service availability. Several checks in this document depend on services whose availability is uneven: the Security Hub GA experience, Bedrock and its guardrails, GuardDuty AI Protection, some declarative policy types, and DNS Firewall tiers. In GovCloud, China Regions, and newer commercial Regions, the correct answer to several checks is "not available here", which is a scope note rather than a finding. Confirm regional availability before scoring a gap.
Remediation advice that must be sequenced. Four recommendations in this document break things when applied directly. Setting an instance metadata hop limit to 1 blocks pods from reaching IMDS, so migrate workload identity first. Tightening a VPC endpoint policy fails closed and surfaces as an application error, so stage it in a non-production VPC. S3 Object Lock in compliance mode cannot be shortened or reversed, so choose the retention period as a business decision rather than a maximum. And deleting an access key marked unused on a four-hour-stale credential report can break a quarterly batch job, so correlate with CloudTrail over a period covering the client's longest operational cycle before deleting anything.
Minimum viable action list
For a team that wants to start on Monday, in this order. The ordering is CloudSecOps's judgment, weighted towards things that are cheap to do and that change what you can see.
- Export
iam get-account-authorization-detailsfrom every account and build the trust graph. Even a rough version answers the question everything else depends on. - Enumerate what runs in the management account, and write down why each item is there.
- List your service-linked roles per account and treat the list as a standing exception register against your SCPs and RCPs.
- Check whether you use CloudTrail Lake, and if so whether your event data stores are organisation-scoped. Account-level stores will not ingest for accounts added from now on.
- Determine which of Security Hub and Security Hub CSPM you have, and whether any organisation policies are in a failed state from the January 2026 migration.
- Read every GuardDuty suppression rule and write down what each removes from Extended Threat Detection's correlation.
- Run
lambda get-policyacross every function and compare the wildcard-principal results against the functions that actually have URLs. - Check the Object Lock mode on your log archive, then run the IAM query for who holds
s3:BypassGovernanceRetention. - Count your Access Analyzer external analyzers against the Regions that hold resources, not against the Regions that are enabled.
- Take your three most privileged roles and, for each, name the detection that would fire if it were assumed from an unfamiliar source.
None of these requires a budget cycle. Several will produce a finding on the first afternoon.
References
All sources below were fetched or re-verified on 2026-08-06 and 2026-08-07.
AWS Organizations and tenancy
- AWS Organizations: managing policies
- Service control policies and what they do not affect
- Resource control policies, their exemptions and supported services
- Declarative policies and the account status report
- Amazon Bedrock policies in AWS Organizations
- Best practices for the management account
- AWS Security Reference Architecture: the Org Management account
Identity and authorization
- Centralised root access management in AWS Organizations
- Privileged root task policies and the Regional STS requirement
- Data perimeters: the three perimeters and their condition keys
- The confused deputy problem and external IDs
- Permissions boundaries, the role session exception and the NotPrincipal warning
- IAM Access Analyzer: analyzer types and Regional behaviour
- Access Analyzer supported resource types
- The AWS ReadOnlyAccess managed policy, version 188
- The AWS SecurityAudit managed policy, version 91
- Access keys and AWS's position on long-lived credentials
- IAM credential reports and their four-hour cache
- STS Regional and global endpoints
- IAM Identity Center: organization and account instances
- Permission set session duration
- Trusted identity propagation
Logging and posture data
- CloudTrail Lake service availability change, effective 31 May 2026
- Creating an organization trail
- Logging data events with CloudTrail
- Logging network activity events for VPC endpoints
- CloudTrail log file integrity validation
- AWS Config: selecting which resources to record
- AWS Config aggregators and their scope
- S3 Object Lock: governance and compliance modes
Detection
- What is Amazon GuardDuty: protection plans and foundational data sources
- GuardDuty Extended Threat Detection and the archived-finding exclusion
- GuardDuty AI Protection and its service-linked CloudTrail channel
- Introducing the Amazon GuardDuty investigation agent
- What is AWS Security Hub CSPM
- Security Hub preview to GA migration and the January 2026 deadline
- Security Hub prerequisites and the service-linked configuration recorder
- Security Hub exposure findings and their severity model
- Security Hub document history
- CIS AWS Foundations Benchmark support in Security Hub CSPM
- AWS security services best practices: Security Hub
- What is Amazon Inspector, including how the Inspector score adjusts for reachability
- Amazon Inspector scan types
Data, keys and secrets
- S3 Block Public Access and how S3 computes a public policy
- S3 Object Ownership and the BucketOwnerEnforced default
- The default KMS key policy and what the account principal statement actually does
- AWS KMS now tracks last usage of all KMS keys
- Identify unused AWS KMS keys and prevent accidental deletion
- Secrets Manager best practices, including the aws:SourceIp rotation trap
Network
- VPC endpoint access control and the default endpoint policy
- VPC Block Public Access
- Route 53 Resolver DNS Firewall
- DNS Firewall rule actions and block response types
- DNS Firewall VPC configuration and the fail-open setting
- DNS Firewall AWS managed domain lists
- Working with a DB instance in a VPC, including "publicly accessible"
Compute and workload identity
- Configuring instance metadata options for new instances
- EBS encryption by default
- Sharing AMIs and the launchPermission attribute
- Block public access for AMIs and its defaults
- Lambda resource-based policies for functions and layers
- Lambda function URL authentication and policy residue
- EKS service accounts: IRSA compared with Pod Identity
- How EKS Pod Identity works
- EKS Pod Identity cross-account target roles and session tags
- The deprecated aws-auth ConfigMap
- EKS access entries and what migration does not carry over
Pipeline
- Immutable subject claims for GitHub Actions OIDC tokens
- GitHub Actions OpenID Connect reference: available claims
- Configuring OpenID Connect in Amazon Web Services
AI and machine learning
- Simplified Amazon Bedrock model access
- Bedrock model invocation logging
- Using the bedrock:GuardrailIdentifier condition key and its caveats
- Sharing a custom Bedrock model
- SageMaker AI network isolation for training and inference
- Amazon SageMaker AI and the wider SageMaker family: naming
Resilience and operations
- AWS Backup logically air-gapped vaults
- AWS Backup restore testing
- AWS Systems Manager Patch Manager and what compliance means
- Patch policies in Systems Manager Quick Setup
- Getting started with AWS Cost Anomaly Detection
Severity
Release timing referenced above
- AWS Security Hub general availability, 2 December 2025
- CIS AWS Foundations Benchmark v5.0 support in Security Hub CSPM, October 2025
- ICYMI: June 2026 AWS Security round-up
- ICYMI: May 2026 AWS Security round-up
Validity and revision
Verified 2026-08-06, re-verified 2026-08-07 against the sources listed above. Where a claim depends on a version or a date, that version or date is stated inline rather than left implicit.
The material below moves fastest and should be re-checked before this document is used to plan an engagement.
| Statement | Verified | Re-check by |
|---|---|---|
| CloudTrail Lake closed to new customers; organisation versus account event data store behaviour | 2026-08-07 | 2027-02-07 |
| Two products named "Security Hub"; GA on 2025-12-02 and the January 2026 migration outcome | 2026-08-07 | 2027-02-07 |
| GuardDuty protection plan list and Extended Threat Detection behaviour | 2026-08-07 | 2026-11-07 |
| The GuardDuty investigation agent, in public preview since 2026-07-20 with per-account preview quotas | 2026-08-06 | 2026-11-07 |
| Organizations declarative policy types, currently eleven | 2026-08-07 | 2026-11-07 |
| RCP supported-service coverage, which has expanded repeatedly and is enumerated on the RCP page | 2026-08-07 | 2026-11-07 |
| GitHub Actions OIDC immutable subject claims, enforced for new and renamed repositories from 2026-07-15 | 2026-08-07 | 2026-11-07 |
EKS access entries versus the deprecated aws-auth ConfigMap, with no published removal date | 2026-08-07 | 2027-02-07 |
ReadOnlyAccess v188 and SecurityAudit v91, both edited 2026-07-21 | 2026-08-07 | 2026-11-07 |
| Bedrock automatic model access and the retired Model Access page | 2026-08-07 | 2026-11-07 |
| Security Hub CSPM's service-linked Config recorder and WARNING behaviour | 2026-08-07 | 2026-11-07 |
| DNS Firewall fail-open default, managed domain list names, and the DNS Firewall Advanced action restriction | 2026-08-07 | 2027-02-07 |
| Access Analyzer resource-type coverage per analyzer type | 2026-08-07 | 2026-11-07 |
Three items are stated as open rather than settled. The exact IAM action set required for a complete read-only collection was not established by iterative testing in a live account, so the assessor-role policy above is a design principle with a worked starting point rather than a verified-complete enumeration. The API and CLI name for retrieving KMS key last-usage data was not confirmed against the KMS API reference; the capability and its announcement date are verified, the call is not. And the Regional behaviour of the internal access analyzer is asserted from documentation that states it explicitly only for the unused access analyzer; confirm it before relying on a single-Region internal analyzer.
Recommended review date: 2026-11-07. Full re-verification by 2027-02-07.
- aws
- cloud-security-assessment
- iam
- cloudtrail
- guardduty
- security-hub
- attack-paths
- data-perimeter
- eks
- bedrock
The service behind this work
AWS security assessment and penetration testing
We audit AWS the way it is actually built — principals, Organizations boundaries, workload roles, and the data paths between them — and hand back the escalation paths we can demonstrate, each with the policy, SCP, or Terraform change that closes it.
Related reading
All articles →Automated remediation on AWS
An engineering guide to automated security remediation on AWS: when to automate, why an organization trail plus a service control policy removes most of the CloudTrail-disruption problem before any code runs, and how to build a loop that is idempotent, bounded, reversible and switchable-off.
· 55 min read
Detection engineering in AI-era clouds
Most evidence you need to detect attacks on AI workloads is off by default, billable, capped or sampled. A field guide to the telemetry that exists today.
· 77 min read
AI governance for engineers, not lawyers
Translate AI governance into systems you can build: inventory reconciled against runtime telemetry, deployment gates that block, human oversight you can measure, and evidence that survives review. With EU AI Act dates as amended in July 2026.
· 77 min read