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_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 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_tsse resuelva a la cadena de fecha completa decodificada (ver Formatos de Timestamp).
secure_link_hmac_message
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";
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 Integradas
$secure_link_hmac
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"".
$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 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_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 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 (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 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_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 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.