Aller au contenu

hmac-secure-link: Module NGINX HMAC Secure Link alternatif avec support pour les hachages OpenSSL

Installation

Vous pouvez installer ce module dans n'importe quelle distribution basée sur RHEL, y compris, mais sans s'y limiter :

  • RedHat Enterprise Linux 7, 8, 9 et 10
  • CentOS 7, 8, 9
  • AlmaLinux 8, 9
  • Rocky Linux 8, 9
  • Amazon Linux 2 et 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

Activez le module en ajoutant ce qui suit en haut de /etc/nginx/nginx.conf :

load_module modules/ngx_http_hmac_secure_link_module.so;

Ce document décrit nginx-module-hmac-secure-link v2.0.0 publié le 02 avril 2026.


Description

Le module Nginx HMAC Secure Link améliore la sécurité et la fonctionnalité du module secure_link standard. Des jetons sécurisés sont créés en utilisant une construction HMAC appropriée (RFC 2104) avec n'importe quel algorithme de hachage pris en charge par OpenSSL 3.x. Les algorithmes disponibles dépendent des fournisseurs chargés dans votre configuration OpenSSL.

Fournisseur par défaut (disponible par défaut sur toute installation 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.

Fournisseur hérité (nécessite que le fournisseur hérité d'OpenSSL soit explicitement chargé dans openssl.cnf ; non disponible par défaut dans OpenSSL 3.x) : md4, mdc2, rmd160, gost.

L'algorithme recommandé est sha256 ou plus fort. md5 et sha1 sont acceptés mais ne devraient pas être utilisés dans de nouveaux déploiements.

L'HMAC est calculé comme H(secret ⊕ opad, H(secret ⊕ ipad, message)) plutôt que l'insécurisé MD5(secret, message, expire) utilisé par le module intégré.

Paquets préconstruits (Ubuntu / Debian)

Des paquets préconstruits pour ce module sont disponibles gratuitement dans le dépôt GetPageSpeed :

## Ajouter le dépôt (exemple Ubuntu — remplacez 'jammy' par votre version)
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

## Directives de configuration

Toutes les directives acceptent des variables NGINX et des valeurs complexes.

### `secure_link_hmac`

**Contexte :** `http`, `server`, `location`

Spécifie l'expression de variable dont la valeur évaluée doit suivre le
format `<token>,<timestamp>[,<expires>]`. **Le séparateur de champ est toujours
une virgule et est requis entre chaque champ.** La virgule est codée en dur dans
le parseur du module ; aucun autre séparateur n'est pris en charge ici.

| Champ       | Description                                               |
|-------------|-----------------------------------------------------------|
| `token`     | HMAC encodé en Base64url (sans remplissage `=`)           |
| `timestamp` | Heure de création de la requête (voir [Formats de Timestamp](#timestamp-formats)) |
| `expires`   | Durée de vie optionnelle en secondes ; omettez ou utilisez `0` pour illimité |

```nginx
secure_link_hmac "$arg_st,$arg_ts,$arg_e";

Important : Lorsque secure_link_hmac est assemblé à partir de paramètres de requête ("$arg_st,$arg_ts,$arg_e"), les valeurs de timestamp et expires ne doivent pas elles-mêmes contenir de virgules non échappées. Les timestamps ISO 8601 et Unix sont sans virgule et fonctionnent sans traitement spécial. Les dates RFC 7231 contiennent une virgule intégrée (par exemple, Sun, 06 Nov …) ; le module gère cela correctement pour le deuxième champ, mais vous devez encoder l'URL de la virgule lorsque vous placez une date RFC 7231 dans un paramètre de requête afin que $arg_ts se résolve à la chaîne de date complète décodée (voir Formats de Timestamp).

Contexte : http, server, location

Le message dont l'HMAC doit être vérifié. Doit correspondre exactement à ce que le client a utilisé lors du calcul du jeton. Inclut généralement l'URI et le timestamp afin que les jetons soient spécifiques à l'URL et limités dans le temps.

Le séparateur entre les champs dans le message est librement choisi par l'opérateur et peut être n'importe quel octet ou séquence d'octets — barre verticale (|), deux-points (:), barre oblique (/), tiret (-), ou même rien du tout. Le module traite secure_link_hmac_message comme une chaîne d'octets opaque et ne parsing jamais son contenu ; le séparateur fait simplement partie de l'image pré-HMAC.

La seule exigence est que le séparateur choisi côté serveur soit identique au séparateur utilisé par le client lors du calcul de l'HMAC. Utiliser un séparateur qui ne peut pas apparaître naturellement dans l'une des valeurs de champ (par exemple | pour les URI et les timestamps Unix) réduit le risque d'ambiguïté d'extension de longueur.

## Séparateur pipe (recommandé — ne peut pas apparaître dans un chemin URI ou un timestamp Unix)
secure_link_hmac_message "$uri|$arg_ts|$arg_e";

## Séparateur deux-points
secure_link_hmac_message "$uri:$arg_ts:$arg_e";

## Pas de séparateur (valide, mais ambigu si les champs partagent un ensemble de caractères)
secure_link_hmac_message "$uri$arg_ts$arg_e";

Contexte : http, server, location

La clé secrète HMAC. Gardez cela hors du contrôle de version.

secure_link_hmac_secret "my_very_secret_key";

Contexte : http, server, location
Par défaut : sha256

Le nom de digest OpenSSL utilisé pour l'HMAC.

secure_link_hmac_algorithm sha256;

Variables intégrées

Défini après le traitement de la directive secure_link_hmac. Valeurs possibles :

Valeur Signification
"1" Le jeton est cryptographiquement valide et le lien n'a pas expiré
"0" Le jeton est valide mais le lien a expiré
(vide) Le jeton est absent, mal formé, échec de l'HMAC, ou timestamp invalide

Utilisez cette variable pour contrôler l'accès. En production, renvoyez le même code d'erreur pour tous les cas d'échec afin qu'un attaquant ne puisse pas distinguer entre un jeton expiré et un faux :

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

Remarque : "1" et "0" sont des chaînes littérales à un seul caractère, pas des nombres. Le cas vide / non trouvé signifie que la variable n'est pas définie, pas qu'elle est égale à "".

La chaîne brute de période d'expiration (en secondes) telle que reçue dans la requête. Cette variable est uniquement définie lorsqu'une expiration était présente dans secure_link_hmac. Elle peut être utilisée pour la journalisation ou la logique conditionnelle :

add_header X-Link-Expires $secure_link_hmac_expires;
  • Si la valeur entrante était "3600", cette variable contient "3600".
  • Si aucun champ d'expiration n'était présent, la variable est non définie (not_found).
  • Cette variable est peuplée comme un effet secondaire de l'évaluation de $secure_link_hmac ; évaluez $secure_link_hmac en premier.

Un jeton HMAC encodé en base64url fraîchement calculé (sans remplissage = à la fin). Utilisez cette variable lorsque NGINX agit en tant que proxy qui doit transmettre des requêtes authentifiées à un backend :

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";
}

Le jeton est encodé en base64url sans remplissage, compatible avec les paramètres de requête URL sans échappement supplémentaire.

Formats de Timestamp

Un timestamp devrait toujours être inclus dans le message signé pour prévenir les attaques par rejeu. Trois formats sont acceptés par le parseur côté serveur. Les clients peuvent utiliser celui qui est le plus pratique.

ISO 8601 avec décalage UTC numérique (recommandé)

YYYY-MM-DDThh:mm:ss+HH:MM
YYYY-MM-DDThh:mm:ss-HH:MM

Exemples :

2025-06-01T14:30:00+00:00   # UTC
2025-06-01T17:30:00+03:00   # UTC+3 (Kiev/Istanbul)
2025-06-01T08:30:00-06:00   # UTC-6 (Chicago CDT)

Le serveur convertit en UTC avant de comparer, donc tout décalage valide est accepté.

ISO 8601 UTC (suffixe Z)

YYYY-MM-DDThh:mm:ssZ

Exemple : 2025-06-01T14:30:00Z

Équivalent à +00:00 mais plus court. La variable intégrée $time_iso8601 émets le format +00:00 ; pour Z, vous devez formater le timestamp côté application.

RFC 7231 / IMF-fixdate (date HTTP)

Comme spécifié dans RFC 7231 §7.1.1.1. Toutes les dates RFC 7231 sont implicitement UTC ; aucun décalage n'est appliqué.

Day, DD Mon YYYY hh:mm:ss GMT

Exemples :

Sun, 01 Jun 2025 14:30:00 GMT
Mon, 23 Mar 2026 08:00:00 GMT

Day est une abréviation de jour de la semaine de trois lettres (MonSun) et Mon (mois) est l'un de Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec. Le parseur est insensible à la casse pour les deux abréviations.

Remarque : La RFC 7231 définit également deux formats obsolètes (RFC 850 et ANSI C asctime). Ceux-ci ne sont pas pris en charge ; seul le format préféré IMF-fixdate est accepté.

Timestamp Unix (entier brut)

Une chaîne de chiffres décimaux représentant les secondes depuis l'époque Unix (1970-01-01T00:00:00Z).

Exemple : 1748785800

C'est le format le plus simple et fonctionne bien dans Bash et Node.js. Le parseur est strict : le champ de timestamp doit contenir uniquement des chiffres décimaux ; tout autre caractère le fait rejeter.

Remarque de sécurité : Les timestamps Unix n'ont qu'une résolution d'une seconde. Utilisez ISO 8601 si la précision sous-seconde est importante, ou si vous devez exprimer un fuseau horaire spécifique.

Exemple d'utilisation — Côté serveur

location ^~ /files/ {
    # Les trois champs séparés par des virgules : jeton, timestamp, expires (secondes)
    secure_link_hmac "$arg_st,$arg_ts,$arg_e";

    # Clé secrète HMAC
    secure_link_hmac_secret "my_secret_key";

    # Le message qui a été signé : URI + timestamp + expiration
    secure_link_hmac_message "$uri|$arg_ts|$arg_e";

    # Algorithme de hachage
    secure_link_hmac_algorithm sha256;

    # En production, ne révélez pas si le jeton était incorrect ou expiré.
    # $secure_link_hmac == "1" → valide et non expiré
    # $secure_link_hmac == "0" → valide mais expiré
    # $secure_link_hmac non défini → invalide / mal formé
    if ($secure_link_hmac != "1") {
        return 403;
    }

    rewrite ^/files/(.*)$ /files/$1 break;
}

Exemples côté client

Perl — Timestamp 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 avec décalage UTC numérique
        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/=+$//;             # supprimer le remplissage

        return "st=$digest&ts=$timestamp&e=$expire";
    }
';

PHP — Timestamp Unix

<?php
$secret    = 'my_very_secret_key';
$expire    = 60;
$algo      = 'sha256';
$timestamp = time();                       // Timestamp 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, '=');              // supprimer le remplissage

$host = $_SERVER['HTTP_HOST'];
$url  = "https://{$host}{$uri}?st={$token}&ts={$timestamp}&e={$expire}";

PHP — Timestamp 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 — Timestamp RFC 7231 / IMF-fixdate

<?php
$secret    = 'my_very_secret_key';
$expire    = 60;
$algo      = 'sha256';
// RFC 7231 IMF-fixdate — toujours UTC, toujours suffixe "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, '=');

// Encoder l'URL de la date RFC 7231 (contient des espaces et des virgules)
$url = "https://example.com{$uri}?st={$token}&ts=" . rawurlencode($timestamp) . "&e={$expire}";

Node.js — Timestamp Unix

const crypto = require('crypto');

const secret    = 'my_very_secret_key';
const expire    = 60;
const timestamp = Math.floor(Date.now() / 1000);   // Timestamp 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 — Timestamp RFC 7231 / IMF-fixdate

const crypto = require('crypto');

const secret    = 'my_very_secret_key';
const expire    = 60;
// toUTCString() produit le format RFC 7231 IMF-fixdate dans tous les environnements modernes
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 — Timestamp ISO 8601 (suffixe 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 — Timestamp 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}"

Utilisation en tant que proxy

Lorsque NGINX agit en tant que proxy qui doit ajouter un jeton HMAC aux requêtes sortantes, utilisez la variable $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";
}

Remarque : $time_iso8601 émet un timestamp ISO 8601 avec un décalage UTC numérique (par exemple, 2025-06-01T14:30:00+00:00), que ce module accepte.

Remarques de sécurité

Séparateur dans secure_link_hmac Le séparateur de champ à l'intérieur de la valeur de la directive secure_link_hmac est toujours une virgule. Les champs timestamp et expires ne doivent pas contenir de virgules nues (Les timestamps ISO 8601 et Unix sont sûrs ; les timestamps RFC 7231 sont gérés par la logique interne de saut de virgule du module mais la virgule intégrée doit survivre à l'encodage/décodage d'URL intact — voir Formats de Timestamp).

Séparateur dans secure_link_hmac_message Choisissez un séparateur qui ne peut pas apparaître dans l'un des champs concaténés. La barre verticale (|) est un bon défaut pour les combinaisons URI + timestamps Unix. Ne pas utiliser de séparateur du tout est valide mais peut permettre une attaque par extension de longueur où un ensemble valide de valeurs de champ est réinterprété comme un autre ensemble ; un séparateur empêche cela.

Autres recommandations - Incluez toujours un timestamp dans le message signé pour prévenir les attaques par rejeu. - Choisissez une valeur expires courte pour votre cas d'utilisation (60–3600 secondes est typique pour les liens de téléchargement). - Retournez le même code d'erreur HTTP (par exemple 403) pour tous les cas d'échec — à la fois "0" (expiré) et non trouvé (invalide) — afin que les attaquants ne puissent pas distinguer un jeton expiré d'un faux. - Utilisez une clé secrète d'au moins 32 octets d'entropie aléatoire. - Préférez sha256 ou plus fort ; évitez md5 et sha1 pour de nouveaux déploiements. - Encodez en URL les valeurs de timestamp qui contiennent des caractères spéciaux dans les chaînes de requête : - Le décalage UTC ISO 8601 + doit être envoyé sous forme de %2B (sinon décodé comme espace) - Les espaces RFC 7231 doivent être envoyés sous forme de %20 et la virgule intégrée sous forme de %2C

GitHub

Vous pouvez trouver des conseils de configuration supplémentaires et de la documentation pour ce module dans le dépôt GitHub pour nginx-module-hmac-secure-link.