Подписанные ссылки — классика. Создаёшь HMAC-подпись с истечением срока, отдаёшь клиенту, nginx проверяет подпись через модуль secure_link. Работает. Но у этого подхода есть неприятное свойство: как только ссылка утекла — файл открыт на время её жизни кому угодно. Увеличить TTL удобнее для кеша, но опаснее. Уменьшить — безопаснее, но больнее для UX.

Мы решили перейти к модели Telegram — авторизация на каждый запрос.

Как это работает

  1. Клиент запрашивает /uploads/messages/2026/04/abc.jpg с заголовком Authorization: Bearer <token>
  2. Nginx не отдаёт файл сразу — сначала делает внутренний sub-request на /internal/auth-attachment
  3. Rust-сервис извлекает путь из X-Original-URI, достаёт user_id из токена и проверяет в Postgres: есть ли у пользователя доступ к сообщению, содержащему этот файл
  4. Если да — отвечает 200, nginx отдаёт файл. Если нет — 403, клиент получает ошибку

Почему это лучше

  • Нулевой «хвост»: выгнали участника из группы — он больше не увидит ни одного файла оттуда, даже со всеми сохранёнными ссылками
  • Нет секрета у клиента: клиент просто шлёт JWT, который у него и так есть. Никаких HMAC-ключей, которые можно украсть
  • Аудит: каждый доступ к файлу проходит через наш сервис — можем логировать, rate-limit’ить, что угодно

Про кеш

Главное возражение против такой модели — дополнительная нагрузка. Каждый медиа-запрос порождает ещё один запрос к базе. Решили кешированием: nginx кеширует результат авторизации на 30 секунд по паре (Authorization, URI). 30 секунд — компромисс: почти не видно на нагрузке при прокрутке чата, и почти не создаёт окна уязвимости при удалении из группы.

Что под капотом

  • nginx с auth_request + proxy_cache
  • axum endpoint на Rust
  • sqlx compile-time query против Postgres
  • Частичный индекс по attachment_path и attachment_thumb_path

Мигрировали атомарно: Kotlin-сервис, nginx-конфиг и Rust-бинарь обновились одной командой. Клиент уже знал как отправлять Authorization заранее — поэтому downtime был буквально секунды.

Так выглядит, когда безопасность и скорость не конфликтуют.