Верификация IAM-политик и сетевых правил
Updated Jul 2026
Почему тестирование IAM критически важно
Ошибки конфигурации IAM-политик стабильно занимают первое место среди рисков облачной безопасности. Одна чрезмерно разрешительная политика может предоставить атакующему доступ к каждому ресурсу в вашем аккаунте. Одно пропущенное условие может разрешить действия из неавторизованных сетей. Тем не менее большинство команд относятся к IAM-политикам по принципу «настроил и забыл» — создают их при начальной настройке и никогда не тестируют.
Программное тестирование IAM-политик означает, что вы можете обнаруживать чрезмерно разрешительный доступ, отсутствующие условия и дрифт политик в рамках CI-пайплайна, а не после инцидента безопасности.
Программное тестирование IAM-политик
Набор тестов на Python для IAM-политик
# tests/test_iam_policies.py
import json
import pytest
import os
import glob
def load_policy(path):
"""Load an IAM policy document from a JSON file."""
with open(path) as f:
return json.load(f)
def get_all_policy_files():
"""Find all IAM policy JSON files in the Terraform directory."""
return glob.glob("terraform/policies/*.json")
class TestLambdaExecutionRole:
"""Tests for the Lambda execution role policy."""
policy = load_policy("terraform/policies/lambda-execution-role.json")
def test_no_wildcard_resources(self):
"""IAM policies must never use Resource: '*' with mutating actions."""
for statement in self.policy["Statement"]:
if statement["Effect"] == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
mutating = [a for a in actions if not a.endswith(":Get*")
and not a.endswith(":List*")
and not a.endswith(":Describe*")]
if mutating:
resources = statement.get("Resource", [])
if isinstance(resources, str):
resources = [resources]
assert "*" not in resources, \
f"Wildcard resource with mutating actions: {mutating}"
def test_no_admin_access(self):
"""No policy should grant full admin access."""
for statement in self.policy["Statement"]:
if statement["Effect"] == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
assert "*" not in actions, "Policy grants full admin access"
assert "iam:*" not in actions, "Policy grants full IAM access"
def test_has_condition_keys(self):
"""Sensitive actions should have condition constraints."""
for statement in self.policy["Statement"]:
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
sensitive = [a for a in actions if "s3:Delete" in a or "dynamodb:Delete" in a]
if sensitive:
assert "Condition" in statement, \
f"Sensitive actions {sensitive} lack Condition constraints"
def test_no_pass_role_without_conditions(self):
"""iam:PassRole must have a condition limiting which roles can be passed."""
for statement in self.policy["Statement"]:
if statement["Effect"] == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
if "iam:PassRole" in actions:
assert "Condition" in statement, \
"iam:PassRole must have conditions (e.g., iam:PassedToService)"
Универсальный сканер политик
# tests/test_all_iam_policies.py
import json
import glob
import pytest
POLICY_FILES = glob.glob("terraform/policies/*.json")
@pytest.mark.parametrize("policy_path", POLICY_FILES)
def test_no_wildcard_actions(policy_path):
"""No policy should grant Action: '*'."""
with open(policy_path) as f:
policy = json.load(f)
for statement in policy.get("Statement", []):
if statement.get("Effect") == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
assert "*" not in actions, \
f"{policy_path}: Statement grants wildcard action"
@pytest.mark.parametrize("policy_path", POLICY_FILES)
def test_no_wildcard_resource_with_write(policy_path):
"""No write action should use Resource: '*'."""
with open(policy_path) as f:
policy = json.load(f)
readonly_suffixes = [":Get*", ":List*", ":Describe*", ":Head*"]
for statement in policy.get("Statement", []):
if statement.get("Effect") == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
has_write = any(
not any(a.endswith(suffix) for suffix in readonly_suffixes)
for a in actions
)
if has_write:
resources = statement.get("Resource", [])
if isinstance(resources, str):
resources = [resources]
assert "*" not in resources, \
f"{policy_path}: Wildcard resource with write actions"
@pytest.mark.parametrize("policy_path", POLICY_FILES)
def test_deny_statements_exist(policy_path):
"""Policies should include explicit Deny statements for dangerous actions."""
with open(policy_path) as f:
policy = json.load(f)
dangerous_actions = ["iam:CreateUser", "iam:CreateAccessKey",
"organizations:LeaveOrganization", "ec2:RunInstances"]
for statement in policy.get("Statement", []):
if statement.get("Effect") == "Allow":
actions = statement.get("Action", [])
if isinstance(actions, str):
actions = [actions]
for dangerous in dangerous_actions:
assert dangerous not in actions, \
f"{policy_path}: Allows dangerous action {dangerous}"
Верификация сетевых правил
Тестирование групп безопасности с Terraform Plan
# tests/test_network_rules.py
import json
import pytest
@pytest.fixture
def tfplan():
"""Load Terraform plan JSON."""
with open("tfplan.json") as f:
return json.load(f)
def test_no_public_ingress_on_database_sg(tfplan):
"""Database security groups must not allow ingress from 0.0.0.0/0."""
for change in tfplan["resource_changes"]:
if change["type"] == "aws_security_group":
if "database" in change["address"] or "rds" in change["address"]:
ingress_rules = change["change"]["after"].get("ingress", [])
for rule in ingress_rules:
cidrs = rule.get("cidr_blocks", [])
assert "0.0.0.0/0" not in cidrs, \
f"Database SG {change['address']} allows public ingress"
assert "::/0" not in cidrs, \
f"Database SG {change['address']} allows public IPv6 ingress"
def test_database_sg_only_allows_specific_ports(tfplan):
"""Database security groups should only allow database ports."""
allowed_ports = {5432, 3306, 27017, 6379} # Postgres, MySQL, Mongo, Redis
for change in tfplan["resource_changes"]:
if change["type"] == "aws_security_group":
if "database" in change["address"] or "rds" in change["address"]:
ingress_rules = change["change"]["after"].get("ingress", [])
for rule in ingress_rules:
from_port = rule.get("from_port", 0)
to_port = rule.get("to_port", 0)
assert from_port in allowed_ports, \
f"Database SG allows unexpected port {from_port}"
assert from_port == to_port, \
f"Database SG has port range {from_port}-{to_port}"
def test_no_unrestricted_egress(tfplan):
"""Security groups should not have unrestricted egress to the internet."""
for change in tfplan["resource_changes"]:
if change["type"] == "aws_security_group":
egress_rules = change["change"]["after"].get("egress", [])
for rule in egress_rules:
if "0.0.0.0/0" in rule.get("cidr_blocks", []):
# If egress is open, it must not be on all ports
assert rule.get("from_port", 0) != 0 or \
rule.get("to_port", 0) != 65535, \
f"SG {change['address']} has unrestricted egress on all ports"
def test_ssh_restricted_to_vpn(tfplan):
"""SSH access (port 22) must be restricted to VPN CIDR blocks only."""
vpn_cidrs = ["10.0.0.0/8", "172.16.0.0/12"]
for change in tfplan["resource_changes"]:
if change["type"] == "aws_security_group_rule":
after = change["change"]["after"]
if after.get("from_port") == 22 and after.get("type") == "ingress":
cidrs = after.get("cidr_blocks", [])
for cidr in cidrs:
assert cidr in vpn_cidrs, \
f"SSH rule in {change['address']} allows access from {cidr}"
Тестирование конфигурации VPC
def test_private_subnets_have_no_public_ips(tfplan):
"""Private subnets must not auto-assign public IPs."""
for change in tfplan["resource_changes"]:
if change["type"] == "aws_subnet":
if "private" in change["address"]:
map_public = change["change"]["after"].get(
"map_public_ip_on_launch", False
)
assert map_public is False, \
f"Private subnet {change['address']} assigns public IPs"
def test_nacl_denies_known_bad_ports(tfplan):
"""Network ACLs should deny traffic on known dangerous ports."""
dangerous_ports = [23, 135, 139, 445, 1433, 3389] # Telnet, RPC, SMB, MSSQL, RDP
for change in tfplan["resource_changes"]:
if change["type"] == "aws_network_acl_rule":
after = change["change"]["after"]
if after.get("rule_action") == "allow":
from_port = after.get("from_port", 0)
to_port = after.get("to_port", 0)
for port in dangerous_ports:
if from_port <= port <= to_port:
pytest.fail(
f"NACL rule {change['address']} allows "
f"dangerous port {port}"
)
Интеграция с AWS IAM Access Analyzer
Помимо статического анализа, AWS IAM Access Analyzer может валидировать политики во время выполнения:
import boto3
def test_policy_with_access_analyzer():
"""Use AWS IAM Access Analyzer to validate policy documents."""
client = boto3.client("accessanalyzer")
with open("terraform/policies/lambda-execution-role.json") as f:
policy_document = f.read()
response = client.validate_policy(
policyDocument=policy_document,
policyType="IDENTITY_POLICY",
)
errors = [
finding for finding in response["findings"]
if finding["findingType"] == "ERROR"
]
warnings = [
finding for finding in response["findings"]
if finding["findingType"] == "WARNING"
]
security_warnings = [
finding for finding in response["findings"]
if finding["findingType"] == "SECURITY_WARNING"
]
assert len(errors) == 0, f"Policy has errors: {errors}"
assert len(security_warnings) == 0, f"Policy has security warnings: {security_warnings}"
if warnings:
for w in warnings:
print(f" WARNING: {w['issueCode']}: {w['message']}")
Интеграция в CI
# .github/workflows/iam-network-tests.yml
name: IAM and Network Policy Tests
on:
pull_request:
paths:
- 'terraform/policies/**'
- 'terraform/modules/networking/**'
- 'terraform/modules/security/**'
jobs:
policy-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install pytest
- name: Run IAM policy tests
run: pytest tests/test_iam_policies.py tests/test_network_rules.py -v
Тестирование IAM и сетевых правил — это работа высокой ценности с минимальными усилиями. Эти тесты выполняются за секунды, не требуют облачных учётных данных (они анализируют JSON-файлы) и выявляют наиболее опасный класс ошибок конфигурации инфраструктуры.