"A Note from the Author"
I work in the Technical Support division of an AWS reseller operating under the AWS Solution Provider Program in Japan. This post is written from that perspective — managing hundreds of AWS Organizations on behalf of our customers and wrestling with the policy sprawl that comes with it.
- Japan's IT industry has traditionally relied heavily on outsourcing to external vendors for building and managing cloud infrastructure, rather than developing in-house capabilities. Decision-making can be slow when multiple organizations are involved, and the AWS reseller model itself is a product of this outsourcing culture. Many Japanese companies purchase AWS through resellers rather than directly, which means resellers like us end up provisioning and managing a large number of AWS accounts and Organizations on their behalf.
- As a reseller, we face a unique challenge: we need to enforce restrictions across all of these Organizations to protect both ourselves and our customers, but the exact restrictions vary depending on each customer's configuration. This leads to a proliferation of policy variants that are mostly the same but differ in small, critical ways — and keeping them all in sync is a real headache.
- This article is based on a presentation I gave at Security-JAWS #40, a community event focused on AWS security in Japan, held on February 12, 2026.
- My English writing skills are limited, so I used GenAI (Kiro CLI) to help translate this article from the original Japanese. I hope it reads well — any awkwardness is on me, not the AI.
Hello, everyone. I'm Ichino (@kazzpapa3), a Technical Support Engineer at an AWS partner company. My favorite AWS services are the AWS CLI and AWS CloudTrail. My least favorite (as a support engineer) is AWS Billing — the billing logic is just too convoluted.
Today I want to talk about a challenge we're facing: how to properly manage IAM and SCP policies across nearly 1,000 AWS Organizations.
Background
I introduced myself as a Technical Support Engineer, but I also work in the department that manages the specifications for our resold AWS accounts.
Here's the situation:
- We have multiple account provisioning configurations, and there are many small differences between them.
- Most of the policy content is shared, but those small differences cause policies to proliferate.
- When the shared parts need updating, we have to make the same change in multiple places.
This is the problem I want to address.
Oh, and personally — I think JSON came too early for humanity. More on that later.
Why the Differences Exist
Many people assume that reseller-provided AWS accounts can't use AWS Organizations or AWS Control Tower. That's actually not the case with our company — we do allow customers to use both.
However, as a reseller, there are certain operations we can't permit. We make the management account available to customers but restrict what they can do with it. The exact restrictions depend on the state of each Organization:
-
Support case access: Some Organizations deny all
support:*actions (customers must go through the reseller for support), while others allow customers to file support cases directly. - Root user management: In some Organizations, the reseller manages root user credentials for member accounts; in others, the customer manages them. This changes the IAM conditions in the policy.
- Organization-level operations: Leaving the Organization is always denied, but other operations may vary.
These differences exist in both IAM policies and SCPs.
The Scale of the Problem
Nearly 1,000 management accounts doesn't mean 1,000 unique policies — but it does mean several distinct patterns have emerged. When a fundamental AWS update requires a policy change (think re:Invent season with its flood of announcements), we need to update the relevant section across every variant. That review process is painful.
Visually, think of each policy as a series of blocks — one per Sid or Condition. Most blocks are shared (shown in blue), but a few differ (shown in other colors). The challenge is keeping the blue blocks in sync across all variants.
The Building-Block Idea
Since this is AWS, why not think in terms of building blocks? What if we could break policies into modular parts and compose them as needed — like LEGO bricks?
Instead of maintaining each policy as a standalone document, we'd maintain individual components at the smallest reasonable granularity and assemble them at build time.
A conceptual view of the building block components.Blue blocks represent shared elements, while red and yellow blocks represent the parts that differ between the two policies. An illustration of how the smaller, modularized blocks are assembled to form the diagram above.
Some elements use different parts, and we also envision embedding different parts into shared components to build larger, coarser-grained modules.
Should We Use IaC?
We considered AWS CDK. You could model PolicyStatement objects as reusable classes or functions, manage shared parts in JSON, and add variant-specific pieces in code.
But there were concerns:
- Deploying to ~1,000 accounts means creating ~1,000 stacks and running them all. That's a lot of CloudFormation overhead.
- The learning curve for CDK felt disproportionate to the problem we're solving.
- We wanted to explore whether a simpler, non-IaC approach could work first.
To be clear: we're not against IaC. We're just exploring alternatives that might better fit our deployment model and team skills.
The Approach: YAML Modules + yq
With some help from generative AI for brainstorming, we arrived at a lightweight approach: write policy components in YAML and use the yq command-line tool to assemble them.
Writing Policies in YAML
Each component is a separate YAML file. For example, even the Version declaration gets its own file:
VersionDeclaration.yaml
# Extracted just the IAM JSON policy Version element.
# Needs review if the Version specification is ever revised.
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html
Version: "2012-10-17"
Statement:
Here's a module that denies support:* (used when customers must go through the reseller for support):
BasicRestrictionAsResaler.yaml
- Sid: BasicRestrictionAsResaler
Effect: Deny
Action:
- support:*
# Summary: Deny all AWS Support actions
# Relaxable?: No — customers must not file support cases directly
- supportplans:*
# Summary: Deny all support plan modifications
# Relaxable?: No — support plan must not be changed
- tax:*
# Summary: Deny all tax setting modifications
# Relaxable?: No — tax settings must remain configured for Japan
# Deny on all resources
Resource: '*'
And here's the variant that allows support:* (for customers who are permitted to file cases directly):
BasicRestrictionAsResalerForResold.yaml
- Sid: BasicRestrictionAsResalerForResold
Effect: Deny
Action:
- supportplans:*
# Summary: Deny all support plan modifications
# Relaxable?: No — support plan must not be changed
- tax:*
# Summary: Deny all tax setting modifications
# Relaxable?: No — tax settings must remain configured for Japan
# Deny on all resources
Resource: '*'
Notice the only difference: the first variant includes support:* in the deny list; the second omits it.
Assembling with yq
To build a policy that denies support access:
yq eval-all '
select(fileIndex == 0) |
.Statement = [
(load("IAMPolicyRestrictionForCustomer.yaml") | .[0]),
(load("BasicRestrictionAsResaler.yaml") | .[0]), # This line differs
(load("RestrictionToAWSOrganizationsAsResaler.yaml") | .[0])
]
' VersionDeclaration.yaml > foo.yaml
To build a policy that allows support access, just swap one module:
yq eval-all '
select(fileIndex == 0) |
.Statement = [
(load("IAMPolicyRestrictionForCustomer.yaml") | .[0]),
(load("BasicRestrictionAsResalerForResold.yaml") | .[0]), # This line differs
(load("RestrictionToAWSOrganizationsAsResaler.yaml") | .[0])
]
' VersionDeclaration.yaml > bar.yaml
Here's what yq is doing:
-
eval-all— operate across multiple files. -
select(fileIndex == 0)— use the first file (VersionDeclaration.yaml) as the base. -
.Statement = [...]— populate theStatementarray by loading each module file and extracting its first element. - Output the assembled YAML.
Converting to JSON
Once assembled, convert to JSON for use as an actual IAM policy:
yq -o=json foo.yaml > foo.json
The result is a standard IAM policy document:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BasicRestrictionAsResaler",
"Effect": "Deny",
"Action": [
"support:*",
"supportplans:*",
"tax:*"
],
"Resource": "*"
}
]
}
(Other statements omitted for brevity.)
Validating the Output
We can validate the generated policy using IAM Access Analyzer:
aws accessanalyzer validate-policy \
--policy-type IDENTITY_POLICY \
--policy-document file://foo.json
{
"findings": []
}
If findings comes back empty, the policy document is syntactically valid. Note that this doesn't verify whether the policy does what you intend — that's a separate review.
What We Learned
The modular approach feels promising. Here's what stood out:
- Single source of truth for shared components. When a shared module needs updating, you change it once. Every policy that includes it picks up the change at build time.
- YAML enables inline documentation. This was an unexpected but significant benefit. JSON doesn't support comments, so our existing policy documents had no way to explain why a particular action was denied. With YAML, we can annotate each action with its rationale — "Know Why" documentation becomes a natural part of the policy source. (Further proof that JSON came too early for humanity.)
-
Low barrier to entry.
yqis a single binary with a straightforward syntax. No SDK, no runtime, no framework to learn.
JSONC as an alternative
After presenting an earlier version of this talk at JAWS-UG DE&I, someone pointed out that JSONC (JSON with Comments) exists as a specification. That's a fair point — but YAML still wins for us because of the modular composition workflow with yq.
The Remaining Challenge: Deployment
The policy composition problem feels solved, but deployment is still an open question.
Currently, we use bulk-update scripts that overwrite policies across all accounts. This works because:
- The accounts are under our management.
- Policy names and role names are stable and guaranteed to exist.
We're considering whether CloudFormation StackSets might be a better deployment mechanism, but there's a migration challenge: the existing IAM resources weren't deployed via CloudFormation, so importing them into StackSets without conflicts requires careful planning.
The policy assembly step (YAML modules + yq) is independent of the deployment mechanism. Whether we stick with scripts, move to StackSets, or even use CDK for deployment, the modular source-of-truth approach works either way.
A likely next step is integrating this into a GitHub Actions pipeline: commit a module change → CI assembles all affected policies → validates them with Access Analyzer → produces deployment-ready JSON artifacts.
Closing Thoughts
We chose not to use IaC for this particular problem, but that's not a rejection of IaC in general. Our reasoning:
- The resources are under our direct control, with stable naming conventions.
- A "nonexistent" state is essentially impossible in our environment.
- A simple bulk-update script is more predictable and faster than rolling out ~1,000 CloudFormation stacks.
That said, I'd love to hear if you have a better approach. This is very much a work in progress, and I'm open to suggestions.
Security is job zero.
Thank you for reading.


Top comments (0)