hmac-secure-link: Módulo alternativo de NGINX HMAC Secure Link 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 secure_link estándar. 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 de forma predeterminada 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 precompilados (Ubuntu / Debian)
Los paquetes precompilados 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 obligatorio 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_hmacse 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 no contienen 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 codificar en URL la coma al colocar una fecha RFC 7231 en un parámetro de consulta para que$arg_tsresuelva a la cadena de fecha completa decodificada (ver Formatos de Timestamp).
secure_link_hmac_message
Contexto: http, server, location
El mensaje cuyo HMAC se va a verificar. 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 exigencia 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 de forma natural 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";
secure_link_hmac_secret
Contexto: http, server, location
La clave secreta HMAC. Mantén esto fuera del control de versiones.
secure_link_hmac_secret "my_very_secret_key";
secure_link_hmac_algorithm
Contexto: http, server, location
Predeterminado: sha256
El nombre del digest de OpenSSL utilizado para el HMAC.
secure_link_hmac_algorithm sha256;
Variables Embebidas
$secure_link_hmac
Se establece 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, malformado, desajuste de HMAC o timestamp inválido |
Usa esta variable para restringir el acceso. En producción, devuelve el mismo código de error para todos los casos de fallo para que un atacante no pueda distinguir entre un token expirado y uno forjado:
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"".
$secure_link_hmac_expires
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 había una expiración presente en secure_link_hmac.
Se puede usar 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 había un campo de expiración presente, la variable no está establecida (not_found).
- Esta variable se pobla como un efecto secundario de evaluar
$secure_link_hmac; evalúa$secure_link_hmacprimero.
$secure_link_hmac_token
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 les resulte 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 de Nginx $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 (Mon–Sun) 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 compatibles; solo se acepta el formato IMF-fixdate preferido.
Timestamp Unix (entero simple)
Una cadena de dígitos decimales que representa 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/=+$//; # eliminar 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, '='); // eliminar 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, '=');
// Codificar en URL 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 Z UTC)
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_iso8601emite 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 de 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 están
concatenando. 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 corto de expires 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 forjado.
- 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.
- Codifica en URL los valores de timestamp que contengan 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.