Learning Platform
Глоссарий Troubleshooting
Урок 19.01 · 30 мин
Начальный
secretssecurityleakawsincident-responseblobhistory

Почему секреты в 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. Через час — ещё 18k.Кутру—18k. К утру — 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. Старый объект никуда не делся, он лежит рядом.

Что происходит при commit с секретом
dag.py с AWS-ключом внутри
git hash-objectGit хеширует содержимое файла -> blob объект
blob a1b2c3...Файл сохранён как blob, имя = SHA содержимого
tree (snapshot)tree снимок директории
commit deadbeefCommit ссылается на tree, который ссылается на blob с ключом
push origin -> blob на GitHubЗапушили в remote — теперь blob на сервере

Теперь представь — ты заметил утечку и делаешь:

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, включая строки с ключом
DANGER

Git история не предназначена для удаления. Она спроектирована как append-only ledger — добавил, осталось навсегда. Это фича для аудита, но именно она превращает leak секрета в катастрофу.


Почему «forced push» не спасает (полностью)

Можно переписать историю через git filter-repo или BFG (разберём в уроке 03 этого модуля) и сделать git push --force. Это удалит blob на твоём remote. Но:

  1. У коллег уже есть клон со старым blob. Когда они сделают git pull, Git попытается merge новой истории с их локальной. Это сломается. Они должны сделать git fetch && git reset --hard origin/main — потерять любые локальные изменения. Или git clone заново.

  2. Боты, которые уже скачали ключ — он у них. Force push не отменяет факт, что ключ утёк. Это не машина времени.

  3. Forks существуют. Если репо public и кто-то сделал fork — ты не управляешь его историей. Blob остаётся в форке, в кеше GitHub.

  4. GitHub кеширует blob-ы в pull requests, issues, web UI. Даже после git push --force ссылка на blob https://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 KeyAKIAAKIA[0-9A-Z]{16}
GitHub PATghp_ghp_[A-Za-z0-9]{36}
Stripe Secretsk_live_sk_live_[0-9a-zA-Z]{24}
Slack Tokenxoxb-xox[baprs]-[0-9a-zA-Z-]+
OpenAI APIsk-proj-sk-(proj-)?[A-Za-z0-9_-]{20,}
Snowflakeprivate key + URL pattern
Google APIAIzaAIza[0-9A-Za-z-_]{35}

Время от push до первой попытки использовать ключ — по статистике GitHub Security Lab — в среднем 4 минуты. Самые быстрые боты — 30 секунд.

Lifecycle утечки AWS-ключа
DE
GitHub
Bot scanner
AWS
git push (ключ в dag.py)Events API: новый pushGET raw blobregex match: AKIA...aws sts get-caller-identity200 OK — ключ валидензапуск 50 EC2 p3.16xlargeSpend alert $11k (через 1 час)

Бот не пытается «сделать что-то полезное» с твоим аккаунтом — он запускает максимальное количество GPU-инстансов для майнинга криптовалюты. На дорогих инстансах p3.16xlarge (24/час)это24/час) это 200-500 в час за каждый. За ночь — десятки тысяч долларов. Аккаунт блокируется AWS только когда сработают spending alerts, обычно через 1-3 часа.

WARNING

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, заплатил атакующим 100k.В2018годуисториявсплыла>100k. В 2018 году история всплыла -> 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 credentialsAWS, GCP, Azure access keys~/.aws/credentials, env-vars
Database connection stringspostgres://user:password@host/dbdatabase.yml, .env
API tokensGitHub PAT, Slack token, Stripe keyenv-vars
OAuth client secretsGoogle OAuth, Auth0конфиги
SSH private keysid_rsa, id_ed25519~/.ssh/
TLS private keys*.key, *.pem (private)nginx configs
JWT signing secretsHMAC ключ для подписи токеновbackend configs
Internal hostnamesprod-db-master-internal.company.localDAG-конфиги
Webhook URLs с токеномhttps://hooks.slack.com/T0/B0/xxxDAG, alert configs
Pepper/salt для passwordsконстанты для криптографииcode
Service account JSONGoogle Cloud SA, Kubernetes SADAG-конфиги
Encryption keysAES keys, RSA privaterare, but critical
Snowflake private keys*.p8 файлы для key-pair authdbt-проекты
TIP

Если ты сомневаешься, секрет ли это — спроси себя: «Если я опубликую это в 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. Чистка после факта — травматична, дорога, и никогда не возвращает к состоянию «как будто не было».

Управление секретами: Vault, AWS Secrets Manager, переменные среды
Проверка знанийKnowledge check
ОтветAnswer

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Junior запушил AWS-ключ в public репо. Сразу заметил, удалил файл следующим коммитом и сделал push. Почему утечка НЕ устранена?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4