Make. Build. Break. Reflect.
1.36K subscribers
160 photos
4 videos
1 file
172 links
Полезные советы, всратые истории, странные шутки и заметки на полях от @kruchkov_alexandr
Download Telegram
#security #git #devops

Часть 1 из 2.

Рано или поздно все инженеры начинают расти профессионально.
Мы узнаем лучшие практики, оптимизируем существующий стек или инфру, внедряем новые технологии.
В какой-то момент мы даже следуем бест практис по security.
Добавление сканеров, проверки на бесящие всех CVE, пайплайны, pre-commit hooks и selinux/securitycontext.
Перечислять всё не буду, это невероятно огромная область.

Но интернет git всё помнит.
Git is the source of truth, как говорит мой коллега.
Это сейчас мы внедрили все практики и больше нет утечек паролей и токенов в гит, а храним всё в vault/aws sm/azure sa.
А что же в истории git?
В гите у нас могут остаться:
- токены
- private keys
- пароли

Давайте проверим и почистим за собой в репозитории.

Экспериментировать мы будем на публичных репах, чтобы вы сперва потренировались, ДО того, как будете ломать свои реальные репозитории🤣.

Предварительная подготовка
- установить gitleaks https://github.com/gitleaks/gitleaks
Эта штука позволяет найти секреты во всех ветках всех файлах всей гит истории.
Описание есть в ридми.
- устанавливаем java
- устанавливаем bfg https://rtyley.github.io/bfg-repo-cleaner/
я просто сделал так
wget https://repo1.maven.org/maven2/com/madgag/bfg/1.14.0/bfg-1.14.0.jar

Эта штука помогает удалить лишнее найденное из гита.
- устанавливаем jq https://github.com/jqlang/jq
эта штука нам нужна для разового парсинга
- ну и git

Приступаем к поиску и очистке.
- форкаем себе рандом популярный проект, а почему бы и да
Ищём тут https://github.com/trending?since=monthly
есть какой-то microsoft/qlib
- клонируем себе форкнутый проект (у вас путь другой будет само собой)
git clone git@github.com:kruchkov-alexandr/qlib.git

- запускаем сканер утечки чувствительных данных не меняя директорию
>gitleaks detect --source qlib --log-opts="--all" --report-path findings.json


│╲
│ ○
○ ░
░ gitleaks

12:23PM INF 1790 commits scanned.
12:23PM INF scan completed in 696ms
12:23PM WRN leaks found: 1

Результатом будет джейсон файл с детальным описанием кто, когда, что и за тип секретов.
Можете открыть и посмотреть, что там утек 1 токен от разработчика(он может быть и реальным и фейком).
- Для удобства очищаем его от всего лишнего
cat findings.json | jq -r '.[].Secret' > passwords.txt

Для чего мы это делаем?
Чтобы сгенерить и проверить содержимое файла passwords.txt и ЕСЛИ в нём есть то, чего НЕ НАДО удалять из гита и оно бы осталось в гите, надо убрать из этого файла.
В этом файле то, что будет в последствии удалено из гита.
- заходим в гит репозиторий
cd qlib

- запускам bfg
>java -jar ../bfg-1.14.0.jar --replace-text ../passwords.txt

Видим длинную простыню текста, типа
Commit Tree-Dirt History
------------------------
Earliest Latest
| |
.........................................................DDm
D = dirty commits (file tree fixed)
m = modified commits (commit message or parents changed)
. = clean commits (no changes to file tree)

Before After
-------------------------------------------
First modified commit | 21f0b394 | b3d34764
Last dirty commit | 63021018 | abf5a1bf
Changed files
-------------
Filename Before & After
---------------------------------------------------
data.py | 8de32f3f ⇒ 82298fe3, f6bd7809 ⇒ 8bd16038
In total, 159 object ids were changed. Full details are logged here:
/home/***/GIT/INTERNET/qlib.bfg-report/2025-06-12/12-24-52

- ещё раз смотрим чего же мы там меняем(если есть хоть любые сомнения, прерывайте процесс! операция без бекапов не поддаётся восстановлению!!!)
cat /home/***/GIT/INTERNET/qlib.bfg-report/202*-0*-**/12-24-52/changed-files.txt
8de32f3f6c0373ca1b41fe6d3f4dbd4c23685d71 82298fe3811f0c36aa982a1ff23ee92289cc9346 data.py
f6bd780905649d8d004c13bd98a91044958b9573 8bd160386147b2007172e618dbe049b02cd4bcc3 data.py
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6😨2
#security #git #devops

Часть 2 из 2.

- теперь мы выдыхаем, принимаем решение и чистим
git reflog expire --expire=now --all && git gc --prune=now --aggressive

Это мы почистили ЛОКАЛЬНО гит, пока на ремоут сервер не заливали.
- проверяем, что локально всё чисто
cd ..
>gitleaks detect --source qlib --log-opts="--all" --report-path findings.json


│╲
│ ○
○ ░
░ gitleaks

12:29PM INF 1790 commits scanned.
12:29PM INF scan completed in 722ms
12:29PM INF no leaks found

То есть проверка показала, что все токены и пароли были удалены.
- крестимся и пушим в ремоут
cd qlib
git push origin --force

