Saltar a contenido

hmac-secure-link: Módulo alternativo de enlace seguro HMAC de NGINX con soporte para hashes de OpenSSL

Instalación

Puedes instalar este módulo en cualquier distribución basada en RHEL, incluyendo, pero no limitado a:

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

Habilita el módulo añadiendo lo siguiente en la parte superior de /etc/nginx/nginx.conf:

load_module modules/ngx_http_hmac_secure_link_module.so;

Este documento describe nginx-module-hmac-secure-link v2.0.0 lanzado el 02 de abril de 2026.


Descripción

El Módulo HMAC Secure Link de Nginx mejora la seguridad y funcionalidad del módulo estándar secure_link. Se crean tokens seguros utilizando una construcción HMAC adecuada (RFC 2104) con cualquier algoritmo de hash soportado por OpenSSL 3.x. Los algoritmos disponibles dependen de los proveedores cargados en tu configuración de OpenSSL.

Proveedor predeterminado (disponible de forma predeterminada en cualquier instalación de 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.

Proveedor legado (requiere que el proveedor legado de OpenSSL sea cargado explícitamente en openssl.cnf; no disponible por defecto en OpenSSL 3.x): md4, mdc2, rmd160, gost.

El algoritmo recomendado es sha256 o más fuerte. md5 y sha1 son aceptados pero no deberían ser utilizados en nuevas implementaciones.

El HMAC se calcula como H(secret ⊕ opad, H(secret ⊕ ipad, message)) en lugar del inseguro MD5(secret, message, expire) utilizado por el módulo incorporado.

Paquetes Preconstruidos (Ubuntu / Debian)

Los paquetes preconstruidos para este módulo están disponibles de forma gratuita en el repositorio de GetPageSpeed:

# Agrega el repositorio (ejemplo de Ubuntu — reemplaza 'jammy' por tu versión)
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

## Directivas de Configuración

Todas las directivas aceptan variables de NGINX y valores complejos.

### `secure_link_hmac`

**Contexto:** `http`, `server`, `location`

Especifica la expresión de variable cuyo valor evaluado debe seguir el
formato `<token>,<timestamp>[,<expires>]`. **El separador de campo es siempre
una coma y es requerido entre cada campo.** La coma está codificada en
el analizador del módulo; no se admite ningún otro separador aquí.

| Campo       | Descripción                                               |
|-------------|-----------------------------------------------------------|
| `token`     | HMAC codificado en Base64url (sin relleno `=`)           |
| `timestamp` | Hora de creación de la solicitud (ver [Formatos de Timestamp](#timestamp-formats)) |
| `expires`   | Tiempo de vida opcional en segundos; omitir o usar `0` para ilimitado |

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

Importante: Cuando secure_link_hmac se ensambla a partir de parámetros de consulta ("$arg_st,$arg_ts,$arg_e"), los valores de timestamp y expires no deben contener comas sin escapar. Los timestamps ISO 8601 y Unix son libres de comas y funcionan sin manejo especial. Las fechas RFC 7231 contienen una coma incrustada (por ejemplo, Sun, 06 Nov …); el módulo maneja esto correctamente para el segundo campo, pero debes URL-encode la coma al colocar una fecha RFC 7231 en un parámetro de consulta para que $arg_ts se resuelva a la cadena de fecha completa decodificada (ver Formatos de Timestamp).

Contexto: http, server, location

El mensaje cuyo HMAC debe ser verificado. Debe coincidir exactamente con lo que el cliente utilizó al calcular el token. Típicamente incluye la URI y el timestamp para que los tokens sean específicos de la URL y limitados en el tiempo.

El separador entre campos en el mensaje es libremente elegido por el operador y puede ser cualquier byte o secuencia de bytes — barra (|), dos puntos (:), barra (/), guion (-), o incluso nada en absoluto. El módulo trata secure_link_hmac_message como una cadena de bytes opaca y nunca analiza su contenido; el separador es simplemente parte de la pre-imagen del HMAC.

La única requisito es que el separador elegido en el lado del servidor sea idéntico al separador utilizado por el cliente al calcular el HMAC. Usar un separador que no pueda aparecer naturalmente en ninguno de los valores de campo (como | para URIs y timestamps Unix) reduce el riesgo de ambigüedad por extensión de longitud.

# Separador de barra (recomendado — no puede aparecer en una ruta URI o timestamp Unix)
secure_link_hmac_message "$uri|$arg_ts|$arg_e";

# Separador de dos puntos
secure_link_hmac_message "$uri:$arg_ts:$arg_e";

# Sin separador (válido, pero ambiguo si los campos comparten un conjunto de caracteres)
secure_link_hmac_message "$uri$arg_ts$arg_e";

Contexto: http, server, location

La clave secreta HMAC. Mantén esto fuera del control de versiones.

secure_link_hmac_secret "my_very_secret_key";

Contexto: http, server, location
Predeterminado: sha256

El nombre del digest de OpenSSL utilizado para el HMAC.

secure_link_hmac_algorithm sha256;

Variables Integradas

Establecido después de procesar la directiva secure_link_hmac. Valores posibles:

Valor Significado
"1" El token es criptográficamente válido y el enlace no ha expirado
"0" El token es válido pero el enlace ha expirado
(vacío) El token está ausente, es malformado, hay un desajuste de HMAC, o el timestamp es inválido

Usa esta variable para controlar el acceso. En producción, devuelve el mismo código de error para todos los casos fallidos para que un atacante no pueda distinguir entre un token expirado y uno falsificado:

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

Nota: "1" y "0" son cadenas literales de un solo carácter, no números. El caso vacío / no encontrado significa que la variable no está establecida, no que sea igual a "".

La cadena de período de expiración en bruto (en segundos) tal como se recibió en la solicitud. Esta variable solo se establece cuando hay una expiración presente en secure_link_hmac. Puede ser utilizada para registro o lógica condicional:

add_header X-Link-Expires $secure_link_hmac_expires;
  • Si el valor entrante fue "3600", esta variable contiene "3600".
  • Si no se presentó un campo de expiración, la variable no está establecida (not_found).
  • Esta variable se llena como un efecto secundario de evaluar $secure_link_hmac; evalúa $secure_link_hmac primero.

Un token HMAC recién calculado codificado en base64url (sin relleno =). Usa esta variable cuando NGINX actúe como un proxy que debe reenviar solicitudes autenticadas a 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";
}

El token está codificado en base64url sin relleno, compatible con parámetros de consulta de URL sin necesidad de más escape.

Formatos de Timestamp

Un timestamp debería incluirse siempre en el mensaje firmado para prevenir ataques de repetición. Tres formatos son aceptados por el analizador del lado del servidor. Los clientes pueden usar el que sea más conveniente.

ISO 8601 con desplazamiento UTC numérico (recomendado)

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

Ejemplos:

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

El servidor convierte a UTC antes de comparar, por lo que se acepta cualquier desplazamiento válido.

ISO 8601 UTC (sufijo Z)

YYYY-MM-DDThh:mm:ssZ

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

Equivalente a +00:00 pero más corto. La variable incorporada $time_iso8601 emite el formato +00:00; para Z debes formatear el timestamp del lado de la aplicación.

RFC 7231 / IMF-fixdate (fecha HTTP)

Como se especifica en RFC 7231 §7.1.1.1. Todas las fechas RFC 7231 son implícitamente UTC; no se aplica ningún desplazamiento.

Day, DD Mon YYYY hh:mm:ss GMT

Ejemplos:

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

Donde Day es una abreviatura de tres letras del día de la semana (MonSun) y Mon (mes) es uno de Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec. El analizador no distingue entre mayúsculas y minúsculas para ambas abreviaturas.

Nota: RFC 7231 también define dos formatos obsoletos (RFC 850 y ANSI C asctime). Esos no son soportados; solo se acepta el formato IMF-fixdate preferido.

Timestamp Unix (entero plano)

Una cadena de dígitos decimales que representan segundos desde la época Unix (1970-01-01T00:00:00Z).

Ejemplo: 1748785800

Este es el formato más simple y funciona bien en Bash y Node.js. El analizador es estricto: el campo de timestamp debe contener solo dígitos decimales; cualquier otro carácter causa que sea rechazado.

Nota de seguridad: Los timestamps Unix tienen solo una resolución de un segundo. Usa ISO 8601 si la precisión de sub-segundos es importante, o si necesitas expresar una zona horaria específica.

Ejemplo de Uso — Lado del Servidor

location ^~ /files/ {
    # Los tres campos separados por comas: token, timestamp, expires (segundos)
    secure_link_hmac "$arg_st,$arg_ts,$arg_e";

    # Clave secreta HMAC
    secure_link_hmac_secret "my_secret_key";

    # El mensaje que fue firmado: URI + timestamp + expiración
    secure_link_hmac_message "$uri|$arg_ts|$arg_e";

    # Algoritmo de hash
    secure_link_hmac_algorithm sha256;

    # En producción, no reveles si el token fue incorrecto o expirado.
    # $secure_link_hmac == "1" → válido y no expirado
    # $secure_link_hmac == "0" → válido pero expirado
    # $secure_link_hmac no establecido  → inválido / malformado
    if ($secure_link_hmac != "1") {
        return 403;
    }

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

Ejemplos del Lado del Cliente

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 con desplazamiento UTC numérico
        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/=+$//;             # quitar relleno

        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, '=');              // quitar relleno

$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 — siempre UTC, siempre sufijo "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-encode la fecha RFC 7231 (contiene espacios y comas)
$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() produce el formato RFC 7231 IMF-fixdate en todos los entornos modernos
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 (sufijo 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}"

Uso de Proxy

Cuando NGINX actúa como un proxy que debe agregar un token HMAC a las solicitudes salientes, usa 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";
}

Nota: $time_iso8601 emite un timestamp ISO 8601 con un desplazamiento UTC numérico (por ejemplo, 2025-06-01T14:30:00+00:00), que este módulo acepta.

Notas de Seguridad

Separador en secure_link_hmac El separador de campo dentro del valor de la directiva secure_link_hmac es siempre una coma. Los campos de timestamp y expires no deben contener comas desnudas (Los timestamps ISO 8601 y Unix son seguros; los timestamps RFC 7231 son manejados por la lógica interna de omisión de comas del módulo, pero la coma incrustada debe sobrevivir a la codificación/decodificación URL intacta — ver Formatos de Timestamp).

Separador en secure_link_hmac_message Elige un separador que no pueda aparecer en ninguno de los campos que se concatenan. La barra (|) es un buen valor predeterminado para combinaciones de URI + timestamp Unix. Usar ningún separador en absoluto es válido, pero puede permitir un ataque de extensión de longitud donde un conjunto válido de valores de campo se reinterpreta como un conjunto diferente; un separador previene esto.

Otras recomendaciones - Siempre incluye un timestamp en el mensaje firmado para prevenir ataques de repetición. - Elige un valor expires corto para tu caso de uso (60–3600 segundos es típico para enlaces de descarga). - Devuelve el mismo código de error HTTP (por ejemplo, 403) para todos los casos de fallo — tanto "0" (expirado) como no encontrado (inválido) — para que los atacantes no puedan distinguir un token expirado de uno falsificado. - Usa una clave secreta de al menos 32 bytes de entropía aleatoria. - Prefiere sha256 o más fuerte; evita md5 y sha1 para nuevas implementaciones. - URL-encode los valores de timestamp que contienen caracteres especiales en cadenas de consulta: - El desplazamiento UTC ISO 8601 + debe enviarse como %2B (de lo contrario se decodifica como espacio) - Los espacios RFC 7231 deben enviarse como %20 y la coma incrustada como %2C

GitHub

Puedes encontrar consejos de configuración adicionales y documentación para este módulo en el repositorio de GitHub para nginx-module-hmac-secure-link.