DEV Community

Cover image for Idea for Managing Policies Across Nearly 1,000 AWS Organizations — A Reseller's Perspective
ICHINO Kazuaki for AWS Community Builders

Posted on • Originally published at speakerdeck.com

Idea for Managing Policies Across Nearly 1,000 AWS Organizations — A Reseller's Perspective

"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:
Enter fullscreen mode Exit fullscreen mode

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: '*'
Enter fullscreen mode Exit fullscreen mode

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: '*'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Here's what yq is doing:

  1. eval-all — operate across multiple files.
  2. select(fileIndex == 0) — use the first file (VersionDeclaration.yaml) as the base.
  3. .Statement = [...] — populate the Statement array by loading each module file and extracting its first element.
  4. 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
Enter fullscreen mode Exit fullscreen mode

The result is a standard IAM policy document:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BasicRestrictionAsResaler",
      "Effect": "Deny",
      "Action": [
        "support:*",
        "supportplans:*",
        "tax:*"
      ],
      "Resource": "*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

(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": []
}
Enter fullscreen mode Exit fullscreen mode

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. yq is 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.

References

Top comments (2)

Collapse
 
sqlxpert profile image
Paul Marcelin

Hello from California, Ichino.

Thank you for explaining clearly and in detail the way you solved a scaling problem that many AWS users share. Because of their importance and their complexity, IAM polices, service control policies, and resource control policies might be some of the last resources templated, in an infrastructure-as-code adoption.

Thank you for taking the time not only to translate your writing to English, but also to explain the context. This is the habit of an excellent technical writer: to complete the context so that readers can understand the content. (I once read a Japan-US collaboration article about airline food service, which even touched on the food served to NASA astronauts. The Japanese co-author opened with information about a Japanese word for feeling at home, a feeling that a familiar meal could provoke. This cultural clue explained why the author took the subject so seriously, and what he'd tried to achieve in planning the meals and the technology to serve them.)

I love your choice of YAML for comments. Security-minded, I chose CloudFormation YAML instead of JSON for least-privilege IAM policies when I started publishing open-source software in 2017.

In the first few months of 2026, I had to switch to publishing a few policies in JSON, either so that I could share base policy source between CloudFormation and Terraform (I support both) or so that I could accept customizations from users. I'll post some links to my source code in a separate message.

I'd say that wise use of statement identifiers is vital, even though we have source-level comments in YAML. Some users, including for example security auditors, might first see policies in the AWS Console. Our Sids, then, serve not only to distinguish statements and explain them, but also to cross-reference related statements, policies, or longer source code comments.

In the past few years I've switched to composing Sids dynamically, where one policy depends on another. For example, the Sid of an S3 bucket policy statement that gives permission to Get KMS-encrypted S3 objects makes reference to the Sid of a KMS key policy that gives permission to decrypt data. If the user of my software declines KMS encryption, no cross-reference is added to the bucket policy statement's Sid.

In 13 years of using AWS, I've thought a lot about metadata and arrived at the conclusion that less is more. I think we should teach people to read an IAM policy statement, so that a comment need only explain non-obvious aspects. I claim that many statements are self-documenting, especially if we control the order of the keys for presentation purposes (in source code and in output; keys in JSON objects form a set and thus have no ordering). No comment is needed to explain what "Deny" "support:*" does. The comment about whether and when this policy can be relaxed, on the other hand, adds information.

Perhaps this view will help you to pare comments that duplicate information contained in the syntax of the statement, drawing your colleagues' (and your clients') attention to comments that enrich understanding.

Tags are a great place for extra information, especially concerning systemic properties (for you, perhaps a type of client, whether a rule can be relaxed, etc.). As you know, you can lock down a tag key prefix with a broad SCP (and certain carve-outs for automations that copy tags). I like "security-", but it could be your company's name. I recently saw someone borrow AWS's own tag key prefix separator, as in "security:".

Using JMESPath --query , you might be able to find policies by tag value and/or tag key.

As for the substitutions, I noticed that the distinguishing information in your module names comes at the end. This could be an artifact of translation, or of an alphabetic language like English; in Japanese your module names might be instantly distinguishable. If the original module names do look the same, do you sometimes notice developers choosing one module when they meant to choose the other? If so, you might be able to reduce coding errors by "front-loading" distinguishing details at the start of the module name.

A pattern I've started to use in my least-privilege policy-writing work is to attach an AWS-managed IAM policy to a role and then write a customer-managed (or sometimes in-line) policy that subtracts from it. For example, I don't want Lambda creating CloudWatch log groups with an unlimited retention period at run-time, so I pre-create properly-configured log groups in CloudFormation, protect them, and remove the function role's permission to create log groups, while still allowing it to create and write to log streams. (I might use Deny and NotResource to limit the log group and stream names.)

If your roles have slots left for more customer-managed or inline policies, and if negated statements wouldn't exceed the character limit, starting with a broad Allow policy for all customers, and adding a Deny policy for some, might simplify your system (writing, deployment, and reading/understanding/interpretation). Come to think of it, this is the semantic model of resource control policies: allow everything and then reduce in some cases.

I hope my comments give you some useful ideas. Perhaps you had already considered similar ideas. I am interested in collaborating with you on the policy structure and on the deployment mechanism. (I am a big proponent of CloudFormation StackSets, and I note that, at the stack level, CloudFormation recently added import support, to match Terraform.)

Cheers!

Collapse
 
kazzpapa3 profile image
ICHINO Kazuaki AWS Community Builders

Thank you for your comment. I truly appreciate such a detailed comment to one of my first posts on dev.to in English.

Although my English translation was done with the help of Kiro, I'm glad to hear that it came across naturally to you as a native speaker.

I'm convinced by your insight that distinguishing information should be front-loaded in Sids and module file names. As you noticed, my original design placed the distinguishing details at the end, but you're absolutely right that front-loading them would help prevent developers from choosing the wrong module.

I also agree with your point about the subtractive design approach — attaching an AWS-managed policy and then writing a customer-managed policy that subtracts from it. This is an elegant pattern.

It was encouraging to receive validation for my choice of YAML as a way to embed comments in policies. As for comment granularity, you make a fair point — I'll work on paring comments that duplicate information already contained in the syntax of the statement, and focus on those that enrich understanding, such as whether and when a policy can be relaxed.

Thank you so much!