Всё, история в нашем форке почищена.
Можно ещё раз проверить всё
- удаляем локальный репозиторий
cd ..
sudo rm -r qlib

- клонируем с ремоута
git clone git@github.com:kruchkov-alexandr/qlib.git

- проверяем на утечки
gitleaks detect --source qlib --log-opts="--all" --report-path findings.json


│╲
│ ○
○ ░
░ gitleaks

12:33PM INF 1790 commits scanned.
12:33PM INF scan completed in 731ms
12:33PM INF no leaks found

- радуемся, что даже в истории гита нет утечек

Инструкция написана максимально подробно, чтобы те, кто подобного раньше никогда не делали, не допустили бы глупых ошибок. Сперва потренируйтесь на паблик репозиториях, отточите все действия до полного понимания и только потом надо чистить реальные репозитории на работе.
На реальных боевых репозиториях в идеале сделать бекап, прям gzip файлом, до всех изменений, если опыта мало и только потом приступать.

Инструкция длинная, кажется сложно, но на самом деле это лишь 5 команд и всё.
Не бойтесь. Вы - инженер.
👍17
#gitlab #github #secrets #security

Спустили с небес на землю.

Недавно я решил, что я мамкин хакер(нет), даже отправил несколько репортов, от medium до high severity.
Вот буквально сегодня в ночи получил ответ. Вкратце: "спасибо, мы знаем, тикет закрыт, пробуйте ещё".

Основная суть всех моих репортов - "пользователь с привилегированным(не админ)/с ограниченным доступом(не админ)/со специальными не частыми у всех условиями, может получить доступ к секретам".

Расставлю сразу свою позицию - я никого ни в чём не обвиняю, мне позиция ясна, ответы понравились, я со всем согласен. Никаких интриг, просто общение между инженером и представителями компаний.
История в 3 частях.

Часть первая, GitLab.

Однажды мне надо было достать значения секрета, помеченного Masked and Hidden*.
У меня были права reporter в этом репозитории.
Базовые вещи echo $secret не давали результата, там было [MASKED] в аутпуте.
Я просто сделал
  script:
- echo "$password" | base64 > /tmp/password.txt
- cat /tmp/password.txt

скопировал значение и локально выполнил команду
echo Wml4TjRUbVdpRDAxbjdoCg== | base64 -d
ZixN4TmWiD01n7h (естесственно это пароль для статьи, вы чо, думали я реальный напишу?)

Сам себя поздравляю - я могу прочитать значение секрета, помеченного Masked and Hidden в 2 команды.
Отмечаю, что это работает для приватных моих репозиториев, а так же для всех проектов/репозиториях, куда меня добавили - то есть во всех для организации. Проверено на SelfHosted/Cloud.
То есть прямо сейчас ВСЕ ваши коллеги не девопсы/админы с ограниченными правами могут прочитать ВСЕ секреты в тех проектах/репозиториях, где у них есть ограниченные доступы. Даже к OpenAI и тратить токены компании или PAT к другими репозиториям с привилегированными правами(например CICD процессы GitOps между репозиториями)

На данный репорт я получаю ответ от https://hackerone.com/
According to current program policy, this reported finding is considered out-of-scope;
CI/CD Variable Disclosure - as it stands, we currently see our guidance to use external secret storage as sufficient for addressing reports that rely on disclosing Masked CI/CD variables to prove impact.
It is a known
security rick, based on the official document https://docs.gitlab.com/ci/variables/#cicd-variable-security
As a result, I'm closing this report as Informative. Your effort is nonetheless appreciated, and we wish that you'll continue to research and submit any future
security issues you find.

Идем по линку и там
Masking a CI/CD variable is not a guaranteed way to prevent malicious users from accessing variable values. To ensure security of sensitive information, consider using external secrets and file type variables to prevent commands such as env/printenv from printing secret variables.
Gitlab.com сотрудничает с https://hackerone.com/ и значит их ответ = ответ GitLab.com.

Записываем себе - гитлаб не гарантирует ничего по сохранности секретов.

* В целом данная уязвимость не новая - опытные инженеры её и так знали и пользуются втихую.
Возможно даже на других CICD
😉
Please open Telegram to view this post
VIEW IN TELEGRAM
1👍8🔥42😨1
#gitlab #github #secrets #security

История вторая. GitHub.

Однажды мне надо было в моём личном репозитории кое-чего вывести в лог текстом. Ну буквально echo "hello Barbie!"
В логах же я увидел "hello ***bie!"
Не сразу понял, пришлось сделать несколько тестов. Оказалось, что GirtHub при совпадении ЛЮБОЙ части ЛЮБОГО доступного(это важно) секрета маскирует символы звёздочкой.
То есть если у нас есть какой-то секрет, внутри которого есть "Bar" то при попытке напечатать Barbie GitHub будет маскировать Bar.
Любопытство вязло вверх и я написал простой воркфлоу.
jobs:
...
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Run print_secret script
run: python scripts/print_secret.py
env:
SUPERSECRET: ${{ secrets.AWS_ACCESS_KEY_ID }}
- name: Upload found secret artifact
uses: actions/upload-artifact@v4
with:
name: found-secret
path: found_secret.txt
if-no-files-found: error

