ssh-mcp 2.3.0 → 2.3.1
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 +22 -18
- package/build/config/windows-acl.js +17 -12
- package/build/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,24 +51,27 @@ approvalPolicy = "auto" # dev is permissive
|
|
|
51
51
|
chmod 700 ~/.config/ssh-mcp && chmod 600 ~/.config/ssh-mcp/config.toml
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
The
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
54
|
+
The config decides which hosts, roles and policy rules this server honours, so it checks
|
|
55
|
+
that nobody but you can read it — and treats the two platforms differently, because the
|
|
56
|
+
question has a much clearer answer on one of them.
|
|
57
|
+
|
|
58
|
+
**Linux and macOS: enforced.** The mode check above, on the file *and* the directory —
|
|
59
|
+
which is why `chmod 700` is in that command, since `mkdir -p` under the default umask
|
|
60
|
+
leaves the directory 0755. The server refuses to start otherwise. "Only the owner" is
|
|
61
|
+
unambiguous here and `chmod` is a one-line fix.
|
|
62
|
+
|
|
63
|
+
**Windows: reported.** There are no mode bits, so it reads the ACL and tells you what it
|
|
64
|
+
grants — a config under `%APPDATA%` inherits access for you, `SYSTEM` and `Administrators`
|
|
65
|
+
and needs nothing done to it, while one created elsewhere does not: a file under `C:\`
|
|
66
|
+
inherits read for every local account and modify for every authenticated one. The message
|
|
67
|
+
names the two `icacls` commands that fix it, and then the config loads anyway.
|
|
68
|
+
|
|
69
|
+
Advisory rather than enforced because enforced was tried first: 2.3.0 refused, and it
|
|
70
|
+
blocked a config at the documented `%APPDATA%` location whose ACL carried a principal the
|
|
71
|
+
allowlist had never seen ([#138](https://github.com/tufantunc/ssh-mcp/issues/138)). A check
|
|
72
|
+
whose worst outcome is locking you out of your own config is not worth that. Pass
|
|
73
|
+
`--strictConfigAcl` to refuse instead; under it, `--allowUncheckedConfigAcl` still loads a
|
|
74
|
+
config whose ACL could not be determined at all.
|
|
72
75
|
|
|
73
76
|
### Exit statuses
|
|
74
77
|
|
|
@@ -602,6 +605,7 @@ Secrets are **never** passed as CLI arguments.
|
|
|
602
605
|
| `--allowedHosts` | bind address + localhost | Comma-separated Host headers accepted by the DNS-rebinding guard |
|
|
603
606
|
| `--insecureHostKey` | false | Disable host key verification (test only!) |
|
|
604
607
|
| `--allowUncheckedConfigAcl` | false | Windows: load the config even if its ACL could not be read |
|
|
608
|
+
| `--strictConfigAcl` | Windows: refuse to start on a broad or unreadable config ACL instead of reporting it |
|
|
605
609
|
| `--disableApproval` | false | Skip the approval gate (quick start profile only) |
|
|
606
610
|
| `--opaUrl` | — | OPA sidecar URL for external policy |
|
|
607
611
|
| `--commandQuota` | 0 (off) | Max commands per rolling 24h per profile |
|
|
@@ -760,25 +760,30 @@ export async function assertPrivateOnWindows(filePath, opts = {}, inspect) {
|
|
|
760
760
|
const verdict = await look(path);
|
|
761
761
|
if (verdict.status === 'restricted')
|
|
762
762
|
continue;
|
|
763
|
-
if (verdict.status === 'broad') {
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
763
|
+
if (verdict.status === 'broad' || verdict.status === 'no-dacl') {
|
|
764
|
+
const grants = verdict.status === 'broad' ? verdict.grants : [];
|
|
765
|
+
const what = verdict.status === 'broad'
|
|
766
|
+
? `is readable beyond its owner: access is granted to ${grants.map((g) => g.name).join(', ')}. ` +
|
|
767
|
+
'Required: this account, SYSTEM and Administrators only.'
|
|
768
|
+
: 'has no access control list at all, which on Windows means full control for ' +
|
|
769
|
+
'every account on the machine.';
|
|
770
|
+
const message = `Config ${kind} ${path} ${what}\n` +
|
|
771
|
+
(isTightenable(path, kind) ? remediation(path, kind, grants) : relocation(path, kind, grants));
|
|
772
|
+
if (opts.strict)
|
|
773
|
+
throw new OperatorError(message);
|
|
774
|
+
// Advisory by default; see AclOptions.strict for why.
|
|
775
|
+
console.error(`Warning: ${message}`);
|
|
776
|
+
continue;
|
|
773
777
|
}
|
|
774
778
|
// status === 'unknown'
|
|
775
|
-
if (verdict.reason === 'tool-missing' || verdict.reason === 'timed-out'
|
|
779
|
+
if (!opts.strict || verdict.reason === 'tool-missing' || verdict.reason === 'timed-out'
|
|
780
|
+
|| opts.allowUnchecked) {
|
|
776
781
|
report({ path, reason: verdict.reason, detail: verdict.detail });
|
|
777
782
|
continue;
|
|
778
783
|
}
|
|
779
784
|
throw new OperatorError(`Config ${kind} ${path} could not be checked for access by other accounts ` +
|
|
780
785
|
`(${verdict.reason}: ${verdict.detail}).\n` +
|
|
781
|
-
'
|
|
786
|
+
'Refused because --strictConfigAcl was given, and the config decides which hosts, ' +
|
|
782
787
|
'roles and policy rules this server honours.\n' +
|
|
783
788
|
'Either fix the cause, move the config under %APPDATA%\\ssh-mcp, or pass ' +
|
|
784
789
|
'--allowUncheckedConfigAcl to load it unverified.');
|
package/build/index.js
CHANGED
|
@@ -130,6 +130,7 @@ async function buildAppConfig(argv) {
|
|
|
130
130
|
// the audit store as well needs an AuditRecord variant for a startup event — that type
|
|
131
131
|
// is hash-chained and tamper-evident, so it is its own change, and it is queued.
|
|
132
132
|
const aclOpts = {
|
|
133
|
+
strict: flagEnabled(argv, 'strictConfigAcl'),
|
|
133
134
|
allowUnchecked: flagEnabled(argv, 'allowUncheckedConfigAcl'),
|
|
134
135
|
onUnverified: (e) => {
|
|
135
136
|
unverified.push(e);
|
package/package.json
CHANGED