Cloud Security · lesson 6 of 8
Intermediate
8- 1 Scanning Terraform for Misconfiguration with Checkov
- 2 Policy as Code: Testing Terraform Plans with OPA and Conftest
- 3 CIS Docker Benchmark Checks with docker-bench-security
- 4 Kubernetes Pod Security Admission: Enforcing the Restricted Profile
- 5 CIS Kubernetes Benchmark Checks with kube-bench in kind
- 6 Linting AWS IAM Policies Before Deploy with Parliament
- 7 Auditing the AWS IAM Credential Report with Python
- 8 Hunting in AWS CloudTrail Logs with Python
Also part of Cloud Security
11- + IAM Concepts That Transfer: AWS vs Azure vs GCP from Multi-Cloud
- + IAM: Users, Roles, and Policies from AWS
- + GCP IAM: Roles and Service Accounts from Google Cloud
- + Entra ID: Users, Roles, and RBAC from Azure
- + IAM and Least Privilege from Cloud Interviews
- + Cloud Networking Basics: What's the Same Everywhere from Multi-Cloud
- + Docker Security from Docker
- + Security, RBAC, and Observability from Kubernetes
- + sensitive = true: What It Hides, and What It Doesn't from Terraform
- + Building an Incident Timeline from Auth, Web and IDS Logs from Defensive Security
- + Envelope Encryption and Key Rotation Without Re-encrypting Data from Cryptography
Also part of: AWS
Linting AWS IAM Policies Before Deploy with Parliament
Lint IAM policy JSON locally with parliament: catch s3:* on *, a mistyped action and a resource mismatch, then gate CI on a least-privilege rewrite.
By SkillByExample · Published
An IAM policy is JSON, and AWS accepts a lot of JSON that doesn’t do what its author meant. A
mistyped action is saved without complaint, and so is an action paired with a resource it can
never match. parliament checks policy files offline, before anything reaches an AWS account,
so it can run in a pre-commit hook or a pull-request check.
Tested on 2026-10-15 with parliament 1.6.4 and setuptools 80.10.2 on Python 3.12.15
(python:3.12-slim on Linux). No AWS account or credentials are involved. The first policy is the
kind that starts out as “just make it work”:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AppStorage",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
]
}
Save it as app-policy.json, then install parliament into a virtual environment:
$ python -m venv .venv && source .venv/bin/activate
$ pip install -q parliament==1.6.4
$ parliament --file app-policy.json 2>&1 | tail -1
ModuleNotFoundError: No module named 'pkg_resources'
The install succeeds, but the first run fails. parliament imports pkg_resources, which is part
of setuptools. Virtual environments on Python 3.12 and later no longer include setuptools, and
setuptools 82 removed pkg_resources altogether (81 still ships it, with a deprecation warning).
So pin a release below 81, as the warning itself advises:
$ pip install -q setuptools==80.10.2
$ parliament --file app-policy.json --minimal
/lab/.venv/lib/python3.12/site-packages/parliament/__init__.py:13: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
import pkg_resources
RESOURCE_STAR
$ export PYTHONWARNINGS="ignore:pkg_resources is deprecated"
Now it runs, with a deprecation warning on every call. PYTHONWARNINGS silences only the warnings
whose message starts with that text, so any other warning still shows. The rest of the lesson runs
in this shell.
What parliament says about s3:* on *
--minimal prints only the issue codes, and the exit code tells a script whether anything was
found:
$ parliament --file app-policy.json --minimal; echo "exit code: $?"
RESOURCE_STAR
exit code: 1
A policy that grants every S3 action on every bucket in the account produced one finding, and
its full output, without --minimal, rates it LOW. parliament’s built-in checks look for policies
that are wrong, not for policies that grant too much. The community auditors that ship with it
cover the second kind. This helper prints one line per finding from the --json output:
# findings.py: one line per finding from `parliament --json` (JSON Lines on stdin)
import json
import sys
for line in sys.stdin:
finding = json.loads(line)
summary = f"{finding['severity']} {finding['issue']}"
if isinstance(finding["detail"], list):
summary += f" ({len(finding['detail'])} items)"
print(summary)
$ parliament --file app-policy.json --include-community-auditors --json | python findings.py
MEDIUM PERMISSIONS_MANAGEMENT_ACTIONS
LOW RESOURCE_STAR (145 items)
The detail of RESOURCE_STAR is the list of actions that s3:* expands to: 145 of them in the
IAM data bundled with this release. PERMISSIONS_MANAGEMENT_ACTIONS is the one that matters. Some
of those actions are s3:PutBucketPolicy and s3:PutBucketAcl, which let the holder rewrite who
can access a bucket, including making it public. Turn the community auditors on in every run.
A severity gate that lets the worst policy through
A CI step usually fails on findings above a threshold. The obvious threshold is the wrong one here:
$ parliament --file app-policy.json --minimum_severity MEDIUM; echo "exit code: $?"
exit code: 0
Without the community auditors, the only finding on s3:* is LOW, so a gate at MEDIUM passes the
broadest S3 policy there is. Either keep the default threshold or add
--include-community-auditors to the gate. Next, the gate has to run where CI runs it:
$ parliament --file app-policy.json --minimal < /dev/null; echo "exit code: $?"
usage: parliament [-h] [--aws-managed-policies AWS_MANAGED_POLICIES]
[--auth-details-file AUTH_DETAILS_FILE] [--string STRING]
[--file FILE] [--files FILES] [--directory DIRECTORY]
[--include_policy_extension INCLUDE_POLICY_EXTENSION]
[--exclude_pattern EXCLUDE_PATTERN] [--minimal] [--json]
[--minimum_severity {CRITICAL,HIGH,MEDIUM,LOW,INFO}]
[--private_auditors PRIVATE_AUDITORS] [--config CONFIG]
[--include-community-auditors] [-v] [--version]
parliament: error: You cannot pass a file with --file and use stdin together
exit code: 2
$ parliament --files app-policy.json --minimal < /dev/null; echo "exit code: $?"
RESOURCE_STAR
exit code: 1
< /dev/null stands in for a CI runner, where stdin is not a terminal. parliament treats any
non-terminal stdin as a policy being piped in, so --file errors out. The command worked in your
terminal and fails in the pipeline. --files (comma-separated paths) and --directory skip that
check, so use them in scripts.
A least-privilege rewrite, one finding at a time
The application only reads and writes objects under one bucket, and lists them. A first rewrite
of uploads-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadWriteUploads",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObjects", "s3:ListBucket"],
"Resource": "arn:aws:s3:::acme-uploads/*"
}
]
}
$ parliament --file uploads-policy.json
LOW - Unknown action - - Unknown action s3:PutObjects - {'string': 's3:PutObjects', 'line': 7, 'column': 34, 'filepath': 'uploads-policy.json'}
s3:PutObjects isn’t an action. AWS would accept this policy, and the app’s uploads would fail
with AccessDenied at runtime. parliament gives the line and column. Fix the typo to
s3:PutObject and run it again:
$ parliament --file uploads-policy.json
MEDIUM - No resources match for the given action - - [{'action': 's3:ListBucket', 'required_format': 'arn:*:s3:::*'}] - {'line': 4, 'column': 5, 'filepath': 'uploads-policy.json'}
A second bug, which the first run didn’t report. s3:ListBucket acts on a bucket
(arn:aws:s3:::acme-uploads), but the statement only names objects (acme-uploads/*), so
that permission can never take effect. That is the most common S3 policy mistake. Each fix can
uncover the next finding, so re-run until the output is empty. The fix splits the statement by
resource type:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListUploads",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::acme-uploads"
},
{
"Sid": "ReadWriteUploads",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::acme-uploads/*"
}
]
}
$ parliament --file uploads-policy.json --include-community-auditors; echo "exit code: $?"
exit code: 0
No output and exit code 0, even with the community auditors on. Three actions on one bucket replace 145 actions on every bucket. A clean run proves the policy is well formed. Whether those three actions are the right ones is still for you to decide, by comparing them with what the application actually calls.
Practice
- The office-only read policy below has a condition that never matches. Save it as
office-read.jsonand lint it. Which key is wrong, and what should it be?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadFromOfficeOnly",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::acme-uploads/*",
"Condition": {
"IpAddress": {"aws:SourceIpAddress": "203.0.113.0/24"}
}
}
]
}
Solution
$ parliament --file office-read.json
MEDIUM - Unknown condition for action - The given condition is not documented to work with the given action - Unknown condition aws:SourceIpAddress for action s3:GetObject - {'line': 10, 'column': 22, 'filepath': 'office-read.json'}
The global key is aws:SourceIp. A condition on a key that is never present doesn’t match, so this
Allow never applies and every read is denied. A typo in a Deny statement’s condition is worse:
the Deny silently never fires, and nothing breaks to tell you.
- A deploy role gets
lambda:UpdateFunctionCodeandiam:PassRoleon*. Save it asdeploy-policy.jsonand run it through the community auditors. What is the highest severity?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DeployLambda",
"Effect": "Allow",
"Action": ["lambda:UpdateFunctionCode", "iam:PassRole"],
"Resource": "*"
}
]
}
Solution
$ parliament --file deploy-policy.json --include-community-auditors --json | python findings.py
MEDIUM PERMISSIONS_MANAGEMENT_ACTIONS
HIGH PRIVILEGE_ESCALATION
LOW RESOURCE_STAR (2 items)
HIGH. iam:PassRole on * lets the holder hand any role in the account to a service, including
an admin role, and then act through that service. Scope Resource to the one execution role’s
ARN. Without the community auditors, this policy too is a single LOW finding.
- Lint every policy in a
policies/directory in one CI step. Copyuploads-policy.jsonanddeploy-policy.jsoninto it. What does the step report, and does it fail?
Solution
$ parliament --directory policies; echo "exit code: $?"
LOW - Unnecessary use of Resource * - - ['iam:PassRole', 'lambda:UpdateFunctionCode'] - {'line': 4, 'column': 5, 'filepath': 'policies/deploy-policy.json'}
exit code: 1
Only the deploy policy is reported, with its path, and the exit code fails the step. --directory
also avoids the stdin check that breaks --file in CI. Notice that this LOW line is all the
step says about the policy rated HIGH in task 2, so the CI step needs
--include-community-auditors too.