AWS Security for Banks: SCPs, RCPs and Declarative Policies Against Real Incidents
Banks should prevent AWS breaches with organization level guardrails rather than policy documents, because incidents like Capital One and Imperva traced to permissions and network paths that should never have existed. Service control policies cap maximum permissions in every account regardless of internal IAM policies, while resource control policies constrain who can access resources such as S3 buckets or KMS keys even from outside the organization. Together they encode the bank standard once at the root level.
September 18, 2026 ยท Andrew Baker
1. Why central guardrails, not guidelines
Most public AWS breaches are not exotic. They are the same handful of mistakes recurring across different companies, and the fix in nearly every case was available beforehand as a preventive control rather than a detective one. The Capital One breach of 2019 exposed over 100 million customer records after an attacker exploited a misconfigured web application firewall to reach the EC2 instance metadata service and pull IAM credentials. The Imperva breach of 2019 traced back to a database snapshot that became reachable after an internal compute instance holding an AWS API key was exposed to the internet. Uber’s 2016 breach began with AWS access keys sitting in a private code repository, and Code Spaces went out of business in 2014 after an attacker with console access deleted its data and its backups in a single attack. In each case the root cause was a permission, a credential or a network path that should never have existed in the first place.
For a bank, the response to this pattern cannot be a policy document that engineers are asked to remember, and it cannot be a detective control that tells you about the problem after the data has left. It has to be a set of guardrails enforced once, centrally, at the AWS Organizations level, so that even a fully privileged administrator in a member account cannot create the conditions for the next breach by accident or by compromise. This post is deliberately limited to that category. Everything below is a control you configure at the root or organizational unit level and never touch again per account: no AWS Config rules, no per workload IAM design, no runbooks, and nothing that depends on a team doing the right thing in their own account.
There are three policy types to build this on, and they work best in a fixed order of preference. Declarative policies, available for EC2, VPC and EBS since December 2024 and for S3 Block Public Access since November 2025, enforce a service’s configuration directly in its control plane, so the setting holds regardless of which API a principal uses and continues to hold as AWS ships new APIs. Service control policies set the maximum permissions available to identities in an account, regardless of what the IAM policies inside that account allow. Resource control policies do the equivalent from the resource side, constraining who can be granted access to a resource such as an S3 bucket, a KMS key or a secret, including principals from outside the organization. Where AWS offers a declarative control for an invariant, that control should come first, with SCPs and RCPs covering what declarative policies do not yet reach.
One structural exception applies to everything in this post. Neither SCPs nor RCPs affect the AWS Organizations management account itself. A bank’s management account therefore cannot be protected the way every member account can, which is exactly why AWS recommends keeping it empty: no workloads, almost no human access, and no function beyond managing the organization. The guardrails below make every member account safe by construction; the management account has to be kept safe by having almost nothing in it to protect.
The rest of this post works through what the central standard has to cover, organized around the incident pattern each guardrail closes off.
2. Credential theft through the metadata service
The Capital One case is the clearest illustration of why identity controls matter as much as network controls. The attacker used a server side request forgery vulnerability in a misconfigured WAF to make an EC2 instance query its own instance metadata service, which returned temporary credentials for the IAM role attached to that instance. The attacker then used those credentials from their own infrastructure, outside Capital One’s network, to list and copy S3 data. Two separate central controls break that chain at two separate points.
The first is IMDSv2, which requires a session token obtained through a PUT request with a custom header before any metadata can be read. Because most SSRF vulnerabilities can only issue GET requests, IMDSv2 closes off the large majority of this attack class. The strongest way to mandate it is no longer an SCP on ec2:RunInstances. EC2 declarative policies now include an enforcement attribute, http_tokens_enforced, which causes any attempt to launch an instance with IMDSv1 available, or to re enable IMDSv1 on an existing instance, to fail at the control plane. The companion http_tokens attribute sets the default so that launches which do not specify metadata options still succeed with IMDSv2.
{
"ec2_attributes": {
"instance_metadata_defaults": {
"http_tokens": { "@@assign": "required" },
"http_tokens_enforced": { "@@assign": "enabled" }
}
}
}The @@assign operator sets the value for every account the policy is attached to, and a member account administrator cannot override it. Setting http_tokens to required alone would only change the default; it is http_tokens_enforced that turns IMDSv2 from a default into an invariant. Existing instances still running IMDSv1 need migrating before this is attached, because enforcement stops IMDSv1 being enabled but the migration of old instances is a one off exercise, not something the policy does for you.
The second control addresses the half of the Capital One chain that IMDSv2 does not: credentials that have been stolen by some other route and are then used from somewhere else. Since 2023, every request signed with EC2 instance role credentials carries the VPC and private IP of the instance they were issued to, in the aws:ec2InstanceSourceVPC and aws:ec2InstanceSourcePrivateIPv4 condition keys. An SCP can compare those against where the request actually came from, and deny it when they differ.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInstanceCredentialsOutsideTheirVpc",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:ec2InstanceSourceVPC": "${aws:SourceVpc}" },
"Null": { "ec2:SourceInstanceARN": "false" },
"BoolIfExists": { "aws:ViaAWSService": "false" },
"ArnNotLike": { "aws:PrincipalArn": "arn:aws:iam::*:role/aws:ec2-infrastructure" }
}
},
{
"Sid": "DenyInstanceCredentialsFromAnotherIp",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:ec2InstanceSourcePrivateIPv4": "${aws:VpcSourceIp}" },
"Null": { "ec2:SourceInstanceARN": "false" },
"BoolIfExists": { "aws:ViaAWSService": "false" },
"ArnNotLike": { "aws:PrincipalArn": "arn:aws:iam::*:role/aws:ec2-infrastructure" }
}
}
]
}The Null condition on ec2:SourceInstanceARN scopes the statements to requests made with instance role credentials, so human and pipeline roles are unaffected. The StringNotEquals comparisons deny any such request whose VPC or source IP does not match the instance the credentials were issued to, which means credentials exfiltrated to an attacker’s laptop are useless the moment they leave the VPC. The aws:ViaAWSService exception allows AWS services to act on the instance’s behalf, and the aws:ec2-infrastructure exception covers EC2’s own use of the role. The precondition worth being explicit about is that this pattern only works when instances reach AWS APIs through VPC endpoints, because the aws:SourceVpc key is only populated on requests that arrive through one. For a bank that already routes all traffic through a controlled network, that is the architecture it should have anyway, but AWS documents that a small number of APIs, such as mounting an EFS file system, do not use VPC endpoints and will need an exception.
3. Long lived credentials and the root user
Uber’s 2016 breach did not require a vulnerability at all. Engineers had stored AWS access keys in a private code repository, attackers obtained access to the repository, and the keys gave them a direct path to an S3 bucket holding data on 57 million riders and drivers. Imperva’s breach followed the same shape: an AWS API key held on a compute instance, found by an attacker, and used to reach snapshot data. Long lived access keys are the single most common starting point for AWS compromise because they work from anywhere, do not expire, and are routinely copied into places nobody tracks.
The central control is to make IAM users and their access keys structurally unavailable. Human access should come through IAM Identity Center with short lived sessions, and workload access through roles, including IAM Roles Anywhere for workloads outside AWS. Once that is in place, an SCP removes the ability to create the alternative.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyLongLivedCredentials",
"Effect": "Deny",
"Action": [
"iam:CreateUser",
"iam:CreateAccessKey",
"iam:CreateLoginProfile",
"iam:UpdateLoginProfile"
],
"Resource": "*",
"Condition": {
"ArnNotLike": { "aws:PrincipalArn": "arn:aws:iam::*:role/IdentityPlatformPipeline" }
}
}
]
}The action list covers creating an IAM user, issuing it an access key, and giving it a console password, which together are every way to mint a credential that outlives a session. The single exception is the pipeline role that owns identity for the platform, so that the rare legitimate case, such as SMTP credentials for SES, goes through one reviewed path rather than being created by hand in a member account.
The root user deserves the same treatment. Since November 2024, IAM supports centralized root access management for organizations, which lets the management account or a delegated administrator remove the root password, access keys, signing certificates and MFA devices from every member account, and prevent them being recovered. The handful of tasks that genuinely require root, such as unlocking an S3 bucket whose policy denies everyone, are then performed through short lived, task scoped root sessions issued centrally through sts:AssumeRoot. With credentials removed, there is no root login in a member account for an attacker to phish, and no root access key for anyone to leak. This is a one time central setting, and new accounts created in the organization are created without root credentials from the start.
4. Public exposure of data stores, snapshots and images
The Imperva breach is also the clearest example of the next pattern: a data store that should only ever have been reachable from inside the bank’s own estate became reachable from outside it. The specific resource type differs across incidents, sometimes an S3 bucket, sometimes an RDS or EBS snapshot, sometimes an AMI with credentials baked into it, but the shape of the failure is the same. A default was changed rather than deliberately configured, and nothing central stopped it.
Every one of these resource types now has a central control, and most of them are declarative. For S3, the Organizations S3 policy type enforces Block Public Access at the organization level, and once it is attached, account level Block Public Access settings are managed by the policy rather than by the account.
{
"s3_attributes": {
"public_access_block_configuration": { "@@assign": "all" }
}
}Setting the value to all enables all four Block Public Access settings together, blocking public ACLs and public bucket policies both for new configuration and for anything already in place.
For EBS snapshots and AMIs, the EC2 declarative policy carries two further attributes. snapshot_block_public_access set to block_all_sharing prevents any EBS snapshot from being shared publicly and makes any snapshot that is already public private again, which is stronger than block_new_sharing, which only stops new public sharing. image_block_public_access set to block_new_sharing prevents any AMI in the account from being made public. Together they replace the older SCP pattern of inspecting ec2:Add/group on ec2:ModifySnapshotAttribute, because the declarative setting holds regardless of which API path is used.
{
"ec2_attributes": {
"snapshot_block_public_access": {
"state": { "@@assign": "block_all_sharing" }
},
"image_block_public_access": {
"state": { "@@assign": "block_new_sharing" }
}
}
}RDS snapshots, which were the resource involved at Imperva, do not yet have a declarative control, and the RDS snapshot sharing APIs do not expose a condition key that distinguishes public sharing from sharing with a named account. The central answer therefore comes from two directions. The first is that encrypted RDS snapshots cannot be shared publicly at all, so an SCP denying rds:CreateDBInstance and rds:CreateDBCluster whenever rds:StorageEncrypted is false removes public RDS snapshots as a category; this needs testing against Aurora, where the key behaves differently for instances created inside an encrypted cluster. The second is to restrict snapshot and image sharing of every kind to a single approved pipeline role, so that sharing even with a specific external account is a reviewed act rather than an API call any administrator can make.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictSnapshotAndImageSharing",
"Effect": "Deny",
"Action": [
"ec2:ModifySnapshotAttribute",
"ec2:ModifyImageAttribute",
"rds:ModifyDBSnapshotAttribute",
"rds:ModifyDBClusterSnapshotAttribute"
],
"Resource": "*",
"Condition": {
"ArnNotLike": { "aws:PrincipalArn": "arn:aws:iam::*:role/SnapshotSharingPipeline" }
}
}
]
}Resource Access Manager is the other quiet sharing path. It can share subnets, transit gateways, Route 53 resolver rules and a growing list of other resource types, and by default a share can include principals outside the organization. AWS publishes the SCP that closes this: deny ram:CreateResourceShare and ram:UpdateResourceShare whenever ram:RequestedAllowsExternalPrincipals is true.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyExternalResourceShares",
"Effect": "Deny",
"Action": ["ram:CreateResourceShare", "ram:UpdateResourceShare"],
"Resource": "*",
"Condition": {
"Bool": { "ram:RequestedAllowsExternalPrincipals": "true" }
}
}
]
}5. The data perimeter: who can reach your resources, and which resources you can reach
Block Public Access stops a resource from becoming public, but public is not the only way data leaves. A bucket policy, key policy or secret policy can grant access to one specific external account, and a compromised principal inside the bank can just as easily copy data out to a bucket the attacker owns. A data perimeter closes both directions centrally, with an RCP on the resource side and an SCP on the identity side.
The RCP says that no resource in the organization can be reached by a principal from outside it, regardless of what an individual resource policy says. RCPs now cover a long list of services, including S3, KMS, SQS, Secrets Manager, DynamoDB, STS, CloudWatch Logs and ECR, so the same pattern can be extended well beyond S3.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceOrgOnlyAccess",
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:*", "kms:*", "sqs:*", "secretsmanager:*", "dynamodb:*"],
"Resource": "*",
"Condition": {
"StringNotEqualsIfExists": { "aws:PrincipalOrgID": "o-exampleorgid" },
"BoolIfExists": { "aws:PrincipalIsAWSService": "false" }
}
},
{
"Sid": "EnforceTls",
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:*", "kms:*", "sqs:*", "secretsmanager:*", "dynamodb:*"],
"Resource": "*",
"Condition": {
"BoolIfExists": { "aws:SecureTransport": "false" }
}
}
]
}Principal is * because an RCP is evaluated against whoever is trying to reach the resource, not against a fixed set of your own roles. It is worth being precise about IfExists, because it is easy to get backwards: on a Deny statement, StringNotEqualsIfExists still denies the request when the key is absent from the request context, so an anonymous request with no aws:PrincipalOrgID is denied rather than waved through. The aws:PrincipalIsAWSService condition exists purely to avoid catching AWS service principals, which have no organization ID of their own; AWS’s published data perimeter examples add a companion statement using aws:SourceOrgID so that those service principals can only act on behalf of your own organization, which closes the confused deputy case. The second statement denies any request to these resources that does not arrive over TLS, which is one of the few encryption in transit controls that can be enforced from a single central policy. STS is deliberately left out of this example: an org only RCP on sts:AssumeRole is valuable, but it has to carry explicit exceptions for any third party that legitimately assumes roles into the bank, and for federation, before it is attached.
The SCP is the mirror image. It says that no principal in the organization can write data to, or encrypt data with, a resource that sits outside the organization, which is the exfiltration route a compromised role takes to copy data into an attacker’s bucket. It also closes a ransomware pattern in which an attacker re encrypts a bank’s objects with a KMS key in an account they control, then deletes or withholds the key.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyWritesToResourcesOutsideOrg",
"Effect": "Deny",
"Action": [
"s3:PutObject",
"s3:ReplicateObject",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringNotEqualsIfExists": { "aws:ResourceOrgID": "o-exampleorgid" }
}
}
]
}aws:ResourceOrgID is the organization that owns the resource being acted on, so the statement matches any write or encryption call against a bucket or key the bank does not own. The action list is intentionally narrow, covering writes and encryption rather than all of S3 and KMS, because many workloads legitimately read from AWS owned buckets such as operating system package repositories, and a broad s3:* version of this policy needs a carefully maintained list of those exceptions before it can be attached safely.
6. Out of band shell access
A bank that has decided administrative access to instances goes only through Session Manager, where every session is authenticated through IAM, logged and recordable, has usually not closed the other doors into a shell. EC2 offers several, and each one bypasses some or all of the network and host controls the bank relies on.
The EC2 serial console gives root level access to an instance through a virtual serial port. It bypasses security groups, network ACLs and the network entirely, and it ignores whatever hardening has been applied to sshd, because it does not use SSH on the instance at all. Serial console access is an account level setting, and it is one of the attributes in the EC2 declarative policy, so it can be switched off centrally in a way no member account can reverse.
{
"ec2_attributes": {
"serial_console_access": {
"status": { "@@assign": "disabled" }
}
}
}EC2 Instance Connect is the second door. The ec2-instance-connect:SendSSHPublicKey API pushes a public key into an instance’s metadata for 60 seconds, and any principal holding that permission can log into any instance running the Instance Connect agent, which includes Amazon Linux by default. EC2 Instance Connect Endpoint goes further: it creates a managed endpoint inside a private subnet that lets SSH and RDP reach instances with no public IP and no internet gateway, which is precisely the inbound path the network design in section 8 exists to prevent. SSH key pairs are the third door, because an instance launched with a key pair accepts that private key for as long as the instance lives, wherever the key has been copied. For Windows, ec2:GetPasswordData retrieves the local administrator password for any instance launched from a standard AMI. And AWS CloudShell provides a browser based shell, with the caller’s credentials, running in an environment that can reach the internet outside the bank’s own egress inspection unless it is restricted.
A single SCP closes all of these centrally, leaving Session Manager as the only interactive route.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyOutOfBandShellAccess",
"Effect": "Deny",
"Action": [
"ec2:EnableSerialConsoleAccess",
"ec2-instance-connect:SendSerialConsoleSSHPublicKey",
"ec2-instance-connect:SendSSHPublicKey",
"ec2-instance-connect:OpenTunnel",
"ec2:CreateInstanceConnectEndpoint",
"ec2:CreateKeyPair",
"ec2:ImportKeyPair",
"ec2:GetPasswordData",
"cloudshell:*"
],
"Resource": "*"
},
{
"Sid": "DenyLaunchWithKeyPair",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:key-pair/*"
}
]
}The first statement denies every API that pushes a temporary key, opens a tunnel, creates or imports a key pair, retrieves a Windows password, or starts CloudShell. The serial console actions are included alongside the declarative setting as defence in depth, so that access is denied at the identity layer as well as disabled at the service layer. The second statement uses the fact that a key pair is one of the resources ec2:RunInstances is authorised against: denying the action on any key-pair resource means an instance cannot be launched with a key pair at all, while launches without one are unaffected. The result is that the claim “administrative access goes through Session Manager” becomes a property of the environment rather than a statement of intent.
7. The image supply chain
Every instance a bank runs inherits whatever was in the AMI it was launched from, and AMIs are a supply chain that most banks do not control centrally. In February 2025, Datadog Security Labs published the “whoAMI” name confusion attack, in which automation that looked up an AMI by name without specifying an owner could be made to launch a public AMI published by an attacker under a matching name. The attack needs no access to the victim’s account at all, only a lookup that is looser than the engineer intended.
The central control is Allowed AMIs, another EC2 declarative attribute. It restricts the AMIs that can be discovered and launched in an account to those matching criteria the bank defines, such as a specific list of image provider accounts or AWS itself, and any launch of an AMI outside those criteria fails. Running instances are not affected, and the audit_mode state lets a bank see what would have been blocked before switching to enforcement.
{
"ec2_attributes": {
"allowed_images_settings": {
"state": { "@@assign": "enabled" },
"image_criteria": {
"criteria_1": {
"allowed_image_providers": {
"@@append": ["amazon", "333333333333"]
}
}
}
}
}
}Here amazon permits AMIs published by AWS, and 333333333333 stands for the account where the bank’s golden image pipeline publishes its hardened images. Anything else, including a community AMI with a convincing name, cannot be launched anywhere in the organization.
8. Network ingress and egress
The network layer is where a bank’s central standard should be most opinionated, because a network guardrail prevents entire classes of exfiltration and exposure regardless of what happens at the application or identity layer above it. The starting position is that no account other than the network account should be able to create its own path to or from the public internet, and that all egress should route through a transit gateway to an inspection VPC where it can be logged and filtered.
The strongest primitive for this is VPC Block Public Access, which is an attribute of the EC2 declarative policy. In block_bidirectional mode it blocks traffic in both directions through internet gateways and egress only internet gateways in every VPC in the account, regardless of route tables, security groups or network ACLs. That matters because EC2 does not expose IAM condition keys for the ports or CIDR ranges inside a security group rule, so no SCP can stop someone opening port 22 to the world; VPC Block Public Access makes that rule irrelevant, because the internet traffic never reaches the security group. Exclusions are needed for the egress VPC in the network account, so exclusions are allowed in the policy and an SCP limits who can create them.
{
"ec2_attributes": {
"vpc_block_public_access": {
"internet_gateway_block": {
"mode": { "@@assign": "block_bidirectional" },
"exclusions_allowed": { "@@assign": "enabled" }
}
}
}
}The SCP then removes the ability to build around the architecture, and restricts exclusions to the network account.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyIndependentInternetPaths",
"Effect": "Deny",
"Action": [
"ec2:CreateInternetGateway",
"ec2:AttachInternetGateway",
"ec2:CreateEgressOnlyInternetGateway",
"ec2:CreateNatGateway",
"ec2:CreateVpcPeeringConnection",
"ec2:AcceptVpcPeeringConnection",
"ec2:CreateVpcBlockPublicAccessExclusion",
"ec2:ModifyVpcBlockPublicAccessExclusion",
"ec2:CreateDefaultVpc",
"globalaccelerator:Create*",
"directconnect:CreatePublicVirtualInterface",
"directconnect:AllocatePublicVirtualInterface",
"directconnect:ConfirmPublicVirtualInterface"
],
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:PrincipalAccount": "111111111111" }
}
},
{
"Sid": "DenyPublicIpOnLaunch",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:network-interface/*",
"Condition": {
"StringEquals": { "ec2:AssociatePublicIpAddress": "true" }
}
}
]
}The first statement denies, in every account except the network account identified by aws:PrincipalAccount, the creation of internet gateways, egress only internet gateways used for IPv6, NAT gateways, VPC peering connections, VPC Block Public Access exclusions, default VPCs, internet facing Global Accelerators, and Direct Connect public virtual interfaces. The public VIF deserves a specific mention, because it is easy to treat Direct Connect as private by nature. A public VIF advertises the bank’s public prefixes to AWS and receives the full range of AWS public service prefixes in return, giving the on premises network a routed path to every AWS public IP address, not just the bank’s own resources, and AWS’s own guidance treats it like an internet connection for security purposes. Where encryption over Direct Connect is the requirement, private IP VPN over a transit VIF removes the need for a public VIF entirely, and the SCP ensures one cannot be created anywhere but the network account. The second statement denies launching any instance whose network interface requests a public IP address, so that compute sits inside the private network by construction.
Since July 2026, VPC Encryption Controls can also be set through the EC2 declarative policy, using the vpc_encryption_control attribute. In enforce mode it requires traffic within and between VPCs in a region to be encrypted in transit, preventing resources that cannot provide that encryption from being attached, with explicitly named exclusions for things like the internet and NAT gateways in the egress VPC. For a bank that has to evidence encryption in transit to a regulator, this turns a design principle into a setting.
9. Serverless exposure
The same public by accident pattern that shows up in S3 and snapshots shows up in serverless resources, with slightly different mechanics. A Lambda function can be made publicly invocable by attaching a resource policy with a principal of everyone, or by creating a function URL with an authentication type of NONE, and either one turns a function that was meant to be called internally into a public endpoint. Lambda exposes a condition key for each route, so both can be closed centrally.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPublicLambdaPermissions",
"Effect": "Deny",
"Action": "lambda:AddPermission",
"Resource": "*",
"Condition": {
"StringEquals": { "lambda:Principal": "*" }
}
},
{
"Sid": "DenyUnauthenticatedFunctionUrls",
"Effect": "Deny",
"Action": [
"lambda:CreateFunctionUrlConfig",
"lambda:UpdateFunctionUrlConfig"
],
"Resource": "*",
"Condition": {
"StringEquals": { "lambda:FunctionUrlAuthType": "NONE" }
}
}
]
}The first statement denies adding any resource policy statement whose principal is everyone. The second denies creating or updating a function URL with no authentication, so the only function URLs that can exist require IAM signed requests. Keeping the two separate means the policy does not rely on the interaction between function URLs and resource policies, which AWS changed in October 2025 when new function URLs began requiring both lambda:InvokeFunctionUrl and lambda:InvokeFunction permissions.
10. Encryption, the audit trail and the security tooling
This category is less about stopping an attacker getting in and more about making sure that if something does go wrong, the bank can see it happened, prove what was taken, and trust that the attacker could not switch off the evidence first. Attackers who reach an administrative role commonly try to do exactly that: stop CloudTrail, disable GuardDuty, or add their own IP address to a GuardDuty trusted IP list so their activity no longer raises findings.
EBS encryption by default is a regional account setting that must be switched on in every permitted region, which is typically done once by the landing zone. After that, an SCP denying ec2:DisableEbsEncryptionByDefault stops anyone weakening it. KMS keys need protecting too, because disabling or scheduling deletion of a key is a fast way to make a bank’s own data unreadable. The larger statement below protects the audit trail, the detection services and the organization membership itself.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ProtectAuditAndDetection",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"cloudtrail:UpdateTrail",
"cloudtrail:PutEventSelectors",
"guardduty:DeleteDetector",
"guardduty:UpdateDetector",
"guardduty:DisassociateFromAdministratorAccount",
"guardduty:CreateIPSet",
"guardduty:UpdateIPSet",
"guardduty:CreateFilter",
"guardduty:UpdateFilter",
"securityhub:DisableSecurityHub",
"securityhub:DisassociateFromAdministratorAccount",
"config:StopConfigurationRecorder",
"config:DeleteConfigurationRecorder",
"config:DeleteDeliveryChannel",
"access-analyzer:DeleteAnalyzer",
"ec2:DisableEbsEncryptionByDefault",
"kms:ScheduleKeyDeletion",
"kms:DisableKey",
"organizations:LeaveOrganization"
],
"Resource": "*",
"Condition": {
"ArnNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/AWSControlTowerExecution"
}
}
},
{
"Sid": "ProtectLogArchiveBucket",
"Effect": "Deny",
"Action": [
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy",
"s3:PutBucketLogging",
"s3:PutLifecycleConfiguration",
"s3:DeleteBucket"
],
"Resource": "arn:aws:s3:::bank-log-archive-*",
"Condition": {
"StringNotEquals": { "aws:PrincipalAccount": "222222222222" }
}
}
]
}The first statement protects CloudTrail, GuardDuty, Security Hub, AWS Config and IAM Access Analyzer from being disabled, disassociated from the central security account, or tuned to ignore an attacker, and it also protects EBS default encryption, KMS keys and the account’s membership of the organization. The last of these matters more than it looks: an account that leaves the organization leaves every guardrail in this post behind with it. The only exception is the landing zone automation role. The second statement is scoped to the bank’s log archive bucket naming convention and denies policy, logging, lifecycle and deletion changes to anyone outside the dedicated log archive account, so a lifecycle rule cannot quietly expire the evidence either. Combined with an organization trail writing to that account and CloudTrail log file integrity validation, which chains signed digests over every delivered log file, this gives an investigator a record that can prove it has not been altered.
11. Destructive attacks and backups
Code Spaces is the incident every cloud team should know. In 2014 an attacker gained access to the company’s AWS console, and when the company tried to regain control, the attacker deleted EBS volumes, snapshots, S3 buckets, AMIs and backups, all of which lived in the same account and were reachable with the same credentials. The company announced it was shutting down within days. The modern version of this is ransomware that deletes or encrypts backups before encrypting production, and the lesson is the same: backups the attacker can delete are not backups.
The central answer has three parts. AWS Organizations backup policies define backup plans once and apply them to every account, so coverage does not depend on each team remembering. Those plans copy recovery points to a vault in a dedicated backup account protected by AWS Backup Vault Lock in compliance mode, which, once its grace period has passed, prevents anyone, including the root user of that account and AWS itself, from deleting recovery points before their retention period ends or removing the lock. And an SCP stops a compromised role in a workload account from tampering with the local side of the chain.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ProtectBackups",
"Effect": "Deny",
"Action": [
"backup:DeleteBackupVault",
"backup:DeleteBackupPlan",
"backup:DeleteRecoveryPoint",
"backup:UpdateRecoveryPointLifecycle",
"backup:PutBackupVaultAccessPolicy",
"backup:DeleteBackupVaultAccessPolicy",
"backup:DeleteBackupVaultLockConfiguration"
],
"Resource": "*",
"Condition": {
"ArnNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/BackupAdministration"
}
}
}
]
}The action list covers deleting vaults, plans and recovery points, shortening retention, rewriting the vault access policy, and removing a vault lock, which are the steps an attacker takes to make recovery impossible before the ransom note arrives.
12. Regions
Two separate risks are closed by restricting which regions a bank can use. The first is residency: a bank operating under data residency obligations needs certainty that nothing, including a test workload, is quietly running in a jurisdiction it has not approved. The second is abuse: stolen credentials are routinely used to launch large numbers of instances for cryptocurrency mining in regions the victim never looks at, precisely because nobody is watching them. An SCP on aws:RequestedRegion denies everything outside the approved list, with an exception for global services whose API calls are made in us-east-1.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnapprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"sts:*",
"organizations:*",
"route53:*",
"cloudfront:*",
"waf:*",
"wafv2:*",
"shield:*",
"globalaccelerator:*",
"support:*",
"health:*",
"budgets:*",
"ce:*",
"trustedadvisor:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["af-south-1", "eu-west-1"]
}
}
}
]
}NotAction exempts global services from the region check, and the StringNotEquals condition denies everything else outside the approved regions, shown here as Cape Town and Ireland. The NotAction list above is abbreviated for readability; AWS maintains the full list of global services in its documentation, and Control Tower offers the same control as a managed region deny setting, which keeps that list current automatically.
13. Bringing it together
None of these guardrails are exotic, and none of them require tooling beyond AWS Organizations, IAM and the services’ own declarative settings. What they require is the discipline to treat them as invariants rather than recommendations, applied at the root or organizational unit level so that no individual account, however senior the person managing it, can opt out. A bank adopting this standard ends up with an estate in which instance credentials are useless outside the VPC they were issued in, no IAM user or root credential exists to leak, no S3 bucket, snapshot or AMI can be made public, no resource can be reached from outside the organization or written to one outside it, the only way into a shell is Session Manager, only approved images can be launched, no account has its own path to the internet, no Lambda function can be made public, the audit trail and detection services cannot be switched off, backups cannot be deleted, and nothing runs in a region the bank has not approved.
The common thread across Capital One, Imperva, Uber, Code Spaces and many of the other best known AWS incidents is that the fix existed before the breach, usually as a single policy statement or a single setting. The work is not finding out what the fix should have been after the fact. It is deciding, in advance and in one place, that the fix is simply how the environment is built.