diff --git a/src/lib/server/logging/redaction.test.ts b/src/lib/server/logging/redaction.test.ts
index 2857565c..fe9292cb 100644
--- a/src/lib/server/logging/redaction.test.ts
+++ b/src/lib/server/logging/redaction.test.ts
@@ -1,4 +1,7 @@
import pino from 'pino';
+import http from 'node:http';
+import type { AddressInfo } from 'node:net';
+import { S3Client, ListBucketsCommand } from '@aws-sdk/client-s3';
import { Writable } from 'node:stream';
import { describe, it, expect } from 'vitest';
import { redactionPaths, redactionCensor } from './redaction.js';
@@ -84,6 +87,85 @@ describe('redaction', () => {
expect(parsed.req.headers['set-cookie']).toBe('[Redacted]');
});
+ it('redacts the storage connection header, which carries the S3 secret key', () => {
+ const { log, lines } = createTestLogger();
+ const header = btoa(
+ JSON.stringify({ type: 's3', credentials: { accessKey: 'AK', secretKey: 'SK-CANARY' } })
+ );
+ log.info({ req: { headers: { 'x-storage-connection': header } } }, 'test');
+ log.flush();
+
+ expect(lines[0]).not.toContain(header);
+ expect(JSON.parse(lines[0]).req.headers['x-storage-connection']).toBe('[Redacted]');
+ });
+
+ it('redacts the storage connection header on a bare headers object', () => {
+ const { log, lines } = createTestLogger();
+ const header = btoa(JSON.stringify({ credentials: { secretKey: 'SK-CANARY' } }));
+ log.info({ headers: { 'x-storage-connection': header } }, 'test');
+ log.flush();
+
+ expect(lines[0]).not.toContain(header);
+ expect(JSON.parse(lines[0]).headers['x-storage-connection']).toBe('[Redacted]');
+ });
+
+ it('redacts the credential material an S3 endpoint echoes back', async () => {
+ const server = http.createServer((_req, res) => {
+ res.writeHead(403, { 'Content-Type': 'application/xml' });
+ res.end(
+ `` +
+ `SignatureDoesNotMatchmismatch` +
+ `AKIA-CANARY` +
+ `STS-CANARY` +
+ `SIG-CANARY` +
+ `CR-CANARY` +
+ ``
+ );
+ });
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
+ const { port } = server.address() as AddressInfo;
+
+ const client = new S3Client({
+ region: 'eu-central-1',
+ endpoint: `http://127.0.0.1:${port}`,
+ forcePathStyle: true,
+ credentials: { accessKeyId: 'AKIA-CANARY', secretAccessKey: 'x' },
+ maxAttempts: 1
+ });
+
+ const { log, lines } = createTestLogger();
+ try {
+ await client.send(new ListBucketsCommand({}));
+ throw new Error('expected the request to fail');
+ } catch (err) {
+ log.warn({ err }, 'storage connection test failed');
+ } finally {
+ client.destroy();
+ await new Promise((resolve) => server.close(() => resolve()));
+ }
+ log.flush();
+
+ const line = lines[0];
+ expect(line).not.toContain('AKIA-CANARY');
+ expect(line).not.toContain('STS-CANARY');
+ expect(line).not.toContain('SIG-CANARY');
+ expect(line).not.toContain('CR-CANARY');
+
+ // The diagnostics support actually needs must survive.
+ const parsed = JSON.parse(line);
+ expect(parsed.err.name).toBe('SignatureDoesNotMatch');
+ expect(parsed.err.$metadata.httpStatusCode).toBe(403);
+ }, 20000);
+
+ it('redacts echoed credentials nested in aggregated errors', () => {
+ const { log, lines } = createTestLogger();
+ const inner = Object.assign(new Error('inner'), { AWSAccessKeyId: 'AKIA-CANARY' });
+ log.warn({ err: new AggregateError([inner], 'all failed') }, 'test');
+ log.flush();
+
+ expect(lines[0]).not.toContain('AKIA-CANARY');
+ });
+
it('does not redact safe fields', () => {
const { log, lines } = createTestLogger();
log.info({ user_id: 'u123', path: '/api/test' }, 'test');
diff --git a/src/lib/server/logging/redaction.ts b/src/lib/server/logging/redaction.ts
index 1136d666..1b5ed98c 100644
--- a/src/lib/server/logging/redaction.ts
+++ b/src/lib/server/logging/redaction.ts
@@ -1,9 +1,28 @@
+const s3ErrorCredentialFields = [
+ 'AWSAccessKeyId',
+ 'StringToSign',
+ 'StringToSignBytes',
+ 'CanonicalRequest',
+ 'CanonicalRequestBytes',
+ 'SignatureProvided'
+];
+
+const s3ErrorPaths = s3ErrorCredentialFields.flatMap((field) => [
+ `*.${field}`,
+ `err.aggregateErrors[*].${field}`
+]);
+
/** Pino redaction paths for sensitive fields. */
export const redactionPaths: string[] = [
+ // S3 error bodies echoed back by the remote endpoint
+ ...s3ErrorPaths,
+
// Request headers
'req.headers.authorization',
'req.headers.cookie',
'req.headers["set-cookie"]',
+ // Carries the base64 JSON S3 connection config, including secretKey.
+ 'req.headers["x-storage-connection"]',
// Token/credential fields (exact)
'accessToken',
@@ -22,6 +41,7 @@ export const redactionPaths: string[] = [
'*.authorization',
'*.cookie',
'*.set-cookie',
+ '*["x-storage-connection"]',
'*.accessToken',
'*.refreshToken',
'*.idToken',