Как мы защитили медиа: пер-запросная авторизация
Телеграм-модель защиты вложений: нет токена — нет доступа. Рассказываем, как и почему мы отказались от подписанных ссылок.
Подписанные ссылки — классика. Создаёшь HMAC-подпись с истечением срока, отдаёшь клиенту, nginx проверяет подпись через модуль secure_link. Работает. Но у этого подхода есть неприятное свойство: как только ссылка утекла — файл открыт на время её жизни кому угодно. Увеличить TTL удобнее для кеша, но опаснее. Уменьшить — безопаснее, но больнее для UX.
Мы решили перейти к модели Telegram — авторизация на каждый запрос.
Как это работает
- Клиент запрашивает
/uploads/messages/2026/04/abc.jpgс заголовкомAuthorization: Bearer <token> - Nginx не отдаёт файл сразу — сначала делает внутренний sub-request на
/internal/auth-attachment - Rust-сервис извлекает путь из
X-Original-URI, достаётuser_idиз токена и проверяет в Postgres: есть ли у пользователя доступ к сообщению, содержащему этот файл - Если да — отвечает
200, nginx отдаёт файл. Если нет —403, клиент получает ошибку
Почему это лучше
- Нулевой «хвост»: выгнали участника из группы — он больше не увидит ни одного файла оттуда, даже со всеми сохранёнными ссылками
- Нет секрета у клиента: клиент просто шлёт JWT, который у него и так есть. Никаких HMAC-ключей, которые можно украсть
- Аудит: каждый доступ к файлу проходит через наш сервис — можем логировать, rate-limit’ить, что угодно
Про кеш
Главное возражение против такой модели — дополнительная нагрузка. Каждый медиа-запрос порождает ещё один запрос к базе. Решили кешированием: nginx кеширует результат авторизации на 30 секунд по паре (Authorization, URI). 30 секунд — компромисс: почти не видно на нагрузке при прокрутке чата, и почти не создаёт окна уязвимости при удалении из группы.
Что под капотом
nginxсauth_request+proxy_cacheaxumendpoint на Rustsqlxcompile-time query против Postgres- Частичный индекс по
attachment_pathиattachment_thumb_path
Мигрировали атомарно: Kotlin-сервис, nginx-конфиг и Rust-бинарь обновились одной командой. Клиент уже знал как отправлять Authorization заранее — поэтому downtime был буквально секунды.
Так выглядит, когда безопасность и скорость не конфликтуют.