Usar URLs assinadas para proteger seu conteúdo na CDN
Sua aplicação pode autorizar um download sem tornar o arquivo público. Com o AWS CloudFront, seu servidor assina a URL de um objeto e define sua expiração. Esta receita fornece um programa executável em TypeScript para gerar assinaturas e uma verificação de que a URL entrega os bytes esperados enquanto outras vias de acesso permanecem bloqueadas.
Entender a segurança de CDN e as URLs assinadas
Uma URL assinada é uma credencial ao portador: qualquer pessoa que a receba pode usá-la até que expire. Autorize o usuário na sua aplicação antes de emitir a URL e escolha o objeto no servidor, em vez de assinar URLs arbitrárias fornecidas por um cliente.
O CloudFront verifica a assinatura com uma chave pública em um grupo de chaves confiável associado ao comportamento de cache correspondente. A chave privada correspondente permanece no seu servidor. O exemplo usa uma política predefinida, que vincula o acesso a uma única URL e a um horário de término; ela não vincula o download à pessoa que o solicitou. Veja como funcionam as URLs assinadas do CloudFront.
Separar a autorização do usuário final do acesso à origem
Dois controles precisam estar alinhados. O grupo de chaves confiável controla as requisições dos usuários finais ao CloudFront. Para uma origem S3 privada, um controle de acesso à origem (OAC) e uma política de bucket permitem que o CloudFront leia o objeto, enquanto as requisições anônimas diretas ao S3 continuam sendo negadas. Configurar apenas um dos controles não estabelece ambas as proteções.
Esta página se concentra no programa de assinatura da aplicação. Para entender a infraestrutura,
veja nosso guia de CDN de imagens com S3 e CloudFront.
Use as instruções de configuração de OAC da AWS
para a política de origem e sua condição AWS:SourceArn específica da distribuição.
Use um bucket S3 comum como origem; um endpoint de site do S3 não pode usar OAC.
Pré-requisitos para a implementação
Tenha estes itens prontos antes de executar o programa de assinatura:
- Uma distribuição do CloudFront implantada, com uma URL de objeto HTTPS e um objeto privado conhecido no S3. Mantenha uma cópia local desse objeto para a comparação de bytes abaixo.
- Um par de chaves RSA-2048 dedicado, em formato PEM. Faça o upload da chave pública para o CloudFront, coloque-a em um grupo de chaves confiável e associe esse grupo ao comportamento de cache que serve o objeto. Siga as instruções da AWS sobre pares de chaves e grupos de chaves confiáveis.
- O ID da chave pública e o arquivo da chave privada correspondente, que deve ser legível apenas pela conta do servidor. O ID da chave não é uma chave de acesso do IAM nem um ID de grupo de chaves. A assinatura local não exige chave de acesso da AWS nem perfil de credenciais do SDK.
- Um bucket privado com o S3 Block Public Access ativado, OAC configurado para sempre assinar as requisições à origem e uma política de bucket que conceda acesso de leitura a essa distribuição. Aguarde a implantação antes de testar. Mantenha sincronizado o relógio do servidor de assinatura.
- Node.js 26 e Corepack instalado separadamente. Use uma versão de
correção do Node.js que ainda receba manutenção. As versões usadas para reprodução são
Node.js 26.8.1, Yarn 4.12.0 e
@aws-sdk/cloudfront-signer3.1069.0; essas são versões testadas, não requisitos mínimos intrínsecos.
Implementar URLs assinadas com o AWS CloudFront
Execute esta configuração no Bash a partir de um diretório com permissão de escrita. Ela cria
cloudfront-url-demo e mantém seu shell no diretório original. Se o diretório já existir,
a execução será recusada; escolha um novo nome em vez de sobrescrever um projeto. O arquivo de
trava de dependências do projeto criado e as configurações locais do Yarn isolam a instalação de
pacotes de um espaço de trabalho que o contenha, inclusive um que use Plug’n’Play.
(
command -v node >/dev/null &&
command -v corepack >/dev/null &&
mkdir cloudfront-url-demo &&
cd cloudfront-url-demo &&
printf '%s\n' '{"private":true,"type":"module","packageManager":"yarn@4.12.0","dependencies":{"@aws-sdk/cloudfront-signer":"3.1069.0"}}' > package.json &&
printf '%s\n' 'nodeLinker: node-modules' 'ignorePath: true' 'enableGlobalCache: false' 'plugins: []' > .yarnrc.yml &&
touch yarn.lock &&
corepack yarn install
)
Se a instalação falhar, mantenha o projeto e examine o erro do gerenciador de pacotes. Depois de
corrigir a causa, execute corepack yarn install dentro de
cloudfront-url-demo; não repita a etapa de criação do diretório. Salve este programa
completo como cloudfront-url-demo/signed-url.mts:
import { createPrivateKey } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { isAbsolute } from 'node:path'
import { getSignedUrl } from '@aws-sdk/cloudfront-signer'
async function main(): Promise<void> {
const args = process.argv.slice(2)
const [resourceUrl, lifetime = '300'] = args
const keyPairId = process.env.CLOUDFRONT_KEY_PAIR_ID
const keyFile = process.env.CLOUDFRONT_PRIVATE_KEY_FILE
if (args.length < 1 || args.length > 2 || !resourceUrl || !keyPairId || !keyFile) {
throw new Error('Missing configuration')
}
if (!/^[A-Za-z0-9]+$/u.test(keyPairId) || !isAbsolute(keyFile)) {
throw new Error('Invalid key configuration')
}
const url = new URL(resourceUrl)
if (
url.protocol !== 'https:' || url.username || url.password || url.search || url.hash ||
url.href !== resourceUrl
) {
throw new Error('Use an exact HTTPS object URL without a query or fragment')
}
const seconds = Number(lifetime)
if (!/^\d+$/u.test(lifetime) || !Number.isSafeInteger(seconds) || seconds < 1 || seconds > 3600) {
throw new Error('Lifetime must be an integer from 1 to 3600 seconds')
}
const privateKey = await readFile(keyFile, 'utf8')
const key = createPrivateKey(privateKey)
if (key.asymmetricKeyType !== 'rsa' || key.asymmetricKeyDetails?.modulusLength !== 2048) {
throw new Error('Use an RSA-2048 private key')
}
const signedUrl = getSignedUrl({
url: resourceUrl,
keyPairId,
privateKey,
dateLessThan: new Date(Date.now() + seconds * 1000).toISOString(),
})
process.stdout.write(`${signedUrl}\n`)
}
main().catch(() => {
console.error('Could not sign URL. Check the HTTPS URL, key ID, RSA-2048 PEM file, and 1–3600 second lifetime.')
process.exitCode = 1
})
Substitua os três valores de configuração pelos seus. O segundo argumento é um prazo de validade em segundos; omiti-lo seleciona 300 segundos. Este exemplo aceita deliberadamente apenas URLs HTTPS exatas, sem strings de consulta nem fragmentos, e limita a validade a uma hora.
CLOUDFRONT_KEY_PAIR_ID='YOUR_PUBLIC_KEY_ID' \
CLOUDFRONT_PRIVATE_KEY_FILE='/absolute/path/to/private-key.pem' \
node cloudfront-url-demo/signed-url.mts \
'https://YOUR_DISTRIBUTION.cloudfront.net/private/guide.pdf' 300
Em caso de sucesso, o programa grava uma URL na saída padrão. Uma configuração inválida, uma chave ilegível ou malformada ou um prazo de validade inválido faz o processo terminar com um código diferente de zero, sem gerar uma URL. Uma chave incompatível, mas bem-formada, ainda pode assinar localmente; somente o CloudFront pode determinar se o comportamento implantado confia nela.
Testar e validar
Gerar uma string assinada não comprova que a CDN aplica as restrições. Salve este código como
cloudfront-url-demo/verify-private.mts. Ele executa o programa de assinatura, baixa a resposta completa e
a compara com o original local antes de testar as quatro vias de acesso que devem ser negadas.
Cada verificação HTTP tem um prazo de 15 segundos, incluindo o corpo da resposta, e qualquer
redirecionamento faz a verificação falhar.
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { readFile } from 'node:fs/promises'
import { setTimeout as delay } from 'node:timers/promises'
import { fileURLToPath } from 'node:url'
async function main(): Promise<void> {
const [objectUrl, directS3Url, originalFile, ...extra] = process.argv.slice(2)
assert(objectUrl && directS3Url && originalFile && extra.length === 0, 'Supply both URLs and the original file')
const expected = await readFile(originalFile)
const signerFile = fileURLToPath(new URL('./signed-url.mts', import.meta.url))
function sign(seconds: number): string {
return execFileSync(process.execPath, [signerFile, objectUrl, String(seconds)], {
encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'],
}).trim()
}
async function check(url: string, allowed: boolean): Promise<void> {
const response = await fetch(url, { redirect: 'error', signal: AbortSignal.timeout(15_000) })
const bytes = Buffer.from(await response.arrayBuffer())
if (allowed) {
assert.equal(response.status, 200, 'Valid URL was denied')
assert.equal(Buffer.compare(bytes, expected), 0, 'Downloaded bytes differ from the original')
} else {
assert.equal(response.status, 403, 'Private access was not denied')
assert.notEqual(Buffer.compare(bytes, expected), 0, 'Denied response disclosed the object')
}
}
const valid = sign(300)
await check(valid, true)
await check(objectUrl, false)
const tampered = new URL(valid)
const signature = tampered.searchParams.get('Signature')
assert(signature, 'Missing signature')
tampered.searchParams.set('Signature', `${signature[0] === 'A' ? 'B' : 'A'}${signature.slice(1)}`)
await check(tampered.href, false)
const expired = sign(1)
await delay(Math.max(0, Number(new URL(expired).searchParams.get('Expires')) * 1000 - Date.now()) + 1500)
await check(expired, false)
await check(directS3Url, false)
console.log('Private delivery checks passed.')
}
main().catch(() => {
console.error('Private delivery check failed. Check the object bytes and the CloudFront and S3 access configuration.')
process.exitCode = 1
})
Execute-o com a URL do objeto no CloudFront, a URL direta do S3 para o mesmo objeto e seu original local:
CLOUDFRONT_KEY_PAIR_ID='YOUR_PUBLIC_KEY_ID' \
CLOUDFRONT_PRIVATE_KEY_FILE='/absolute/path/to/private-key.pem' \
node cloudfront-url-demo/verify-private.mts \
'https://YOUR_DISTRIBUTION.cloudfront.net/private/guide.pdf' \
'https://YOUR_BUCKET.s3.us-east-1.amazonaws.com/private/guide.pdf' \
'/absolute/path/to/original-guide.pdf'
O script imprime Private delivery checks passed. somente depois que os bytes esperados chegam e todas as quatro requisições que devem ser negadas retornam 403 sem revelar esses bytes. A verificação de expiração inicia uma nova requisição após a expiração. O CloudFront verifica a expiração quando uma requisição começa; ele não encerra um download já em andamento quando esse horário passa.
Resolver problemas comuns
Se a requisição válida falhar, confirme primeiro o comportamento de cache implantado, o ID da chave pública do grupo confiável e a chave privada correspondente. Um 403, por si só, não permite distinguir uma assinatura inválida de um problema de acesso à origem. Verifique também o caminho do objeto, o OAC e a política do bucket. O guia da AWS para solucionar erros 403 detalha essas verificações.
Se a requisição válida retornar bytes diferentes, examine a URL do objeto e o original local e, em seguida, verifique se há uma versão mais antiga do objeto em cache. Se uma requisição sem assinatura tiver sucesso, examine o comportamento de cache que de fato corresponde àquele caminho. Se o acesso direto ao S3 tiver sucesso, corrija a política de acesso do bucket antes de emitir links de download. O verificador testa esse único objeto e caminho da distribuição; repita o teste para outros comportamentos protegidos.
Boas práticas de segurança
Mantenha a chave privada fora dos bundles do navegador, do controle de versão e dos logs. Monte-a como um arquivo com acesso restrito a partir do armazenamento de segredos do servidor. Trate a URL assinada também como um segredo: evite registrá-la em ferramentas de análise, logs de requisições ou mensagens de suporte. Compartilhe-a por HTTPS somente com o destinatário autorizado.
Escolha um prazo de validade curto que deixe tempo suficiente para iniciar o download. Para a rotação, implante a nova chave pública junto com a antiga antes de alterar o programa de assinatura; mantenha a chave antiga até que as URLs ainda válidas tenham expirado, a menos que você pretenda revogá-las. A AWS documenta esse período de coexistência em seu procedimento de rotação de chaves.
Depois que a entrega privada passar na validação, conecte o programa de assinatura à decisão de autorização da sua aplicação. Retorne uma URL somente para o objeto ao qual o usuário autenticado tem direito de acesso e mantenha as mesmas verificações de entrega ao alterar a distribuição ou sua política de origem.
