openship 0.6.6 → 0.6.7
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/bare-7QD623QV.js +29 -0
- package/dist/{chunk-Q7WGTQQ5.js → chunk-2ANV5ZQ5.js} +1 -1
- package/dist/{chunk-UJ6JZM65.js → chunk-2VSW6Q6C.js} +601 -382
- package/dist/{chunk-PCPVTH6M.js → chunk-5T4UVOM3.js} +6 -4
- package/dist/{chunk-YMMQRE5J.js → chunk-BLEXTYRK.js} +1 -1
- package/dist/{chunk-OBPKYXQL.js → chunk-CIJV43LH.js} +71 -32
- package/dist/{chunk-NK5N5LXN.js → chunk-DDFUESPJ.js} +24 -9
- package/dist/{chunk-6FH4AXVY.js → chunk-E2X26PXR.js} +1 -1
- package/dist/{chunk-HKRQXPTI.js → chunk-FYPFEDPZ.js} +1 -1
- package/dist/{chunk-UVNMQM7W.js → chunk-FZ6QCF6B.js} +8 -6
- package/dist/{chunk-MLYSEZQB.js → chunk-GGIAIGUL.js} +15 -66
- package/dist/{chunk-2YM3NI2E.js → chunk-GIPJNNI5.js} +38 -44
- package/dist/{chunk-ZFJXPCQP.js → chunk-H3WT3A6R.js} +4 -3
- package/dist/chunk-H4FS5BED.js +936 -0
- package/dist/{chunk-CBLD2HS5.js → chunk-J2XM3QSP.js} +8 -5
- package/dist/{chunk-FE7ZZQSE.js → chunk-J3K6XABC.js} +3 -3
- package/dist/{chunk-77QDF6UD.js → chunk-LHFXSC36.js} +428 -93
- package/dist/{chunk-YIMOCZBZ.js → chunk-MEE6OZFP.js} +3 -3
- package/dist/chunk-N5YZMO33.js +111 -0
- package/dist/{chunk-QVW76WDL.js → chunk-NRTJRNAE.js} +58 -16
- package/dist/{chunk-WVAML4H6.js → chunk-PPEBIY5F.js} +247 -61
- package/dist/{chunk-4I2BM2DT.js → chunk-PVP4RGWT.js} +3 -3
- package/dist/{chunk-RSR7PFIQ.js → chunk-QHMDCYZ3.js} +4 -4
- package/dist/chunk-R4VDPFLS.js +106 -0
- package/dist/{chunk-VTGPEC6B.js → chunk-RDXT4BQB.js} +6 -4
- package/dist/{chunk-LFE5KQ7Y.js → chunk-S6B2Z3OO.js} +142 -57
- package/dist/chunk-UWEV6XTS.js +249 -0
- package/dist/{chunk-5DMWSTOM.js → chunk-VQM6JHJB.js} +940 -224
- package/dist/{chunk-SM26HDS2.js → chunk-VSTFHUCV.js} +44 -50
- package/dist/chunk-WU54BXFE.js +70 -0
- package/dist/{chunk-LUKYPVJ4.js → chunk-XWHWTHEL.js} +313 -469
- package/dist/{chunk-A4GXYE4L.js → chunk-ZPJ74LXE.js} +22 -20
- package/dist/cloud-KNNABE3O.js +40 -0
- package/dist/{detect-OIE5V3VZ.js → detect-G4DR3U2N.js} +9 -6
- package/dist/{docker-VTR2GK5Y.js → docker-YOVPCIB4.js} +11 -8
- package/dist/{docker-edge-executor-22IBXNYJ.js → docker-edge-executor-QFXD3337.js} +8 -6
- package/dist/edge-import-CRFVSR6P.js +56 -0
- package/dist/{edge-preflight-GBNCPNVU.js → edge-preflight-MXERVAMG.js} +15 -12
- package/dist/{ensure-container-edge-KPI2CVXJ.js → ensure-container-edge-F4JUNQIK.js} +13 -10
- package/dist/{executor-7LXXZPBR.js → executor-CVPAYYHR.js} +8 -6
- package/dist/host-channel-preflight-QTDNGLCK.js +54 -0
- package/dist/index.js +84 -48
- package/dist/{local-executor-WOFZD72X.js → local-executor-ODC6RANU.js} +3 -2
- package/dist/{nginx-KUFMLMAC.js → nginx-KLFF43TC.js} +11 -8
- package/dist/{openresty-lua-ATTH35BE.js → openresty-lua-VKWUV4GB.js} +6 -5
- package/dist/{privilege-I4W3ZNZK.js → privilege-7D3SSOL3.js} +6 -5
- package/dist/server/index.js +8813 -5232
- package/dist/setup-I5AJC6NJ.js +25 -0
- package/dist/{ssh-client-JBMA7SN4.js → ssh-client-625VWGKP.js} +3 -2
- package/dist/up-QXTOOVXC.js +61 -0
- package/package.json +1 -1
- package/dist/bare-OXJVW3AK.js +0 -25
- package/dist/chunk-LH2PLRQO.js +0 -434
- package/dist/cloud-75VIT4KV.js +0 -37
- package/dist/edge-import-WJUB4M7F.js +0 -52
- package/dist/host-channel-preflight-KE6OONYK.js +0 -50
- package/dist/setup-XGLGUUFW.js +0 -22
- package/dist/up-K4BV7466.js +0 -57
|
@@ -10,28 +10,28 @@ import {
|
|
|
10
10
|
readComposeEnvFile,
|
|
11
11
|
resolveLocalEnvironmentSync,
|
|
12
12
|
thisHost
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-VQM6JHJB.js";
|
|
14
14
|
import {
|
|
15
15
|
OS_DIR
|
|
16
16
|
} from "./chunk-ELGRMZOW.js";
|
|
17
17
|
import {
|
|
18
18
|
systemCatalog
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-ZPJ74LXE.js";
|
|
20
20
|
import {
|
|
21
21
|
EDGE_CONTAINER_MOUNTS,
|
|
22
22
|
EDGE_HOST_STATE_DIR
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-QHMDCYZ3.js";
|
|
24
24
|
import {
|
|
25
25
|
DEFAULT_DOCKER_SOCKET_PATH,
|
|
26
26
|
resolveLocalDockerSocketPath
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-LHFXSC36.js";
|
|
28
28
|
import {
|
|
29
29
|
LocalExecutor
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-E2X26PXR.js";
|
|
31
31
|
import {
|
|
32
32
|
invalidateEdgeContainer,
|
|
33
33
|
sanitizeEdgeVhosts
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-GIPJNNI5.js";
|
|
35
35
|
import {
|
|
36
36
|
DEFAULT_IMAGE_REGISTRY,
|
|
37
37
|
HOST_CHANNEL_BLOCKED,
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
hostFirewallRule,
|
|
52
52
|
hostFirewallRuleRemoval,
|
|
53
53
|
wrapText
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-2VSW6Q6C.js";
|
|
55
55
|
|
|
56
56
|
// src/lib/host-channel-preflight.ts
|
|
57
57
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -775,32 +775,66 @@ function stripHostAuthorizedKeys(existing) {
|
|
|
775
775
|
}
|
|
776
776
|
var ROOT_AUTHORIZED_KEYS = "/root/.ssh/authorized_keys";
|
|
777
777
|
function chooseHostChannelUser(input) {
|
|
778
|
+
const keysFor = (home) => join4(home, ".ssh", "authorized_keys");
|
|
779
|
+
if (input.pinned) {
|
|
780
|
+
return [
|
|
781
|
+
{
|
|
782
|
+
user: input.pinned.user,
|
|
783
|
+
authKeysPath: input.pinned.user === "root" ? ROOT_AUTHORIZED_KEYS : keysFor(input.pinned.home),
|
|
784
|
+
// Unknowable for an arbitrary account; the dial measures it (see provisionHostSshChannel).
|
|
785
|
+
elevation: input.pinned.user === "root" ? "login" : "sudo",
|
|
786
|
+
source: "pin"
|
|
787
|
+
}
|
|
788
|
+
];
|
|
789
|
+
}
|
|
790
|
+
const candidates = [];
|
|
778
791
|
if (input.invokerUid === 0) {
|
|
779
|
-
|
|
792
|
+
candidates.push({
|
|
780
793
|
user: "root",
|
|
781
794
|
authKeysPath: ROOT_AUTHORIZED_KEYS,
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
795
|
+
elevation: "login",
|
|
796
|
+
source: "root"
|
|
797
|
+
});
|
|
798
|
+
if (input.sudoUser && input.sudoUser.user !== "root") {
|
|
799
|
+
candidates.push({
|
|
800
|
+
user: input.sudoUser.user,
|
|
801
|
+
authKeysPath: keysFor(input.sudoUser.home),
|
|
802
|
+
elevation: "sudo",
|
|
803
|
+
source: "sudo-user"
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
candidates.push({
|
|
808
|
+
user: input.invokerName,
|
|
809
|
+
authKeysPath: keysFor(input.invokerHome),
|
|
810
|
+
// Reaches root through sudo at run time, so this is NOT `rootUnavailable` — the
|
|
811
|
+
// caller must not warn that mail and edge will fail, because they won't.
|
|
812
|
+
elevation: input.hasPasswordlessSudo ? "sudo" : "none",
|
|
813
|
+
source: "invoker"
|
|
814
|
+
});
|
|
792
815
|
}
|
|
793
|
-
|
|
816
|
+
const carried = input.carried?.trim();
|
|
817
|
+
if (!carried) return candidates;
|
|
818
|
+
const already = candidates.findIndex((c) => c.user === carried);
|
|
819
|
+
if (already <= 0) return candidates;
|
|
820
|
+
const [promoted] = candidates.splice(already, 1);
|
|
821
|
+
return [{ ...promoted, source: "carried" }, ...candidates];
|
|
794
822
|
}
|
|
795
823
|
function hasPasswordlessSudo() {
|
|
796
824
|
try {
|
|
797
|
-
return spawnSync3("sudo", ["-n", "true"], { stdio: "ignore" }).status === 0;
|
|
825
|
+
return spawnSync3("sudo", ["-n", "-k", "true"], { stdio: "ignore" }).status === 0;
|
|
798
826
|
} catch {
|
|
799
827
|
return false;
|
|
800
828
|
}
|
|
801
829
|
}
|
|
802
|
-
function
|
|
803
|
-
|
|
830
|
+
function sudoInvoker() {
|
|
831
|
+
const user = process.env.SUDO_USER?.trim();
|
|
832
|
+
if (!user || user === "root") return null;
|
|
833
|
+
const home = passwdHome(user);
|
|
834
|
+
return home ? { user, home } : null;
|
|
835
|
+
}
|
|
836
|
+
function plannedHostChannel(cfg) {
|
|
837
|
+
if (!cfg.hostControl || process.platform !== "linux") return null;
|
|
804
838
|
let invoker;
|
|
805
839
|
try {
|
|
806
840
|
const info = userInfo();
|
|
@@ -808,14 +842,43 @@ function plannedHostChannel(hostControl) {
|
|
|
808
842
|
} catch {
|
|
809
843
|
invoker = { uid: -1, username: process.env.USER || process.env.LOGNAME || "root", home: homedir() };
|
|
810
844
|
}
|
|
811
|
-
const
|
|
845
|
+
const pin = cfg.hostSshUser?.trim();
|
|
846
|
+
const candidates = chooseHostChannelUser({
|
|
812
847
|
invokerUid: invoker.uid,
|
|
813
848
|
invokerName: invoker.username,
|
|
814
849
|
invokerHome: invoker.home,
|
|
815
850
|
// Only probe sudo when it could change the outcome — a root invoker never needs it.
|
|
816
|
-
hasPasswordlessSudo: invoker.uid !== 0 && hasPasswordlessSudo()
|
|
851
|
+
hasPasswordlessSudo: invoker.uid !== 0 && hasPasswordlessSudo(),
|
|
852
|
+
// A pinned account is dialed, never written to on our guess about its home: root's
|
|
853
|
+
// file is fixed, and any other account's comes from the passwd database.
|
|
854
|
+
pinned: pin ? { user: pin, home: passwdHome(pin) ?? join4("/home", pin) } : null,
|
|
855
|
+
sudoUser: invoker.uid === 0 ? sudoInvoker() : null,
|
|
856
|
+
carried: cfg.hostSshUserCarried ?? null
|
|
817
857
|
});
|
|
818
|
-
return {
|
|
858
|
+
return { candidates, keyPath: join4(COMPOSE_DIR, "host-ssh", "id_ed25519") };
|
|
859
|
+
}
|
|
860
|
+
function passwdHome(user) {
|
|
861
|
+
try {
|
|
862
|
+
const r = spawnSync3("getent", ["passwd", user], { encoding: "utf8" });
|
|
863
|
+
if (r.status !== 0) return null;
|
|
864
|
+
return (r.stdout ?? "").split("\n")[0]?.split(":")[5]?.trim() || null;
|
|
865
|
+
} catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function previewHostChannel(cfg) {
|
|
870
|
+
const plan = plannedHostChannel(cfg);
|
|
871
|
+
if (!plan) return null;
|
|
872
|
+
const [first, ...rest] = plan.candidates;
|
|
873
|
+
if (!first) return null;
|
|
874
|
+
return {
|
|
875
|
+
user: first.user,
|
|
876
|
+
keyPath: plan.keyPath,
|
|
877
|
+
authKeysPath: first.authKeysPath,
|
|
878
|
+
rootUnavailable: first.elevation === "none",
|
|
879
|
+
elevation: first.elevation,
|
|
880
|
+
fallbacks: rest.map((c) => c.user)
|
|
881
|
+
};
|
|
819
882
|
}
|
|
820
883
|
function authorizeKeyAt(authKeysPath, pub) {
|
|
821
884
|
mkdirSync3(dirname(authKeysPath), { recursive: true, mode: 448 });
|
|
@@ -855,15 +918,20 @@ function retireHostSshChannel(prev) {
|
|
|
855
918
|
const keyDir = join4(COMPOSE_DIR, "host-ssh");
|
|
856
919
|
if (!existsSync4(keyDir) && !prev.OPENSHIP_HOST_SSH_HOST) return;
|
|
857
920
|
const invokerKeys = join4(homedir(), ".ssh", "authorized_keys");
|
|
858
|
-
const
|
|
921
|
+
const account = hostChannelAccount(prev);
|
|
922
|
+
const asRoot = account === HOST_CHANNEL_DEFAULT_ACCOUNT;
|
|
923
|
+
const accountKeys = asRoot ? ROOT_AUTHORIZED_KEYS : (() => {
|
|
924
|
+
const home = passwdHome(account);
|
|
925
|
+
return home ? join4(home, ".ssh", "authorized_keys") : invokerKeys;
|
|
926
|
+
})();
|
|
859
927
|
const weAreRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
860
|
-
const revoked = asRoot ? weAreRoot ? revokeKeyAt(ROOT_AUTHORIZED_KEYS) : hasPasswordlessSudo() && revokeRootKeyViaSudo() : revokeKeyAt(
|
|
861
|
-
const alsoInvoker =
|
|
928
|
+
const revoked = asRoot ? weAreRoot ? revokeKeyAt(ROOT_AUTHORIZED_KEYS) : hasPasswordlessSudo() && revokeRootKeyViaSudo() : revokeKeyAt(accountKeys);
|
|
929
|
+
const alsoInvoker = accountKeys === invokerKeys ? true : revokeKeyAt(invokerKeys);
|
|
862
930
|
try {
|
|
863
931
|
rmSync(keyDir, { recursive: true, force: true });
|
|
864
932
|
} catch {
|
|
865
933
|
}
|
|
866
|
-
const stuck =
|
|
934
|
+
const stuck = accountKeys;
|
|
867
935
|
console.log(
|
|
868
936
|
revoked && alsoInvoker ? " Host control off: dropped OPENSHIP_HOST_SSH_* from .env, deleted the host key and\n revoked its authorized_keys line." : ` Host control off: dropped OPENSHIP_HOST_SSH_* from .env and deleted the host key (it
|
|
869
937
|
can no longer be used). Removing its line needs root \u2014 run:
|
|
@@ -871,17 +939,10 @@ function retireHostSshChannel(prev) {
|
|
|
871
939
|
);
|
|
872
940
|
}
|
|
873
941
|
function provisionHostSshChannel(cfg) {
|
|
874
|
-
const
|
|
875
|
-
if (!
|
|
876
|
-
const {
|
|
877
|
-
const
|
|
878
|
-
const port = toPort(cfg.hostSshPort) ?? HOST_CHANNEL_DEFAULT_PORT;
|
|
879
|
-
if (probeHostSshd(port) === "absent") {
|
|
880
|
-
return { channel, issue: { code: "no-sshd", detail: `port ${port}` } };
|
|
881
|
-
}
|
|
882
|
-
const auth = verifyChannelAuth(channel.user, channel.keyPath, port);
|
|
883
|
-
return auth.state === "refused" ? { channel, issue: { code: "auth-refused", detail: auth.detail, account: channel.user } } : { channel };
|
|
884
|
-
};
|
|
942
|
+
const plan = plannedHostChannel(cfg);
|
|
943
|
+
if (!plan) return { channel: null };
|
|
944
|
+
const { candidates, keyPath } = plan;
|
|
945
|
+
const port = toPort(cfg.hostSshPort) ?? HOST_CHANNEL_DEFAULT_PORT;
|
|
885
946
|
try {
|
|
886
947
|
mkdirSync3(join4(COMPOSE_DIR, "host-ssh"), { recursive: true, mode: 448 });
|
|
887
948
|
if (!existsSync4(keyPath)) {
|
|
@@ -896,24 +957,98 @@ function provisionHostSshChannel(cfg) {
|
|
|
896
957
|
}
|
|
897
958
|
const pub = readFileSync3(`${keyPath}.pub`, "utf8").trim();
|
|
898
959
|
if (!pub) return { channel: null, issue: { code: "empty-key" } };
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
960
|
+
const tried = [];
|
|
961
|
+
const losers = [];
|
|
962
|
+
let lastFailure = null;
|
|
963
|
+
for (const candidate of candidates) {
|
|
964
|
+
tried.push(candidate.user);
|
|
965
|
+
authorizeKeyAt(candidate.authKeysPath, pub);
|
|
966
|
+
if (probeHostSshd(port) === "absent") {
|
|
967
|
+
return { channel: { user: candidate.user, keyPath }, issue: { code: "no-sshd", detail: `port ${port}` } };
|
|
968
|
+
}
|
|
969
|
+
const auth = verifyChannelAuth(candidate.user, keyPath, port);
|
|
970
|
+
if (auth.state === "unreachable") {
|
|
971
|
+
return {
|
|
972
|
+
channel: { user: candidate.user, keyPath },
|
|
973
|
+
issue: { code: "unreachable", detail: auth.detail }
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
if (auth.state === "refused" || auth.state === "forced-command") {
|
|
977
|
+
lastFailure = { state: auth.state, detail: auth.detail };
|
|
978
|
+
losers.push(candidate);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const elevation = candidate.user === "root" ? "login" : auth.state === "ok" && auth.elevation === "refused" ? (
|
|
982
|
+
// MEASURED, over the session shape the api will use. This is the arm
|
|
983
|
+
// `hasPasswordlessSudo` used to get wrong from a cached ticket, which is what
|
|
984
|
+
// suppressed the warning below on boxes that genuinely could not reach root.
|
|
985
|
+
"none"
|
|
986
|
+
) : candidate.elevation;
|
|
987
|
+
for (const loser of losers) revokeKeyAt(loser.authKeysPath);
|
|
988
|
+
if (elevation === "sudo") {
|
|
989
|
+
revokeRootKeyViaSudo();
|
|
990
|
+
}
|
|
991
|
+
if (elevation === "none") {
|
|
992
|
+
console.warn(
|
|
993
|
+
` \u26A0 Host control needs root, and this channel has no route to it: it logs in as
|
|
994
|
+
\`${candidate.user}\`, and \`sudo -n\` was refused over that session. Mail and edge
|
|
995
|
+
host operations will fail. Grant that account passwordless sudo, or re-run
|
|
996
|
+
\`openship up\` as root, to fix. Continuing with a limited host channel.`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (candidate.source === "sudo-user") {
|
|
1000
|
+
console.log(
|
|
1001
|
+
` \u2192 Host channel settled on \`${candidate.user}\` rather than root: this host refused a
|
|
1002
|
+
root login, and that account's key was accepted. It elevates per operation with
|
|
1003
|
+
sudo, so no standing root credential is created. Pin one with
|
|
1004
|
+
\`openship up --host-ssh-user <name>\`.`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return { channel: { user: candidate.user, keyPath }, issue: void 0 };
|
|
1008
|
+
}
|
|
1009
|
+
const carried = cfg.hostSshUserCarried?.trim();
|
|
1010
|
+
for (const loser of losers) {
|
|
1011
|
+
if (loser.user !== carried) revokeKeyAt(loser.authKeysPath);
|
|
905
1012
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1013
|
+
return {
|
|
1014
|
+
channel: null,
|
|
1015
|
+
issue: {
|
|
1016
|
+
code: lastFailure?.state === "forced-command" ? "forced-command" : "auth-refused",
|
|
1017
|
+
...lastFailure?.detail ? { detail: lastFailure.detail } : {},
|
|
1018
|
+
account: tried[tried.length - 1] ?? "",
|
|
1019
|
+
tried,
|
|
1020
|
+
...tried.includes("root") ? (() => {
|
|
1021
|
+
const setting = permitRootLoginSetting(port);
|
|
1022
|
+
return setting ? { permitRootLogin: setting } : {};
|
|
1023
|
+
})() : {}
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
909
1026
|
} catch (err) {
|
|
910
1027
|
return { channel: null, issue: { code: "error", detail: err?.message } };
|
|
911
1028
|
}
|
|
912
1029
|
}
|
|
1030
|
+
function permitRootLoginSetting(port) {
|
|
1031
|
+
try {
|
|
1032
|
+
const r = spawnSync3(
|
|
1033
|
+
"sudo",
|
|
1034
|
+
["-n", "sshd", "-T", "-C", `user=root,host=localhost,addr=127.0.0.1,laddr=127.0.0.1,lport=${port}`],
|
|
1035
|
+
{ encoding: "utf8" }
|
|
1036
|
+
);
|
|
1037
|
+
if (r.status !== 0) return null;
|
|
1038
|
+
return /^permitrootlogin\s+(\S+)/m.exec(r.stdout ?? "")?.[1] ?? null;
|
|
1039
|
+
} catch {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function sshdRefusesRootLogin(value) {
|
|
1044
|
+
const v = value?.trim().toLowerCase();
|
|
1045
|
+
return v === "no" || v === "forced-commands-only";
|
|
1046
|
+
}
|
|
913
1047
|
function verifyChannelAuth(user, keyPath, port) {
|
|
914
1048
|
const r = spawnSync3(
|
|
915
1049
|
"ssh",
|
|
916
1050
|
[
|
|
1051
|
+
"-v",
|
|
917
1052
|
"-i",
|
|
918
1053
|
keyPath,
|
|
919
1054
|
"-p",
|
|
@@ -931,16 +1066,34 @@ function verifyChannelAuth(user, keyPath, port) {
|
|
|
931
1066
|
"-o",
|
|
932
1067
|
"IdentitiesOnly=yes",
|
|
933
1068
|
`${user}@127.0.0.1`,
|
|
934
|
-
|
|
1069
|
+
CHANNEL_PROBE_COMMAND
|
|
935
1070
|
],
|
|
936
|
-
{ encoding: "utf8", stdio: ["ignore", "
|
|
1071
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 }
|
|
937
1072
|
);
|
|
938
|
-
if (r.status === 0) return { state: "ok" };
|
|
939
1073
|
if (r.error?.code === "ENOENT" || r.status === null) {
|
|
940
1074
|
return { state: "unknown" };
|
|
941
1075
|
}
|
|
942
|
-
const
|
|
943
|
-
|
|
1076
|
+
const stderr = r.stderr || "";
|
|
1077
|
+
const stdout = r.stdout || "";
|
|
1078
|
+
const sudo = /openship-sudo=(ok|refused)/.exec(stdout)?.[1];
|
|
1079
|
+
if (r.status === 0 && sudo) {
|
|
1080
|
+
return { state: "ok", elevation: sudo === "ok" ? "ok" : "refused" };
|
|
1081
|
+
}
|
|
1082
|
+
if (/Authenticated to |debug1: Authentication succeeded/.test(stderr)) {
|
|
1083
|
+
return r.status === 0 ? { state: "ok", elevation: "unknown" } : { state: "forced-command", detail: sshProbeDetail(stderr, r.status) };
|
|
1084
|
+
}
|
|
1085
|
+
if (/^ssh: connect to host/m.test(stderr)) {
|
|
1086
|
+
return { state: "unreachable", detail: sshProbeDetail(stderr, r.status) };
|
|
1087
|
+
}
|
|
1088
|
+
if (r.status === 0) return { state: "ok", elevation: "unknown" };
|
|
1089
|
+
return { state: "refused", detail: sshProbeDetail(stderr, r.status) };
|
|
1090
|
+
}
|
|
1091
|
+
var CHANNEL_PROBE_COMMAND = "if sudo -n -k true 2>/dev/null; then echo openship-sudo=ok; else echo openship-sudo=refused; fi; exit 0";
|
|
1092
|
+
function sshProbeDetail(stderr, status) {
|
|
1093
|
+
const detail = stderr.split("\n").map((l) => l.trim()).filter(
|
|
1094
|
+
(l) => l && !/^debug\d*:/.test(l) && !/^OpenSSH_/.test(l) && !/^Warning: Permanently added/.test(l)
|
|
1095
|
+
).join(" ");
|
|
1096
|
+
return detail || `ssh exited ${status}`;
|
|
944
1097
|
}
|
|
945
1098
|
function keygenWhy(r) {
|
|
946
1099
|
if (r.error?.code === "ENOENT") {
|
|
@@ -977,7 +1130,16 @@ function hostIssueNote(text) {
|
|
|
977
1130
|
}
|
|
978
1131
|
function renderHostChannelIssue(issue, ctx) {
|
|
979
1132
|
const detail = issue.detail ? ` (${issue.detail})` : "";
|
|
980
|
-
const headline = issue.code === "no-sshd" ? ` \u26A0 Host control is provisioned, but nothing on this host is listening for SSH${detail}.` : issue.code === "
|
|
1133
|
+
const headline = issue.code === "no-sshd" ? ` \u26A0 Host control is provisioned, but nothing on this host is listening for SSH${detail}.` : issue.code === "unreachable" ? ` \u26A0 Host control is provisioned, but nothing answered at the SSH address we dialed${detail}.` : issue.code === "forced-command" ? (
|
|
1134
|
+
// Like `auth-refused` and unlike `no-sshd`: a key that can't run a command is
|
|
1135
|
+
// not a channel, so nothing was written and the headline must not imply it was.
|
|
1136
|
+
` \u26A0 Host control could NOT be provisioned: this host's sshd allows the channel no commands.`
|
|
1137
|
+
) : issue.code === "auth-refused" ? (
|
|
1138
|
+
// NOT "is provisioned, but": a refusal no longer leaves a channel behind. The
|
|
1139
|
+
// run tried every account it could authorize and none of them were let in, so
|
|
1140
|
+
// there is nothing provisioned to describe.
|
|
1141
|
+
` \u26A0 Host control could NOT be provisioned: this host REFUSED the channel's key.`
|
|
1142
|
+
) : ` \u26A0 Host control could NOT be provisioned on this box.`;
|
|
981
1143
|
let cause;
|
|
982
1144
|
switch (issue.code) {
|
|
983
1145
|
case "keygen":
|
|
@@ -996,14 +1158,27 @@ function renderHostChannelIssue(issue, ctx) {
|
|
|
996
1158
|
sshdEnableHint: thisHost().sshdEnableHint()
|
|
997
1159
|
}).body;
|
|
998
1160
|
break;
|
|
999
|
-
case "
|
|
1000
|
-
cause = `
|
|
1161
|
+
case "unreachable":
|
|
1162
|
+
cause = `sshd is listening on this host, but the verification dial to 127.0.0.1 was not answered${detail}. That is an ADDRESS problem, not a key problem: a \`ListenAddress\` that excludes loopback, or a local firewall rule. The api container dials ${ctx.target}, so check that sshd accepts connections there \u2014 \`sudo sshd -T | grep -i listenaddress\` shows what it binds.`;
|
|
1001
1163
|
break;
|
|
1164
|
+
case "forced-command":
|
|
1165
|
+
cause = `sshd ACCEPTED the channel's key for \`${issue.account ?? "the channel account"}\` and then closed the session without running anything${detail}. That is what \`PermitRootLogin forced-commands-only\` does to a key with no \`command=\` of its own, and it is indistinguishable from a working channel until a host operation runs. Either permit this account a normal login, or use an account that has one: \`openship up --host-ssh-user <name>\`.`;
|
|
1166
|
+
break;
|
|
1167
|
+
case "auth-refused": {
|
|
1168
|
+
const accounts = issue.tried?.length ? issue.tried.map((a) => `\`${a}\``).join(" then ") : `\`${issue.account ?? "the channel account"}\``;
|
|
1169
|
+
const rootNote = issue.permitRootLogin ? ` This host's sshd reports \`PermitRootLogin ${issue.permitRootLogin}\`${sshdRefusesRootLogin(issue.permitRootLogin) ? ", which refuses a root login outright" : ""}.` : "";
|
|
1170
|
+
cause = `The key was authorized for ${accounts} and sshd is listening, but every dial was refused${detail}.${rootNote} sshd is not telling us which of several causes it is \u2014 \`Permission denied (publickey)\` is the same message for an account that may not log in, an \`authorized_keys\` sshd doesn't read for it, and a home directory whose permissions it rejects. Three things fix it:
|
|
1171
|
+
\u2022 name an account that CAN log in: \`openship up --host-ssh-user <name>\`
|
|
1172
|
+
\u2022 permit root a key login: \`PermitRootLogin prohibit-password\` (still refuses passwords), or scope it to the container with a \`Match Address\` block
|
|
1173
|
+
\u2022 or leave host control off: \`openship up --no-host-control\`
|
|
1174
|
+
Until one of them is done the api container will be refused every time it dials ${ctx.target}.`;
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1002
1177
|
case "error":
|
|
1003
1178
|
cause = `Setting up the channel's key failed${detail}.`;
|
|
1004
1179
|
break;
|
|
1005
1180
|
}
|
|
1006
|
-
const continuity = issue.code === "no-sshd" || issue.code === "
|
|
1181
|
+
const continuity = issue.code === "no-sshd" || issue.code === "unreachable" ? "" : ctx.kept ? hostIssueNote(
|
|
1007
1182
|
"Kept the host channel this install already had in `.env` rather than dropping it: its key is still authorized, so the api container keeps the access it had instead of losing it to this failure."
|
|
1008
1183
|
) : hostIssueNote(
|
|
1009
1184
|
"OPENSHIP_HOST_SSH_HOST is therefore unset, and host operations refuse rather than run against the api container's own filesystem."
|
|
@@ -1345,7 +1520,14 @@ function resolveEnvConfig(prev, opts) {
|
|
|
1345
1520
|
// so, and every `openship up` overwrote a hand-edited `.env` back to the
|
|
1346
1521
|
// value that didn't work (#490/#482).
|
|
1347
1522
|
hostSshHost: keepConfig(prev, "OPENSHIP_HOST_SSH_HOST", opts.hostSshHost) ?? HOST_CHANNEL_DEFAULT_HOST,
|
|
1348
|
-
hostSshPort: keepConfig(prev, "OPENSHIP_HOST_SSH_PORT", opts.hostSshPort) ?? String(HOST_CHANNEL_DEFAULT_PORT)
|
|
1523
|
+
hostSshPort: keepConfig(prev, "OPENSHIP_HOST_SSH_PORT", opts.hostSshPort) ?? String(HOST_CHANNEL_DEFAULT_PORT),
|
|
1524
|
+
// Sticky like its siblings, but kept in its OWN key rather than reusing
|
|
1525
|
+
// OPENSHIP_HOST_SSH_USER. That key records what provisioning SETTLED on, and the two
|
|
1526
|
+
// must not be conflated: reading a settled value back as a pin would freeze the account
|
|
1527
|
+
// a box happens to have — including the refused `root` on every install #527 broke — and
|
|
1528
|
+
// the fallback that repairs them could never run.
|
|
1529
|
+
...keepConfig(prev, "OPENSHIP_HOST_SSH_USER_PIN", opts.hostSshUser) ? { hostSshUser: keepConfig(prev, "OPENSHIP_HOST_SSH_USER_PIN", opts.hostSshUser) } : {},
|
|
1530
|
+
...prev.OPENSHIP_HOST_SSH_USER?.trim() ? { hostSshUserCarried: prev.OPENSHIP_HOST_SSH_USER.trim() } : {}
|
|
1349
1531
|
};
|
|
1350
1532
|
}
|
|
1351
1533
|
function toPort(value) {
|
|
@@ -1409,6 +1591,7 @@ async function resolveComposePorts(prefs, opts = {}) {
|
|
|
1409
1591
|
var OMITTABLE_MANAGED_KEYS = /* @__PURE__ */ new Set([
|
|
1410
1592
|
"OPENSHIP_HOST_SSH_HOST",
|
|
1411
1593
|
"OPENSHIP_HOST_SSH_USER",
|
|
1594
|
+
"OPENSHIP_HOST_SSH_USER_PIN",
|
|
1412
1595
|
"OPENSHIP_HOST_SSH_PORT",
|
|
1413
1596
|
"OPENSHIP_HOST_SSH_KEY",
|
|
1414
1597
|
"OPENSHIP_HOST_KEY_PATH",
|
|
@@ -1421,7 +1604,7 @@ function operatorEnvPassthrough(managed, prev) {
|
|
|
1421
1604
|
}
|
|
1422
1605
|
var PRESERVED_ENV_HEADER = "# Preserved from your existing .env \u2014 set by you, not by `openship up` (#485).";
|
|
1423
1606
|
function resolveImageVersion(opts) {
|
|
1424
|
-
return opts.version?.trim() || process.env.OPENSHIP_VERSION?.trim() || (true ? "0.6.
|
|
1607
|
+
return opts.version?.trim() || process.env.OPENSHIP_VERSION?.trim() || (true ? "0.6.7" : "latest");
|
|
1425
1608
|
}
|
|
1426
1609
|
function managedEnvLines(opts, host, cfg, prev) {
|
|
1427
1610
|
const lines = [
|
|
@@ -1470,6 +1653,8 @@ function managedEnvLines(opts, host, cfg, prev) {
|
|
|
1470
1653
|
if (host) {
|
|
1471
1654
|
lines.push(
|
|
1472
1655
|
`OPENSHIP_HOST_SSH_HOST=${cfg.hostSshHost}`,
|
|
1656
|
+
// What provisioning SETTLED on this run — a fact about the box, not a setting. The
|
|
1657
|
+
// operator's pin, when there is one, is written OUTSIDE this block (see below).
|
|
1473
1658
|
`OPENSHIP_HOST_SSH_USER=${host.user}`,
|
|
1474
1659
|
`OPENSHIP_HOST_SSH_PORT=${cfg.hostSshPort}`,
|
|
1475
1660
|
"OPENSHIP_HOST_SSH_KEY=/run/secrets/openship_host_key",
|
|
@@ -1477,6 +1662,7 @@ function managedEnvLines(opts, host, cfg, prev) {
|
|
|
1477
1662
|
...hostGatewayEnv(prev)
|
|
1478
1663
|
);
|
|
1479
1664
|
}
|
|
1665
|
+
if (cfg.hostSshUser) lines.push(`OPENSHIP_HOST_SSH_USER_PIN=${cfg.hostSshUser}`);
|
|
1480
1666
|
return lines;
|
|
1481
1667
|
}
|
|
1482
1668
|
function renderEnvAndCarried(opts, host, cfg, prev) {
|
|
@@ -1561,7 +1747,7 @@ function composePlan(opts) {
|
|
|
1561
1747
|
const prev = readEnvFile();
|
|
1562
1748
|
const cfg = resolveEnvConfig(prev, opts);
|
|
1563
1749
|
const buildDir = opts.build === false ? null : sourceBuildDir();
|
|
1564
|
-
const hostChannel =
|
|
1750
|
+
const hostChannel = previewHostChannel(cfg);
|
|
1565
1751
|
const settings = [];
|
|
1566
1752
|
const newSecrets = [];
|
|
1567
1753
|
for (const line of renderEnv(opts, hostChannel, cfg).split("\n")) {
|
|
@@ -4,15 +4,15 @@ import { createRequire as __ospCreateRequire } from "node:module";
|
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
hostRefusal
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-BLEXTYRK.js";
|
|
8
8
|
import {
|
|
9
9
|
elevatedExecutor,
|
|
10
10
|
resolveEnvironment
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FZ6QCF6B.js";
|
|
12
12
|
import {
|
|
13
13
|
answered,
|
|
14
14
|
refused
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-2VSW6Q6C.js";
|
|
16
16
|
|
|
17
17
|
// ../../packages/adapters/src/system/privilege.ts
|
|
18
18
|
function rootChecked(executor, why) {
|
|
@@ -5,19 +5,19 @@ const require = __ospCreateRequire(import.meta.url);
|
|
|
5
5
|
import {
|
|
6
6
|
envOps,
|
|
7
7
|
opScript
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-BLEXTYRK.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveEnvironment
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FZ6QCF6B.js";
|
|
12
12
|
import {
|
|
13
13
|
sq
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-H3WT3A6R.js";
|
|
15
15
|
import {
|
|
16
16
|
SYSTEM,
|
|
17
17
|
answered,
|
|
18
18
|
refused,
|
|
19
19
|
safeErrorMessage
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-2VSW6Q6C.js";
|
|
21
21
|
|
|
22
22
|
// ../../packages/adapters/src/infra/openresty-lua.ts
|
|
23
23
|
import { createHash, randomBytes } from "crypto";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
|
+
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
|
+
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// ../../packages/adapters/src/system/reachability.ts
|
|
7
|
+
import { connect } from "net";
|
|
8
|
+
import { request } from "http";
|
|
9
|
+
function classifyDialError(code) {
|
|
10
|
+
switch (code) {
|
|
11
|
+
case "ECONNREFUSED":
|
|
12
|
+
case "ECONNRESET":
|
|
13
|
+
return "refused";
|
|
14
|
+
case "ENOTFOUND":
|
|
15
|
+
case "EAI_AGAIN":
|
|
16
|
+
return "unresolved";
|
|
17
|
+
case "EHOSTUNREACH":
|
|
18
|
+
case "ENETUNREACH":
|
|
19
|
+
case "ENETDOWN":
|
|
20
|
+
return "no_route";
|
|
21
|
+
// The kernel's own connect timeout, i.e. our `timeout` reported for us.
|
|
22
|
+
case "ETIMEDOUT":
|
|
23
|
+
return "timeout";
|
|
24
|
+
default:
|
|
25
|
+
return "error";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function probeTcpDetailed(host, port, timeoutMs = 2500) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
let settled = false;
|
|
31
|
+
const done = (result, socket2) => {
|
|
32
|
+
if (settled) return;
|
|
33
|
+
settled = true;
|
|
34
|
+
try {
|
|
35
|
+
socket2?.destroy();
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
resolve(result);
|
|
39
|
+
};
|
|
40
|
+
const socket = connect({ host, port });
|
|
41
|
+
socket.setTimeout(timeoutMs);
|
|
42
|
+
socket.once("connect", () => done({ ok: true }, socket));
|
|
43
|
+
socket.once(
|
|
44
|
+
"timeout",
|
|
45
|
+
() => done({ ok: false, reason: "timeout", message: `no response within ${timeoutMs}ms` }, socket)
|
|
46
|
+
);
|
|
47
|
+
socket.once("error", (err) => {
|
|
48
|
+
const errno = err;
|
|
49
|
+
done(
|
|
50
|
+
{
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: classifyDialError(errno.code),
|
|
53
|
+
...errno.code ? { code: errno.code } : {},
|
|
54
|
+
...errno.message ? { message: errno.message } : {}
|
|
55
|
+
},
|
|
56
|
+
socket
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function probeHostedHttp(opts) {
|
|
62
|
+
const { hostname, path = "/", port = 80, timeoutMs = 3e3 } = opts;
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
let settled = false;
|
|
65
|
+
const done = (value) => {
|
|
66
|
+
if (settled) return;
|
|
67
|
+
settled = true;
|
|
68
|
+
resolve(value);
|
|
69
|
+
};
|
|
70
|
+
const req = request(
|
|
71
|
+
{
|
|
72
|
+
host: "127.0.0.1",
|
|
73
|
+
port,
|
|
74
|
+
path,
|
|
75
|
+
method: "GET",
|
|
76
|
+
timeout: timeoutMs,
|
|
77
|
+
// Claim https so the :80 block SERVES instead of answering our own
|
|
78
|
+
// HTTP→HTTPS redirect before the doc root is consulted — see the curl twin in
|
|
79
|
+
// output-exists.ts for why a 301 here read as a healthy site.
|
|
80
|
+
headers: { Host: hostname, "X-Forwarded-Proto": "https" }
|
|
81
|
+
},
|
|
82
|
+
(res) => {
|
|
83
|
+
const status = res.statusCode ?? 0;
|
|
84
|
+
res.resume();
|
|
85
|
+
done(
|
|
86
|
+
status > 0 ? { status, served: status >= 200 && status < 400 || status === 401 || status === 403 } : null
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
const fail = () => {
|
|
91
|
+
try {
|
|
92
|
+
req.destroy();
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
done(null);
|
|
96
|
+
};
|
|
97
|
+
req.once("timeout", fail);
|
|
98
|
+
req.once("error", fail);
|
|
99
|
+
req.end();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
probeTcpDetailed,
|
|
105
|
+
probeHostedHttp
|
|
106
|
+
};
|
|
@@ -7,18 +7,20 @@ import {
|
|
|
7
7
|
composeInstallExists,
|
|
8
8
|
readComposeEnvFile,
|
|
9
9
|
startUnitHint
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-VQM6JHJB.js";
|
|
11
11
|
import {
|
|
12
12
|
INTERNAL_TOKEN_FILE,
|
|
13
13
|
OS_DIR
|
|
14
14
|
} from "./chunk-ELGRMZOW.js";
|
|
15
15
|
import {
|
|
16
16
|
LocalExecutor
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-E2X26PXR.js";
|
|
18
18
|
import {
|
|
19
|
-
EDGE_CONTAINER_NAME,
|
|
20
19
|
edgeCrashReason
|
|
21
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-GIPJNNI5.js";
|
|
21
|
+
import {
|
|
22
|
+
EDGE_CONTAINER_NAME
|
|
23
|
+
} from "./chunk-UWEV6XTS.js";
|
|
22
24
|
|
|
23
25
|
// src/lib/edge-import.ts
|
|
24
26
|
import chalk from "chalk";
|