AWS IAM Policies Deep Dive: Mastering Fine-Grained Access Control

Last Updated: June 30, 2025
Controlling who can do what in your AWS account is critical for security and compliance, as well as to enable teams to move fast without fear of unintended breaches. In this hands-on deep dive, we’ll demystify AWS Identity and Access Management (IAM) policies, breaking down their structure, evaluation logic, and best practices for implementing least-privilege at scale.
You will learn how to:
- Create and organize JSON policy documents that precisely grant or deny actions
- Automate policy validation and enforcement using GitHub Actions, Terraform, and IAM Access Analyzer
- Embed policy testing into your CI/CD pipelines to catch misconfigurations before they ever reach production
Whether you’re an SRE, DevOps engineer, or security lead, by the end of this tutorial you’ll have a battle-tested workflow for authoring, reviewing, and deploying IAM policies with confidence! Complete with reusable Terraform modules, pre-rendered diagrams, and step-by-step code samples. Let’s get started!
- An AWS account with IAM Administrator or equivalent permissions
- AWS CLI v2 installed and configured (
aws configure
) - Basic JSON literacy
- You have a working knowledge of AWS core services (EC2, S3, IAM fundamentals).
- Familiarity with basic Git workflows (clone, branch, pull request).
- Your organization uses modern CI/CD pipelines (e.g., GitHub Actions, CodePipeline).
- You’ll apply these examples in a sandbox or test account before production.
- Who? (Principal)
- What? (Action on Resource under optional Conditions)
They encapsulate permissions in a single source of truth for your AWS account, enabling consistent, version-controlled access definitions
Type | Attached To | Use Case |
---|---|---|
AWS Managed | IAM users, groups, roles | Common job-function permissions (e.g., Read-Only) |
Customer Managed | IAM users, groups, roles | Custom, sharable policies with full version control |
Inline (Identity) | Single IAM entity | One-off or tightly scoped permissions |
Resource-Based | S3 buckets, SQS queues, KMS… | Cross-account grants, public access |
Permissions Boundary | IAM users or roles | Maximum allowed permissions, irrespective of identity policies |
Service Control Policy | AWS Organizations | Account-level guardrails across member accounts |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "OptionalStatementID",
"Effect": "Allow" | "Deny",
"Action": ["service:Operation", …],
"Resource": ["arn:aws:…", …],
"Condition": { /* optional context keys */ }
}
]
}
- Version: language date; use
"2012-10-17"
for all new policies - Effect:
Allow
orDeny
- Action: one or more AWS API operations
- Resource: one or more ARNs
- Condition: optional fine-grained context tests
- Implicit Deny: everything is denied by default
- Explicit Deny: any matching Deny in any policy wins immediately
- Explicit Allow: only if no Deny matches and at least one Allow matches
When multiple policy types apply (identity, resource, SCP, boundary, session), AWS effectively computes an intersection of allows bounded by any denies
- Least Privilege: start broad, then refine down to exact actions and resources
- Use Roles & Temporary Credentials: leverage STS and remove long-lived access keys
- Avoid Wildcards: narrow
Action
andResource
or scope withCondition
keys - Version & Review: track policy changes in Git; use CloudTrail last-used data to prune stale permissions
- Permission Boundaries: enforce maximum permissions for IAM entities in regulated environments
1. Enable Analyzer via console or CLI:
aws accessanalyzer create-analyzer --analyzer-name ProdAnalyzer --type ACCOUNT
2. Review Findings in the console or via:
aws accessanalyzer list-findings --analyzer-name ProdAnalyzer
3. Generate Refinement: export “policy templates” based on actual usage and attach them.
# .github/workflows/iam-policy-test.yml
name: 'IAM Policy Lint & Simulate'
on:
pull_request:
paths:
- 'policies/**/*.json'
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate JSON
run: jq empty policies/*.json
- name: Simulate Custom Policy
env:
AWS_REGION: us-east-1
run: |
aws iam simulate-custom-policy \
--policy-input-list file://policies/${{ github.event.pull_request.head.ref }}.json \
--action-names ec2:DescribeInstances \
--resource-arns arn:aws:ec2:us-east-1:123456789012:instance/*
# buildspec.yml
version: 0.2
phases:
install:
commands:
- pip install awscli
build:
commands:
- aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/IAMTester \
--action-names s3:ListBucket s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/*
Symptom | Diagnosis | Solution |
---|---|---|
Service won’t start | sudo journalctl -xe or systemctl status <service> | Check for typos, missing paths |
Timer isn’t firing | systemctl list-timers | Ensure .timer file is enabled |
Service fails silently | Missing logs | Add StandardOutput=journal to unit |
Unit file changes ignored | Daemon not reloaded | Run sudo systemctl daemon-reload |
Logs missing | journalctl empty | Enable persistent logging under /var/log/journal |
Terraform Module for IAM Policies
A reusable module enforces consistency and DRY principles.
repo/
├── modules/
│ └── iam-policy/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── policies/
└── s3-read-only.json
modules/iam-policy/main.tf
variable "policy_name" { type = string }
variable "policy_json" { type = string }
resource "aws_iam_policy" "this" {
name = var.policy_name
policy = var.policy_json
}
output "arn" { value = aws_iam_policy.this.arn }
Root invocation
module "s3_read_only" {
source = "./modules/iam-policy"
policy_name = "S3ReadOnlyPolicy"
policy_json = file("${path.module}/policies/s3-read-only.json")
}
Visual Workflow Diagram

Related Posts
Ansible vs. Puppet vs. Native Tools: Which Automation Approach Is Right for You?
Automate Linux Patch Management with Ansible: Zero-Touch Updates for Your Fleet
Building a full cloud-native alerting pipeline
