Saltar a contenido

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_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 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_ts resuelva a la cadena de fecha completa decodificada (ver Formatos de Timestamp).

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

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 Embebidas

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 "".

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_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 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 (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 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_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 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.