Сам скрипт простой, как три копейки.
https://gist.github.com/kruchkov-alexandr/241bb761b183800e2220c10878089214
Что делает скрипт? Берёт на вход название секрета, печатает символы из чарсета, ожидает * при совпадении часть секрета, идёт дальше.
В логе это выглядит буквально
Run python scripts/print_secret.py
*???????????????????
**??????????????????
***?????????????????
****????????????????
*****???????????????
******??????????????
*******?????????????
********????????????
*********???????????
**********??????????
***********?????????
************????????
*************???????
**************??????
***************?????
****************????
*****************???
******************??
*******************?
********************

За 0.0001мс мы "расшифровываем" любой секрет. Просто заходим в артефакты, качаем файл и забираем значение секрета). Алгоритм простой, как три копейки - гитхаб нам сам показывает, какие и где символы он маскирует.
А количество звёздочек - длина секрета)

На самом деле у GitHub работает и через base64, как в первой истории, но со скриптом мне было просто интересно повозиться 😀
Прикольная же идея!


Я зарепортил и это, отметив, что это работает на личных репозиториях и в репозиториях организации с сильно ограниченным доступом. То есть любой ваш коллега может расшифровать любой доступный ему секрет.

Получаю ответ:
Thanks for your submission! As noted at https://developer.github.com/v3/actions/secrets/#create-or-update-a-secret-for-a-repository ... "Anyone with write access to the repository can use this endpoint". It is intentional that write-access to a repository grants both read and write access to secrets. Naturally, write-access users can read all secrets. Likewise, write-access users are intended to be able to create/edit secrets because they should be able to edit/create workflows and would need to be able to configure secrets to do so. GitHub is aware that there may be some discrepancies between the API and what is apparently accessible from the web UI. GitHub is tracking several issues related to this feature and Actions generally. There may be modifications or more strict enforcement in the future, but we have nothing to announce at the moment.

Записываем себе - гитхаб не гарантирует ничего по сохранности секретов(когда мы же сами даём права в репозиторию*).
Никакой иронии - мне понятна позиция в целом.
Please open Telegram to view this post
VIEW IN TELEGRAM
1🔥76👍1
#gitlab #github #secrets #security

История третья. GitHub.

Третья история это не баг репорт, а собственное исследование в целях повышения опыта и экспертизы в вопросах безопасности.
НЕ ПОВТОРЯТЬ, ЭТО ПРОТИВОЗАКОННО, НЕЛЬЗЯ ПОВТОРЯТЬ
История лишь для ознакомления, повторять абсолютно запрещено, заметка исключительно в целях сокрытия СВОИХ проектов от глупых действий.

Все эти игрища были лишь в организации, где изначально у меня были хоть какие-то, но минимальные права.
Будет ли это работать при публичных репозиториях?
Как мне проверить это и защитить свои публичные репозитории от хакеров?

Немного теории. Выжимка для самых маленьких - вы ведь не хотите читать всю документацию GitHub.
1) Когда я запускаю у себя(личный репо, репо организации) воркфлоу - это МОЙ секьюрити контекст.
Мой securitycontext = мне доступны все секреты.
Если я сделаю форк от любого публичного репозитория, затем "что-то подменяю" в коде и отправлю pull request - это Fork а значит недоступен SecurityContext и секреты. То есть при любом форке - секреты недоступны, только если автор репозитория специально руками не сделает это(по умолчанию выключено).
Однако есть директива pull_request_target
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target
Если в каком-то репозитории есть такая директива = при форке и пулл реквесте мы получаем доступ к SecurityContext.
2)GitHub не дурак и отслеживает любые изменения файлов воркфлоу.
Даже если у нас есть pull_request_target, но мы взяли и подменили workflow файл, добавив туда нечто типа run: python scripts/print_secret.py - этих изменений не будет в воркфлоу - есть дифф между оригиналом

Это всё теория, а что насчёт практики.
Чтобы "взломать все интернеты" нам надо соблюдение таких условий
- чтобы была опасная директива pull_request_target - "pull_request_target"
- чтобы директория была - path:.github/workflows/
- чтобы мы могли как-то где-то что-то запустить, НЕ изменяя файл воркфлоу, но заинжектить свой вредоносный код, а точнее защититься от такой ситуации в своих публичных репозиториях.

Как мы можем заинжектить, не меняя ничего в воркфлоу?
Нам надо найти куски кода, где в run есть:
- нечто типа python script.py (подменяем сам скрипт и он выполняется, извлекая секреты)
- pip install setup.py инжектим pre-install и выполнение нашего кода, получаем доступ к секретам)
-npm install/command (ну тут понятно - лепи что хочешь)
- всё подобное, что выше, но для других языков/утилит

В двух словах поиск будет типа
org:*****  path:.github/workflows/ "pull_request_target" "${{ secrets." "install"


В общем и целом это помогает злоумышленнику получить доступ к вашим секретам через форк и пулл реквест.
Однако когда я начал писать об этом репорт, я уже решил почитать документацию и везде написано, что такие действия опасные.
То есть тот, кто включил у себя pull_request_target - сам себе злобный буратино.
Та же ситуация с запуском скриптов из всех примеров выше.
Вероятно от https://hackerone.com/ я получу такой же ответ.
Репорт не стал писать. И слишком много условий и в документации есть миллионы ворнингов.

