Почему секреты в Git — это катастрофа
Junior DE на третьей неделе работы получает задачу: написать DAG, который выгружает данные из S3 и грузит в Snowflake. Чтобы быстрее проверить, что всё работает, он копирует AWS-ключи прямо в dag.py:
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
Локально всё работает. Он коммитит, пушит в feat/s3-loader ветку, открывает Pull Request. Code review откладывается на завтра. В 02:13 ночи приходит SMS от AWS: Spend alert: $11,847 in last hour. Через час — ещё 52k. Аккаунт заблокирован за подозрительную активность, инженеру неделю отвечать на вопросы security team.
Так выглядит typical secret leak incident в 2026 году. И главная боль — даже когда инженер узнал об утечке, исправить уже нельзя. Git устроен так, что удалить секрет «как будто его не было» — это операция, после которой все коллеги force-clone репозиторий, CI падает, открытые PR ломаются. А боты, которые уже скачали ключ — они продолжают его использовать, пока ты не отзовёшь.
В этом уроке разберёмся, почему именно Git делает утечку настолько разрушительной, как боты находят ключи за секунды, и почему «удалил файл следующим commit-ом» — это не решение.
Git история immutable: что это значит на физическом уровне
Когда ты делаешь git commit, Git создаёт несколько объектов на диске в .git/objects/:
- blob — содержимое каждого файла, по SHA-1.
- tree — снимок директории (список blob и tree).
- commit — метаданные (автор, дата, сообщение, parent commits, root tree SHA).
Все эти объекты — content-addressed. Их имя — это SHA-1 от содержимого. Поменял хоть один байт — получился другой объект с другим SHA. Старый объект никуда не делся, он лежит рядом.
Теперь представь — ты заметил утечку и делаешь:
git rm dag.py
echo "AWS_KEY=loaded_from_env" > dag.py
git add dag.py
git commit -m "Remove secret"
git push
Что произошло? Создан новый blob (без ключа), новый tree, новый commit. Старый blob с ключом — на диске. На GitHub. У всех, кто сделал git clone или git pull после твоего первого push.
$ git log --oneline
9f8e7d6 Remove secret # ← новый commit
deadbeef Add S3 loader DAG # ← старый commit, blob с ключом всё ещё доступен
abc1234 Initial commit
Любой может достать старый blob:
$ git show deadbeef:dag.py
# выводит файл с AWS_KEY=AKIA...
Или через git log:
$ git log -p -- dag.py
# покажет полный diff, включая строки с ключом
Git история не предназначена для удаления. Она спроектирована как append-only ledger — добавил, осталось навсегда. Это фича для аудита, но именно она превращает leak секрета в катастрофу.
Почему «forced push» не спасает (полностью)
Можно переписать историю через git filter-repo или BFG (разберём в уроке 03 этого модуля) и сделать git push --force. Это удалит blob на твоём remote. Но:
-
У коллег уже есть клон со старым blob. Когда они сделают
git pull, Git попытается merge новой истории с их локальной. Это сломается. Они должны сделатьgit fetch && git reset --hard origin/main— потерять любые локальные изменения. Илиgit cloneзаново. -
Боты, которые уже скачали ключ — он у них. Force push не отменяет факт, что ключ утёк. Это не машина времени.
-
Forks существуют. Если репо public и кто-то сделал fork — ты не управляешь его историей. Blob остаётся в форке, в кеше GitHub.
-
GitHub кеширует blob-ы в pull requests, issues, web UI. Даже после
git push --forceссылка на blobhttps://github.com/org/repo/blob/deadbeef/dag.pyможет ещё несколько часов работать.
Поэтому правило безопасности номер один: никогда не пуш секрет в Git, лучше предотвратить, чем чистить.
Как боты находят утёкшие ключи
GitHub получает порядка миллиона push-событий в минуту в пиковые часы. Каждый публичный push индексируется. Боты (как легитимные secret scanners типа TruffleHog, GitGuardian, так и злонамеренные) подписываются на GitHub Events API или просто scrap-ят:
https://api.github.com/events
Каждое событие содержит метаданные коммита, и любой может скачать diff. Боты применяют regex-патерны:
| Сервис | Префикс ключа | Regex |
|---|---|---|
| AWS Access Key | AKIA | AKIA[0-9A-Z]{16} |
| GitHub PAT | ghp_ | ghp_[A-Za-z0-9]{36} |
| Stripe Secret | sk_live_ | sk_live_[0-9a-zA-Z]{24} |
| Slack Token | xoxb- | xox[baprs]-[0-9a-zA-Z-]+ |
| OpenAI API | sk-proj- | sk-(proj-)?[A-Za-z0-9_-]{20,} |
| Snowflake | — | private key + URL pattern |
| Google API | AIza | AIza[0-9A-Za-z-_]{35} |
Время от push до первой попытки использовать ключ — по статистике GitHub Security Lab — в среднем 4 минуты. Самые быстрые боты — 30 секунд.
Бот не пытается «сделать что-то полезное» с твоим аккаунтом — он запускает максимальное количество GPU-инстансов для майнинга криптовалюты. На дорогих инстансах p3.16xlarge (200-500 в час за каждый. За ночь — десятки тысяч долларов. Аккаунт блокируется AWS только когда сработают spending alerts, обычно через 1-3 часа.
GitHub Secret Scanning встроен бесплатно в публичные репозитории. Когда GitHub видит push с AWS/Stripe/GitHub ключом, он автоматически уведомляет провайдера (AWS получит push), и провайдер обычно сразу revoke-ит ключ. Это спасает в 60-70% случаев, но не всегда успевает раньше бота. И не работает для private репозиториев на бесплатном плане.
Реальные incident reports (что произошло на самом деле)
Uber 2016 — $148M штраф
Инженер закоммитил AWS-credentials в private GitHub репозиторий. Думал — раз private, никто не увидит. Через несколько месяцев атакующий получил доступ к чужому корпоративному GitHub аккаунту (через утечку с другого сервиса) — и в его подписках был тот самый private репо Uber. Достал credentials, получил доступ к S3-бакетам с данными 57 миллионов пользователей. Uber пытался скрыть incident, заплатил атакующим 148M штраф от FTC, увольнение CISO.
Урок: private не равно безопасный. Любой коллабораtor или скомпрометированный аккаунт = leak.
Toyota 2023 — credentials к customer DB на 10 лет
Подрядчик закоммитил access credentials к Toyota cloud server в public GitHub репозиторий. Они там пролежали с декабря 2017 по сентябрь 2022. 296,000 customer records потенциально доступны. Никто не сканировал — никто не находил. Toyota уведомила клиентов через 5 лет, когда сами обнаружили.
Урок: даже если бот не пришёл сразу — кто-то может прийти через год.
Internet Archive 2024 — 31M user data leak
Внутренний GitLab Personal Access Token попал в публичный конфиг. Bot нашёл, через PAT получил доступ к private репо с production credentials, оттуда — к user database. 31 миллион записей утекли.
Урок: PAT (Personal Access Token) — это «ключ ко всему» с правами разработчика. Утёкший PAT даёт доступ ко всем репо, к которым у этого юзера был доступ.
Что такое «секрет» — конкретный список
Junior должен научиться распознавать секреты на уровне reflex. Вот полный список, что никогда не должно попадать в Git:
| Тип | Пример | Где встречается |
|---|---|---|
| Cloud credentials | AWS, GCP, Azure access keys | ~/.aws/credentials, env-vars |
| Database connection strings | postgres://user:password@host/db | database.yml, .env |
| API tokens | GitHub PAT, Slack token, Stripe key | env-vars |
| OAuth client secrets | Google OAuth, Auth0 | конфиги |
| SSH private keys | id_rsa, id_ed25519 | ~/.ssh/ |
| TLS private keys | *.key, *.pem (private) | nginx configs |
| JWT signing secrets | HMAC ключ для подписи токенов | backend configs |
| Internal hostnames | prod-db-master-internal.company.local | DAG-конфиги |
| Webhook URLs с токеном | https://hooks.slack.com/T0/B0/xxx | DAG, alert configs |
| Pepper/salt для passwords | константы для криптографии | code |
| Service account JSON | Google Cloud SA, Kubernetes SA | DAG-конфиги |
| Encryption keys | AES keys, RSA private | rare, but critical |
| Snowflake private keys | *.p8 файлы для key-pair auth | dbt-проекты |
Если ты сомневаешься, секрет ли это — спроси себя: «Если я опубликую это в Twitter, кто-то сможет использовать это против моей компании или меня?». Если хоть малейшее «да» — это секрет. В кейсе сомнений — не пуш.
DE-специфика: где часто прячутся секреты
В DE-проектах есть несколько типичных мест, куда секреты «случайно» попадают:
1. Airflow DAG-и с hardcoded connection strings
# ПЛОХО
postgres_hook = PostgresHook(
postgres_conn_id=None,
schema="public",
host="prod-db.internal",
login="admin",
password="SuperSecret123!", # ← это секрет
)
Правильный путь — через Airflow Connections (UI или Secrets backend) и PostgresHook(postgres_conn_id="prod_db").
2. dbt-профили с key-pair authentication
# profiles.yml — НЕ коммитить
my_project:
outputs:
prod:
type: snowflake
account: company.snowflakecomputing.com
user: dbt_runner
private_key_path: /home/runner/snowflake_key.p8
private_key_passphrase: "secret_passphrase" # ← секрет
profiles.yml всегда в .gitignore. Templates — да, реальные — нет.
3. Spark jars/configs с inline credentials
// ПЛОХО
spark.conf.set("fs.s3a.access.key", "AKIAIOSFODNN7EXAMPLE")
spark.conf.set("fs.s3a.secret.key", "wJalrXUt...")
Должно быть через IAM роли (EMR, EKS) или AssumeRole.
4. Jupyter ноутбуки (.ipynb)
Самое коварное место. Junior data analyst запускает conn = create_engine("postgres://...:password@..."), забывает очистить, коммитит ноутбук. JSON-структура .ipynb хранит output cells — там может быть результат print(conn) с паролем. Очисти outputs перед commit (jupyter nbconvert --clear-output) или используй nbstripout (pre-commit hook).
5. Terraform state и tfvars
*.tfvars # содержит реальные значения, в .gitignore
*.tfvars.example # template без значений, коммитим
terraform.tfstate # сам state может содержать пароли, в .gitignore
Попробуй сам
Создадим mini-repo и проиграем «leak scenario» в безопасной форме:
$ mkdir leak-demo && cd leak-demo
$ git init
$ echo "FAKE_AWS_KEY=AKIAIOSFODNN7FAKEKEY" > config.py
$ git add config.py
$ git commit -m "Add config"
# Сделаем вид что заметили утечку и "удалили"
$ echo "FAKE_AWS_KEY=loaded_from_env" > config.py
$ git commit -am "Remove key"
$ git log --oneline
# два коммита
# А теперь достанем "удалённый" секрет
$ git log -p config.py
# увидим оба варианта файла, включая FAKE ключ
Это симуляция — на твоём диске. На public GitHub — то же самое, только видят все.
Killer takeaway
Git хранит историю append-only — это его суперсила (audit trail) и его уязвимость (секрет не удаляется одним коммитом). Запуш секрет в public репо — он попадает к ботам за 30-300 секунд. AWS spend alert за ночь — десятки тысяч долларов. Toyota держала secret 5 лет — никто не сканировал; пришли — забрали всё. Junior DE должен: (1) понимать что любой commit-ит навсегда; (2) знать список «что есть секрет»; (3) использовать prevention — gitleaks pre-commit, .gitignore правильно, secrets backend в Airflow/dbt. Чистка после факта — травматична, дорога, и никогда не возвращает к состоянию «как будто не было».