Terraform и IaC: как читать чужую инфраструктуру как открытую книгу
Почему IaC — это золотая жила для аналитика
Когда инфраструктура описана кодом — это одновременно сила и слабость. Сила для команды разработки: всё воспроизводимо, версионировано, читаемо. Слабость для безопасности: тот, кто умеет читать .tf файлы, видит всю архитектуру сразу — без необходимости сканировать сеть часами.
Terraform стал стандартом де-факто для описания облачной инфраструктуры. AWS, GCP, Azure, DigitalOcean — всё это описывается одним языком HCL (HashiCorp Configuration Language). И если ты попал в репозиторий компании или нашёл утечку конфигов — ты буквально держишь в руках карту всей их инфраструктуры.
Анатомия Terraform-проекта
Типичная структура репозитория выглядит так:
|
1 2 3 4 5 6 7 8 9 10 |
infra/ ├── main.tf # Основные ресурсы ├── variables.tf # Переменные (часто с дефолтами — находка!) ├── outputs.tf # Выходные данные (IP, endpoints, ARN) ├── terraform.tfvars # Значения переменных (НИКОГДА не должен быть в git) ├── backend.tf # Где хранится state-файл └── modules/ ├── vpc/ ├── ec2/ └── rds/ |
Первым делом смотришь именно в этом порядке: outputs.tf → terraform.tfvars → backend.tf.
Что искать и где
terraform.tfvars и .tfvars.json
Это главный приз. Разработчики обязаны класть его в .gitignore, но регулярно забывают.
|
1 2 3 4 5 6 |
# terraform.tfvars — типичная находка db_password = "SuperSecret123!" aws_access_key = "AKIAIOSFODNN7EXAMPLE" aws_secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" slack_webhook_url = "https://hooks.slack.com/services/T00/B00/XXXX" jwt_secret = "my-super-jwt-secret" |
Ищи их через GitHub/GitLab поиск:
|
1 2 3 4 |
# Дорки для поиска утечек filename:terraform.tfvars filename:.tfvars password extension:tf aws_secret_key |
outputs.tf — карта эндпоинтов
|
1 2 3 4 5 6 7 8 9 10 11 12 |
output "db_endpoint" { value = aws_db_instance.main.endpoint # Здесь будет что-то вроде: mydb.xxxx.us-east-1.rds.amazonaws.com:5432 } output "alb_dns" { value = aws_lb.main.dns_name } output "redis_endpoint" { value = aws_elasticache_cluster.main.cache_nodes[0].address } |
Из outputs.tf ты узнаёшь все публичные и приватные эндпоинты без единого скана.
backend.tf — где лежит state
|
1 2 3 4 5 6 7 |
terraform { backend "s3" { bucket = "company-terraform-state" key = "prod/terraform.tfstate" region = "us-east-1" } } |
State-файл — это святой Грааль. Он содержит актуальное состояние всей инфраструктуры, включая все атрибуты ресурсов. Если S3 bucket публичный или ты нашёл ключи — качай state первым делом.
Разбор terraform.tfstate
State-файл — это JSON с полным описанием всего, что Terraform создал. Парсим его скриптом:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
import json import sys def parse_tfstate(filepath): with open(filepath) as f: state = json.load(f) findings = { 'ips': [], 'endpoints': [], 'secrets': [], 'security_groups': [] } keywords = ['password', 'secret', 'key', 'token', 'credential', 'api_key'] for resource in state.get('resources', []): rtype = resource.get('type', '') for instance in resource.get('instances', []): attrs = instance.get('attributes', {}) # Собираем IP-адреса for field in ['public_ip', 'private_ip', 'ip_address']: if attrs.get(field): findings['ips'].append({ 'resource': rtype, 'ip': attrs[field] }) # Собираем эндпоинты for field in ['endpoint', 'dns_name', 'address', 'hostname']: if attrs.get(field): findings['endpoints'].append({ 'resource': rtype, 'endpoint': attrs[field] }) # Ищем секреты рекурсивно def search_secrets(obj, path=''): if isinstance(obj, dict): for k, v in obj.items(): if any(kw in k.lower() for kw in keywords): if v and str(v) not in ('', 'null', 'None'): findings['secrets'].append({ 'path': f'{path}.{k}', 'value': str(v)[:50] + '...' if len(str(v)) > 50 else str(v) }) search_secrets(v, f'{path}.{k}') elif isinstance(obj, list): for i, item in enumerate(obj): search_secrets(item, f'{path}[{i}]') search_secrets(attrs, rtype) return findings if __name__ == '__main__': results = parse_tfstate(sys.argv[1]) print(json.dumps(results, indent=2, ensure_ascii=False)) |
Запускаешь так:
|
1 |
python3 parse_state.py terraform.tfstate |
И получаешь структурированный дамп всего интересного.
Читаем Security Groups — находим дыры в файерволе
Security groups в Terraform описывают правила доступа. Ищи конфиги с 0.0.0.0/0:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
resource "aws_security_group" "web" { ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] # SSH открыт на весь интернет — jackpot } ingress { from_port = 5432 to_port = 5432 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] # PostgreSQL наружу — это уже катастрофа } } |
Автоматизируем поиск таких дыр:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import hcl2 # pip install python-hcl2 import glob def find_open_ports(directory): findings = [] for tf_file in glob.glob(f"{directory}/**/*.tf", recursive=True): with open(tf_file) as f: try: config = hcl2.load(f) except: continue for resource_block in config.get('resource', []): for rtype, resources in resource_block.items(): if 'security_group' in rtype: for rname, rconfig in resources.items(): for ingress in rconfig.get('ingress', []): cidrs = ingress.get('cidr_blocks', []) if '0.0.0.0/0' in cidrs or '::/0' in cidrs: findings.append({ 'file': tf_file, 'resource': f"{rtype}.{rname}", 'port_range': f"{ingress.get('from_port')}-{ingress.get('to_port')}", 'protocol': ingress.get('protocol') }) return findings results = find_open_ports('./infra') for r in results: print(f"[!] {r['resource']} | Port {r['port_range']}/{r['protocol']} | {r['file']}") |
Модули — где зарыты переиспользуемые секреты
Смотри на вызовы модулей — туда часто прокидывают чувствительные данные:
|
1 2 3 4 5 6 7 8 9 10 |
module "database" { source = "./modules/rds" db_name = "production" db_user = "admin" db_password = var.db_password # Откуда берётся? Смотри variables.tf # Или хуже — прямо хардкодом: db_password = "HardcodedPassword2024!" } |
Быстрый grep для поиска хардкода прямо в .tf файлах:
|
1 2 3 4 |
# Ищем потенциально захардкоженные секреты grep -rE '(password|secret|token|api_key)\s*=\s*"[^$][^"]{6,}"' . \ --include="*.tf" \ --exclude-file="*.tfvars" |
Инструменты для автоматического аудита
Не изобретай велосипед — используй готовые тулзы:
- tfsec — статический анализ безопасности Terraform-кода
- checkov — широкий IaC-сканер (поддерживает Terraform, CloudFormation, K8s)
- terrascan — ещё один статический анализатор с кучей политик
- trufflehog — ищет секреты в git-истории, включая
.tfфайлы
|
1 2 3 4 5 6 7 8 |
# Быстрый старт с tfsec docker run --rm -v "$(pwd):/src" aquasec/tfsec /src # Checkov с выводом только HIGH severity checkov -d ./infra --compact --check HIGH # Trufflehog по всему репозиторию trufflehog git file://./infra --only-verified |
Разведка через GitHub
Иногда не нужно ничего ломать — достаточно уметь искать:
|
1 2 3 4 5 6 7 8 |
# GitHub дорки для поиска Terraform конфигов с секретами org:target-company filename:*.tf aws_access_key_id org:target-company filename:terraform.tfvars org:target-company path:/.terraform backend # Поиск утёкших state-файлов filename:terraform.tfstate filename:*.tfstate db_password |
Также проверяй GitHub Actions / GitLab CI конфиги — туда часто прокидывают TF_VAR_* переменные прямо в env, которые могут засветиться в логах.
Итог: чеклист при анализе чужой инфраструктуры
terraform.tfvars— первым делом, ищи в git-истории даже если удалёнterraform.tfstate— проверь доступность S3/GCS bucket из backend.tfoutputs.tf— мгновенная карта всех эндпоинтов- Security groups с
0.0.0.0/0— это прямые векторы входа - Хардкодные секреты в
main.tfиmodules/*/main.tf - Git-история:
git log --all --full-diff -p -- '*.tf' | grep -i password
IaC превращает инфраструктуру в документацию. Тот, кто умеет её читать — видит всё. Пользуйся этим знанием для защиты своих систем и аудита клиентских инфраструктур.

На этом все. Всем хорошего дня!