Итоги.
Первые две истории - это реальные репорты с реальным ответом. Все семь репортов мне закрыли с
Informative (Closed). Само собой без каких-либо выплат. Остальные пять репортов я тут публиковать не буду - их них ещё можно протянуть дальше кейсы и попробовать дальше эволюционировать в реальную уязвимость (они уровнем выше, чем эти истории на мой взгляд).
Третья же история это самостоятельное погружение "а как это работает".

Для себя же стоит запомнить:
- никогда не использовать в своих публичных репозиториях pull_request_target - при форке идёт передача SecurityContext и все секреты отдаются публично.
- нникогда не использовать в своих публичных репозиториях запуск сторонник скриптов типа python script.py
- не рассчитывать на сохранность всех секретов GitLab
- я ещё поиграю в хакера и попробую заработать на других потенциальных уязвимостях

И да - я опять поленился писать лонгрид в один пост, сорян.
1👍174🔥2😴1
#devops #security #nginx #tls #privacy

Тихо и незаметно (для меня) NGINX 1.29.4 официально добавил поддержку Encrypted Client Hello (ECH).
Для инженеров, которые следят за приватностью трафика, это важная новость.

Что такое ECH и почему это важно.
ECH - расширение TLS 1.3, которое шифрует полностью весь ClientHello, включая Server Name Indication (SNI).
Раньше SNI передавался открытым текстом: любой наблюдатель на сети видел, к какому домену вы коннектитесь, даже если содержимое соединения зашифровано. ECH устраняет этот пробел.


ECH использует два ClientHello:
- ClientHelloOuter - видимый, с "dummy" SNI
- ClientHelloInner - зашифрованный, с реальным SNI и другими параметрами

Шифрование происходит через HPKE (Hybrid Public Key Encryption).
Если сервер не может расшифровать inner, он завершает handshake с outer и сообщает клиенту актуальный ключ для повторной попытки.

Важные требования:
- DNS-over-HTTPS обязателен - без него публикация ключа в DNS небезопасна
- Частая ротация - рекомендуется ротировать ключи каждый час (как Cloudflare)
- HTTPS-записи -должны быть корректно настроены с параметром ech=

Для инженера это значит:
- провайдер или DPI-бокс не увидит, куда идёт трафик из вашей инфраструктуры
- метаданные перестают быть источником утечек
- комплаенс с требованиями privacy-by-design становится проще

Что уже работает:

- NGINX принимает директиву ssl_ech_file /path/to/ech-keys.pem
В контексте http/server - она указывает PEM‑файл с ECHConfig и ключами, необходимыми для включения TLS 1.3 ECH в shared‑режиме.
​- поддержка HTTP и Stream модулей
​- ротация ключей через несколько файлов и nginx -s reload - директива ssl_ech_file может указываться несколько раз для разных файлов, что позволяет операционно ротировать ECH‑ключи.
​- мониторинг через переменные $ssl_ech_status и $ssl_ech_outer_server_name - они дают статус обработки ECH

Чего мне пока не хватает для продакшена:
- стабильная версия OpenSSL с ECH
ECH поддерживается только в экспериментальном бранче feature/ech OpenSSL.
Хотя OpenSSL 3.5 LTS вышел в апреле 2025, ECH не вошел в основной релиз и требует сборки из специального бранча.
- отсутствие интеграции с инфраструктурой
Let’s Encrypt и другие CA пока не выдают ECH‑ключи через ACME‑протокол - механизм не стандартизирован. Обычные TLS‑сертификаты работают с ECH без изменений, но получить и автоматически обновить ECHConfig‑ключи придётся вручную.
Публикация ключей в DNS (через HTTPS/SVCB‑записи) требует ручного управления или кастомных скриптов.
- ограниченная поддержка клиентами
Хотя Chrome и Firefox поддерживают ECH, некоторые корпоративные библиотеки еще не обновлены.

Как же это плюсы для для девопс-команд?
1) Privacy-focused сервисы🔥🔥🔥
server {
listen 443 ssl http2;
ssl_ech_file /etc/nginx/ech-keys.pem;
# Теперь SNI не виден провайдеру!
}


2) Мультитенант платформы
- скрываете имена клиентских доменов от внешних наблюдателей
- полезно при shared infrastructure с чувствительными данными

3) Корпоративные VPN-шлюзы 🔥🔥🔥
- скрываете конечные точки внутри корпоративной сети
- провайдер видит только коннект к VPN-шлюзу, не к конкретным сервисам

Что делал я для домашней лаборатории:
git clone -b feature/ech https://github.com/openssl/openssl.git
./config --prefix=/opt/openssl-ech && make && sudo make install

./configure --with-openssl=/opt/openssl-ech \
--with-http_ssl_module \
--with-stream_ssl_module
make && sudo make install

/opt/openssl-ech/bin/openssl ech_keygen -out /etc/nginx/ech-keys.pem

