ssh-agent-workspace 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +319 -0
- package/dist/__tests__/SSHManager.test.d.ts +2 -0
- package/dist/__tests__/SSHManager.test.d.ts.map +1 -0
- package/dist/__tests__/SSHManager.test.js +134 -0
- package/dist/__tests__/SSHManager.test.js.map +1 -0
- package/dist/__tests__/SessionManager.test.d.ts +2 -0
- package/dist/__tests__/SessionManager.test.d.ts.map +1 -0
- package/dist/__tests__/SessionManager.test.js +141 -0
- package/dist/__tests__/SessionManager.test.js.map +1 -0
- package/dist/__tests__/StorageManager.test.d.ts +2 -0
- package/dist/__tests__/StorageManager.test.d.ts.map +1 -0
- package/dist/__tests__/StorageManager.test.js +171 -0
- package/dist/__tests__/StorageManager.test.js.map +1 -0
- package/dist/__tests__/ansi.test.d.ts +2 -0
- package/dist/__tests__/ansi.test.d.ts.map +1 -0
- package/dist/__tests__/ansi.test.js +41 -0
- package/dist/__tests__/ansi.test.js.map +1 -0
- package/dist/__tests__/security.test.d.ts +2 -0
- package/dist/__tests__/security.test.d.ts.map +1 -0
- package/dist/__tests__/security.test.js +87 -0
- package/dist/__tests__/security.test.js.map +1 -0
- package/dist/__tests__/validation.test.d.ts +2 -0
- package/dist/__tests__/validation.test.d.ts.map +1 -0
- package/dist/__tests__/validation.test.js +23 -0
- package/dist/__tests__/validation.test.js.map +1 -0
- package/dist/core/HostSecurityManager.d.ts +25 -0
- package/dist/core/HostSecurityManager.d.ts.map +1 -0
- package/dist/core/HostSecurityManager.js +76 -0
- package/dist/core/HostSecurityManager.js.map +1 -0
- package/dist/core/SSHManager.d.ts +48 -0
- package/dist/core/SSHManager.d.ts.map +1 -0
- package/dist/core/SSHManager.js +288 -0
- package/dist/core/SSHManager.js.map +1 -0
- package/dist/core/SessionManager.d.ts +15 -0
- package/dist/core/SessionManager.d.ts.map +1 -0
- package/dist/core/SessionManager.js +96 -0
- package/dist/core/SessionManager.js.map +1 -0
- package/dist/core/StorageManager.d.ts +27 -0
- package/dist/core/StorageManager.d.ts.map +1 -0
- package/dist/core/StorageManager.js +87 -0
- package/dist/core/StorageManager.js.map +1 -0
- package/dist/core/TmuxManager.d.ts +21 -0
- package/dist/core/TmuxManager.d.ts.map +1 -0
- package/dist/core/TmuxManager.js +110 -0
- package/dist/core/TmuxManager.js.map +1 -0
- package/dist/core/ToolConfigManager.d.ts +15 -0
- package/dist/core/ToolConfigManager.d.ts.map +1 -0
- package/dist/core/ToolConfigManager.js +57 -0
- package/dist/core/ToolConfigManager.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +152 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/backup.d.ts +74 -0
- package/dist/tools/backup.d.ts.map +1 -0
- package/dist/tools/backup.js +152 -0
- package/dist/tools/backup.js.map +1 -0
- package/dist/tools/connect.d.ts +46 -0
- package/dist/tools/connect.d.ts.map +1 -0
- package/dist/tools/connect.js +235 -0
- package/dist/tools/connect.js.map +1 -0
- package/dist/tools/connection_status.d.ts +39 -0
- package/dist/tools/connection_status.d.ts.map +1 -0
- package/dist/tools/connection_status.js +67 -0
- package/dist/tools/connection_status.js.map +1 -0
- package/dist/tools/db_query.d.ts +103 -0
- package/dist/tools/db_query.d.ts.map +1 -0
- package/dist/tools/db_query.js +194 -0
- package/dist/tools/db_query.js.map +1 -0
- package/dist/tools/deploy.d.ts +127 -0
- package/dist/tools/deploy.d.ts.map +1 -0
- package/dist/tools/deploy.js +201 -0
- package/dist/tools/deploy.js.map +1 -0
- package/dist/tools/disconnect.d.ts +46 -0
- package/dist/tools/disconnect.d.ts.map +1 -0
- package/dist/tools/disconnect.js +77 -0
- package/dist/tools/disconnect.js.map +1 -0
- package/dist/tools/exec.d.ts +69 -0
- package/dist/tools/exec.d.ts.map +1 -0
- package/dist/tools/exec.js +188 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/group_exec.d.ts +80 -0
- package/dist/tools/group_exec.d.ts.map +1 -0
- package/dist/tools/group_exec.js +150 -0
- package/dist/tools/group_exec.js.map +1 -0
- package/dist/tools/health_check.d.ts +38 -0
- package/dist/tools/health_check.d.ts.map +1 -0
- package/dist/tools/health_check.js +161 -0
- package/dist/tools/health_check.js.map +1 -0
- package/dist/tools/host_security.d.ts +52 -0
- package/dist/tools/host_security.d.ts.map +1 -0
- package/dist/tools/host_security.js +127 -0
- package/dist/tools/host_security.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +24 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/interrupt.d.ts +47 -0
- package/dist/tools/interrupt.d.ts.map +1 -0
- package/dist/tools/interrupt.js +77 -0
- package/dist/tools/interrupt.js.map +1 -0
- package/dist/tools/list_hosts.d.ts +15 -0
- package/dist/tools/list_hosts.d.ts.map +1 -0
- package/dist/tools/list_hosts.js +18 -0
- package/dist/tools/list_hosts.js.map +1 -0
- package/dist/tools/list_sessions.d.ts +16 -0
- package/dist/tools/list_sessions.d.ts.map +1 -0
- package/dist/tools/list_sessions.js +20 -0
- package/dist/tools/list_sessions.js.map +1 -0
- package/dist/tools/read_output.d.ts +46 -0
- package/dist/tools/read_output.d.ts.map +1 -0
- package/dist/tools/read_output.js +73 -0
- package/dist/tools/read_output.js.map +1 -0
- package/dist/tools/reconnect_to_tmux.d.ts +53 -0
- package/dist/tools/reconnect_to_tmux.d.ts.map +1 -0
- package/dist/tools/reconnect_to_tmux.js +199 -0
- package/dist/tools/reconnect_to_tmux.js.map +1 -0
- package/dist/tools/send_input.d.ts +45 -0
- package/dist/tools/send_input.d.ts.map +1 -0
- package/dist/tools/send_input.js +83 -0
- package/dist/tools/send_input.js.map +1 -0
- package/dist/tools/sftp_download.d.ts +52 -0
- package/dist/tools/sftp_download.d.ts.map +1 -0
- package/dist/tools/sftp_download.js +90 -0
- package/dist/tools/sftp_download.js.map +1 -0
- package/dist/tools/sftp_list.d.ts +46 -0
- package/dist/tools/sftp_list.d.ts.map +1 -0
- package/dist/tools/sftp_list.js +93 -0
- package/dist/tools/sftp_list.js.map +1 -0
- package/dist/tools/sftp_upload.d.ts +52 -0
- package/dist/tools/sftp_upload.d.ts.map +1 -0
- package/dist/tools/sftp_upload.js +98 -0
- package/dist/tools/sftp_upload.js.map +1 -0
- package/dist/tools/ssh_tunnel.d.ts +116 -0
- package/dist/tools/ssh_tunnel.d.ts.map +1 -0
- package/dist/tools/ssh_tunnel.js +282 -0
- package/dist/tools/ssh_tunnel.js.map +1 -0
- package/dist/tools/sync.d.ts +71 -0
- package/dist/tools/sync.d.ts.map +1 -0
- package/dist/tools/sync.js +310 -0
- package/dist/tools/sync.js.map +1 -0
- package/dist/tools/tail_log.d.ts +61 -0
- package/dist/tools/tail_log.d.ts.map +1 -0
- package/dist/tools/tail_log.js +111 -0
- package/dist/tools/tail_log.js.map +1 -0
- package/dist/tools/tools_config.d.ts +34 -0
- package/dist/tools/tools_config.d.ts.map +1 -0
- package/dist/tools/tools_config.js +98 -0
- package/dist/tools/tools_config.js.map +1 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/ansi.d.ts +2 -0
- package/dist/utils/ansi.d.ts.map +1 -0
- package/dist/utils/ansi.js +7 -0
- package/dist/utils/ansi.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +8 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/security.d.ts +7 -0
- package/dist/utils/security.d.ts.map +1 -0
- package/dist/utils/security.js +58 -0
- package/dist/utils/security.js.map +1 -0
- package/dist/utils/ssh.d.ts +4 -0
- package/dist/utils/ssh.d.ts.map +1 -0
- package/dist/utils/ssh.js +29 -0
- package/dist/utils/ssh.js.map +1 -0
- package/dist/utils/sshConfig.d.ts +4 -0
- package/dist/utils/sshConfig.d.ts.map +1 -0
- package/dist/utils/sshConfig.js +85 -0
- package/dist/utils/sshConfig.js.map +1 -0
- package/dist/utils/validation.d.ts +4 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +12 -0
- package/dist/utils/validation.js.map +1 -0
- package/docs/SECURITY.md +213 -0
- package/docs/TOOLS.md +425 -0
- package/keygen.bat +325 -0
- package/package.json +48 -0
- package/test_check.bat +9 -0
- package/test_delayed.bat +12 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Client } from 'ssh2';
|
|
2
|
+
export interface Session {
|
|
3
|
+
id: string;
|
|
4
|
+
host: string;
|
|
5
|
+
ssh: Client;
|
|
6
|
+
connectedAt: number;
|
|
7
|
+
lastActivity: number;
|
|
8
|
+
tmuxSession: string;
|
|
9
|
+
shell?: string;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface SSHHostConfig {
|
|
14
|
+
name: string;
|
|
15
|
+
hostname?: string;
|
|
16
|
+
user?: string;
|
|
17
|
+
port?: number;
|
|
18
|
+
identityFile?: string;
|
|
19
|
+
proxyJump?: string;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ansi.d.ts","sourceRoot":"","sources":["../../src/utils/ansi.ts"],"names":[],"mappings":"AAIA,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const ansiRegex =
|
|
2
|
+
// eslint-disable-next-line no-control-regex
|
|
3
|
+
/[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
4
|
+
export function stripAnsi(input) {
|
|
5
|
+
return input.replace(ansiRegex, '');
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=ansi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ansi.js","sourceRoot":"","sources":["../../src/utils/ansi.ts"],"names":[],"mappings":"AAAA,MAAM,SAAS;AACb,4CAA4C;AAC5C,0KAA0K,CAAC;AAE7K,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,eAAO,MAAM,MAAM,6BAQlB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CACxB;IACE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM;IACtC,IAAI,EAAE;QACJ,GAAG,EAAE,OAAO,CAAC,GAAG;KACjB;CACF,EACD,OAAO,CAAC,MAAM,CACf,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { HostSecurityManager } from '../core/HostSecurityManager.js';
|
|
2
|
+
export declare function setHostSecurityManager(manager: HostSecurityManager): void;
|
|
3
|
+
export declare function isReadOnlyMode(host?: string): boolean;
|
|
4
|
+
export declare function isHostCommandDenied(host: string, command: string): boolean;
|
|
5
|
+
export declare function isHostAllowed(host: string): boolean;
|
|
6
|
+
export declare function isCommandDenied(command: string): boolean;
|
|
7
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/utils/security.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAKrE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,mBAAmB,QAElE;AAED,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAmB1E;AAcD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASnD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
let hostSecurity = null;
|
|
2
|
+
export function setHostSecurityManager(manager) {
|
|
3
|
+
hostSecurity = manager;
|
|
4
|
+
}
|
|
5
|
+
export function isReadOnlyMode(host) {
|
|
6
|
+
if (host && hostSecurity?.isReadOnly(host))
|
|
7
|
+
return true;
|
|
8
|
+
return process.env.MCP_SSH_READONLY === 'true';
|
|
9
|
+
}
|
|
10
|
+
export function isHostCommandDenied(host, command) {
|
|
11
|
+
const global = isGlobalCommandDenied(command);
|
|
12
|
+
if (global)
|
|
13
|
+
return true;
|
|
14
|
+
if (!hostSecurity)
|
|
15
|
+
return false;
|
|
16
|
+
const allowlist = hostSecurity.getAllowCommands(host);
|
|
17
|
+
if (allowlist.length > 0) {
|
|
18
|
+
const lower = command.toLowerCase();
|
|
19
|
+
if (!allowlist.some((a) => lower.includes(a.toLowerCase())))
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
const denylist = hostSecurity.getDenyCommands(host);
|
|
23
|
+
if (denylist.length > 0) {
|
|
24
|
+
const lower = command.toLowerCase();
|
|
25
|
+
if (denylist.some((d) => lower.includes(d.toLowerCase())))
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function isGlobalCommandDenied(command) {
|
|
31
|
+
const denylist = process.env.MCP_SSH_DENYLIST_COMMANDS;
|
|
32
|
+
if (!denylist)
|
|
33
|
+
return false;
|
|
34
|
+
const list = denylist
|
|
35
|
+
.split(',')
|
|
36
|
+
.map((c) => c.trim())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
if (list.length === 0)
|
|
39
|
+
return false;
|
|
40
|
+
const lower = command.toLowerCase();
|
|
41
|
+
return list.some((denied) => lower.includes(denied.toLowerCase()));
|
|
42
|
+
}
|
|
43
|
+
export function isHostAllowed(host) {
|
|
44
|
+
const allowed = process.env.MCP_SSH_ALLOWED_HOSTS;
|
|
45
|
+
if (!allowed)
|
|
46
|
+
return true;
|
|
47
|
+
const list = allowed
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((h) => h.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
if (list.length === 0)
|
|
52
|
+
return true;
|
|
53
|
+
return list.includes(host);
|
|
54
|
+
}
|
|
55
|
+
export function isCommandDenied(command) {
|
|
56
|
+
return isGlobalCommandDenied(command);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/utils/security.ts"],"names":[],"mappings":"AAGA,IAAI,YAAY,GAA+B,IAAI,CAAC;AAEpD,MAAM,UAAU,sBAAsB,CAAC,OAA4B;IACjE,YAAY,GAAG,OAAO,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAa;IAC1C,IAAI,IAAI,IAAI,YAAY,EAAE,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACxD,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,MAAM,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,OAAe;IAC/D,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,MAAM;QAAE,OAAO,IAAI,CAAC;IAExB,IAAI,CAAC,YAAY;QAAE,OAAO,KAAK,CAAC;IAEhC,MAAM,SAAS,GAAG,YAAY,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QACpC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;IAC3E,CAAC;IAED,MAAM,QAAQ,GAAG,YAAY,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QACpC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;IACzE,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,qBAAqB,CAAC,OAAe;IAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC;IACvD,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5B,MAAM,IAAI,GAAG,QAAQ;SAClB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IACpC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;IAClD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,IAAI,GAAG,OAAO;SACjB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,OAAO,qBAAqB,CAAC,OAAO,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../../src/utils/ssh.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,wBAAgB,cAAc,CAAC,UAAU,EAAE,aAAa,EAAE,YAAY,SAAQ,GAAG,aAAa,GAAG,IAAI,CAyBpG"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
export function buildSshConfig(hostConfig, readyTimeout = 20000) {
|
|
5
|
+
const config = {
|
|
6
|
+
host: hostConfig.hostname || hostConfig.name,
|
|
7
|
+
port: hostConfig.port || 22,
|
|
8
|
+
username: hostConfig.user || process.env.USER || 'root',
|
|
9
|
+
readyTimeout,
|
|
10
|
+
keepaliveInterval: 10000,
|
|
11
|
+
keepaliveCountMax: 3,
|
|
12
|
+
// Try SSH agent first if available (ssh-agent / Pageant)
|
|
13
|
+
agent: process.env.SSH_AUTH_SOCK || undefined,
|
|
14
|
+
};
|
|
15
|
+
if (hostConfig.identityFile) {
|
|
16
|
+
const keyPath = hostConfig.identityFile.replace(/^~(?=$|\/)/, os.homedir());
|
|
17
|
+
if (fs.existsSync(keyPath)) {
|
|
18
|
+
try {
|
|
19
|
+
config.privateKey = fs.readFileSync(keyPath);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
logger.error({ keyPath, error: err.message }, 'Failed to read SSH key');
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=ssh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh.js","sourceRoot":"","sources":["../../src/utils/ssh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,MAAM,UAAU,cAAc,CAAC,UAAyB,EAAE,YAAY,GAAG,KAAK;IAC5E,MAAM,MAAM,GAAkB;QAC5B,IAAI,EAAE,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,IAAI;QAC5C,IAAI,EAAE,UAAU,CAAC,IAAI,IAAI,EAAE;QAC3B,QAAQ,EAAE,UAAU,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM;QACvD,YAAY;QACZ,iBAAiB,EAAE,KAAK;QACxB,iBAAiB,EAAE,CAAC;QACpB,yDAAyD;QACzD,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,SAAS;KAC9C,CAAC;IAEF,IAAI,UAAU,CAAC,YAAY,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5E,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,CAAC,UAAU,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;YAC/C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,EAAE,wBAAwB,CAAC,CAAC;gBACnF,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sshConfig.d.ts","sourceRoot":"","sources":["../../src/utils/sshConfig.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AA+BvD,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAqB1C;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAwCjE"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { SSHConfig } from 'ssh-config';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
let cachedConfig = null;
|
|
7
|
+
let cachedMtime = 0;
|
|
8
|
+
function getConfigPath() {
|
|
9
|
+
return path.join(os.homedir(), '.ssh', 'config');
|
|
10
|
+
}
|
|
11
|
+
function loadConfig() {
|
|
12
|
+
const configPath = getConfigPath();
|
|
13
|
+
try {
|
|
14
|
+
const stats = fs.statSync(configPath);
|
|
15
|
+
if (!cachedConfig || stats.mtimeMs !== cachedMtime) {
|
|
16
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
17
|
+
cachedConfig = SSHConfig.parse(content);
|
|
18
|
+
cachedMtime = stats.mtimeMs;
|
|
19
|
+
logger.debug({ path: configPath }, 'SSH config loaded');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (err.code === 'ENOENT') {
|
|
24
|
+
logger.warn({ path: configPath }, 'SSH config not found');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
logger.error({ error: err.message }, 'Failed to load SSH config');
|
|
28
|
+
}
|
|
29
|
+
cachedConfig = SSHConfig.parse('');
|
|
30
|
+
cachedMtime = 0;
|
|
31
|
+
}
|
|
32
|
+
return cachedConfig;
|
|
33
|
+
}
|
|
34
|
+
export function listHostAliases() {
|
|
35
|
+
const config = loadConfig();
|
|
36
|
+
const aliases = [];
|
|
37
|
+
for (const line of config) {
|
|
38
|
+
if (line.param === 'Host' && line.value) {
|
|
39
|
+
const values = Array.isArray(line.value) ? line.value : [line.value];
|
|
40
|
+
for (const value of values) {
|
|
41
|
+
// Skip wildcard patterns - we only list concrete aliases
|
|
42
|
+
if (!value.includes('*') && !value.includes('?')) {
|
|
43
|
+
aliases.push(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...new Set(aliases)];
|
|
49
|
+
}
|
|
50
|
+
export function getHostConfig(alias) {
|
|
51
|
+
const config = loadConfig();
|
|
52
|
+
try {
|
|
53
|
+
const computed = config.compute(alias);
|
|
54
|
+
if (!computed || Object.keys(computed).length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const getValue = (key) => {
|
|
58
|
+
const val = computed[key];
|
|
59
|
+
if (Array.isArray(val))
|
|
60
|
+
return val[0];
|
|
61
|
+
if (typeof val === 'string')
|
|
62
|
+
return val;
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
name: alias,
|
|
67
|
+
hostname: getValue('HostName'),
|
|
68
|
+
user: getValue('User'),
|
|
69
|
+
port: (() => {
|
|
70
|
+
const p = getValue('Port');
|
|
71
|
+
if (!p)
|
|
72
|
+
return undefined;
|
|
73
|
+
const n = parseInt(p, 10);
|
|
74
|
+
return isNaN(n) ? undefined : n;
|
|
75
|
+
})(),
|
|
76
|
+
identityFile: getValue('IdentityFile'),
|
|
77
|
+
proxyJump: getValue('ProxyJump'),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
logger.error({ alias, error: err.message }, 'Failed to compute SSH host config');
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=sshConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sshConfig.js","sourceRoot":"","sources":["../../src/utils/sshConfig.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,eAAe,aAA0C,CAAC;AAEvE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQ5D"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const sessionIdSchema = z.string().regex(/^sess_[a-f0-9]{32}$/);
|
|
3
|
+
export function sanitizeTmuxSessionName(name) {
|
|
4
|
+
// tmux session names can contain: letters, digits, underscore, hyphen, dot
|
|
5
|
+
let sanitized = name.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
6
|
+
// Must not start with a hyphen or dot, and should start with a letter for safety
|
|
7
|
+
if (/^[^a-zA-Z]/.test(sanitized)) {
|
|
8
|
+
sanitized = 'mcp_' + sanitized;
|
|
9
|
+
}
|
|
10
|
+
return sanitized;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;AAEvE,MAAM,UAAU,uBAAuB,CAAC,IAAY;IAClD,2EAA2E;IAC3E,IAAI,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IACtD,iFAAiF;IACjF,IAAI,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACjC,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IACjC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/docs/SECURITY.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Security Model
|
|
2
|
+
|
|
3
|
+
`ssh-agent-workspace` implements a **three-layer defense** architecture. Each layer adds granularity without breaking the layers above it.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Layer 1: Global (Environment Variables)
|
|
8
|
+
|
|
9
|
+
Set at server startup via environment variables. Applies to **all hosts**.
|
|
10
|
+
|
|
11
|
+
| Variable | Effect |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `MCP_SSH_READONLY=true` | Blocks all write operations globally |
|
|
14
|
+
| `MCP_SSH_ALLOWED_HOSTS=prod,staging` | Only these host aliases can be connected to |
|
|
15
|
+
| `MCP_SSH_DENYLIST_COMMANDS=rm,shutdown,dd` | Blocks commands containing these substrings (case-insensitive) |
|
|
16
|
+
|
|
17
|
+
### Read-Only Mode: Blocked Tools
|
|
18
|
+
|
|
19
|
+
When `MCP_SSH_READONLY=true`, the following tools return errors:
|
|
20
|
+
|
|
21
|
+
- `exec` — Command execution disabled
|
|
22
|
+
- `send_input` — Input sending disabled
|
|
23
|
+
- `sftp_upload` — SFTP upload disabled
|
|
24
|
+
- `sftp_download` — SFTP download disabled
|
|
25
|
+
- `deploy` — Deploy disabled
|
|
26
|
+
- `backup` — Backup disabled
|
|
27
|
+
- `sync` — Sync disabled
|
|
28
|
+
- `group_exec` — Group exec disabled
|
|
29
|
+
- `db_query` — DB query disabled
|
|
30
|
+
- `ssh_tunnel_open` — Tunnels disabled
|
|
31
|
+
|
|
32
|
+
### Tools Always Allowed (Read Operations)
|
|
33
|
+
|
|
34
|
+
`list_hosts`, `connect`, `reconnect_to_tmux`, `read_output`, `interrupt`, `disconnect`, `list_sessions`, `sftp_list`, `connection_status`, `health_check`, `tail_log`, `ssh_tunnel_close`, `ssh_tunnel_list`, `tools_config`, `host_security`
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Layer 2: Per-Host (Host Security)
|
|
39
|
+
|
|
40
|
+
Managed via the `host_security` tool and persisted in `~/.dynamic-ssh-mcp/host_security.json`.
|
|
41
|
+
|
|
42
|
+
Per-host settings **override** global settings for that specific host only.
|
|
43
|
+
|
|
44
|
+
### Configuration
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"prod": {
|
|
49
|
+
"readonly": true,
|
|
50
|
+
"deny_commands": ["shutdown", "reboot", "dd if="]
|
|
51
|
+
},
|
|
52
|
+
"staging": {
|
|
53
|
+
"deny_commands": ["rm -rf /", "shutdown"]
|
|
54
|
+
},
|
|
55
|
+
"dev": {
|
|
56
|
+
"allow_commands": [
|
|
57
|
+
"ls", "cat", "echo", "ps aux",
|
|
58
|
+
"docker ps", "docker logs",
|
|
59
|
+
"git status", "git diff", "git log",
|
|
60
|
+
"npm run", "node"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Rules
|
|
67
|
+
|
|
68
|
+
| Field | Behavior |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `readonly: true` | Host locked to read-only, regardless of global `MCP_SSH_READONLY` |
|
|
71
|
+
| `readonly: false` | Host explicitly writeable (does **not** override global readonly) |
|
|
72
|
+
| `allow_commands: [...]` | If set, **only** commands matching these patterns are allowed (case-insensitive substring match) |
|
|
73
|
+
| `deny_commands: [...]` | Commands matching these patterns are blocked (case-insensitive substring match) |
|
|
74
|
+
|
|
75
|
+
**Precedence:** `deny_commands` wins over `allow_commands`. If a command matches both, it's blocked.
|
|
76
|
+
|
|
77
|
+
### Enforcement Points
|
|
78
|
+
|
|
79
|
+
Per-host read-only is checked **after session lookup** in every write tool:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
exec, send_input, sftp_upload, sftp_download,
|
|
83
|
+
deploy, backup, sync, group_exec, db_query, ssh_tunnel_open
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If a session's host has `readonly: true`, the tool returns:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Error: Host 'prod' is in read-only mode. <operation> is disabled.
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Per-Host Command Filtering
|
|
93
|
+
|
|
94
|
+
`isHostCommandDenied(host, command)` checks:
|
|
95
|
+
1. Global `MCP_SSH_DENYLIST_COMMANDS`
|
|
96
|
+
2. Per-host `allow_commands` (if set, command must match at least one)
|
|
97
|
+
3. Per-host `deny_commands`
|
|
98
|
+
|
|
99
|
+
Per-host command filtering is available to all tools via the shared security utility.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Layer 3: Per-Operation (Tool-Level)
|
|
104
|
+
|
|
105
|
+
### Path Sanitization
|
|
106
|
+
|
|
107
|
+
All file paths used in SFTP, backup, deploy, sync, and tail_log are validated:
|
|
108
|
+
|
|
109
|
+
- Rejects paths containing `;`, `&&`, `||`, `|`
|
|
110
|
+
- Rejects paths starting with `-` (option injection)
|
|
111
|
+
- Paths are shell-escaped before passing to exec commands
|
|
112
|
+
|
|
113
|
+
### Command Denylist (Global)
|
|
114
|
+
|
|
115
|
+
Commands containing these substrings are blocked (case-insensitive):
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
rm -rf, shutdown, reboot, dd, mkfs, fdisk, :(){ :|:& };: (fork bomb)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Configured via `MCP_SSH_DENYLIST_COMMANDS` — comma-separated:
|
|
122
|
+
```
|
|
123
|
+
MCP_SSH_DENYLIST_COMMANDS=rm -rf,shutdown,dd if=,mkfs,chmod 777
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### SQL/MongoDB Query Security
|
|
127
|
+
|
|
128
|
+
The `db_query` tool enforces read-only queries:
|
|
129
|
+
|
|
130
|
+
**SQL (MySQL/PostgreSQL):**
|
|
131
|
+
- Blocked keywords: `DROP`, `DELETE`, `INSERT`, `UPDATE`, `ALTER`, `CREATE`, `TRUNCATE`, `GRANT`, `REVOKE`, `RENAME`, `REPLACE`, `MERGE`, `UPSERT`, `LOAD`
|
|
132
|
+
- Only allowed prefixes: `SELECT`, `SHOW`, `EXPLAIN`, `DESCRIBE`, `WITH`
|
|
133
|
+
|
|
134
|
+
**MongoDB:**
|
|
135
|
+
- Blocked methods: `deleteOne`, `deleteMany`, `insertOne`, `insertMany`, `updateOne`, `updateMany`, `replaceOne`, `drop`, `dropDatabase`, `createIndex`, `createCollection`
|
|
136
|
+
|
|
137
|
+
### Shell Injection Protection
|
|
138
|
+
|
|
139
|
+
All arguments passed to tmux commands are:
|
|
140
|
+
1. Validated via Zod schemas before use
|
|
141
|
+
2. Passed through `sanitizeTmuxSessionName()` for session names
|
|
142
|
+
3. Base64-encoded buffer pipeline for command input (not raw string interpolation)
|
|
143
|
+
4. Escaped via `escapeShellArg()` for exec channel commands
|
|
144
|
+
|
|
145
|
+
### SFTP Security
|
|
146
|
+
|
|
147
|
+
- `sftp_upload` / `sftp_download`: paths validated before transfer
|
|
148
|
+
- `sftp_list`: read-only, allowed even in read-only mode
|
|
149
|
+
- `deploy`: validates all paths in the files array, plus exec commands for chmod/chown/restart
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Proxy Jump / Bastion Security
|
|
154
|
+
|
|
155
|
+
- Both the bastion host AND the target host must pass `isHostAllowed()` checks
|
|
156
|
+
- Bastion resolved from `~/.ssh/config` (`ProxyJump` directive) or explicit `proxy_jump` parameter
|
|
157
|
+
- Each hop uses its own SSH key from the corresponding `Host` config block
|
|
158
|
+
- The bastion connection is established first, then `forwardOut` is used to reach the target
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Session Isolation
|
|
163
|
+
|
|
164
|
+
- Each session = **one dedicated SSH connection** + **one dedicated tmux session**
|
|
165
|
+
- Sessions cannot see each other's tmux panes
|
|
166
|
+
- Session metadata stored locally at `~/.dynamic-ssh-mcp/sessions.json`
|
|
167
|
+
- No passwords are stored — auth is key-based via `~/.ssh/config`
|
|
168
|
+
- SSH key passphrases not stored; use `ssh-agent` for passphrase-protected keys
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Hardening Recommendations
|
|
173
|
+
|
|
174
|
+
### Production
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Global read-only on production
|
|
178
|
+
export MCP_SSH_READONLY=true
|
|
179
|
+
|
|
180
|
+
# Only prod and staging hosts
|
|
181
|
+
export MCP_SSH_ALLOWED_HOSTS=prod,staging
|
|
182
|
+
|
|
183
|
+
# Block dangerous operations
|
|
184
|
+
export MCP_SSH_DENYLIST_COMMANDS="rm -rf,shutdown,reboot,dd if=,chmod 777,fdisk,mkfs"
|
|
185
|
+
|
|
186
|
+
# Only restore known sessions (don't discover new ones)
|
|
187
|
+
export MCP_SSH_RESTORE_SESSIONS=true
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Per-Host Fine-Tuning (via tools_config / host_security)
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
# Lock production to read-only
|
|
194
|
+
host_security set host=prod readonly=true
|
|
195
|
+
|
|
196
|
+
# Limit dev commands
|
|
197
|
+
host_security set host=dev deny_commands=["rm -rf", "shutdown", "reboot"]
|
|
198
|
+
|
|
199
|
+
# Disable unused tools to reduce attack surface
|
|
200
|
+
tools_config disable db_query
|
|
201
|
+
tools_config disable backup
|
|
202
|
+
tools_config disable sync
|
|
203
|
+
tools_config disable deploy
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Audit & Monitoring
|
|
209
|
+
|
|
210
|
+
- All tool invocations logged to stderr via pino (configurable `LOG_LEVEL`)
|
|
211
|
+
- Session create/remove/restore events logged with session IDs
|
|
212
|
+
- Failed connections, auth rejections, and denied commands logged at `warn` level
|
|
213
|
+
- Log format: structured JSON with timestamps
|