hmac-secure-link: Módulo alternativo NGINX HMAC Secure Link com suporte para hashes OpenSSL
Instalação
Você pode instalar este módulo em qualquer distribuição baseada em RHEL, incluindo, mas não se limitando a:
- RedHat Enterprise Linux 7, 8, 9 e 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 e 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
Ative o módulo adicionando o seguinte no topo de /etc/nginx/nginx.conf:
load_module modules/ngx_http_hmac_secure_link_module.so;
Este documento descreve o nginx-module-hmac-secure-link v2.0.0 lançado em 02 de abril de 2026.
Descrição
O Módulo HMAC Secure Link do Nginx aprimora a segurança e a funcionalidade do módulo secure_link padrão. Tokens seguros são criados usando uma construção HMAC adequada (RFC 2104) com qualquer algoritmo de hash suportado pelo OpenSSL 3.x. Os algoritmos disponíveis dependem dos provedores carregados na sua configuração do OpenSSL.
Provedor padrão (disponível por padrão em qualquer instalação do 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.
Provedor legado (exige que o provedor legado do OpenSSL seja explicitamente carregado em openssl.cnf; não disponível por padrão no OpenSSL 3.x):
md4, mdc2, rmd160, gost.
O algoritmo recomendado é sha256 ou mais forte. md5 e sha1 são aceitos, mas não devem ser usados em novas implementações.
O HMAC é calculado como H(secret ⊕ opad, H(secret ⊕ ipad, message)) em vez do inseguro MD5(secret, message, expire) usado pelo módulo embutido.
Pacotes Pré-compilados (Ubuntu / Debian)
Pacotes pré-compilados para este módulo estão disponíveis gratuitamente no repositório GetPageSpeed:
## Adicione o repositório (exemplo para Ubuntu — substitua 'jammy' pela sua versão)
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
## Diretrizes de Configuração
Todas as diretrizes aceitam variáveis NGINX e valores complexos.
### `secure_link_hmac`
**Contexto:** `http`, `server`, `location`
Especifica a expressão da variável cujo valor avaliado deve seguir o formato `<token>,<timestamp>[,<expires>]`. **O separador de campo é sempre uma vírgula e é necessário entre cada campo.** A vírgula é codificada no analisador do módulo; nenhum outro separador é suportado aqui.
| Campo | Descrição |
|-------------|---------------------------------------------------------|
| `token` | HMAC codificado em Base64url (sem preenchimento `=`) |
| `timestamp` | Hora de criação do pedido (veja [Formatos de Timestamp](#timestamp-formats)) |
| `expires` | Tempo de vida opcional em segundos; omita ou use `0` para ilimitado |
```nginx
secure_link_hmac "$arg_st,$arg_ts,$arg_e";
Importante: Quando
secure_link_hmacé montado a partir de parâmetros de consulta ("$arg_st,$arg_ts,$arg_e"), os valores de timestamp e expires não devem conter vírgulas não escapadas. Timestamps ISO 8601 e Unix não contêm vírgulas e funcionam sem tratamento especial. Datas RFC 7231 contêm uma vírgula embutida (por exemplo,Dom, 06 Nov …); o módulo lida com isso corretamente para o segundo campo, mas você deve codificar a vírgula em URL ao colocar uma data RFC 7231 em um parâmetro de consulta para que$arg_tsresolva para a string de data decodificada completa (veja Formatos de Timestamp).
secure_link_hmac_message
Contexto: http, server, location
A mensagem cujo HMAC deve ser verificado. Deve corresponder exatamente ao que o cliente usou ao calcular o token. Normalmente inclui o URI e o timestamp para que os tokens sejam específicos de URL e limitados no tempo.
O separador entre os campos na mensagem é livremente escolhido pelo operador e pode ser qualquer byte ou sequência de bytes — pipe (|), dois pontos (:), barra (/), hífen (-), ou até mesmo nada. O módulo trata secure_link_hmac_message como uma string de bytes opaca e nunca analisa seu conteúdo; o separador é simplesmente parte da pré-imagem do HMAC.
A única exigência é que o separador escolhido no lado do servidor seja idêntico ao separador usado pelo cliente ao calcular o HMAC. Usar um separador que não pode aparecer naturalmente em nenhum dos valores de campo (como | para URIs e timestamps Unix) reduz o risco de ambiguidade de extensão de comprimento.
## Separador pipe (recomendado — não pode aparecer em um caminho de URI ou timestamp Unix)
secure_link_hmac_message "$uri|$arg_ts|$arg_e";
## Separador dois pontos
secure_link_hmac_message "$uri:$arg_ts:$arg_e";
## Sem separador (válido, mas ambíguo se os campos compartilharem um conjunto de caracteres)
secure_link_hmac_message "$uri$arg_ts$arg_e";
secure_link_hmac_secret
Contexto: http, server, location
A chave secreta HMAC. Mantenha isso fora do controle de versão.
secure_link_hmac_secret "my_very_secret_key";
secure_link_hmac_algorithm
Contexto: http, server, location
Padrão: sha256
O nome do digest do OpenSSL usado para o HMAC.
secure_link_hmac_algorithm sha256;
Variáveis Embutidas
$secure_link_hmac
Definido após o processamento da diretiva secure_link_hmac. Valores possíveis:
| Valor | Significado |
|---|---|
"1" |
Token é criptograficamente válido e o link não expirou |
"0" |
Token é válido, mas o link expirou |
| (vazio) | Token está ausente, malformado, incompatibilidade de HMAC ou timestamp inválido |
Use esta variável para controlar o acesso. Em produção, retorne o mesmo código de erro para todos os casos de falha para que um atacante não possa distinguir entre um token expirado e um forjado:
if ($secure_link_hmac != "1") {
return 403;
}
Nota:
"1"e"0"são strings literais de um único caractere, não números. O caso vazio / não encontrado significa que a variável não está definida, não que ela é igual a"".
$secure_link_hmac_expires
A string bruta do período de expiração (em segundos) conforme recebida na solicitação. Esta variável só é definida quando uma expiração estava presente em secure_link_hmac. Pode ser usada para registro ou lógica condicional:
add_header X-Link-Expires $secure_link_hmac_expires;
- Se o valor recebido foi
"3600", esta variável contém"3600". - Se nenhum campo de expiração estava presente, a variável não está definida (not_found).
- Esta variável é populada como um efeito colateral da avaliação de
$secure_link_hmac; avalie$secure_link_hmacprimeiro.
$secure_link_hmac_token
Um token HMAC codificado em base64url recém-computado (sem preenchimento = no final). Use esta variável quando o NGINX atua como um proxy que deve encaminhar solicitações autenticadas para um 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";
}
O token é codificado em base64url sem preenchimento, compatível com parâmetros de consulta de URL sem necessidade de escape adicional.
Formatos de Timestamp
Um timestamp deve sempre ser incluído na mensagem assinada para prevenir ataques de repetição. Três formatos são aceitos pelo analisador do lado do servidor. Os clientes podem usar o que for mais conveniente.
ISO 8601 com deslocamento UTC numérico (recomendado)
YYYY-MM-DDThh:mm:ss+HH:MM
YYYY-MM-DDThh:mm:ss-HH:MM
Exemplos:
2025-06-01T14:30:00+00:00 # UTC
2025-06-01T17:30:00+03:00 # UTC+3 (Kiev/Istambul)
2025-06-01T08:30:00-06:00 # UTC-6 (Chicago CDT)
O servidor converte para UTC antes de comparar, então qualquer deslocamento válido é aceito.
ISO 8601 UTC (sufixo Z)
YYYY-MM-DDThh:mm:ssZ
Exemplo: 2025-06-01T14:30:00Z
Equivalente a +00:00, mas mais curto. A variável embutida $time_iso8601 do Nginx emite o formato +00:00; para Z você deve formatar o timestamp do lado da aplicação.
RFC 7231 / IMF-fixdate (data HTTP)
Conforme especificado na RFC 7231 §7.1.1.1. Todas as datas RFC 7231 são implicitamente UTC; nenhum deslocamento é aplicado.
Dia, DD Mon YYYY hh:mm:ss GMT
Exemplos:
Dom, 01 Jun 2025 14:30:00 GMT
Seg, 23 Mar 2026 08:00:00 GMT
Onde Dia é uma abreviação de dia da semana de três letras (Seg–Dom) e Mon (mês) é um dos Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec. O analisador não diferencia maiúsculas de minúsculas para ambas as abreviações.
Nota: A RFC 7231 também define dois formatos obsoletos (RFC 850 e ANSI C
asctime). Esses não são suportados; apenas o formato preferido IMF-fixdate é aceito.
Timestamp Unix (inteiro simples)
Uma string de dígitos decimais representando segundos desde a época Unix (1970-01-01T00:00:00Z).
Exemplo: 1748785800
Este é o formato mais simples e funciona bem em Bash e Node.js. O analisador é rigoroso: o campo de timestamp deve conter apenas dígitos decimais; qualquer outro caractere faz com que seja rejeitado.
Nota de segurança: Timestamps Unix têm apenas uma resolução de um segundo. Use ISO 8601 se a precisão de sub-segundos for importante ou se você precisar expressar um fuso horário específico.
Exemplo de Uso — Lado do Servidor
location ^~ /files/ {
# Os três campos separados por vírgula: token, timestamp, expires (segundos)
secure_link_hmac "$arg_st,$arg_ts,$arg_e";
# Chave secreta HMAC
secure_link_hmac_secret "my_secret_key";
# A mensagem que foi assinada: URI + timestamp + expiração
secure_link_hmac_message "$uri|$arg_ts|$arg_e";
# Algoritmo de hash
secure_link_hmac_algorithm sha256;
# Em produção, não revele se o token estava errado ou expirado.
# $secure_link_hmac == "1" → válido e não expirado
# $secure_link_hmac == "0" → válido, mas expirado
# $secure_link_hmac não definido → inválido / malformado
if ($secure_link_hmac != "1") {
return 403;
}
rewrite ^/files/(.*)$ /files/$1 break;
}
Exemplos do Lado do 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 com deslocamento 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/=+$//; # remove preenchimento
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, '='); // remove preenchimento
$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 — sempre UTC, sempre sufixo "GMT"
$timestamp = gmdate('D, d M Y H:i:s') . ' GMT'; // "Dom, 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 a data RFC 7231 (contém espaços e vírgulas)
$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() produz o formato RFC 7231 IMF-fixdate em todos os ambientes modernos
const timestamp = new Date().toUTCString(); // "Dom, 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 (sufixo 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
Quando o NGINX atua como um proxy que deve adicionar um token HMAC a solicitações de saída, use a variável $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 um timestamp ISO 8601 com um deslocamento UTC numérico (por exemplo,2025-06-01T14:30:00+00:00), que este módulo aceita.
Notas de Segurança
Separador em secure_link_hmac
O separador de campo dentro do valor da diretiva secure_link_hmac é sempre uma vírgula. Os campos de timestamp e expires não devem conter vírgulas soltas (timestamps ISO 8601 e Unix são seguros; timestamps RFC 7231 são tratados pela lógica interna de pular vírgulas do módulo, mas a vírgula embutida deve sobreviver à codificação/decodificação de URL intacta — veja Formatos de Timestamp).
Separador em secure_link_hmac_message
Escolha um separador que não possa aparecer em nenhum dos campos que estão sendo concatenados. Pipe (|) é um bom padrão para combinações de URI + timestamps Unix. Usar nenhum separador é válido, mas pode permitir um ataque de extensão de comprimento onde um conjunto válido de valores de campo é reinterpretado como um conjunto diferente; um separador previne isso.
Outras recomendações
- Sempre inclua um timestamp na mensagem assinada para prevenir ataques de repetição.
- Escolha um valor expires curto para seu caso de uso (60–3600 segundos é típico para links de download).
- Retorne o mesmo código de erro HTTP (por exemplo, 403) para todos os casos de falha — tanto "0" (expirado) quanto não encontrado (inválido) — para que atacantes não possam distinguir um token expirado de um forjado.
- Use uma chave secreta de pelo menos 32 bytes de entropia aleatória.
- Prefira sha256 ou mais forte; evite md5 e sha1 para novas implementações.
- Codifique em URL valores de timestamp que contêm caracteres especiais em strings de consulta:
- O deslocamento UTC ISO 8601 + deve ser enviado como %2B (caso contrário, decodificado como espaço)
- Espaços RFC 7231 devem ser enviados como %20 e a vírgula embutida como %2C
GitHub
Você pode encontrar dicas adicionais de configuração e documentação para este módulo no repositório GitHub do nginx-module-hmac-secure-link.