+ крайне важно не забыть добавить DNS публикацию ECH-ключей.
Ключи ECH не работают изолированно - их обязательно нужно опубликовать в DNS через HTTPS-записи (SVCB/HTTPS-тип).
Без этого клиенты не узнают, что сервер поддерживает ECH.
_443._https.example.com. IN HTTPS 1 . ech=AEj+DQBE..
314🆒3👍1🤯1
#guardduty #aws #eks #security #troubleshooting

Guardduty, фишинг. Часть 1 из 2.

Никого не трогаю. Прилетает алёрт от AWS GuardDuty.
Backdoor:EC2/C&CActivity.B!DNS

Продакшн нода в EKS делает DNS-запрос к x****.cn - по базам GuardDuty это известный фишинговый/DGA домен.
Серьёзность - High. В слаке шум, все смотрят на меня.
Блин, почему в мою смену секьюрити инциденты 😬

Окей. Поехали.
Cмотрим что за нода
aws ec2 describe-instances \
--instance-ids i-0xxxxxxxxxxxxxxxxx \
--region us-east-1 \
--query 'Reservations[0].Instances[0].{IP:PrivateIpAddress,State:State.Name,LaunchTime:LaunchTime}'

Нода уже terminated. Karpenter убил её раньше, чем я добрался до расследования.
Живого форензика (digital forensics) нет.
Прекрасное начало.
kubectl get nodes -o wide
# ноды нет

Ладно, живыми данными не поработать, идём в логи.

Cмотрим что вообще крутилось на ноде
Хорошо, что у нас включено всё логирование control plane EKS. Идём в CloudWatch, смотрим логи шедулера - кто и когда был назначен на эту ноду.
aws logs start-query \
--log-group-name /aws/eks/my-cluster/cluster \
--start-time $(date -v-83H +%s) \
--end-time $(date +%s) \
--query-string 'fields @timestamp, @message | filter @logStream like "scheduler" | filter @message like "ip-10-0-25-132" | sort @timestamp asc | limit 50' \
--region us-east-1

Первая попытка падает с MalformedQueryException. Оказывается, ретеншн у лог группы EKS - 84 часа. Нода жила 4 дня. Часть ранних данных уже недоступна. Запоминаем это на потом.

За те данные, что есть, видим волну деплоя примерно за час до алёрта: новые ReplicaSet хеши, несколько сервисов прокатились роллинг апдейтом. Подозрительно по тайминингу. Ухожу копать туда.

Cмотрим образы, которые пушились незадолго до инцидента
aws ecr describe-images \
--repository-name my-ecr-repo \
--region us-east-1 \
--query 'sort_by(imageDetails, &imagePushedAt)[-10:].[imagePushedAt,imageTags[0],imageDigest]' \
--output table

Вижу два образа, которые запушились за ~30-60 минут до алёрта GuardDuty. Первая гипотеза: кто-то накатил заражённый образ.

Лезу в git, смотрю коммиты.
Один коммит - это Dependabot, обновил grpc. Проверяю go.sum - стандартные пакеты, нормальные хеши. Второй коммит - двухфайловый вайтспейс-фикс в SQL query builder, strings.TrimSpace в одном файле.

Всё это вообще не то. Red herring, мать ее.

Исключаем DaemonSet
kubectl get daemonsets -A

DaemonSet крутится на всех нодах кластера - их там под 44 штуки. Если бы DaemonSet был заражён, GuardDuty бы полыхал по всем нодам разом. Сработало только на одну. Значит, не DaemonSet.

Базовый образ Dockerfile смотрю - официальный golang bookworm, мультистейдж, всё чисто.

Смотрю дополнительные файндинги GuardDuty
aws guardduty list-findings \
--detector-id $(aws guardduty list-detectors --query 'DetectorIds[0]' --output text) \
--finding-criteria '{"Criterion":{"type":{"Eq":["Backdoor:EC2/C&CActivity.B!DNS","Trojan:EC2/DNSDataExfiltration","DefenseEvasion:EC2/UnusualDoHActivity"]}}}' \
--region us-east-1

Нашлось ещё пара файндингов - не игнорируем, но пока продолжаем копать основное.

Наконец-то заглядываю в логи приложения (когда же я, бл, научусь это смотреть первым 🤡)
Открываю OpenSearch, ищу по домену x********.cn , в индексе EKS-логов.
8 записей. Все - из одного контейнера. Смотрю на таймстемпы:
12:51:19 - PaymentWebhookProcessor: starting to parse note
12:51:19 - PaymentWebhookProcessor: no images found in note
...
13:21:38 - PaymentWebhookProcessor: starting to parse note <- ВОООТ ОНО
13:21:38 - PaymentWebhookProc...

13:21:38 UTC - точь-в-точь время алёрта GuardDuty.

Попался!
Please open Telegram to view this post
VIEW IN TELEGRAM
😁3👍1
#guardduty #aws #eks #security #troubleshooting

Guardduty, фишинг. Часть 2 из 2.


Что произошло на самом деле
Схема простая до слёз:
Пользователь отправил сообщение с URL -> x*****.cn
|
Сервис обработки контента внутри кубера получил вебхук(?)
|
Функция(?) PaymentWebhookProcessor внутри POD-а попыталась резолвить домен
(логика: ищет превью/изображения по URL-ам в тексте)
|
DNS-запрос к x*****.cn с EKS ноды, на которой запущен под
|
GuardDuty: ALARM


