CDN-Inhalte mit signierten URLs absichern
Ihre Anwendung kann einen Download autorisieren, ohne die Datei öffentlich zugänglich zu machen. Mit AWS CloudFront signiert Ihr Server eine Objekt-URL und versieht sie mit einem Ablaufzeitpunkt. Diese Anleitung bietet ein ausführbares TypeScript-Programm zum Signieren und einen Test, der prüft, ob die URL die vorgesehenen Bytes ausliefert und andere Zugriffswege gesperrt bleiben.
CDN-Sicherheit und signierte URLs verstehen
Eine signierte URL ist ein Bearer-Zugangsnachweis: Wer sie erhält, kann sie bis zu ihrem Ablauf verwenden. Autorisieren Sie den Nutzer in Ihrer Anwendung, bevor Sie die URL ausstellen. Wählen Sie das Objekt auf dem Server aus, statt beliebige URLs zu signieren, die ein Client übermittelt.
CloudFront prüft die Signatur anhand eines öffentlichen Schlüssels in einer vertrauenswürdigen Schlüsselgruppe, die dem passenden Cache-Verhalten zugeordnet ist. Der zugehörige private Schlüssel bleibt auf Ihrem Server. Das Beispiel verwendet eine vordefinierte Richtlinie, die den Zugriff an eine URL und einen Endzeitpunkt bindet; sie bindet den Download nicht an die Person, die ihn angefordert hat. Lesen Sie, wie signierte CloudFront-URLs funktionieren.
Zugriffsberechtigung der Nutzer vom Zugriff auf den Ursprung trennen
Zwei Zugriffskontrollen müssen aufeinander abgestimmt sein. Die vertrauenswürdige Schlüsselgruppe kontrolliert Anfragen von Nutzern an CloudFront. Bei einem privaten S3-Ursprung erlauben eine Ursprungszugriffskontrolle (OAC) und eine Bucket-Richtlinie CloudFront, das Objekt zu lesen, während anonyme direkte S3-Anfragen weiterhin abgewiesen werden. Nur eine der beiden Kontrollen zu konfigurieren, stellt nicht beide Schutzmaßnahmen sicher.
Diese Seite konzentriert sich auf das Signieren in der Anwendung. Hintergrundwissen zur
Infrastruktur finden Sie in unserer
Anleitung für ein Bilder-CDN mit S3 und CloudFront.
Nutzen Sie die OAC-Einrichtungsanleitung von AWS
für die Ursprungsrichtlinie und ihre distributionsspezifische Bedingung AWS:SourceArn.
Verwenden Sie einen regulären S3-Bucket als Ursprung; ein S3-Website-Endpunkt kann OAC nicht nutzen.
Voraussetzungen für die Implementierung
Halten Sie Folgendes bereit, bevor Sie das Programm zum Signieren ausführen:
- Eine bereitgestellte CloudFront-Distribution mit einer HTTPS-Objekt-URL und einem bekannten privaten Objekt in S3. Bewahren Sie für den folgenden Bytevergleich eine lokale Kopie dieses Objekts auf.
- Ein dediziertes RSA-2048-Schlüsselpaar im PEM-Format. Laden Sie den öffentlichen Schlüssel zu CloudFront hoch, fügen Sie ihn einer vertrauenswürdigen Schlüsselgruppe hinzu und ordnen Sie diese Gruppe dem Cache-Verhalten zu, das das Objekt ausliefert. Folgen Sie der AWS-Anleitung für Schlüsselpaare und vertrauenswürdige Schlüsselgruppen.
- Die ID des öffentlichen Schlüssels und die passende private Schlüsseldatei, die nur das Serverkonto lesen darf. Die Schlüssel-ID ist weder ein IAM-Zugriffsschlüssel noch eine Schlüsselgruppen-ID. Zum lokalen Signieren sind weder ein AWS-Zugriffsschlüssel noch ein SDK-Zugangsdatenprofil erforderlich.
- Einen privaten Bucket mit aktiviertem S3 Block Public Access, einer OAC-Konfiguration, die Ursprungsanfragen immer signiert, und einer Bucket-Richtlinie, die dieser Distribution Lesezugriff gewährt. Warten Sie vor dem Testen, bis die Bereitstellung abgeschlossen ist. Halten Sie die Uhr des signierenden Servers synchronisiert.
- Node.js 26 und separat installiertes Corepack. Verwenden Sie eine
gepflegte Node.js-Patchversion. Die Versionen zur Reproduktion sind Node.js 26.8.1, Yarn 4.12.0 und
@aws-sdk/cloudfront-signer3.1069.0; dies sind getestete Versionen, keine zwingenden Mindestversionen.
Signierte URLs mit AWS CloudFront implementieren
Führen Sie diese Einrichtung in Bash aus einem beschreibbaren Verzeichnis aus. Sie erstellt
cloudfront-url-demo und belässt Ihre Shell im ursprünglichen Verzeichnis. Ein bereits vorhandenes
Verzeichnis wird abgelehnt; wählen Sie einen neuen Namen, statt ein Projekt zu überschreiben.
Die Lockdatei im Unterverzeichnis und die lokalen Yarn-Einstellungen isolieren die Paketinstallation
vom übergeordneten Workspace, auch wenn dieser Plug’n’Play verwendet.
(
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
)
Wenn die Installation fehlschlägt, behalten Sie das Projekt und untersuchen Sie den Fehler des
Paketmanagers. Nachdem Sie die Ursache behoben haben, führen Sie corepack yarn install in
cloudfront-url-demo aus; wiederholen Sie nicht den Schritt zur Verzeichniserstellung. Speichern
Sie dieses vollständige Programm als 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
})
Ersetzen Sie die drei Konfigurationswerte durch Ihre eigenen. Das zweite Argument ist eine Gültigkeitsdauer in Sekunden; ohne dieses Argument werden 300 Sekunden verwendet. Dieses Beispiel akzeptiert bewusst nur exakte HTTPS-URLs ohne Abfragezeichenfolgen oder Fragmente und begrenzt die Gültigkeitsdauer auf eine Stunde.
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
Bei Erfolg wird eine URL in die Standardausgabe geschrieben. Eine ungültige Konfiguration, ein unlesbarer oder fehlerhaft formatierter Schlüssel oder eine ungültige Gültigkeitsdauer führen zu einem Exit-Code ungleich null, ohne dass eine URL ausgegeben wird. Mit einem unpassenden, aber korrekt formatierten Schlüssel lässt sich dennoch lokal signieren; nur CloudFront kann feststellen, ob das bereitgestellte Cache-Verhalten diesem Schlüssel vertraut.
Tests und Validierung
Eine signierte Zeichenfolge zu erzeugen, beweist nicht, dass das CDN die Zugriffsbeschränkungen
durchsetzt. Speichern Sie dies als cloudfront-url-demo/verify-private.mts. Das Skript führt das Programm zum
Signieren aus, lädt die vollständige Antwort herunter und vergleicht sie mit Ihrem lokalen Original,
bevor es die vier gesperrten Zugriffswege testet. Für jede HTTP-Prüfung gilt eine Frist von
15 Sekunden, einschließlich des Antwortkörpers. Weiterleitungen lassen die Prüfung fehlschlagen.
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
})
Führen Sie es mit Ihrer CloudFront-Objekt-URL, der direkten S3-URL für dasselbe Objekt und dessen lokalem Original aus:
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'
Das Skript gibt Private delivery checks passed. erst aus, nachdem die vorgesehenen Bytes angekommen sind und alle vier abzuweisenden Anfragen den Status 403 zurückgegeben haben, ohne diese Bytes offenzulegen. Die Ablaufprüfung startet nach dem Ablauf eine neue Anfrage. CloudFront prüft den Ablaufzeitpunkt beim Beginn einer Anfrage; ein bereits laufender Download wird nicht beendet, wenn dieser Zeitpunkt verstreicht.
Häufige Probleme beheben
Wenn die gültige Anfrage fehlschlägt, prüfen Sie zuerst das bereitgestellte Cache-Verhalten, die ID des öffentlichen Schlüssels der vertrauenswürdigen Gruppe und den passenden privaten Schlüssel. Ein Status 403 allein lässt nicht erkennen, ob eine ungültige Signatur oder ein Problem beim Zugriff auf den Ursprung vorliegt. Prüfen Sie auch den Objektpfad, OAC und die Bucket-Richtlinie. Die AWS-Anleitung zur Fehlerbehebung bei Status 403 führt Sie durch diese Prüfungen.
Wenn die gültige Anfrage andere Bytes zurückgibt, prüfen Sie die Objekt-URL und das lokale Original und suchen Sie anschließend nach einem älteren Objekt im Cache. Wenn eine unsignierte Anfrage erfolgreich ist, prüfen Sie das Cache-Verhalten, das tatsächlich auf diesen Pfad zutrifft. Wenn der direkte S3-Zugriff erfolgreich ist, korrigieren Sie die Zugriffsrichtlinie des Buckets, bevor Sie Downloadlinks ausstellen. Das Prüfprogramm testet dieses eine Objekt und den zugehörigen Pfad durch die Distribution; wiederholen Sie den Test für andere geschützte Cache-Verhalten.
Bewährte Verfahren für die Sicherheit
Halten Sie den privaten Schlüssel aus Browser-Bundles, der Versionsverwaltung und Protokollen heraus. Binden Sie ihn aus dem Speicher für sensible Zugangsdaten Ihres Servers als Datei mit Zugriffsbeschränkungen ein. Behandeln Sie auch die signierte URL als sensible Zugangsinformation: Vermeiden Sie es, sie in Analysedaten, Anfrageprotokollen oder Supportnachrichten aufzuzeichnen. Geben Sie sie nur über HTTPS an den autorisierten Empfänger weiter.
Wählen Sie eine kurze Gültigkeitsdauer, die genug Zeit lässt, den Download zu beginnen. Stellen Sie zur Schlüsselrotation den neuen öffentlichen Schlüssel neben dem alten bereit, bevor Sie das Programm zum Signieren umstellen. Behalten Sie den alten Schlüssel bei, bis noch ausstehende URLs abgelaufen sind, sofern Sie diese nicht widerrufen möchten. AWS dokumentiert diese Überschneidung im Verfahren zur Schlüsselrotation.
Sobald die Tests zur privaten Auslieferung erfolgreich sind, verknüpfen Sie das Signieren mit der Autorisierungsentscheidung Ihrer Anwendung. Geben Sie eine URL nur für das Objekt zurück, auf das der authentifizierte Nutzer zugreifen darf. Behalten Sie dieselben Prüfungen zur Auslieferung bei, wenn Sie die Distribution oder ihre Ursprungsrichtlinie ändern.
