@malloy-publisher/server 0.0.218 → 0.0.219
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/dist/app/api-doc.yaml +22 -6
- package/dist/app/assets/{EnvironmentPage-DxOiCxcd.js → EnvironmentPage-gehnjfC6.js} +1 -1
- package/dist/app/assets/{HomePage-i8qAtD6x.js → HomePage-8LQBytE4.js} +1 -1
- package/dist/app/assets/{MainPage-B58d5pmX.js → MainPage-DDaZLJw-.js} +1 -1
- package/dist/app/assets/{MaterializationsPage-6OEVM543.js → MaterializationsPage-D7P1Kp6O.js} +1 -1
- package/dist/app/assets/{ModelPage-8d5l7YkU.js → ModelPage-Do_vhxOc.js} +1 -1
- package/dist/app/assets/{PackagePage-BVmoVsPQ.js → PackagePage-Cz9fVwSG.js} +1 -1
- package/dist/app/assets/{RouteError-DFX521_t.js → RouteError-UONCloyN.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CzucDJ-T.js → WorkbookPage-Bhzqvbq_.js} +1 -1
- package/dist/app/assets/{core-B95VQkAV.es-Cbn-yKG-.js → core-BiGj7BML.es-kMHAa8tP.js} +1 -1
- package/dist/app/assets/{index--xJ1pzL-.js → index-VzRbxcF7.js} +3 -3
- package/dist/app/assets/{index-DxRKma36.js → index-ddq4-5hu.js} +1 -1
- package/dist/app/assets/{index-Bxkza-hz.js → index-qOQF9CXq.js} +1 -1
- package/dist/app/assets/{index.umd-o-yUIEtS.js → index.umd-B2kmxDh7.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +267 -220
- package/package.json +1 -1
- package/src/service/connection.spec.ts +70 -0
- package/src/service/connection.ts +50 -5
- package/src/service/connection_config.spec.ts +138 -0
- package/src/service/connection_config.ts +81 -2
- package/src/service/proxy.spec.ts +82 -35
- package/src/service/proxy.ts +55 -40
- package/dist/sshcrypto-8m50vnmb.node +0 -0
package/src/service/proxy.ts
CHANGED
|
@@ -8,24 +8,26 @@
|
|
|
8
8
|
* A connection proxy is a normal connection capability (authorized by whoever
|
|
9
9
|
* configures the connection); it is intentionally NOT behind an env-flag gate,
|
|
10
10
|
* and is kept separate from the `publisher` HTTP multi-hop type's
|
|
11
|
-
* PUBLISHER_ALLOW_PROXY_CONNECTIONS gate.
|
|
12
|
-
*
|
|
11
|
+
* PUBLISHER_ALLOW_PROXY_CONNECTIONS gate. When host-key pinning is configured it
|
|
12
|
+
* is fail-closed (below).
|
|
13
13
|
*
|
|
14
14
|
* # Host-key policy
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
* is
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* `ssh.hostKey` is optional. When set, it pins the bastion's host key(s) and the
|
|
17
|
+
* tunnel is fail-closed on mismatch; it may list multiple known_hosts lines (a
|
|
18
|
+
* load-balanced/HA bastion presents a different key per backend), and any listed
|
|
19
|
+
* key is accepted. When absent, the tunnel connects without host-key
|
|
20
|
+
* verification — the self-service default, matching mainstream BI tools' SSH
|
|
21
|
+
* tunnels. The SSH transport is still encrypted; unpinned, a MITM on the
|
|
22
|
+
* publisher→bastion hop is possible, mitigated by the customer allowlisting our
|
|
23
|
+
* egress on the bastion.
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
import net from "net";
|
|
26
27
|
import { Client as SshClient } from "ssh2";
|
|
27
28
|
import type { ConnectConfig, SyncHostVerifier } from "ssh2";
|
|
28
29
|
import { components } from "../api";
|
|
30
|
+
import { logger } from "../logger";
|
|
29
31
|
|
|
30
32
|
type ConnectionProxy = components["schemas"]["ConnectionProxy"];
|
|
31
33
|
|
|
@@ -39,17 +41,27 @@ const SSH_CONNECT_TIMEOUT_MS = 15_000;
|
|
|
39
41
|
const SSH_KEEPALIVE_INTERVAL_MS = 15_000;
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* Parse a stored hostKey into the set of base64 wire-format key blobs to accept.
|
|
45
|
+
* The value may hold one or more OpenSSH known_hosts entries (one per line) — a
|
|
46
|
+
* load-balanced bastion presents a different host key per backend, so pinning
|
|
47
|
+
* such a bastion means listing every backend's key. Blank lines and `#` comments
|
|
48
|
+
* are skipped. Each entry may be a full known_hosts line (`[markers] host keytype
|
|
49
|
+
* AAAA…`), a `keytype AAAA…` pair, or a bare base64 blob; SSH key blobs
|
|
50
|
+
* base64-encode to a string starting with "AAAA" (the length prefix of the
|
|
51
|
+
* key-type name), so we pick that token, else the last whitespace-delimited
|
|
52
|
+
* token. Compared against what ssh2 hands us (`key.toString("base64")`).
|
|
48
53
|
*/
|
|
49
|
-
function
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
54
|
+
export function parseHostKeys(raw: string): Set<string> {
|
|
55
|
+
const keys = new Set<string>();
|
|
56
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
59
|
+
const tokens = trimmed.split(/\s+/);
|
|
60
|
+
const blob = tokens.find((t) => /^AAAA[A-Za-z0-9+/]+={0,2}$/.test(t));
|
|
61
|
+
const key = blob ?? tokens[tokens.length - 1];
|
|
62
|
+
if (key) keys.add(key);
|
|
63
|
+
}
|
|
64
|
+
return keys;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
/**
|
|
@@ -112,31 +124,34 @@ function openSshProxy(
|
|
|
112
124
|
hostVerifier: ((key: Buffer): boolean => {
|
|
113
125
|
// `key` is the raw Buffer of the host's public key.
|
|
114
126
|
const presented = key.toString("base64");
|
|
127
|
+
// "Pinned" is gated on hostKey being a non-empty value, not on it
|
|
128
|
+
// parsing to ≥1 key: a set-but-degenerate hostKey (only whitespace,
|
|
129
|
+
// blanks, or comments) must fail closed, never fall through to the
|
|
130
|
+
// unpinned branch. Config load (validateConnectionShape) already
|
|
131
|
+
// rejects that case; this is the security-boundary backstop for any
|
|
132
|
+
// path that reaches here directly. ("" is the unpinned signal.)
|
|
115
133
|
if (ssh.hostKey) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
),
|
|
122
|
-
);
|
|
123
|
-
return false;
|
|
134
|
+
// Fail-closed, accepting any listed key — an LB/HA bastion presents
|
|
135
|
+
// a different host key per backend.
|
|
136
|
+
const pinned = parseHostKeys(ssh.hostKey);
|
|
137
|
+
if (pinned.has(presented)) {
|
|
138
|
+
return true;
|
|
124
139
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
fail(
|
|
141
|
+
new Error(
|
|
142
|
+
`SSH host-key verification failed for ${ssh.host}: the presented key ` +
|
|
143
|
+
`(${presented.slice(0, 24)}…) is not among the ${pinned.size} pinned ` +
|
|
144
|
+
`host key(s).`,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
return false;
|
|
130
148
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
`Set hostKey to the bastion's host public key (an OpenSSH ` +
|
|
136
|
-
`known_hosts line or its base64 blob, e.g. from ssh-keyscan).`,
|
|
137
|
-
),
|
|
149
|
+
// Unpinned (no hostKey): connect without host-key verification — the
|
|
150
|
+
// self-service default (see file header).
|
|
151
|
+
logger.warn(
|
|
152
|
+
`Connecting to SSH bastion ${ssh.host} without host-key verification (no hostKey pinned).`,
|
|
138
153
|
);
|
|
139
|
-
return
|
|
154
|
+
return true;
|
|
140
155
|
}) as SyncHostVerifier,
|
|
141
156
|
};
|
|
142
157
|
|
|
Binary file
|