Перейти к содержанию

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 разрешался в полную декодированную строку даты (см. Форматы временных меток).

Контекст: 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";

Контекст: http, server, location

Секретный ключ HMAC. Держите это вне системы контроля версий.

secure_link_hmac_secret "my_very_secret_key";

Контекст: http, server, location
По умолчанию: sha256

Имя дайджеста OpenSSL, используемое для HMAC.

secure_link_hmac_algorithm sha256;

Встроенные переменные

Устанавливается после обработки директивы secure_link_hmac. Возможные значения:

Значение Значение
"1" Токен криптографически действителен и ссылка не истекла
"0" Токен действителен, но ссылка истекла
(пусто) Токен отсутствует, неправильно сформирован, несоответствие HMAC или недействительная временная метка

Используйте эту переменную для ограничения доступа. В производственной среде возвращайте один и тот же код ошибки для всех случаев неудачи, чтобы злоумышленник не мог отличить истекший токен от поддельного:

if ($secure_link_hmac != "1") {
    return 403;
}

Примечание: "1" и "0" — это буквальные строки из одного символа, а не числа. Пустой / не найденный случай означает, что переменная не установлена, а не равна "".

Сырая строка срока действия (в секундах), полученная в запросе. Эта переменная устанавливается только тогда, когда срок действия был указан в secure_link_hmac. Она может быть использована для ведения журнала или условной логики:

add_header X-Link-Expires $secure_link_hmac_expires;
  • Если входящее значение было "3600", эта переменная содержит "3600".
  • Если поле срока действия отсутствовало, переменная не установлена (not_found).
  • Эта переменная заполняется как побочный эффект оценки $secure_link_hmac; сначала оцените $secure_link_hmac.

Свежесформированный токен 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 — это трехбуквенное сокращение дня недели (MonSun), а 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.