Modern QA2026Верификация IAM-политик и сетевых правил
Join

Course08 Infrastructure as Code Testing

Cutting-edge · Chapter 08

Верификация 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-файлы) и выявляют наиболее опасный класс ошибок конфигурации инфраструктуры.