Ваши поддомены уже в публичном журнале: 84 секунды от certbot до первого чужого запроса

Вы подняли панель на свежем поддомене — panel.example.com, о котором не знает ни один поисковик и ни один ваш знакомый. Запустили certbot, получили зелёный замок, пошли заваривать чай. Через полторы минуты в access-логе уже лежат чужие GET /.env и GET /.git/config.

Это не утечка и не взлом. Это штатная работа механизма, который встроен в HTTPS специально и отключить который нельзя: Certificate Transparency. Любое имя, на которое публичный центр сертификации выписал сертификат, в тот же момент оказывается в открытом журнале — навсегда, с точностью до секунды выдачи.

Для OSINT это один из самых дешёвых способов собрать карту чужой инфраструктуры. Для того, кто эту инфраструктуру держит, — постоянно работающий канал раскрытия, который многие обнаруживают уже по факту.

Info:

Материал полезен, если у вас есть свой домен и хоть один сервис за HTTPS: панель Remnawave или Marzban, 3x-ui, Grafana, Vaultwarden, Immich, тестовый стенд. Если вы пользуетесь чужим VPN и своих доменов не держите — это скорее про то, как устроена разведка по открытым источникам.

Почему у сертификата нет режима «не публиковать»

Certificate Transparency появился как ответ на подделку сертификатов удостоверяющими центрами: если каждая выдача попадает в публичный append-only журнал на основе дерева Меркла, владелец домена может заметить чужой сертификат на своё имя. Логика здравая, и работает она ровно так, как задумано.

Побочный эффект в том, что «публичный журнал» означает публичный для всех, а не только для владельца домена. И обойти его нельзя: браузеры перестают доверять сертификату, которого нет в логах.

Политика Chrome требует, чтобы у сертификата было минимум два SCT — подписанных отметок о включении в лог — для срока жизни до 180 дней, и минимум три при большем сроке, причём как минимум два SCT должны прийти от разных операторов логов. Safari требует не меньше двух SCT. SCT — это обещание лога включить запись в дерево в пределах Maximum Merge Delay, обычно 24 часа; на практике запись появляется в поиске за минуты.

Important:

Опции «выпустите мне сертификат, но не пишите имя в лог» не существует ни у Let’s Encrypt, ни у любого другого публично доверенного центра. Выбор проходит не между «логировать и не логировать», а между «какое имя вы отдадите в лог» и «нужен ли вам вообще публичный сертификат на этот хост».

Хронология: 23 секунды до DNS, 84 — до HTTPS

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

Группа Шайтле и коллег (IMC 2018, TU München и партнёры) подняла 11 honeypot-поддоменов со случайными именами из 12 символов и слила их в мир единственным способом — через CT-лог. Первые DNS-запросы пришли через 73 секунды; крупные резолверы Google, 1&1 и Amazon обошли все одиннадцать имён за 12 минут. Первое HTTP-соединение приходило в диапазоне от 59 минут до 19 суток, чаще всего в первые час-два. Одна машина просканировала на honeypot’ах 30 портов.

Команда Плетинкса из UCSB повторила измерение в 2022-м (EuroS&P 2023) — и цифры сжались на порядок. Медиана до первого DNS-запроса — 23 секунды. До первого HTTPS-запроса — 84 секунды.

Самая показательная часть их эксперимента длилась 200 дней. Контрольная группа хостов жила с самоподписанными сертификатами и в CT не попадала, экспериментальная — получала обычный публичный сертификат. По IPv6 контрольная группа за 200 дней собрала ровно ноль входящих пакетов. Экспериментальная — около 2 700, от 959 уникальных адресов из 112 автономных систем. По IPv4 к именам из CT пришли запросы с 2 291 адреса из 267 AS.

IPv6-часть здесь ключевая: адресное пространство слишком велико, чтобы его перебирать, — значит, единственным источником знания об этих хостах был лог.

Что видно постороннему за пять минут

Мониторы вроде crt.sh, Censys и merklemap зеркалят все логи и дают по ним поиск. Базовый запрос — обычный HTTP:

