- Published on
Implementing a Global Data Perimeter on AWS: Lessons From a Banking-Scale Rollout
- Authors

- Name
- Christian TCH
Implementing a Global Data Perimeter on AWS: Lessons From a Banking-Scale Rollout
This article is drawn from a mission I carried out for a large European banking group, running several hundred AWS accounts under a single AWS Organization. Names, account counts, and specific figures below are illustrative rather than exact — the client relationship is confidential — but the architecture, the rollout mechanics, and the problems described are real.
What Is a Data Perimeter?
A data perimeter is an AWS Well-Architected concept for enforcing "who can access what data, from where, and under what conditions" — not just at the account level, but consistently across an entire AWS Organization. Instead of relying purely on individual IAM policies or security groups scattered across hundreds of accounts, a data perimeter defines organization-wide guardrails built on three overlapping controls:
- Identity perimeter — only trusted identities (IAM principals belonging to your organization, or explicitly allow-listed third parties) can access your resources.
- Resource perimeter — your trusted identities can only access resources that belong to your organization (or an explicitly approved list), preventing data from being pushed to an external AWS account.
- Network perimeter — access to your resources is only permitted from expected network locations (your VPCs, on-premises ranges, or trusted networks) — even if the credentials being used are valid.
These three perimeters are implemented primarily through Service Control Policies (SCPs) and Resource Control Policies (RCPs) at the AWS Organizations level, combined with VPC endpoint policies and condition keys (aws:PrincipalOrgID, aws:ResourceOrgID, aws:SourceVpc, aws:ViaAWSService, etc.) — enforced centrally so no single account, team, or misconfigured IAM policy can silently create an exfiltration path. I covered how SCPs and RCPs sit in the IAM evaluation engine in a previous article — the data perimeter is essentially the applied version of that theory.
In practice, we did not deploy these mechanisms for every AWS service on day one. The rollout started narrow, with a single service, and expanded outward from there — service by service, each one preceded by its own risk analysis. That sequencing is the part of this article I want to spend the most time on, because it mattered more to the outcome than the policy JSON did.
Why It Matters — The Stakes
In a regulated environment — a bank, in this case — the stakes go well beyond "best practice":
- Preventing data exfiltration. A compromised credential or an overly permissive IAM policy should not be enough, on its own, to move sensitive data to an attacker-controlled AWS account. The data perimeter is the last line of defense even when identity-level controls fail.
- Scale and consistency. With hundreds of AWS accounts across business lines, you cannot rely on every team correctly configuring least-privilege IAM policies. Org-wide guardrails apply the same non-negotiable rules everywhere, regardless of team maturity.
- Regulatory and audit pressure. Financial institutions operate under strict data residency, third-party access, and audit-trail requirements (EBA outsourcing guidelines and national banking supervisor expectations in the EU context). A data perimeter gives auditors a demonstrable, centrally-enforced control rather than a patchwork of per-account promises.
- Balancing security with usability. The hard part is not writing restrictive SCPs — it's writing guardrails precise enough to stop exfiltration paths without breaking legitimate cross-account workflows (shared services accounts, third-party SaaS integrations, CI/CD pipelines, log aggregation).
The Starting Point
The organization had grown the way most large enterprises do: an AWS footprint that started with a handful of accounts and expanded, over several years, into hundreds of accounts spread across business units, environments (dev/test/prod), and shared platform teams. Each account had its own IAM policies, its own S3 bucket policies, its own idea of what "least privilege" meant.
A few structural facts shaped the whole engagement:
- AWS Organizations was already in place, with a multi-level OU hierarchy separating business lines, environments, and a dedicated Security/Platform OU.
- Some org-wide SCPs already existed — denying the use of unauthorized services and denying regions outside the approved set were already enforced. But nothing addressed who could reach which resource, from where. The identity, resource, and network perimeters were simply not part of the existing guardrail set.
- A dedicated Security Tooling account ran centralized CloudTrail, GuardDuty, and Security Hub aggregation — but detection, not prevention.
In other words: reasonable baseline hygiene (approved services, approved regions) and good visibility, but no org-wide control over cross-account or cross-org data access. The mission was to close that specific gap — for every service already in production use, and for every new service adopted afterward, from day one — without breaking the several hundred production workloads already running.
Starting With S3: A Risk-Driven, Service-by-Service Rollout
We did not attempt to stand up all three perimeters across every AWS service at once. The rollout was scoped service by service, starting with S3.
S3 came first for a simple reason: it held the largest share of the organization's sensitive data, it was the service most exposed to the classic exfiltration pattern (a bucket policy or IAM policy quietly granting access to a principal outside the org), and AWS's own data perimeter guidance and policy examples are most mature for S3 — there was a well-trodden path to follow rather than one we had to invent from scratch.
Before writing a single SCP or RCP statement for a given service, we ran a risk analysis on that service specifically:
- What does the service currently expose, and to whom — which accounts, which external principals, which network paths already have access today?
- What existing controls are already mitigating that exposure (bucket policies, KMS key policies, IAM boundaries), and how much of the risk do they actually cover versus how much is undocumented convention?
- What residual risk remains once those existing controls are accounted for — and specifically, which part of that residual risk would an identity, resource, or network perimeter actually close?
- What legitimate cross-account or cross-service patterns exist today that a guardrail would need to explicitly accommodate, so the exception list is anticipated rather than discovered after an outage?
The risk analysis output directly shaped the guardrail's design for that service — not just whether to apply an RCP, but which condition keys mattered most, which resource types needed carve-outs, and how aggressively the account-by-account rollout described below could move for that service.
Once S3 was stable in enforce mode across the estate, we moved on to the next highest-value services using the same sequence — risk analysis first, guardrail second: KMS (key policies are as exploitable as bucket policies, and encryption is the last line of defense if S3 itself is ever bypassed), Secrets Manager, SQS, and STS cross-account AssumeRole patterns, among others. Each service got its own risk assessment before any policy was written, rather than one blanket analysis applied to all of them up front. That risk-analysis-to-guardrail cycle typically took anywhere from one to four or five weeks per service, and the spread came almost entirely from how many distinct resource types and access patterns the service exposed — S3 or SQS, with a narrow and well-documented surface, moved in about a week; a service like Glue, which spans crawlers, jobs, connections, dev endpoints, and more, each with its own permission model, took the full four to five weeks before we trusted a guardrail written against it.
AWS itself was part of this. Solutions Architects and the account team supported the design reviews, sanity-checked our SCP/RCP drafts against AWS's own published data perimeter policy examples, and weighed in on sequencing and exception handling as we moved from one service to the next. This was not a "buy a product and turn it on" engagement — it was ongoing design collaboration with people who had seen this kind of rollout done elsewhere, at similar scale.
Enforcing the Identity Perimeter Before RCPs Existed
RCPs did not exist when this rollout started — AWS only announced them in late 2024. For the earlier part of the mission, the identity perimeter had to be enforced the only way available at the time: mandatory statements injected into every resource-based policy (RBP), deployed per account, per resource.
Concretely, every S3 bucket policy, every KMS key policy, and every Secrets Manager resource policy in scope had to carry the same non-negotiable statement block — a Deny on non-org principals, plus the same explicit exceptions the RCP examples above show. There was no single org-level enforcement point to attach a policy to; the guarantee had to be re-created on every individual resource, in every individual account.
That only works if you can deploy those mandatory statements everywhere reliably, and know the moment any of them go missing — whether through a legitimate policy change, a new resource created with its service's permissive default policy, or someone quietly reverting a fix. Both problems were solved by the deployment and verification setup below, not by the policy content itself.
Deployment: Two Jenkins Pipelines
SCPs and RBPs were pushed through Jenkins, but not through a single pipeline — deploying policy changes across the account fleet from a single source of truth, instead of relying on individual account teams to hand-edit policies in the console, actually meant two separate pipelines, picked depending on what the change needed. A per-account pipeline ran against one account at a time and was used for anything that needed staging — moving a single account from audit to enforce, or rolling one back per the previous section. A generic, fleet-wide pipeline fanned the same job out to many or all accounts in parallel in a single run, used once a policy was already stable and just needed to land uniformly everywhere — a new exception added to an already-enforced SCP, for instance, or the RBP mandatory-statement merge applied across every account that already had it.
- SCPs were also deployed per account through the automation, rather than attached once at a shared OU and left alone — the pipeline pushed and attached the data perimeter SCPs account by account, which gave us the ability to stage the audit-to-enforce rollout described earlier one account at a time instead of flipping an entire OU in one shot.
- RBPs had to be deployed into every target account individually — the pipeline assumed a role in each account, read the resource's current policy, merged in the mandatory statement block without touching the account team's own legitimate statements, and pushed the merged policy back.
Deploying the mandatory statements once was not enough by itself. A bucket or key policy is not immutable — nothing stops someone from editing it afterward, or creating a brand-new resource with the account's out-of-the-box default policy that never had the mandatory statements to begin with. So a second, continuous control ran alongside the pipeline: whenever a resource's policy was modified, or a new resource was created, an automated process verified that the mandatory statements were still present — flagging the gap immediately, and re-applying the statements automatically for the highest-risk resource types. That is what turned "we deployed the right policy once" into "the right policy is always there," without depending on every engineer remembering to include it by hand.
This RBP-plus-verification setup is what carried the identity perimeter for every service that had already gone through its risk analysis before RCPs became available. Once RCPs launched, they did not replace this work outright — they gave us a single org-level enforcement point for the statements an RCP could express, which made the per-account verification loop redundant for those cases. RBPs, and their verification process, stayed in place for anything an RCP still could not cover.
The Architecture: Mapping Perimeters to AWS Mechanisms
Each of the three perimeters maps to a combination of AWS enforcement mechanisms rather than a single one — one artifact restricting the resource side, one restricting the identity side, both attached at the OU or org level so no individual account can opt out.
Identity Perimeter — RCPs / RBPs and VPC Endpoint Policies
The identity perimeter answers: can this resource be reached by an untrusted principal, regardless of what its own resource policy says? That question has to be answered on the resource side, not the identity side — an SCP is only ever evaluated for principals that already belong to accounts inside your org, so it structurally cannot see or block a truly external caller. RCPs (and, before they existed, mandatory statements on the resource-based policy itself) close that gap by attaching the check to the resource instead.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceOrgIdentitiesOnly",
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:*", "sqs:*", "kms:Decrypt", "kms:GenerateDataKey", "secretsmanager:GetSecretValue"],
"Resource": "*",
"Condition": {
"StringNotEqualsIfExists": {
"aws:PrincipalOrgID": "o-xxxxxxxxxxxx"
},
"BoolIfExists": {
"aws:PrincipalIsAWSService": "false"
},
"ArnNotLike": {
"aws:PrincipalARN": [
"arn:aws:iam::*:role/approved-third-party-integration-*"
]
}
}
}
]
}
A developer who fat-fingers a bucket policy and grants Principal: "*" no longer creates an exfiltration path — this statement overrides the resource's own policy regardless of what it says. The ArnNotLike clause is where the allow-list for approved third-party integrations (payment processors, monitoring SaaS, etc.) lives, kept intentionally short and reviewed quarterly. This single org-level RCP statement is the centralized evolution of what the mandatory-statement RBP pipeline (described above) had to achieve one resource at a time before RCPs existed — same guarantee, now enforced at one control point instead of needing continuous per-resource verification.
The VPC endpoints used to reach these services privately (described under the network perimeter below) reinforce the same check from a second angle: the endpoint policy's Principal element can independently restrict which identities are allowed to use that network path at all, so even a call originating from inside the org's own network is denied if it doesn't carry an org identity.
Resource Perimeter — SCPs and VPC Endpoint Policies
The resource perimeter answers the opposite question: can my own identities reach only trusted resources, regardless of how permissive their IAM policy might otherwise be? Unlike the identity perimeter, this one belongs on the SCP side — it restricts what accounts inside the org are allowed to do, which is exactly the thing SCPs are built to control.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAccessToNonOrgResources",
"Effect": "Deny",
"Action": ["s3:*", "sqs:*", "kms:*", "secretsmanager:*"],
"Resource": "*",
"Condition": {
"StringNotEqualsIfExists": {
"aws:ResourceOrgID": "o-xxxxxxxxxxxx"
},
"StringNotEquals": {
"aws:ResourceAccount": ["123456789012"]
},
"BoolIfExists": {
"aws:PrincipalIsAWSService": "false"
}
}
}
]
}
aws:ResourceOrgID is what makes this the mirror image of the identity perimeter's RCP: instead of checking who's calling in, it checks where an org identity is calling out to, and denies anything outside the org by default. The aws:ResourceAccount exception is the narrow, account-ID-keyed carve-out for the handful of legitimate external resources — the regional subsidiary account described in the challenges below is exactly this case, since it isn't part of the org and an org-ID condition can't cover it.
Here too, VPC endpoint policies reinforce the SCP from a second angle: the endpoint policy's Resource element can independently restrict which resources are reachable through that network path, so an org identity can't reach a non-org bucket or key even if it somehow found a path around the SCP.
Network Perimeter — SCPs and RCPs
The network perimeter answers: even with valid org credentials and an org-trusted resource on both ends, is this call coming from an expected network path? Like the other two, it's enforced from both sides at once — an SCP restricting where org identities are allowed to call from, and an RCP restricting where org resources are allowed to be reached from — both keyed on aws:SourceVpc / aws:ViaAWSService, the same condition keys the VPC endpoints populate.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyCallsOutsideExpectedNetwork",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEqualsIfExists": {
"aws:SourceVpc": ["vpc-xxxxxxxxxxxxxxxxx"]
},
"BoolIfExists": {
"aws:ViaAWSService": "false",
"aws:PrincipalIsAWSService": "false"
}
}
}
]
}
This is the SCP half, denying an org identity from calling out except through the org's own VPC (or through another AWS service acting on its behalf). The RCP half mirrors it on the resource side — denying a resource from being reached except from that same VPC — so the guarantee holds even if the SCP were somehow bypassed. This is the perimeter most likely to break something unexpectedly — Lambda functions without VPC attachment, on-call engineers using the CLI from a laptop, third-party SaaS pulling from S3 directly over the public endpoint. It was, unsurprisingly, the last of the three to move from audit mode to enforce mode.
None of this works without the VPC endpoints themselves being in place first — aws:SourceVpc only exists on a request because it went through one. Operationally, these endpoints were not deployed independently, endpoint by endpoint, inside every workload account. Each interface endpoint was created once, in a central network account, with its policy attached there — and then shared out to every other account in the organization, so application accounts could resolve and use it privately without owning or managing an endpoint of their own. That centralization meant a policy change to the allow-list happened in one place, not in every account that talked to S3, KMS, or Secrets Manager privately.
The Rollout: Sandbox First, Then Progressive Deployment
Nobody ships a Deny-effect SCP or RCP straight to several hundred production accounts and hopes for the best. In practice, the rollout was simpler than a formal audit-mode pipeline: every SCP and RCP was deployed to a dedicated sandbox account first, through the same Jenkins automation used everywhere else, and validated there before touching anything real.
Validation was not manual review alone. Before a policy went anywhere near production, we ran it through AWS Data Perimeter Helper (dph) — an open-source tool AWS built specifically for this problem. dph runs Athena queries against historical CloudTrail data, cross-references the calling principals and networks against the perimeter you are about to enforce, and reports exactly which real API calls from the analysis window would have been denied. That surfaced a large share of legitimate cross-account and cross-service patterns before they ever produced a Deny in production, instead of leaving the sandbox account and production traffic as the only way to find them.
Sandbox validation and dph together caught the obvious breakage — a malformed condition key, a typo in the org ID, a Deny broader than intended — plus most of the recurring legitimate access patterns already visible in CloudTrail history. What neither could catch was a pattern that simply hadn't fired during the analysis window, or that never touched the account being tested — a job that only runs quarterly, a log delivery mechanism nobody thought to check for. Those still surfaced at discovery, in production: an account hit a Deny it should not have, the app team flagged it, and the affected accounts were rolled back before anything else was analyzed — how that recovery actually worked is its own section below.
This reactive, discover-as-you-go approach to exceptions is exactly why the SCP and RCP size limits (below) turned into a real operational problem rather than a theoretical one. Every newly discovered legitimate pattern meant one more condition or one more ARN added to an existing policy, none of it scoped in advance the way a full pre-deployment audit might have allowed. The exception list only ever grew — account by account, discovery by discovery — until it started running into the character ceiling.
Rollback: Reverting on Incident
Rollback was scoped to whatever the incident actually touched — not fixed in advance to a single account, and not a program-wide switch either. The two Jenkins pipelines already covered both scopes (see Deployment above): the per-account pipeline and the generic, fleet-wide one. So when a bad Deny surfaced, reverting simply meant running whichever pipeline matched however many accounts were actually affected: the per-account one for a localized false positive, the fleet-wide one if the incident turned out to be a broader problem with the guardrail itself.
When an app team flagged a Deny that should not have fired, the first action was mechanical, not analytical: run the pipeline against the affected accounts to revert them to audit mode, unblocking the workload before anyone had root-caused anything. That distinction mattered in a bank — a handful of accounts sitting in audit mode for a few hours is a contained, explainable event; a workload stuck behind a bad Deny on a critical path is not something you leave running while you investigate.
Only once the workload was unblocked did the actual analysis start: which principal, which condition key, which legitimate pattern the guardrail hadn't anticipated. That analysis fed a targeted exception into the SCP or RCP, redeployed through the same pipeline — and the affected accounts moved back to enforce mode as the last step, not a separate one.
Two things made this workable at scale rather than a one-off firefight:
- Neither pipeline was an "emergency" build. Because a per-account and a fleet-wide pipeline already existed for everyday deployment, reverting was just the same pipeline run in reverse, at whatever scope the incident actually had — nothing separate to build, test, or trust for the incident case.
- Every rollback was tracked like any other exception. Accounts sitting back in audit mode are a standing gap in the perimeter, exactly like an allow-list entry — so each rollback was logged with the accounts involved, the reason, and a target date to move back to enforce, rather than left open indefinitely.
This is a fix-forward model, not a fixed unit of rollback: nothing about a bad Deny ever justified reverting more than what the incident actually touched — and nothing about the mechanism stopped it from reverting more, if that's genuinely what the incident required.
Challenges Encountered
A few of the more interesting challenges we had to work through along the way:
- Cross-account log aggregation. Centralized CloudTrail and application logs shipped from workload accounts into the Security Tooling account looked, from the RCP's perspective, exactly like the exfiltration pattern it was designed to block. This required an explicit, narrowly-scoped exception for the logging pipeline's role ARNs rather than a blanket allow.
- ELB access log delivery in older regions. For load balancers in regions that predate Elastic Load Balancing's move to a service-principal-based bucket policy, log delivery authenticates as an AWS-owned, region-specific account rather than as an org identity or the
logdelivery.elasticloadbalancing.amazonaws.comservice principal — for example, AWS documents account009996457667for Europe (Paris) and156460612806for Europe (Ireland). From the identity perimeter's point of view, that account is indistinguishable from any other external account trying to write to the bucket, so every regional ELB access-log bucket needed an explicit, documented exception for that region's ELB service account in its RBP — and later in RCP scope — otherwise log delivery broke silently the moment the guardrail moved to enforce. - Third-party SaaS integrations. Fraud detection and market data vendors with legitimate, contractually-approved access to specific S3 buckets or Secrets Manager secrets needed to be threaded through the identity perimeter's allow-list — and kept there under change control, since every entry is a standing exception to the default-deny posture.
- Lambda functions without VPC attachment. The network perimeter assumes workloads reach AWS services through VPC endpoints, and for Lambda that became a hard requirement rather than a standing exception: an SCP denies
lambda:CreateFunctionunless the function is created with VPC configuration attached, so every new function is forced onto the org's own network from day one and legitimately carriesaws:SourceVpc. Functions that predated the guardrail had to be remediated — attached to a VPC — rather than exempted. - Service roles for AWS services that cannot run inside a VPC at all. Lambda can be fixed by attaching it to a VPC; a number of other AWS services cannot be deployed inside a VPC by design — their control plane, and any calls they make to other AWS services on your behalf, travel over AWS's own internal network, not through one of the org's VPC endpoints. For those service roles,
aws:SourceVpcwill never be present no matter what you do on the account side — there is no VPC attachment to add. Those service principals needed a standing, explicit exception in the network perimeter instead of a remediation task, because on our side there was nothing left to fix. - Data residency nuances. A handful of workloads had legitimate reasons to reach resources in a specific external AWS account belonging to a regional subsidiary under a separate but related AWS Organization — a case the simple "org ID" condition does not cover cleanly, requiring a dedicated allow-list keyed on account ID rather than org ID for that narrow case.
- SCP and RCP size limits. Because exceptions were added reactively — one at a time, at discovery, as described above — the identity and resource perimeter policies kept growing: one more
ArnNotLikeentry, one more condition, for every newly discovered legitimate pattern, none of it trimmed or consolidated in advance. Both SCPs and RCPs are capped at 5,120 characters per policy, which we hit first. The fix was to split the logic across multiple policies by concern — core deny-non-org logic in one, the exception allow-list in another. That bought room, but only up to a point: an entity can have at most 5 policies attached at once (one slot always taken by the AWS-managedFullAWSAccess/RCPFullAWSAccessdefault, leaving 4 usable), and with the exception list still growing account by account, we eventually hit that ceiling too. At that point splitting further wasn't an option — we had to go back through the exception list and prune entries that were no longer actually needed to make room for new ones, which is a more disciplined habit we should have built in from the start rather than after running out of slots.
None of these needed a heroic fix — each one had a clean answer once we understood exactly why the perimeter and the legitimate pattern were colliding. That, more than the policy JSON, is what the work actually was.
What a Data Perimeter Does Not Solve
Worth being explicit about scope, the same way I was with VPC Lattice:
- It does not replace least-privilege IAM. A data perimeter stops resources from leaving the org and stops external principals from getting in — it says nothing about whether a role inside the org has more permissions than it needs. Access Analyzer-driven cleanup (the subject of a previous article) is a separate, complementary effort.
- It does not stop insider misuse within the perimeter. An authorized employee with excessive but "in-org" access can still misuse data legitimately reachable under the perimeter's rules. This is a detection and access-review problem, not a perimeter problem.
- It adds an operational dependency on the exception list. Every entry in the identity or resource perimeter's allow-list is a standing exception to a default-deny posture. If that list isn't reviewed on a schedule, the perimeter degrades exactly the way unreviewed IAM policies do — just at the org level instead of the account level.
Closing Thoughts
A global data perimeter does not make an AWS estate secure by adding another control on top of the pile — it makes it secure by removing the assumption that every account, every team, and every IAM policy will independently get least-privilege right. That assumption does not survive contact with a few hundred accounts and several years of organic growth.
What made this rollout survive contact with production was not the SCP or RCP JSON — that part is genuinely short. It was the discipline of shipping every guardrail in audit mode first, watching real traffic before touching the Deny switch, and treating every exception as a tracked, reviewable exemption rather than a permanent carve-out. The guardrails are only as good as the process that got them from "deployed" to "enforced" without an outage along the way.
Related reading:
- AWS Guardrails: SCPs, RCPs, Permission Boundaries and Session Policies — What Actually Limits What — the mechanics behind the identity and resource perimeters described here.
- IAM Cleanup: How I Built an Automated IAM Governance Tool on AWS — the least-privilege half of the problem a data perimeter does not solve on its own.
- 3 AWS Security Misconceptions That Create Blind Spots — why identity-based controls matter more than network-based ones.
- AWS Data Perimeters documentation — official reference and policy examples.