Никакой малвари. Нода не скомпрометирована. Credentials не угнаны.
Просто сервис резолвит любой домен из пользовательского контента - а юзер прислал фишинговую ссылку.
Фишинг не удался (дополнительно логи смотрел).

Несколько часов расследования, CloudTrail, CloudWatch, ECR history, git blame - ответ лежал в логах приложения и нашёлся бы за 15 минут, если бы я туда полез первым делом.

Что реально плохо и надо починить
Само расследование закончилось словами "малвари нет", но по ходу вскрылись настоящие проблемы 🚬.

- SSRF
PaymentWebhookProcessor резолвит произвольные URL из пользовательского контента без какой-либо валидации.
Это означает, что любой клиент может прислать:
http://169.254.169.254/latest/meta-data/
И сервис спокойно попробует его резолвнуть - прямо с production ноды, у которой есть IAM-роль со повышенными правами (это вообще, бл, отдельный разговор какого рожна нода имела доступ к некоторым AWS ресурсам).

Фикс очевидный - блокировать приватные диапазоны перед любым исходящим запросом:
169.254.0.0/16  (AWS metadata endpoint)
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
127.0.0.0/8

- GuardDuty будет шуметь вечно
Пока сервис зачем-то резолвит пользовательские URL без фильтрации - каждый раз, когда кто-то пришлёт фишинговую ссылку, GuardDuty будет стрелять алёртом. Это шум, который убивает доверие к алёртингу.

Решение: AWS Route 53 Resolver DNS Firewall с managed threat lists.
- AWSManagedDomainsMalwareDomainList
- AWSManagedDomainsAggregateThreatList
Известные вредоносные домены блокируются до DNS-резолва автоматически, без ручных списков.
https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall-managed-domain-lists.html

- URL-резолвинг в основном процессе
Длиннее задача, но правильная: вынести резолвинг URL в изолированный асинхронный воркер с ограниченным egress.
Без доступа во внутреннюю сеть. Без IAM-роли с правами на всё.

Выводы и итоги:
- в логи приложения надо идти первым. Не после нескольких часов инфра-форензики.☔️
- ретеншн CloudWatch-логов для EKS control plane 84 часа - маловато. Нода жила 4 дня, ранние события уже недоступны. Надо поднять. Не критикал, но надо бы.
- сервисы, которые резолвят URL из пользовательского контента, будут встречать фишинг. Это не вопрос если, это вопрос когда. Надо документировать и обрабатывать проактивно, а не разбираться в панике с High severity алёртом.
- SSRF - это не гипотетическая угроза, это прямая дыра в production при текущей архитектуре.
- кто дает права EKS нодам на доступ к AWS ресурсам, то негодяй, нельзя так
- с одной стороны я переживал тогда, что потратил много времени на расследование, копая изначально не туда, с другой стороны рад, ведь без полной картины происходящего я не смог бы понять были ли скомпроментированы креды или нет, был ли хак или нет, а так вроде и пополнил знания
- используйте сервисы AWS, многие из них хорошо помогают в траблшутинге, пусть и стоят денег

False positive закрыт, но работы добавилось.

- - -
В последствии все косяки были закрыты, все Security/AWS бест практисы внедрены.
Все потенциальные известные "дыры" закрыты, PII защищены, даже инвесторы спали спокойно после детальнейшнего постмортема.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17
#aws #eks #kubernetes #bottlerocket #troubleshooting #security #linux #CVE

Сорри за частые посты сегодня☔️

Пока я был в отпуске и спал, снова сломали все интернеты.

В мае 2026 вышла пара свежих уязвимостей ядра linux в ос
- первая: CVE-2026-31431, она же Copy Fail
https://copy.fail
- вторая: Dirty Frag, смежная к первой
https://github.com/V4bel/dirtyfrag

Обе уязвимости это локальное повышение привилегий (LPE).
То есть если кто-то запустил код на вашей ноде, он может стать рутом.
В контексте Kubernetes это означает выход из контейнера на хост.

Первым делом пошёл смотреть, а вообще мы уязвимы или нет.
Способ 1:
kubectl debug node/<имя_ноды> -it --image=busybox -- sh

Внутри:
chroot /host modprobe algif_aead


Если в ответ получаете:
modprobe: ERROR: could not insert 'algif_aead': Operation not permitted

Вы не уязвимы на практике, даже если версия AMI старая.
Если модуль загрузился без ошибок, уязвимы, читайте дальше.

Способ 2: через SSM напрямую на ноду (только для Bottlerocket)
Это нативный способ для Bottlerocket.
Я писал об этом не раз, искать по тегу #bottlerocket

Подключаемся к ноде и проверяем модули:
cat /proc/modules | grep -E 'algif_aead|esp4|esp6|rxrpc'

Если пусто, модули не загружены.
Пробуем загрузить принудительно:
modprobe algif_aead

Если Operation not permitted, не уязвимы.

Почему у нас не сработало, хотя версия ядра старая
У нас EKS с Bottlerocket AMI.
Bottlerocket по умолчанию запускается с
kernel.lockdown = integrity.