curl -s 'https://crt.sh/?q=example.com&output=json' | jq -r '.[].name_value' | tr ';' '\n' | sort -u

Тот же поиск с подстановкой % (в URL — %25) даёт все поддомены сразу. Если crt.sh отвечает Unsupported use of '%', остаётся прямой доступ к его базе, который проект держит открытым:

psql -h crt.sh -p 5432 -U guest certwatch
Warning:

Синтаксис поиска на crt.sh менялся: поддержку % в разное время сужали и возвращали. Не закладывайтесь на неё в скриптах — проверяйте ответ, а при отказе переключайтесь на psql или на другой монитор. Само содержимое логов от этого не меняется.

Масштаб того, что там лежит, оценили Робертс и Левин (WPES 2019, University of Maryland) на полном срезе сертификатов из Censys. Из имён хостов они вытащили больше 130 000 имён пользователей на семи сайтах, 5 861 адрес электронной почты в поддоменах одного облачного сервиса баз данных, 77 825 доменов, по которым восстанавливаются связи «поставщик — клиент», и 25 346 уникальных поддоменов одной только компании Discovery. В выборку регулярно попадали внутренние QA- и staging-серверы, админки и кодовые имена невыпущенных продуктов.

Свою сторону этой картины видно одной командой:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

Всё, что перечислено в выводе, уже прочитал кто-то, кроме вас.

Четыре развилки — и цена каждой

Подход Имя в логе Чем платите
Сертификат на каждый хост Все имена Полная карта инфраструктуры наружу
Один wildcard на зону Только *.example.com DNS-01, общий ключ на всю зону
REALITY (XTLS) Ничего Только прокси, не веб-сервисы
Свой CA, самоподписанный Ничего Ручная раздача корня на клиенты

Wildcard — самый простой шаг и ровно то, что рекомендуют Робертс и Левин. Важное ограничение: он прячет один уровень слева и только его. Имя a.b.example.com под *.example.com не подпадает, для него нужен отдельный сертификат — и это имя снова уедет в лог. У Let’s Encrypt wildcard выдаётся исключительно через DNS-01: HTTP-01 и TLS-ALPN-01 для этого прямо непригодны.

certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cf.ini \
  -d 'example.com' -d '*.example.com'

Плата — один приватный ключ на всю зону: скомпрометировали хост, где он лежит, — скомпрометировали всё под доменом.

REALITY обходит проблему целиком, потому что не выпускает сертификат вообще: сервер заимствует TLS-рукопожатие чужого сайта, указанного в dest/target, и предъявляет клиенту его сертификат. В CT-лог при этом не уходит ничего — просто нечему уходить. Работает это только для прокси-трафика; Grafana и Immich так не спрячешь.

Свой CA уместен для внутренних сервисов, куда ходите вы и три ваших устройства. Корневой сертификат придётся руками занести в каждый клиент, зато имена остаются вашим личным делом.

Четвёртый вариант — принять публичность и закрыть периметр. Тогда всё, что не соответствует известному имени, не должно получать даже рукопожатия:

server {
    listen               443 ssl default_server;
    ssl_reject_handshake on;   # nginx 1.19.4 и новее
}

Плюс аутентификация на входе, а не за формой логина: mTLS, basic-auth на уровне reverse-proxy или доступ только из VPN-подсети. Смысл в том, что 84 секунды — это не «мало времени, чтобы успеть настроить», а «времени нет вообще»: к моменту первого запроса всё уже должно стоять.

Security:

Поиск по CT-логам — работа с публичными данными и сам по себе законен. Законным он перестаёт быть ровно в тот момент, когда найденные хосты начинают проверять на живость и уязвимости без письменного разрешения владельца. Граница проходит между «посмотрел в открытый журнал» и «отправил пакет на чужой сервер».

Смежное на форуме: Hysteria2 inbound + wildcard TLS для Remnawave, Гайд: OSINT для начинающих, Гайд: как работает Reality (XTLS).

Источники

Question:

Проверьте свой домен через crt.sh — сколько имён вы там увидели и сколько из них вы бы предпочли не публиковать? И если вы уже перешли на wildcard: как решили вопрос с общим приватным ключом на всю зону?