hmac-secure-link: Альтернативный модуль NGINX HMAC Secure Link с поддержкой хешей OpenSSL
Установка
Вы можете установить этот модуль в любой дистрибутив на базе RHEL, включая, но не ограничиваясь:
- RedHat Enterprise Linux 7, 8, 9 и 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 и Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-hmac-secure-link
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-hmac-secure-link
Включите модуль, добавив следующее в начало файла /etc/nginx/nginx.conf:
load_module modules/ngx_http_hmac_secure_link_module.so;
Этот документ описывает nginx-module-hmac-secure-link v2.0.0, выпущенный 2 апреля 2026 года.
Описание
Модуль Nginx HMAC Secure Link улучшает безопасность и функциональность стандартного модуля secure_link. Безопасные токены создаются с использованием правильной конструкции HMAC (RFC 2104) с любым алгоритмом хеширования, поддерживаемым OpenSSL 3.x. Доступные алгоритмы зависят от поставщиков, загруженных в вашей конфигурации OpenSSL.
Стандартный поставщик (доступен из коробки на любой установке OpenSSL 3.x):
md5, sha1, sha224, sha256, sha384, sha512, sha512-224, sha512-256,
sha3-224, sha3-256, sha3-384, sha3-512, shake128, shake256,
blake2b512, blake2s256, sm3.
Устаревший поставщик (требует явной загрузки устаревшего поставщика OpenSSL в openssl.cnf; по умолчанию не доступен в OpenSSL 3.x):
md4, mdc2, rmd160, gost.
Рекомендуемый алгоритм — sha256 или более сильный. md5 и sha1 принимаются, но не должны использоваться в новых развертываниях.
HMAC вычисляется как H(secret ⊕ opad, H(secret ⊕ ipad, message)), а не как небезопасный MD5(secret, message, expire), используемый встроенным модулем.
Предварительно собранные пакеты (Ubuntu / Debian)
Предварительно собранные пакеты для этого модуля доступны бесплатно из репозитория GetPageSpeed:
## Добавьте репозиторий (пример для Ubuntu — замените 'jammy' на вашу версию)
echo "deb [signed-by=/etc/apt/keyrings/getpagespeed.gpg] \
https://extras.getpagespeed.com/ubuntu jammy main" \
| sudo tee /etc/apt/sources.list.d/getpagespeed-extras.list
## Директивы конфигурации
Все директивы принимают переменные NGINX и сложные значения.
### `secure_link_hmac`
**Контекст:** `http`, `server`, `location`
Указывает выражение переменной, значение которого должно соответствовать формату `<token>,<timestamp>[,<expires>]`. **Разделитель полей всегда запятая и обязателен между каждым полем.** Запятая жестко закодирована в парсере модуля; другие разделители здесь не поддерживаются.
| Поле | Описание |
|-------------|-------------------------------------------------------|
| `token` | HMAC в формате Base64url (без заполнения `=`) |
| `timestamp` | Время создания запроса (см. [Форматы временных меток](#timestamp-formats)) |
| `expires` | Необязательный срок действия в секундах; опустите или используйте `0` для неограниченного |
```nginx
secure_link_hmac "$arg_st,$arg_ts,$arg_e";
Важно: Когда
secure_link_hmacсобирается из параметров запроса ("$arg_st,$arg_ts,$arg_e"), значения временной метки и срока действия не должны содержать необработанные запятые. ISO 8601 и Unix временные метки не содержат запятых и работают без специальной обработки. Даты RFC 7231 содержат встроенную запятую (например,Sun, 06 Nov …); модуль обрабатывает это правильно для второго поля, но вы должны URL-кодировать запятую, когда помещаете дату RFC 7231 в параметр запроса, чтобы$arg_tsразрешался в полную декодированную строку даты (см. Форматы временных меток).
secure_link_hmac_message
Контекст: http, server, location
Сообщение, HMAC которого должно быть проверено. Должно точно соответствовать тому, что клиент использовал при вычислении токена. Обычно включает URI и временную метку, чтобы токены были специфичными для URL и привязанными ко времени.
Разделитель между полями в сообщении выбирается оператором и может быть любым байтом или последовательностью байтов — вертикальная черта (|), двоеточие (:), слэш (/), дефис (-) или даже ничего. Модуль рассматривает secure_link_hmac_message как непрозрачную байтовую строку и никогда не разбирает ее содержимое; разделитель просто является частью предобраза HMAC.
Единственное требование заключается в том, что выбранный на стороне сервера разделитель должен быть идентичен разделителю, использованному клиентом при вычислении HMAC. Использование разделителя, который не может естественно появляться в любом из значений полей (например, | для URI и Unix временных меток), снижает риск неоднозначности увеличения длины.
## Разделитель вертикальной чертой (рекомендуется — не может появляться в пути URI или Unix временной метке)
secure_link_hmac_message "$uri|$arg_ts|$arg_e";
## Разделитель двоеточием
secure_link_hmac_message "$uri:$arg_ts:$arg_e";
## Без разделителя (действительно, но неоднозначно, если поля имеют общий набор символов)
secure_link_hmac_message "$uri$arg_ts$arg_e";
secure_link_hmac_secret
Контекст: http, server, location
Секретный ключ HMAC. Держите это вне системы контроля версий.
secure_link_hmac_secret "my_very_secret_key";
secure_link_hmac_algorithm
Контекст: http, server, location
По умолчанию: sha256
Имя дайджеста OpenSSL, используемое для HMAC.
secure_link_hmac_algorithm sha256;
Встроенные переменные
$secure_link_hmac
Устанавливается после обработки директивы secure_link_hmac. Возможные значения:
| Значение | Значение |
|---|---|
"1" |
Токен криптографически действителен и ссылка не истекла |
"0" |
Токен действителен, но ссылка истекла |
| (пусто) | Токен отсутствует, неправильно сформирован, несоответствие HMAC или недействительная временная метка |
Используйте эту переменную для ограничения доступа. В производственной среде возвращайте один и тот же код ошибки для всех случаев неудачи, чтобы злоумышленник не мог отличить истекший токен от поддельного:
if ($secure_link_hmac != "1") {
return 403;
}
Примечание:
"1"и"0"— это буквальные строки из одного символа, а не числа. Пустой / не найденный случай означает, что переменная не установлена, а не равна"".
$secure_link_hmac_expires
Сырая строка срока действия (в секундах), полученная в запросе. Эта переменная устанавливается только тогда, когда срок действия был указан в secure_link_hmac. Она может быть использована для ведения журнала или условной логики:
add_header X-Link-Expires $secure_link_hmac_expires;
- Если входящее значение было
"3600", эта переменная содержит"3600". - Если поле срока действия отсутствовало, переменная не установлена (not_found).
- Эта переменная заполняется как побочный эффект оценки
$secure_link_hmac; сначала оцените$secure_link_hmac.
$secure_link_hmac_token
Свежесформированный токен HMAC, закодированный в base64url (без завершающего заполнения =). Используйте эту переменную, когда NGINX действует как прокси, который должен пересылать аутентифицированные запросы на бэкенд:
location ^~ /backend/ {
set $expire 60;
secure_link_hmac_message "$uri|$time_iso8601|$expire";
secure_link_hmac_secret "my_very_secret_key";
secure_link_hmac_algorithm sha256;
proxy_pass "http://backend$uri?st=$secure_link_hmac_token&ts=$time_iso8601&e=$expire";
}
Токен закодирован в base64url без заполнения, совместим с параметрами URL без дальнейшего экранирования.
Форматы временных меток
Временная метка должна всегда включаться в подписанное сообщение, чтобы предотвратить атаки повторного воспроизведения. Три формата принимаются парсером на стороне сервера. Клиенты могут использовать любой из них, который наиболее удобен.
ISO 8601 с числовым смещением UTC (рекомендуется)
YYYY-MM-DDThh:mm:ss+HH:MM
YYYY-MM-DDThh:mm:ss-HH:MM
Примеры:
2025-06-01T14:30:00+00:00 # UTC
2025-06-01T17:30:00+03:00 # UTC+3 (Киев/Стамбул)
2025-06-01T08:30:00-06:00 # UTC-6 (Чикаго CDT)
Сервер преобразует в UTC перед сравнением, поэтому принимается любое действительное смещение.
ISO 8601 UTC (с суффиксом Z)
YYYY-MM-DDThh:mm:ssZ
Пример: 2025-06-01T14:30:00Z
Эквивалентно +00:00, но короче. Встроенная переменная Nginx $time_iso8601 выдает формат +00:00; для Z вы должны форматировать временную метку на стороне приложения.
RFC 7231 / IMF-fixdate (HTTP дата)
Как указано в RFC 7231 §7.1.1.1. Все даты RFC 7231 являются неявно UTC; смещение не применяется.
Day, DD Mon YYYY hh:mm:ss GMT
Примеры:
Sun, 01 Jun 2025 14:30:00 GMT
Mon, 23 Mar 2026 08:00:00 GMT
Где Day — это трехбуквенное сокращение дня недели (Mon–Sun), а Mon (месяц) — это одно из Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec. Парсер нечувствителен к регистру для обоих сокращений.
Примечание: RFC 7231 также определяет два устаревших формата (RFC 850 и ANSI C
asctime). Они не поддерживаются; только предпочтительный формат IMF-fixdate принимается.
Unix временная метка (обычное целое число)
Строка десятичных цифр, представляющая секунды с момента начала эпохи Unix (1970-01-01T00:00:00Z).
Пример: 1748785800
Это самый простой формат и хорошо работает в Bash и Node.js. Парсер строгий: поле временной метки должно содержать только десятичные цифры; любой другой символ приводит к его отклонению.
Примечание по безопасности: Unix временные метки имеют разрешение только в одну секунду. Используйте ISO 8601, если важна субсекундная точность или если вам нужно выразить конкретный часовой пояс.
Пример использования — Сторона сервера
location ^~ /files/ {
# Три поля, разделенные запятыми: токен, временная метка, срок действия (в секундах)
secure_link_hmac "$arg_st,$arg_ts,$arg_e";
# Секретный ключ HMAC
secure_link_hmac_secret "my_secret_key";
# Сообщение, которое было подписано: URI + временная метка + срок действия
secure_link_hmac_message "$uri|$arg_ts|$arg_e";
# Алгоритм хеширования
secure_link_hmac_algorithm sha256;
# В производственной среде не раскрывайте, был ли токен неверным или истекшим.
# $secure_link_hmac == "1" → действителен и не истек
# $secure_link_hmac == "0" → действителен, но истек
# $secure_link_hmac не установлен → недействителен / неправильно сформирован
if ($secure_link_hmac != "1") {
return 403;
}
rewrite ^/files/(.*)$ /files/$1 break;
}
Примеры на стороне клиента
Perl — временная метка ISO 8601
perl_set $secure_token '
sub {
use Digest::SHA qw(hmac_sha256_base64);
use POSIX qw(strftime);
my $r = shift;
my $key = "my_very_secret_key";
my $expire = 60;
my $now = time();
# ISO 8601 с числовым смещением UTC
my $tz = strftime("%z", localtime($now));
$tz =~ s/(\d{2})(\d{2})/$1:$2/;
my $timestamp = strftime("%Y-%m-%dT%H:%M:%S", localtime($now)) . $tz;
my $message = $r->uri . "|" . $timestamp . "|" . $expire;
my $digest = hmac_sha256_base64($message, $key);
$digest =~ tr(+/)(-_); # base64 → base64url
$digest =~ s/=+$//; # убрать заполнение
return "st=$digest&ts=$timestamp&e=$expire";
}
';
PHP — Unix временная метка
<?php
$secret = 'my_very_secret_key';
$expire = 60;
$algo = 'sha256';
$timestamp = time(); // Unix временная метка
$uri = '/files/top_secret.pdf';
$message = "{$uri}|{$timestamp}|{$expire}";
$token = base64_encode(hash_hmac($algo, $message, $secret, true));
$token = strtr($token, '+/', '-_'); // base64 → base64url
$token = rtrim($token, '='); // убрать заполнение
$host = $_SERVER['HTTP_HOST'];
$url = "https://{$host}{$uri}?st={$token}&ts={$timestamp}&e={$expire}";
PHP — временная метка ISO 8601
<?php
$secret = 'my_very_secret_key';
$expire = 60;
$algo = 'sha256';
$timestamp = (new DateTimeImmutable('now', new DateTimeZone('UTC')))
->format(DateTimeInterface::RFC3339); // "2025-06-01T14:30:00+00:00"
$uri = '/files/top_secret.pdf';
$message = "{$uri}|{$timestamp}|{$expire}";
$token = base64_encode(hash_hmac($algo, $message, $secret, true));
$token = strtr($token, '+/', '-_');
$token = rtrim($token, '=');
$url = "https://example.com{$uri}?st={$token}&ts=" . urlencode($timestamp) . "&e={$expire}";
PHP — временная метка RFC 7231 / IMF-fixdate
<?php
$secret = 'my_very_secret_key';
$expire = 60;
$algo = 'sha256';
// RFC 7231 IMF-fixdate — всегда UTC, всегда с суффиксом "GMT"
$timestamp = gmdate('D, d M Y H:i:s') . ' GMT'; // "Sun, 01 Jun 2025 14:30:00 GMT"
$uri = '/files/top_secret.pdf';
$message = "{$uri}|{$timestamp}|{$expire}";
$token = base64_encode(hash_hmac($algo, $message, $secret, true));
$token = strtr($token, '+/', '-_');
$token = rtrim($token, '=');
// URL-кодируйте дату RFC 7231 (содержит пробелы и запятые)
$url = "https://example.com{$uri}?st={$token}&ts=" . rawurlencode($timestamp) . "&e={$expire}";
Node.js — Unix временная метка
const crypto = require('crypto');
const secret = 'my_very_secret_key';
const expire = 60;
const timestamp = Math.floor(Date.now() / 1000); // Unix временная метка
const uri = '/files/top_secret.pdf';
const message = `${uri}|${timestamp}|${expire}`;
const token = crypto.createHmac('sha256', secret)
.update(message)
.digest('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const url = `https://example.com${uri}?st=${token}&ts=${timestamp}&e=${expire}`;
Node.js — временная метка RFC 7231 / IMF-fixdate
const crypto = require('crypto');
const secret = 'my_very_secret_key';
const expire = 60;
// toUTCString() производит формат RFC 7231 IMF-fixdate во всех современных средах выполнения
const timestamp = new Date().toUTCString(); // "Sun, 01 Jun 2025 14:30:00 GMT"
const uri = '/files/top_secret.pdf';
const message = `${uri}|${timestamp}|${expire}`;
const token = crypto.createHmac('sha256', secret)
.update(message)
.digest('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const url = `https://example.com${uri}?st=${token}&ts=${encodeURIComponent(timestamp)}&e=${expire}`;
Python — временная метка ISO 8601 (с суффиксом UTC Z)
import hmac, hashlib, base64, urllib.parse
from datetime import datetime, timezone
secret = b'my_very_secret_key'
expire = 60
timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
uri = '/files/top_secret.pdf'
message = f'{uri}|{timestamp}|{expire}'.encode()
token = base64.urlsafe_b64encode(
hmac.new(secret, message, hashlib.sha256).digest()
).rstrip(b'=').decode()
url = f'https://example.com{uri}?st={token}&ts={urllib.parse.quote(timestamp)}&e={expire}'
Bash — Unix временная метка
#!/bin/bash
SECRET="my_super_secret"
URI="/file/my_secret_file.txt"
TIMESTAMP="$(date +%s)"
EXPIRES=3600
MESSAGE="${URI}|${TIMESTAMP}|${EXPIRES}"
TOKEN="$(printf '%s' "$MESSAGE" \
| openssl dgst -sha256 -hmac "$SECRET" -binary \
| openssl base64 \
| tr '+/' '-_' \
| tr -d '=')"
echo "http://127.0.0.1${URI}?st=${TOKEN}&ts=${TIMESTAMP}&e=${EXPIRES}"
Использование прокси
Когда NGINX действует как прокси, который должен добавить токен HMAC к исходящим запросам, используйте переменную $secure_link_hmac_token:
location ^~ /backend_location/ {
set $expire 60;
secure_link_hmac_message "$uri|$time_iso8601|$expire";
secure_link_hmac_secret "my_very_secret_key";
secure_link_hmac_algorithm sha256;
proxy_pass "http://backend_server$uri?st=$secure_link_hmac_token&ts=$time_iso8601&e=$expire";
}
Примечание:
$time_iso8601выдает временную метку ISO 8601 с числовым смещением UTC (например,2025-06-01T14:30:00+00:00), которую этот модуль принимает.
Примечания по безопасности
Разделитель в secure_link_hmac
Разделитель полей внутри значения директивы secure_link_hmac всегда запятая. Поля временной метки и срока действия не должны содержать необработанные запятые (ISO 8601 и Unix временные метки безопасны; временные метки RFC 7231 обрабатываются внутренней логикой модуля по пропуску запятых, но встроенная запятая должна оставаться нетронутой при URL-кодировании/декодировании — см. Форматы временных меток).
Разделитель в secure_link_hmac_message
Выберите разделитель, который не может появляться в любом из объединяемых полей. Вертикальная черта (|) является хорошим значением по умолчанию для комбинаций URI + Unix временных меток. Использование вообще без разделителя допустимо, но может позволить провести атаку увеличения длины, когда один действительный набор значений полей переинтерпретируется как другой набор; разделитель предотвращает это.
Другие рекомендации
- Всегда включайте временную метку в подписанное сообщение, чтобы предотвратить атаки повторного воспроизведения.
- Выберите короткое значение expires для вашего случая использования (60–3600 секунд — это типично для ссылок на загрузку).
- Возвращайте один и тот же код ошибки HTTP (например, 403) для всех случаев неудачи — как "0" (истекший), так и не найденный (недействительный) — чтобы злоумышленники не могли отличить истекший токен от поддельного.
- Используйте секретный ключ длиной не менее 32 байт случайной энтропии.
- Предпочитайте sha256 или более сильные; избегайте md5 и sha1 для новых развертываний.
- URL-кодируйте значения временных меток, содержащие символы, специальные для строк запроса:
- Смещение UTC ISO 8601 + должно отправляться как %2B (в противном случае декодируется как пробел)
- Пробелы RFC 7231 должны отправляться как %20, а встроенная запятая как %2C
GitHub
Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-hmac-secure-link.