Skip to content

Commit 526fa3b

Browse files
authored
feat: add dictionary-based redaction for secrets in logs (#508)
1 parent 5a17126 commit 526fa3b

File tree

23 files changed

+3110
-1714
lines changed

23 files changed

+3110
-1714
lines changed

package-lock.json

Lines changed: 2810 additions & 1665 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,15 @@
100100
"@mongodb-js/devtools-connect": "^3.9.3",
101101
"@mongodb-js/devtools-proxy-support": "^0.5.2",
102102
"@mongosh/arg-parser": "^3.14.0",
103-
"@mongosh/service-provider-node-driver": "^3.12.0",
103+
"@mongosh/service-provider-node-driver": "~3.12.0",
104104
"@vitest/eslint-plugin": "^1.3.4",
105105
"bson": "^6.10.4",
106106
"express": "^5.1.0",
107107
"lru-cache": "^11.1.0",
108108
"mongodb": "^6.19.0",
109109
"mongodb-connection-string-url": "^3.0.2",
110110
"mongodb-log-writer": "^2.4.1",
111-
"mongodb-redact": "^1.1.8",
111+
"mongodb-redact": "^1.2.0",
112112
"mongodb-schema": "^12.6.2",
113113
"node-fetch": "^3.3.2",
114114
"node-machine-id": "1.1.12",

src/common/config.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import os from "os";
33
import argv from "yargs-parser";
44
import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser";
55
import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
6+
import { Keychain } from "./keychain.js";
7+
import type { Secret } from "./keychain.js";
68

79
// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts
810
const OPTIONS = {
@@ -316,6 +318,29 @@ function commaSeparatedToArray<T extends string[]>(str: string | string[] | unde
316318
return str as T;
317319
}
318320

321+
export function registerKnownSecretsInRootKeychain(userConfig: Partial<UserConfig>): void {
322+
const keychain = Keychain.root;
323+
324+
const maybeRegister = (value: string | undefined, kind: Secret["kind"]): void => {
325+
if (value) {
326+
keychain.register(value, kind);
327+
}
328+
};
329+
330+
maybeRegister(userConfig.apiClientId, "user");
331+
maybeRegister(userConfig.apiClientSecret, "password");
332+
maybeRegister(userConfig.awsAccessKeyId, "password");
333+
maybeRegister(userConfig.awsIamSessionToken, "password");
334+
maybeRegister(userConfig.awsSecretAccessKey, "password");
335+
maybeRegister(userConfig.awsSessionToken, "password");
336+
maybeRegister(userConfig.password, "password");
337+
maybeRegister(userConfig.tlsCAFile, "url");
338+
maybeRegister(userConfig.tlsCRLFile, "url");
339+
maybeRegister(userConfig.tlsCertificateKeyFile, "url");
340+
maybeRegister(userConfig.tlsCertificateKeyFilePassword, "password");
341+
maybeRegister(userConfig.username, "user");
342+
}
343+
319344
export function setupUserConfig({
320345
cli,
321346
env,
@@ -369,6 +394,7 @@ export function setupUserConfig({
369394
}
370395
}
371396

397+
registerKnownSecretsInRootKeychain(userConfig);
372398
return userConfig;
373399
}
374400

src/common/connectionManager.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,19 +199,15 @@ export class MCPConnectionManager extends ConnectionManager {
199199
}
200200

201201
try {
202-
const connectionType = MCPConnectionManager.inferConnectionTypeFromSettings(
203-
this.userConfig,
204-
connectionInfo
205-
);
206-
if (connectionType.startsWith("oidc")) {
202+
if (connectionStringAuthType.startsWith("oidc")) {
207203
void this.pingAndForget(serviceProvider);
208204

209205
return this.changeState("connection-request", {
210206
tag: "connecting",
211207
connectedAtlasCluster: settings.atlas,
212208
serviceProvider,
213-
connectionStringAuthType: connectionType,
214-
oidcConnectionType: connectionType as OIDCConnectionAuthType,
209+
connectionStringAuthType,
210+
oidcConnectionType: connectionStringAuthType as OIDCConnectionAuthType,
215211
});
216212
}
217213

@@ -221,7 +217,7 @@ export class MCPConnectionManager extends ConnectionManager {
221217
tag: "connected",
222218
connectedAtlasCluster: settings.atlas,
223219
serviceProvider,
224-
connectionStringAuthType: connectionType,
220+
connectionStringAuthType,
225221
});
226222
} catch (error: unknown) {
227223
const errorReason = error instanceof Error ? error.message : `${error as string}`;

src/common/keychain.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Secret } from "mongodb-redact";
2+
export type { Secret } from "mongodb-redact";
3+
4+
/**
5+
* This class holds the secrets of a single server. Ideally, we might want to have a keychain
6+
* per session, but right now the loggers are set up by server and are not aware of the concept
7+
* of session and this would require a bigger refactor.
8+
*
9+
* Whenever we identify or create a secret (for example, Atlas login, CLI arguments...) we
10+
* should register them in the root Keychain (`Keychain.root.register`) or preferably
11+
* on the session keychain if available `this.session.keychain`.
12+
**/
13+
export class Keychain {
14+
private secrets: Secret[];
15+
private static rootKeychain: Keychain = new Keychain();
16+
17+
constructor() {
18+
this.secrets = [];
19+
}
20+
21+
static get root(): Keychain {
22+
return Keychain.rootKeychain;
23+
}
24+
25+
register(value: Secret["value"], kind: Secret["kind"]): void {
26+
this.secrets.push({ value, kind });
27+
}
28+
29+
clearAllSecrets(): void {
30+
this.secrets = [];
31+
}
32+
33+
get allSecrets(): Secret[] {
34+
return [...this.secrets];
35+
}
36+
}
37+
38+
export function registerGlobalSecretToRedact(value: Secret["value"], kind: Secret["kind"]): void {
39+
Keychain.root.register(value, kind);
40+
}

src/common/logger.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import fs from "fs/promises";
22
import type { MongoLogId, MongoLogWriter } from "mongodb-log-writer";
33
import { mongoLogId, MongoLogManager } from "mongodb-log-writer";
4-
import redact from "mongodb-redact";
4+
import { redact } from "mongodb-redact";
55
import type { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js";
66
import { EventEmitter } from "events";
77
import type { Server } from "../lib.js";
8+
import type { Keychain } from "./keychain.js";
89

910
export type LogLevel = LoggingMessageNotification["params"]["level"];
1011

@@ -84,6 +85,10 @@ type DefaultEventMap = [never];
8485
export abstract class LoggerBase<T extends EventMap<T> = DefaultEventMap> extends EventEmitter<T> {
8586
private readonly defaultUnredactedLogger: LoggerType = "mcp";
8687

88+
constructor(private readonly keychain: Keychain | undefined) {
89+
super();
90+
}
91+
8792
public log(level: LogLevel, payload: LogPayload): void {
8893
// If no explicit value is supplied for unredacted loggers, default to "mcp"
8994
const noRedaction = payload.noRedaction !== undefined ? payload.noRedaction : this.defaultUnredactedLogger;
@@ -122,7 +127,7 @@ export abstract class LoggerBase<T extends EventMap<T> = DefaultEventMap> extend
122127
return message;
123128
}
124129

125-
return redact(message);
130+
return redact(message, this.keychain?.allSecrets ?? []);
126131
}
127132

128133
public info(payload: LogPayload): void {
@@ -180,6 +185,10 @@ export abstract class LoggerBase<T extends EventMap<T> = DefaultEventMap> extend
180185
export class ConsoleLogger extends LoggerBase {
181186
protected readonly type: LoggerType = "console";
182187

188+
public constructor(keychain: Keychain) {
189+
super(keychain);
190+
}
191+
183192
protected logCore(level: LogLevel, payload: LogPayload): void {
184193
const { id, context, message } = payload;
185194
console.error(
@@ -201,8 +210,8 @@ export class DiskLogger extends LoggerBase<{ initialized: [] }> {
201210
private bufferedMessages: { level: LogLevel; payload: LogPayload }[] = [];
202211
private logWriter?: MongoLogWriter;
203212

204-
public constructor(logPath: string, onError: (error: Error) => void) {
205-
super();
213+
public constructor(logPath: string, onError: (error: Error) => void, keychain: Keychain) {
214+
super(keychain);
206215

207216
void this.initialize(logPath, onError);
208217
}
@@ -262,8 +271,11 @@ export class McpLogger extends LoggerBase {
262271
"emergency",
263272
] as const;
264273

265-
public constructor(private readonly server: Server) {
266-
super();
274+
public constructor(
275+
private readonly server: Server,
276+
keychain: Keychain
277+
) {
278+
super(keychain);
267279
}
268280

269281
protected readonly type: LoggerType = "mcp";
@@ -295,7 +307,9 @@ export class CompositeLogger extends LoggerBase {
295307
private readonly attributes: Record<string, string> = {};
296308

297309
constructor(...loggers: LoggerBase[]) {
298-
super();
310+
// composite logger does not redact, only the actual delegates do the work
311+
// so we don't need the Keychain here
312+
super(undefined);
299313

300314
this.loggers = loggers;
301315
}
@@ -327,6 +341,10 @@ export class CompositeLogger extends LoggerBase {
327341
export class NullLogger extends LoggerBase {
328342
protected type?: LoggerType;
329343

344+
constructor() {
345+
super(undefined);
346+
}
347+
330348
protected logCore(): void {
331349
// No-op logger, does not log anything
332350
}

src/common/session.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
1616
import { ErrorCodes, MongoDBError } from "./errors.js";
1717
import type { ExportsManager } from "./exportsManager.js";
18+
import type { Keychain } from "./keychain.js";
1819

1920
export interface SessionOptions {
2021
apiBaseUrl: string;
@@ -23,6 +24,7 @@ export interface SessionOptions {
2324
logger: CompositeLogger;
2425
exportsManager: ExportsManager;
2526
connectionManager: ConnectionManager;
27+
keychain: Keychain;
2628
}
2729

2830
export type SessionEvents = {
@@ -37,6 +39,8 @@ export class Session extends EventEmitter<SessionEvents> {
3739
readonly exportsManager: ExportsManager;
3840
readonly connectionManager: ConnectionManager;
3941
readonly apiClient: ApiClient;
42+
readonly keychain: Keychain;
43+
4044
mcpClient?: {
4145
name?: string;
4246
version?: string;
@@ -52,9 +56,11 @@ export class Session extends EventEmitter<SessionEvents> {
5256
logger,
5357
connectionManager,
5458
exportsManager,
59+
keychain,
5560
}: SessionOptions) {
5661
super();
5762

63+
this.keychain = keychain;
5864
this.logger = logger;
5965
const credentials: ApiClientCredentials | undefined =
6066
apiClientId && apiClientSecret

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { packageInfo } from "./common/packageInfo.js";
4242
import { StdioRunner } from "./transports/stdio.js";
4343
import { StreamableHttpRunner } from "./transports/streamableHttp.js";
4444
import { systemCA } from "@mongodb-js/devtools-proxy-support";
45+
import { Keychain } from "./common/keychain.js";
4546

4647
async function main(): Promise<void> {
4748
systemCA().catch(() => undefined); // load system CA asynchronously as in mongosh
@@ -121,7 +122,7 @@ main().catch((error: unknown) => {
121122
// At this point, we may be in a very broken state, so we can't rely on the logger
122123
// being functional. Instead, create a brand new ConsoleLogger and log the error
123124
// to the console.
124-
const logger = new ConsoleLogger();
125+
const logger = new ConsoleLogger(Keychain.root);
125126
logger.emergency({
126127
id: LogId.serverStartFailure,
127128
context: "server",

src/lib.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ export type {
1919
} from "./common/connectionErrorHandler.js";
2020
export { ErrorCodes } from "./common/errors.js";
2121
export { Telemetry } from "./telemetry/telemetry.js";
22+
export { Keychain, registerGlobalSecretToRedact } from "./common/keychain.js";
23+
export type { Secret } from "./common/keychain.js";

src/tools/atlas/connect/connectCluster.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ export class ConnectClusterTool extends AtlasToolBase {
115115
cn.password = password;
116116
cn.searchParams.set("authSource", "admin");
117117

118+
this.session.keychain.register(username, "user");
119+
this.session.keychain.register(password, "password");
120+
118121
return { connectionString: cn.toString(), atlas: connectedAtlasCluster };
119122
}
120123

0 commit comments

Comments
 (0)