В этом режиме загрузка не подписанных модулей через modprobe из пространства пользователя запрещена.
То есть Copy Fail + Dirty Frag требуют загрузить модуль algif_aead (или esp4/esp6/rxrpc как обход), но lockdown просто не даёт это сделать.

Это не значит, что можно расслабиться.
Это значит, что есть один слой защиты.
Defence-in-depth требует добавить второй явный слой и всё равно обновить ядро.

Что делать в любом случае: явно блокируем модули
Независимо от того уязвимы вы или нет, правильнее явно запретить загрузку этих модулей.
Один lockdown это хорошо, но явный запрет лучше.

Bottlerocket (Terraform, launch template + user data):
Добавляем в bottlerocket.tpl:
[settings.kernel]
lockdown = "integrity"

[settings.kernel.modules.algif_aead]
allowed = false

[settings.kernel.modules.esp4]
allowed = false

[settings.kernel.modules.esp6]
allowed = false

[settings.kernel.modules.rxrpc]
allowed = false

Это работает на уровне Bottlerocket API.
Он сам создаст нужные записи в /etc/modprobe.d/ при старте ноды.

Если у вас Karpenter, то добавляете в NodeClass или EC2NodeClass в секцию userData аналогичный блок для вашего distro.
Если у вас самописные Terraform-модули для нод-групп, то смотрите где у вас формируется user_data для launch template.
Добавляете туда блок выше.
Паттерн один и тот же, разница только в синтаксисе шаблона.

Как проверить что всё применилось
Для Bottlerocket есть нативный способ: через apiclient прямо с ноды, без debug подов и без sheltie.
Работает через SSM send-command из любого места где есть aws cli.

Узнаём instance ID:
kubectl get node <имя_ноды> -o jsonpath='{.spec.providerID}' | sed 's|.*\/||'

Запускаем команду через SSM:
aws ssm send-command \
--instance-ids <instance-id> \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["apiclient get settings.kernel"]' \
--region us-east-1


Смотрим результат:
aws ssm get-command-invocation \
--command-id <command-id> \
--instance-id <instance-id> \
--region us-east-1 \
--query 'StandardOutputContent' \
--output text

Если всё применилось, увидите:
{
"settings": {
"kernel": {
"lockdown": "integrity",
"modules": {
"algif_aead": { "allowed": false },
"esp4": { "allowed": false },
"esp6": { "allowed": false },
"rxrpc": { "allowed": false }
}
}
}
}


Если патч не применён, только lockdown, без modules:
{
"settings": {
"kernel": {
"lockdown": "integrity"
}
}
}


Это показывает конфиг именно так, как его видит сама OS.
Лучше чем смотреть файлы руками.
Можно спать дальше 🛌
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11🔥21
#devops #linux #security

Только-только отгремели два LPE
- https://copy.fail/
- https://github.com/V4bel/dirtyfrag

Не успели донести ещё до продакшна изменения-фиксы, так уже третья проблема прилетела.
- https://github.com/v12-security/pocs/tree/main/fragnesia

Какой ад, если честно 🤦‍♂️🤦‍♂️🤦‍♂️

Чот всё как-то проклято с этими LPE.
Ведь точно не последняя будет с такой-то волной интереса и доступностью AI агентов и ассистентов.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
#security #docker #devops и немного #всратость

Заметка носит скорее исследовательский и развлекательный характер.
Никакого поиска правды, настаивании на этом мнении или призыва к действию.


Однажды приходит алерт от инфобеза: критическая уязвимость, CVE-666lupa666pupa в zlib, надо фиксить.

Смотрю: zlib - это какая-то неизвестная мне хрень.
Возможно и инфобезе, может и никому.
Дебиан букворм держит zlib1g 1.2.13, в которой этот CVE есть.
Триви его видит, алерт красный, белки-истерички кричат и бегают по кругу.

Говорю инфобезу: это нас не касается, камон.
Во-первых, статус в самом триви - will not fix.
Во-вторых, есть флаг --ignore-unfixed, который именно для этого и придуман.
Мы в тот момент работали без этого флага - по какой-то причине, уже не помню.
Поэтому жалобка и прилетела. Поставили флаг + игнор файл для другой неэксплуатируемой штуки и вроде отстали от нас.
В-третьих, сканер это опционально у нас, ну чо ты, какие блокеры, иди чай попей.

Пока я пытался всё это объяснить, у меня в голове крутился вопрос:
а насколько вообще можно доверять тому, что триви находит или не находит?
Решил потом проверить.

- - -
Тест первый.
Пересобрал образ, вручную скомпилировал из сорсов прямо в имадж:
RUN apt-get update && apt-get install -y build-essential wget \
&& wget https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz \
&& tar -xf zlib-1.3.1.tar.gz \
&& cd zlib-1.3.1 \
&& ./configure \
&& make \
&& make install \
&& ldconfig \
&& cd .. \
&& rm -rf zlib-1.3.1 zlib-1.3.1.tar.gz \
&& apt-get purge -y build-essential wget \
&& apt-get autoremove -y

