🌩️ feat: Strict CloudFront signed cookie enforcement via requireSignedAccess (#13078)

* feat(cloudfront): add requireSignedAccess to enforce strict signed access

Introduces cloudfront.requireSignedAccess (default false). When enabled,
initializeCloudFront requires both CLOUDFRONT_KEY_PAIR_ID and
CLOUDFRONT_PRIVATE_KEY, rejects the unimplemented imageSigning="url"
mode, and initializeFileStorage throws to block startup on any
CloudFront init failure. OSS path is unchanged: missing keys still
log-and-continue when requireSignedAccess is false.

Adds low-noise startup and cookie-issuance logs without leaking signed
URLs, policies, signatures, private keys, or cookie values.

* fix(cloudfront): reject requireSignedAccess unless imageSigning is "cookies"

Previously requireSignedAccess=true was accepted with imageSigning="none"
or "url", but setCloudFrontCookies() only runs for "cookies" — leaving
strict mode toothless: CloudFront stayed publicly accessible, or image
delivery broke on a distribution that actually requires signed access.

Adds a Zod refinement plus a runtime guard in initializeCloudFront so
the only currently-functional strict configuration is imageSigning
"cookies". Signed URL mode can lift this restriction once implemented.

* fix(cloudfront): resolve strict access type checks

* chore(cloudfront): reduce strict startup log noise

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Ravi Kumar L 2026-05-12 05:30:01 +02:00 committed by GitHub
parent 7b9a57a467
commit 05d4e90f91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 181 additions and 3 deletions

View file

@ -21,6 +21,7 @@ jest.mock('~/cdn/cloudfront', () => ({
}));
import type { AppConfig } from '@librechat/data-schemas';
import type { CloudFrontConfig } from 'librechat-data-provider';
import { initializeFileStorage } from '../cdn';
import { initializeFirebase } from '~/cdn/firebase';
import { initializeAzureBlobService } from '~/cdn/azure';
@ -33,6 +34,23 @@ const baseAppConfig: AppConfig = {
imageOutputType: 'png',
};
type RequiredCloudFrontConfig = NonNullable<CloudFrontConfig>;
function makeCloudFrontConfig(
overrides: Partial<RequiredCloudFrontConfig> = {},
): RequiredCloudFrontConfig {
return {
domain: 'https://d123.cloudfront.net',
invalidateOnDelete: false,
imageSigning: 'none',
urlExpiry: 3600,
cookieExpiry: 1800,
includeRegionInPath: false,
requireSignedAccess: false,
...overrides,
};
}
describe('initializeFileStorage', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -88,7 +106,7 @@ describe('initializeFileStorage', () => {
});
it('initializes CloudFront with config when fileStrategy is cloudfront', () => {
const cloudfrontConfig = { domain: 'https://d123.cloudfront.net' };
const cloudfrontConfig = makeCloudFrontConfig();
const appConfig = {
...baseAppConfig,
fileStrategy: FileSources.cloudfront,
@ -109,7 +127,7 @@ describe('initializeFileStorage', () => {
});
it('initializes CloudFront from fileStrategies when fileStrategy is local, but avatar is configured for CloudFront', () => {
const cloudfrontConfig = { domain: 'https://d123.cloudfront.net' };
const cloudfrontConfig = makeCloudFrontConfig();
const appConfig = {
...baseAppConfig,
fileStrategy: FileSources.local,
@ -122,6 +140,32 @@ describe('initializeFileStorage', () => {
expect(initializeS3).not.toHaveBeenCalled();
});
it('throws when CloudFront init fails and requireSignedAccess is true', () => {
(initializeCloudFront as jest.Mock).mockReturnValue(false);
const cloudfrontConfig = makeCloudFrontConfig({
imageSigning: 'cookies',
cookieDomain: '.example.com',
requireSignedAccess: true,
});
const appConfig = {
...baseAppConfig,
fileStrategy: FileSources.cloudfront,
cloudfront: cloudfrontConfig,
} as AppConfig;
expect(() => initializeFileStorage(appConfig)).toThrow(/requireSignedAccess=true/);
});
it('does not throw when CloudFront init fails and requireSignedAccess is false', () => {
(initializeCloudFront as jest.Mock).mockReturnValue(false);
const cloudfrontConfig = makeCloudFrontConfig();
const appConfig = {
...baseAppConfig,
fileStrategy: FileSources.cloudfront,
cloudfront: cloudfrontConfig,
} as AppConfig;
expect(() => initializeFileStorage(appConfig)).not.toThrow();
});
it('does not call any initializer when fileStrategy is local with no fileStrategies', () => {
const appConfig = { ...baseAppConfig, fileStrategy: FileSources.local } as AppConfig;
initializeFileStorage(appConfig);

View file

@ -25,6 +25,11 @@ function initializeStrategy(strategy: FileSources, appConfig: AppConfig): void {
}
const initialized = initializeCloudFront(cloudfrontConfig);
if (!initialized) {
if (cloudfrontConfig.requireSignedAccess === true) {
throw new Error(
'[initializeFileStorage] CloudFront initialization failed and cloudfront.requireSignedAccess=true; refusing to start.',
);
}
logger.error(
'[initializeFileStorage] CloudFront initialization failed. CloudFront operations will not work.',
);

View file

@ -22,6 +22,7 @@ function makeConfig(overrides: Partial<RequiredCloudFrontConfig> = {}): Required
urlExpiry: 3600,
cookieExpiry: 1800,
includeRegionInPath: false,
requireSignedAccess: false,
...overrides,
};
}
@ -136,6 +137,64 @@ describe('CloudFront CDN module', () => {
),
);
});
describe('requireSignedAccess (strict mode)', () => {
it('logs strict-mode startup info when enabled and keys are present', async () => {
process.env.CLOUDFRONT_KEY_PAIR_ID = 'K123';
process.env.CLOUDFRONT_PRIVATE_KEY = 'my-private-key';
const { initializeCloudFront } = await load();
expect(
initializeCloudFront(makeConfig({ imageSigning: 'cookies', requireSignedAccess: true })),
).toBe(true);
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringContaining('Strict signed CloudFront access enabled at startup'),
);
expect(mockLogger.info).not.toHaveBeenCalledWith(
expect.stringContaining('CloudFront cookie signing enabled'),
);
});
it('returns false and logs strict failure when keys are missing', async () => {
const { initializeCloudFront } = await load();
expect(
initializeCloudFront(makeConfig({ imageSigning: 'cookies', requireSignedAccess: true })),
).toBe(false);
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('Strict startup failure'),
);
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('CLOUDFRONT_KEY_PAIR_ID'),
);
});
it('returns false when imageSigning is "none" even if keys are present', async () => {
process.env.CLOUDFRONT_KEY_PAIR_ID = 'K123';
process.env.CLOUDFRONT_PRIVATE_KEY = 'my-private-key';
const { initializeCloudFront } = await load();
expect(
initializeCloudFront(makeConfig({ imageSigning: 'none', requireSignedAccess: true })),
).toBe(false);
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining(
'cloudfront.requireSignedAccess=true requires cloudfront.imageSigning="cookies"',
),
);
});
it('returns false when imageSigning is "url" even if keys are present', async () => {
process.env.CLOUDFRONT_KEY_PAIR_ID = 'K123';
process.env.CLOUDFRONT_PRIVATE_KEY = 'my-private-key';
const { initializeCloudFront } = await load();
expect(
initializeCloudFront(makeConfig({ imageSigning: 'url', requireSignedAccess: true })),
).toBe(false);
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining(
'cloudfront.requireSignedAccess=true requires cloudfront.imageSigning="cookies"',
),
);
});
});
});
describe('getCloudFrontConfig', () => {

View file

@ -341,6 +341,10 @@ export function setCloudFrontCookies(
path: '/',
});
logger.debug(
`[setCloudFrontCookies] Issued signed CloudFront cookies (paths=${signedCookieSets.length}, expiresInSec=${cookieExpiry}).`,
);
return true;
} catch (error) {
logger.error('[setCloudFrontCookies] Failed to generate signed cookies:', error);

View file

@ -29,6 +29,26 @@ export function initializeCloudFront(config: CloudFrontConfig): boolean {
const keyPairId = process.env.CLOUDFRONT_KEY_PAIR_ID ?? null;
const privateKey = process.env.CLOUDFRONT_PRIVATE_KEY ?? null;
const requireSignedAccess = config.requireSignedAccess === true;
if (requireSignedAccess) {
logger.info('[initializeCloudFront] Strict signed CloudFront access enabled at startup.');
if (config.imageSigning !== 'cookies') {
logger.error(
`[initializeCloudFront] Strict startup failure: cloudfront.requireSignedAccess=true requires cloudfront.imageSigning="cookies" (got "${config.imageSigning ?? 'none'}"); signed URL mode is not yet implemented.`,
);
return false;
}
if (!keyPairId || !privateKey) {
logger.error(
'[initializeCloudFront] Strict startup failure: cloudfront.requireSignedAccess=true but CLOUDFRONT_KEY_PAIR_ID and/or CLOUDFRONT_PRIVATE_KEY are missing.',
);
return false;
}
}
if (config.imageSigning === 'cookies' && (!keyPairId || !privateKey)) {
logger.error(
'[initializeCloudFront] imageSigning="cookies" requires CLOUDFRONT_KEY_PAIR_ID and CLOUDFRONT_PRIVATE_KEY env vars.',
@ -38,7 +58,7 @@ export function initializeCloudFront(config: CloudFrontConfig): boolean {
cloudFrontConfig = { ...config, privateKey, keyPairId };
if (config.imageSigning === 'cookies') {
if (config.imageSigning === 'cookies' && !requireSignedAccess) {
logger.info(
'[initializeCloudFront] CloudFront cookie signing enabled. Cookies will be set during auth.',
);

View file

@ -54,6 +54,7 @@ function makeConfig(overrides: Partial<CloudFrontFullConfig> = {}): CloudFrontFu
urlExpiry: 3600,
cookieExpiry: 1800,
includeRegionInPath: false,
requireSignedAccess: false,
privateKey: null,
keyPairId: null,
...overrides,

View file

@ -95,4 +95,43 @@ describe('cloudfrontConfigSchema cross-field refinements', () => {
});
expect(result.success).toBe(true);
});
it('rejects requireSignedAccess=true when imageSigning is "none"', () => {
const result = cloudfrontConfigSchema.safeParse({
domain: 'https://cdn.example.com',
requireSignedAccess: true,
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].message).toContain(
'cloudfront.requireSignedAccess=true requires cloudfront.imageSigning="cookies"',
);
expect(result.error.issues[0].path).toEqual(['requireSignedAccess']);
}
});
it('rejects requireSignedAccess=true when imageSigning is "url"', () => {
const result = cloudfrontConfigSchema.safeParse({
domain: 'https://cdn.example.com',
imageSigning: 'url',
requireSignedAccess: true,
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].message).toContain(
'cloudfront.requireSignedAccess=true requires cloudfront.imageSigning="cookies"',
);
expect(result.error.issues[0].path).toEqual(['requireSignedAccess']);
}
});
it('accepts requireSignedAccess=true with imageSigning="cookies" and cookieDomain', () => {
const result = cloudfrontConfigSchema.safeParse({
domain: 'https://cdn.example.com',
imageSigning: 'cookies',
cookieDomain: '.example.com',
requireSignedAccess: true,
});
expect(result.success).toBe(true);
});
});

View file

@ -215,6 +215,7 @@ export const cloudfrontConfigSchema = z
.optional(),
storageRegion: z.string().min(1).optional(),
includeRegionInPath: z.boolean().default(false),
requireSignedAccess: z.boolean().default(false),
})
.refine((data) => !data.invalidateOnDelete || !!data.distributionId, {
message: 'distributionId is required when invalidateOnDelete is true',
@ -225,6 +226,11 @@ export const cloudfrontConfigSchema = z
'cookieDomain is required when imageSigning is "cookies" (e.g., ".example.com" for API at api.example.com and CDN at cdn.example.com)',
path: ['cookieDomain'],
})
.refine((data) => !data.requireSignedAccess || data.imageSigning === 'cookies', {
message:
'cloudfront.requireSignedAccess=true requires cloudfront.imageSigning="cookies" (signed URL mode is not yet implemented)',
path: ['requireSignedAccess'],
})
.optional();
export type CloudFrontConfig = z.infer<typeof cloudfrontConfigSchema>;