Protecting files behind CloudFront requires two decisions: who can read the origin, and which viewers may receive the content. A signed viewer request does not help if the same private object remains publicly readable directly from S3.
Protect the origin
The earlier experiment first made a bucket public, then added a legacy Origin Access Identity and removed the public-read grant. That intermediate public policy is omitted here. AWS now documents Origin Access Control for the S3 boundary.
Authorize the viewer
The original Nest service generated signed URLs and cookies:
nest g s aws/cloudfront
import {Injectable} from '@nestjs/common';
import CloudFront from "aws-sdk/clients/cloudfront";
import AWS from "aws-sdk";
import {Signer} from "aws-sdk/lib/cloudfront/signer";
import * as fs from 'fs';
@Injectable()
export class CloudfrontService {
cloudFront: CloudFront;
keyPairId: string;
privateKey: string;
constructor() {
this.keyPairId = process.env.AWS_CLOUDFRONT_ACCESSKEY_ID || '';
this.privateKey = fs.readFileSync(`./keys/pk-${this.keyPairId}.pem`).toString();
this.cloudFront = new CloudFront();
}
// Get specific signed url
getSignedUrl(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const expiry = Math.floor(Date.now() / 1000) + 120; // 2 minutes
const signer = new AWS.CloudFront.Signer(this.keyPairId, this.privateKey);
const options = {url: url, expires: expiry};
signer.getSignedUrl(options, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
// Get specific signed cookies
getSignedCookie(domain: string): Promise<Signer.CustomPolicy> {
return new Promise((resolve, reject) => {
const expiry = Math.floor(Date.now() / 1000) + 86400; // 1day
const policy = {
'Statement': [{
'Resource': "https://" + domain + "/*",
'Condition': {
'DateLessThan': {'AWS:EpochTime': expiry}
}
}]
};
const policyString = JSON.stringify(policy);
const signer = new AWS.CloudFront.Signer(this.keyPairId, this.privateKey);
const options = {url: "https://" + domain, policy: policyString};
signer.getSignedCookie(options, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
}
Source: signer Gist revision. Its key file remains private; no signing key is included here.
The earlier calls were:
import {NestFactory} from '@nestjs/core';import {AppModule} from './app.module';import {CloudfrontService} from "./aws/cloudfront/cloudfront.service";async function bootstrap() { const app = await NestFactory.create(AppModule); const cloudFront = app.get(CloudfrontService); const singedUrl = await cloudFront.getSignedUrl('https://d1ywg9iw63kklz.cloudfront.net/avatar.jpg'); console.log({singedUrl});}bootstrap();
const singedCookie = await cloudFront.getSignedCookie('d1ywg9iw63kklz.cloudfront.net');console.log({singedCookie});
Those distribution names record the old environment and are not presented as live destinations.
An HTML page can request more than one file
<!--index.html--><img src="./avatar.jpg">
const singedUrl = await cloudFront.getSignedUrl('https://d1ywg9iw63kklz.cloudfront.net/index.html');console.log({singedUrl});
Authorizing the HTML request does not automatically authorize the browser’s separate image request. Signed cookies can cover a policy-scoped collection of requests while retaining ordinary resource URLs. AWS describes that choice in its signed-URL versus signed-cookie guidance.
Why the old cookie bootstrap is omitted
The source later placed signed-cookie material into URL parameters, loaded a page that wrote cookies, and redirected. Those bearer values can appear in browser history, request logs, or referrers, and the JavaScript-created cookies lacked a complete security policy. The bootstrap is described rather than republished as a recommended access flow.
A single 403 does not prove every access boundary is correct, and an account-verification failure is not automatically a quota-increase problem. No CloudFront distribution, cookie, signed URL, or AWS account setting was created for this revision.