AWS Security for Banks: SCPs and RCPs 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 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 test database snapshot that ended up reachable from a public compute instance holding an API key. In both cases, and in many of the best-known publicly disclosed AWS incidents, the root cause was a permission 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. It has to be a set of guardrails enforced at the organization level, so that even a fully privileged administrator in a member account cannot create the conditions for the next breach by accident. That is what service control policies and resource control policies are for. An SCP sets the maximum permissions available in an account regardless of what the IAM policies inside that account allow, and an RCP does the equivalent from the resource side, constraining who can be granted access to a resource such as an S3 bucket or a KMS key even from outside the organization. Used together across a multi account structure, they let a bank encode its golden standard once, at the root or organizational unit level, and have it apply everywhere without relying on every engineer to get every account right.
Since late 2025, AWS has added a third layer worth building this standard around: declarative policies. Where SCPs and RCPs are authorization policies that restrict which API calls a principal or a resource can accept, declarative policies enforce a service’s configuration directly in its control plane, and that configuration is automatically maintained even as AWS ships new APIs and features. The strongest version of this standard therefore layers three policy types rather than two: declarative policies define what the environment must always look like, SCPs define what identities can ever do, and RCPs define what anyone can ever do to a resource. Where AWS offers a native declarative control for something in this list, that control should come first, with SCPs and RCPs filling the gaps declarative policies do not yet cover.
The rest of this post works through what that 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 trick an EC2 instance into querying its own instance metadata service, which returned temporary credentials for an IAM role attached to that instance. Those credentials were overprivileged, so the attacker could reach far more than the application itself ever needed. AWS’s response was IMDSv2, which requires a session token obtained through a PUT request with a custom header before any metadata can be read, a mechanism that closes off the great majority of SSRF based credential theft because most SSRF vulnerabilities can only issue GET requests.
For a bank, this points to two separate guardrails rather than one. The first is mandating IMDSv2 across every account, which can be enforced with an SCP that denies the launch of EC2 instances unless the metadata options specify token required, so that IMDSv1 simply is not available as an option regardless of what any individual engineer configures. The second, and the one that would have limited the blast radius even if the first had failed, is least privilege on the instance role itself. No EC2 instance role should be able to reach every S3 bucket in the account by default, and a permissions boundary or SCP condition that restricts what roles attached to compute instances can be granted closes off the second half of the failure chain.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRunInstanceWithoutImdsv2",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
}
]
}Working through the elements: Sid is just a label for the statement. Effect is Deny, since SCPs only ever narrow what is possible, they never grant anything. Action targets ec2:RunInstances, the API call that launches a new instance. Resource is scoped to instance ARNs, which is how EC2 exposes this particular condition key. The Condition block is where the real enforcement sits: StringNotEquals on ec2:MetadataHttpTokens set to required means the statement matches, and therefore denies, any launch request that does not explicitly require an IMDSv2 token. An engineer who forgets to set the flag is blocked at the API level rather than shipping an instance still reachable through the old, tokenless metadata path.
3. Public exposure of data stores and snapshots
The Imperva breach followed a different but equally common pattern. A database snapshot created for testing ended up reachable because an internal compute instance holding an AWS API key was made accessible from the public internet, and once an attacker compromised that instance, the key gave them a path straight to the snapshot data. The specific resource type differs across incidents, sometimes an S3 bucket, sometimes an RDS or EBS snapshot, sometimes an Elasticsearch domain, but the shape of the failure is the same: a data store that should only ever be reachable from inside the network perimeter was left reachable from outside it, usually because a default was changed rather than deliberately configured.
The strongest available guardrail here is no longer only an SCP. Since November 2025, AWS Organizations can enforce S3 Block Public Access as a declarative policy at the organization level, and once it is set that way, no account administrator, including one with full account access, can turn it off. That is the correct home for this particular invariant: a bank’s own data should never be able to become public, full stop, with no break glass exception carved out anywhere. Where a resource type does not yet have a declarative control, an SCP remains the right fallback, and the same logic applies to EBS snapshots: deny ec2:ModifySnapshotAttribute whenever the request tries to add the group all to the permission set, so public snapshots are closed off as a category rather than caught after the fact.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPublicSnapshotSharing",
"Effect": "Deny",
"Action": "ec2:ModifySnapshotAttribute",
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:Add/group": "all"
}
}
}
]
}This statement denies ec2:ModifySnapshotAttribute whenever the request’s ec2:Add/group condition key equals all, which is the specific EC2 mechanism for sharing an EBS snapshot with every AWS account on earth. There is deliberately no exception role here: an invariant with a carve out is not an invariant, and for a resource type where AWS does not yet offer a declarative control, the SCP itself has to be the hard boundary.
An SCP only controls what principals inside your accounts are allowed to do. An RCP works from the other direction, constraining who can be granted access to a resource in the first place, and it needs an explicit Principal element, which SCPs do not:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceOrgOnlyAccess",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"StringNotEqualsIfExists": {
"aws:PrincipalOrgID": "o-exampleorgid"
},
"BoolIfExists": {
"aws:PrincipalIsAWSService": "false"
}
}
}
]
}Here Principal is set to * because an RCP is evaluated against whoever is trying to reach the resource, not against a fixed set of your own roles. Action covers all S3 actions, and Resource is left as * because the RCP itself is attached at the organization or OU level, so it applies to every bucket underneath it. The condition pairs StringNotEqualsIfExists on aws:PrincipalOrgID with BoolIfExists on aws:PrincipalIsAWSService set to false. It is worth being precise about what IfExists does here, because it is easy to get backwards: on a Deny statement, a negated IfExists operator such as StringNotEqualsIfExists still denies the request even when the named key is absent from the request context. So this statement is not softened by a missing aws:PrincipalOrgID; an anonymous or unusual request is denied by default rather than waved through, which is the safer failure mode for a data perimeter control. The BoolIfExists on aws:PrincipalIsAWSService exists purely to avoid catching AWS service principals, which legitimately have no organization ID of their own. The practical effect is that no S3 bucket anywhere in the organization can be reached by a principal outside the bank’s own AWS Organization, no matter what an individual account’s bucket policy says.
4. Network egress and the transit gateway boundary
The network layer is where a bank’s golden standard should be most opinionated, because a well built network guardrail prevents entire classes of exfiltration regardless of what happens at the application or identity layer above it. The starting position worth adopting is that no account should be able to create its own path to the public internet. NAT gateways, in particular, are a common way for a compromised workload to reach out to a command and control server or exfiltrate data, precisely because they are simple for a team to add to a VPC without any centralized review.
The recommended architecture is to route all egress through a centralized transit gateway that terminates in an inspection or egress VPC, so that every packet leaving any account passes through a point where it can be logged, filtered and, if necessary, blocked. This is enforced with an SCP that denies the creation and modification of internet gateways and NAT gateways in every account other than the network account that owns the transit gateway, which prevents a team from working around the architecture simply because it is inconvenient for a specific workload. The same SCP pattern should extend to blocking the creation of VPC peering connections outside the organization, and to preventing the association of any CIDR range with a VPC that was not allocated from the bank’s approved IP address pool, which stops accidental or deliberate network sprawl before it happens rather than catching it in a later audit.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInternetAndNatGatewayCreation",
"Effect": "Deny",
"Action": [
"ec2:CreateInternetGateway",
"ec2:AttachInternetGateway",
"ec2:CreateNatGateway"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalAccount": "111111111111"
}
}
}
]
}The Action list covers creating an internet gateway, attaching one to a VPC, and creating a NAT gateway, which together are the three ways a team could stand up an independent path to the public internet. The Condition uses StringNotEquals on aws:PrincipalAccount against the twelve digit account ID of the dedicated network account that owns the transit gateway and the inspection VPC, so that account is the only place in the organization where this statement does not match, and therefore the only place these actions remain possible. Every other account in the bank’s organization is denied, which forces all egress through the centralized, inspectable path by construction rather than by policy document.
This SCP is necessary but not sufficient on its own. It does not by itself account for IPv6 internet connectivity through an egress only internet gateway, VPC peering to a network outside the organization, or an internet facing Global Accelerator, and AWS’s own reference pattern for this kind of egress control covers those paths too. Since late 2025, AWS Organizations also offers VPC Block Public Access as a declarative policy, which can block internet bound traffic through internet gateways and egress only internet gateways at the account or organization level in a way that a member account cannot override. Where that declarative control is available, it is the stronger primitive to lean on, with the SCP above serving as the fallback for whatever it does not yet cover.
5. Direct Connect and the public VIF trap
Everything so far assumes traffic reaches AWS over the internet or through the transit gateway inside AWS’s own network. Direct Connect changes that picture, and it introduces a guardrail gap that is easy to miss because it sits on the on premises side of the boundary rather than inside an account. A Direct Connect connection can carry a public virtual interface, and a public VIF opens access between all AWS public services and the customer’s on premises network over public IP address space, precisely the kind of broad reachability the rest of this standard exists to close off elsewhere.
A private or transit VIF only reaches the specific VPCs it is attached to, kept entirely off the public internet. A public VIF is a different animal. It advertises the bank’s own public IP prefixes to AWS and, in return, AWS advertises the full range of its own public service prefixes back, meaning the on premises network gains a routed path to every AWS public IP address, not just the resources the bank actually intended to reach. AWS’s own guidance treats a public VIF exactly like an internet connection for security purposes, meaning it needs a firewall or NAT device in the path rather than an assumption that Direct Connect’s dedicated circuit implies isolation by itself.
For a bank, the practical guardrail is to avoid the public VIF entirely wherever an alternative exists. Where the requirement is a VPN over Direct Connect for encryption, AWS’s private IP VPN capability allows a Site to Site VPN to run over a transit VIF using private IP addresses, replacing the older pattern that forced a public VIF and public facing VPN endpoints just to get encryption on the circuit. If a public VIF cannot be avoided, for instance because Direct Connect access to a public AWS service such as S3 is a genuine requirement, two controls become mandatory rather than optional. The first is prefix filtering, using either the BGP communities AWS documents for scoping which routes are exchanged or the published AWS IP address range file, so the bank only advertises and accepts the specific prefixes it actually needs rather than the entire AWS public route table. The second is that the circuit terminates behind the same firewall or NAT device that would guard an internet facing connection, so the public VIF is held to the same inspection standard as any other path leaving the bank’s network rather than being treated as implicitly trusted because it rides a private circuit.
6. Compute and serverless exposure
The same public by accident pattern that shows up in S3 and snapshots shows up just as often in compute and serverless resources, and it deserves its own guardrails because the failure modes are slightly different. A Lambda function can be made publicly invocable by attaching a resource policy with a principal of everyone, or through a function URL configured with an authentication type of none, and either one turns a function that was meant to be called internally into a public endpoint. The fix is an SCP that denies the lambda AddPermission action whenever the principal in the request is everyone, which closes off both the direct resource policy route and, in most configurations, the unauthenticated function URL route at the same time.
EC2 instances carry the equivalent risk through public IP addresses and open security groups. A golden standard for a bank should deny the launch of any EC2 instance with a public IP association by default, forcing every instance to sit entirely inside the private network and reach the internet, if it needs to at all, only through the transit gateway egress path described above. SSH access deserves the same attention, but it is worth being honest about the limits of an SCP here: EC2 does not expose IAM condition keys for the port or CIDR range inside a security group rule, so an SCP cannot directly inspect or block a rule that opens port 22 to the entire internet. The more reliable guardrail is to restrict who can manage security groups at all, pair that with VPC Block Public Access or Firewall Manager policies that enforce network topology, and use AWS Config rules to detect and remediate any security group that does end up open to the world. Legitimate administrative access should go through Session Manager or a bastion that itself sits behind the transit gateway rather than through a direct inbound rule.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPublicLambdaPermissions",
"Effect": "Deny",
"Action": "lambda:AddPermission",
"Resource": "*",
"Condition": {
"StringEquals": {
"lambda:Principal": "*"
}
}
}
]
}This statement denies lambda:AddPermission whenever the request’s lambda:Principal condition key equals *, which is the exact call a function’s resource policy goes through when it is opened up to anyone, so this closes off both a directly public function and, in most configurations, a function URL set to no authentication.
7. Encryption and the integrity of the audit trail
The last category of guardrail is less about stopping an attacker from getting in and more about making sure that if something does go wrong, the bank can see it happened and prove what was taken. Default encryption should be non negotiable, though it takes two steps rather than one: EBS encryption by default has to actually be turned on in every permitted region first, since it is a regional account setting rather than something an SCP can switch on, and only then does an SCP denying ec2:DisableEbsEncryptionByDefault stop anyone from weakening that setting later. A parallel policy denying changes to a bucket’s encryption configuration outside of an approved automation role keeps S3 in the same position.
Logging is the other half of this. CloudTrail needs to be protected as its own security invariant, with an SCP that denies anyone from stopping or deleting a trail, and a matching policy that protects the S3 bucket CloudTrail writes to, denying changes to that bucket’s policy and denying s3:PutBucketLogging changes to anyone outside the account that owns the log archive. This is precisely the kind of control that turns an incident that would otherwise be undetectable into one with a full forensic trail, and for a regulated bank it is often the difference between a contained incident and a disclosure event, because you cannot investigate what you never recorded.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ProtectCloudTrail",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"cloudtrail:UpdateTrail"
],
"Resource": "*",
"Condition": {
"ArnNotLike": {
"aws:PrincipalARN": "arn:aws:iam::*:role/AWSControlTowerExecution"
}
}
},
{
"Sid": "ProtectLogArchiveBucket",
"Effect": "Deny",
"Action": [
"s3:PutBucketPolicy",
"s3:PutBucketLogging",
"s3:DeleteBucket"
],
"Resource": "arn:aws:s3:::bank-log-archive-*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalAccount": "222222222222"
}
}
}
]
}The first statement denies stopping, deleting or updating any CloudTrail trail, with an ArnNotLike exception carved out only for the automation role that manages the landing zone itself, so CloudTrail cannot be switched off by a compromised or overprivileged account administrator. The second statement is scoped by Resource to the bank’s log archive bucket naming convention and denies policy changes, logging configuration changes and deletion on those buckets to anyone outside the dedicated log archive account identified by aws:PrincipalAccount, which keeps the audit trail tamper resistant even from someone with broad permissions in a compromised member account.
A bank grade version of this pattern goes one step further: an organization trail that writes to a dedicated log archive account, an S3 bucket in that account locked down the way section 3 describes, and CloudTrail’s own log file integrity validation turned on, which chains signed digests over the delivered log files so an investigator can prove whether anything was altered or deleted after the fact. Logging is not only about keeping a record. The record itself has to be able to prove it has not been tampered with.
8. Approval workflows for privileged access
Alongside SCPs, RCPs and declarative policies, it is worth naming the pattern AWS supports for privileged, time bound access: temporary elevated access through IAM Identity Center. One reference implementation is Temporary Elevated Access Management, known as TEAM, an open source AWS sample solution rather than a managed AWS service, which a bank deploys and operates itself. Where SCPs, RCPs and declarative policies are static guardrails that apply all the time to everyone, this pattern addresses the opposite problem, which is the rare but genuine case where an engineer needs standing, admin style access for a limited task such as incident response or a manual production fix, and that access should never simply exist as an always on role sitting unused between incidents.
TEAM lets a bank define eligible groups, meaning who is authorized to request a given scope of elevated access, and separate approver groups, meaning who is authorized to grant it, and it runs every request through an explicit review before access is activated through the IAM Identity Center portal. Every grant carries a start and end time, every privileged session is visible in CloudTrail, and access is revoked automatically when the window closes rather than depending on someone remembering to remove it afterward. Because TEAM is customer operated rather than an AWS managed service, a bank running it takes on responsibility for keeping it patched: AWS disclosed CVE-2026-86830 on September 14, 2026, a privilege assignment flaw in versions before 1.5.1 that could let an authenticated user obtain elevated access they were not entitled to, and any bank running TEAM should confirm it is on 1.5.1 or later. Paired with the guardrail layers above, this pattern gives a bank complementary controls working together. Declarative policies, SCPs and RCPs remove entire categories of action from the realm of possibility for everyone, all the time, while a temporary elevated access pattern like TEAM ensures that the narrower set of genuinely privileged actions still permitted are logged, time bound and explicitly approved rather than resting on a standing role that outlives the task it was created for.
One structural exception applies to everything in this post. Neither SCPs nor RCPs affect the AWS Organizations management account itself, and declarative policies are the only one of the three that can reach it. 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 what genuinely requires managing the organization. The guardrails in this post make every member account safe by construction. The management account has to be kept safe by having almost nothing in it to protect.
9. Bringing it together
None of these guardrails are exotic, and none of them require custom tooling beyond AWS Organizations itself. 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 a multi account structure where every account sits behind a transit gateway with no independent path to the internet, where S3 and snapshot public access are structurally impossible rather than merely discouraged, where compute instances carry no public IP and accept no inbound SSH from the open internet, where Lambda functions cannot be made public, and where encryption and the audit trail are protected as tightly as the data they cover.
The common thread across Capital One, Imperva and many of the other best-known publicly disclosed AWS incidents is that the fix existed before the breach, usually as a single policy statement. The work is not finding out what the fix should have been after the fact. It is deciding, in advance, that the fix is simply how the environment is built.