Что на самом деле обещает подписанная ссылка
Три вещи, и здесь стоит быть точным, потому что люди обычно ждут четвёртую.
Она доказывает, что ссылка от вас. Подпись — это хеш от пути, срока действия и секрета, который знает только ваш сервер. Без секрета никто не подделает рабочую ссылку.
У неё есть срок действия. Срок — часть того, что подписывается, поэтому изменить его, не сломав подпись, нельзя.
Её проверяют до обращения к вашему origin. На edge CDN неподписанный запрос отклоняется прямо на edge; ваш сервер его вообще не увидит.
Чего она не обещает: что человек, открывший ссылку, не сохранит файл и не отправит его другу по почте. Подписанная ссылка контролирует доступ к URL, а не к самим байтам. Если нужен контроль после скачивания, вам нужен DRM — совсем другой и гораздо более тяжёлый продукт.
Два семейства: подписи на edge и presigned URL
Подписи пути на edge — то, что даёт CDN. Ваше приложение вычисляет подпись для пути и срока действия; edge проверяет её при получении запроса. Файл может лежать где угодно, куда дотягивается CDN, а проверка происходит рядом с посетителем.
Presigned URL от объектного хранилища — то, что даёт S3 и S3-совместимые хранилища. Сам сервис хранения выдаёт временную ссылку, подписанную вашим ключом доступа, обычно на несколько минут. Подпись едет в строке запроса как X-Amz-Signature и её соседи.
Это не конкуренты. Разумная и распространённая схема — объектное хранилище за CDN: подпись на edge защищает публичный URL, а бакет остаётся приватным, чтобы никто не мог обойти edge. Чего делать не стоит — выставлять presigned-ссылку хранилища напрямую наружу и считать, что её защищает CDN: если ссылка ведёт прямо на бакет, edge вообще не участвует в пути запроса.
Как это работает на cdn.com.tr
Это настройка на правиле доставки, поэтому можно защитить /downloads, оставив остальной сайт публичным. Включите истекающие ссылки для этого правила, задайте секрет и генерируйте ссылки в своём приложении.
Подпись — это base64url-форма от «сырого» MD5 трёх вещей, склеенных вместе: срока действия, пути и секрета с одним пробелом перед ним.
``` $uri = "/downloads/report.pdf"; $expires = time() + 600; // ten minutes $secret = "your-long-random-secret";
$token = rtrim(strtr(base64_encode( md5($expires . $uri . " " . $secret, true) ), "+/", "-_"), "=");
$url = "https://cdn.example.com{$uri}?md5={$token}&expires={$expires}"; ```
Три исхода, и они намеренно различны, чтобы их можно было различить по логам:
| Запрос | Ответ | |---|---| | Подпись верна, срок не истёк | файл, кэшируется как обычно | | Подписи нет или она неверна | 403 | | Подпись верна, срок истёк | 410 |
410 значит больше, чем кажется. Когда клиент говорит «ваша ссылка не работает», один только код ответа показывает, отправили ли ему нерабочую ссылку или он просто слишком долго ждал, — и для этого не нужно ничего у него уточнять.
Ловушка кэша, которая обесценивает CDN
Это та часть, которой не хватает в большинстве документаций, и именно она бьёт больнее всего.
Каждая подписанная ссылка уникальна: свой md5 и свой expires для каждого посетителя и каждой выдачи. Если ключ кэша включает строку запроса — а это обычное поведение по умолчанию, и у нас тоже, пока вы это не измените, — то каждый подписанный запрос становится отдельной записью в кэше. Доля попаданий для защищённых файлов падает до нуля, каждая загрузка идёт на ваш origin, и вы платите за CDN, который на деле работает как обычный прокси.
Исправление — это одна настройка. В правиле доставки задайте режим обработки строки запроса так, чтобы параметры подписи не попадали в ключ кэша:
- Список игнорируемых — игнорировать md5 и expires, оставить всё остальное значимое. Правильный выбор, если тот же файл запрашивают ещё и с настоящими параметрами.
- Игнорировать всё — самый простой вариант, когда у защищённых путей вообще нет значимых параметров, что обычно верно для загрузок.
Проверяйте это, а не считайте само собой разумеющимся: запросите один и тот же файл дважды с двумя разными действительными подписями и посмотрите на заголовок статуса кэша. Второй запрос должен быть попаданием. Если оба — промахи, значит подпись всё ещё в ключе.
Как выбрать срок действия
Достаточно коротко, чтобы утёкшая ссылка была бесполезна; достаточно долго, чтобы загрузка успела завершиться на плохом соединении.
Документы и изображения: от пяти до пятнадцати минут. Их запрашивают сразу после клика.
Большое видео и архивы: от одного до шести часов. Файл на два гигабайта на медленном мобильном соединении грузится дольше, чем кажется, а срок действия, истёкший посреди загрузки, порождает тикет в поддержку, который выглядит как повреждённый файл.
Манифесты стриминга требуют аккуратности. В HLS плеер сначала запрашивает манифест, а затем — множество сегментов на протяжении всего воспроизведения. Если подписывать каждый сегмент с коротким сроком действия, воспроизведение обрывается где-то в середине длинного видео. Подписывайте со сроком, который покрывает весь сеанс целиком, либо подписывайте только манифест, а сегменты защищайте другим способом.
Чего нельзя сделать — отозвать одну конкретную выданную ссылку. Она действительна до истечения срока, и точка. Если нужен немедленный отзыв, меняйте секрет — это аннулирует сразу все выданные ссылки.
Ошибки, из-за которых утекает секрет
Подписывать в браузере. Если секрет лежит в JavaScript, он публичный. Подписывать нужно только на сервере, всегда. Звучит очевидно, но это самый частый способ утечки секретов.
Зашивать ссылки на этапе сборки. Ссылка, сгенерированная во время статической сборки, несёт срок действия, установленный в момент сборки, и он истекает, пока страница ещё живая. Генерируйте ссылку в момент запроса или через небольшой эндпоинт, который делает редирект.
Рассинхронизация часов. Срок действия сверяется с часами edge. Если часы вашего сервера приложения отстают на пару минут, ссылки рождаются уже истёкшими. Следите, чтобы NTP работал; если ссылка не срабатывает сразу после выдачи, проверьте часы раньше, чем код.
Хранить секрет в репозитории. Относитесь к нему как к учётным данным: переменная окружения, а не система контроля версий. Когда кто-то покидает команду, меняйте секрет.
Забыть, что смена секрета глобальна. Смена секрета разом аннулирует все выданные ссылки, включая те, что уже пять минут как ушли в письмах. Это именно то, что нужно во время инцидента, и именно то, что не нужно случайно в пятницу под вечер.
Частые вопросы
В чём разница между подписанной ссылкой и presigned URL?
В основном — где проходит проверка. «Presigned URL» — это термин S3 для временной ссылки, которую выдаёт и проверяет само хранилище. Подписанная ссылка на edge CDN проверяется на edge, рядом с посетителем, и работает для любого origin, а не только для бакета. Концепция та же: подпись плюс срок действия в строке запроса.
Может ли кто-то передать подписанную ссылку дальше?
Да, пока она не истекла. Подпись доказывает, что ссылку выдали вы, но ничего не говорит о том, у кого она сейчас на руках. Короткий срок действия ограничивает ущерб. Если нужно привязать ссылку к конкретному человеку, включите в подписываемый путь что-то идентифицирующее и проверяйте это на своей стороне — и смиритесь с тем, что настроенный на это человек всё равно сможет переслать уже скачанный файл.
Подписи вредят кэшированию?
Только если подпись попадает в ключ кэша, и тогда вредят сильно: каждая уникальная ссылка превращается в отдельную запись кэша, и каждая загрузка идёт на ваш origin. Настройте режим строки запроса так, чтобы параметры подписи игнорировались, — и файл будет кэшироваться как обычно, общим для всех, у кого есть действующая ссылка.
Можно ли отозвать одну ссылку?
По отдельности — нет. Подпись действительна до истечения срока, потому что проверка не хранит состояние: нет списка, из которого её можно было бы удалить. Доступные рычаги — короткий срок действия и смена секрета, которая аннулирует всё сразу.
Разве MD5 не «сломан» для этой задачи?
MD5 «сломан» в смысле стойкости к коллизиям, а это важно, когда атакующий может выбирать оба сообщения. Здесь же атакующему нужно подделать хеш строки, содержащей секрет, которого у него нет, — это задача поиска прообраза, а не коллизии. Реальный риск — утечка секрета или то, что его можно подобрать: используйте длинный случайный секрет, храните его только на сервере и периодически меняйте.
Подходит ли это для HLS-видео?
Работает, с оговоркой про длительность сеанса: срок действия должен покрывать всё воспроизведение целиком, а не только запрос манифеста, иначе воспроизведение оборвётся на середине. Для длинного контента лучше сочетать щедрый срок действия с rate limiting, чем ставить очень короткий срок, который ломает плеер.