Проверяю внутри контейнера - 1.3.1 там:
$ ls -la /usr/local/lib/libz*
lrwxrwxrwx /usr/local/lib/libz.so -> libz.so.1.3.1
lrwxrwxrwx /usr/local/lib/libz.so.1 -> libz.so.1.3.1
-rwxr-xr-x /usr/local/lib/libz.so.1.3.1

$ ldconfig -p | grep libz
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 <- dpkg-пакет, 1.2.13
libz.so.1 => /usr/local/lib/libz.so.1 <- скомпилированная 1.3.1


Запускаю триви - снова цве критикал 😬

Вероятно триви читает /var/lib/dpkg/status - базу пакетного менеджера.
Что реально лежит в /usr/local/lib его не интересует.
Скомпилированная версия для него невидима. Безопасность)

Наверняка это не баг - это архитектурное решение, бинарный анализ каждой библиотеки был бы на порядки дороже. Но это означает, что сканер сообщает о CVE на основе базы пакетного менеджера, а не того, что реально резолвит линкер в рантайме - а это, вообще-то, отдельный вопрос, который одним ldconfig -p не закрыть. Ну не глупость ли, а?
Сканер говорит "уязвимость есть" - а есть ли она реально в работающем процессе, это уже совсем другая история, и я её тут не проверял.

Тест второй.
Раз уж проверяем, проверим до конца.
Берём образ с реальной уязвимостью - log4shell
FROM alpine:3.18
WORKDIR /app
RUN apk add --no-cache openjdk8-jre-base wget
RUN wget https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.14.1/log4j-core-2.14.1.jar && \
wget https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-api/2.14.1/log4j-api-2.14.1.jar
CMD ["/usr/bin/java", "-version"]

Триви весело находит три CVE:
log4shell, CVE-biba, CVE-boba. Всё верно.

Теперь берём тот же JAR с уязвимым байткодом и просто меняем строку версии в метаданных внутри архива:
RUN mkdir temp && \
unzip log4j-core-2.14.1.jar -d temp && \
sed -i 's/version=2.14.1/version=2.17.1/g' \
temp/META-INF/maven/org.apache.logging.log4j/log4j-core/pom.properties && \
cd temp && \
zip -r ../mylog4j-core.jar . && \
cd .. && \
rm -rf temp log4j-core-2.14.1.jar
RUN mv log4j-api-2.14.1.jar mylog4j-api.jar

Байткод не тронут, язвимый код на месте.
Только pom.properties теперь говорит версию 2.17.1.
trivy image test:latest --severity HIGH,CRITICAL
Total: 0 (HIGH: 0, CRITICAL: 0)

Триви для джавы читает мета pom.properties внутри JAR.
Не байткод, не хэши классов - метаданные, так что поменял строку и тут же сканер доволен 😀.
Сканер говорит "всё чисто" - а уязвимость есть 🤣.

Тест третий.
Проверим ещё - го бинари.
Триви умеет читать зависимости прямо из скомпилированного го бинаря. В каком-то файле есть секция .go.buildinfo, куда компилятор записывает все модули с версиями.
Это реально удобная фича - никаких go.sum в образе не нужно, сканер находит всё сам.
Берём минимальное приложение с намеренно старой версией:
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4

Триви после сборки находит 20 CVE только в го депенденси.

Теперь удаляем секцию билдинфо из бинаря с помощью objcopy:
RUN go build -o myapp . \
&& apt-get update && apt-get install -y --no-install-recommends binutils \
&& objcopy --remove-section=.go.buildinfo myapp myapp-stripped \
&& apt-get purge -y binutils && apt-get autoremove -y


Бинарь работает и функционально идентичен, проверяю:
$ objdump -s -j .go.buildinfo /myapp-stripped
objdump: section '.go.buildinfo' mentioned in a -j option, but not found in any input file

скан:
trivy image test-go-stripped:latest --severity HIGH,CRITICAL
Total: 9 (HIGH: 6, CRITICAL: 3) только пакеты операционки

Все 20 гошные CVE исчезли, остались только 9 от Дебиан, которые will not fix. Безопасность 😎

На мультистейдже с финальным FROM scratch это тоже легко обходится - например, через upx, который пакует бинарь в свой формат и триви перестаёт читать билдинфо.
Суть та же: все варианты работают, сканер молчит. Безопасность 😁


Так что с итогом:
Да ничо, это просто было увлекательно. Изначальная проблема была решена чисто флагом вообще то.
А вопросы остались.
Я не говорю, что CVE сканеры имаджей бесполезны, наоборот - они полезны в штатных сиуациях.
Они находят реальные проблемы, они дают точку отсчёта, они вполне работают как первый фильтр.
Но некоторые сканеры, такие как триви, дают лишь аппроксимацию на основе сигнатур и метаданных.
И в целом они не гарантируют ничего - ни того, что уязвимость есть, ни того, что её нет.

Финальное решение - применимо ли эта CVE к вашей системе, надо ли бежать чинить прямо сейчас или игнорировать месяцами - пока принимает человек.
Это не игнорирование безопасности - как по мне это и есть работа с безопасностью.

- - -
Эта заметка - аккуратно переработанный текст моих старых сообщений в чате куберентис ру, но без обсценной лексики 😬.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍181🥰1