sliftutils 1.7.128 → 1.7.130
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/bin/addmachine.js +9 -0
- package/bin/machineclient.js +9 -0
- package/bin/machineserver.js +10 -0
- package/index.d.ts +15 -0
- package/misc/dist/ownIPs.ts.cache +69 -0
- package/misc/ownIPs.d.ts +11 -0
- package/misc/ownIPs.ts +62 -0
- package/package.json +9 -3
- package/security/authorizedKeys/authorizedKeys.ts +34 -0
- package/security/authorizedKeys/daemon/authLog.ts +12 -5
- package/security/authorizedKeys/daemon/changes.ts +16 -1
- package/security/authorizedKeys/daemon/daemon.ts +43 -34
- package/security/authorizedKeys/daemon/dist/changes.ts.cache +40 -0
- package/security/authorizedKeys/daemon/dist/git.ts.cache +121 -0
- package/security/authorizedKeys/daemon/dist/notify.ts.cache +39 -0
- package/security/authorizedKeys/daemon/dist/paths.ts.cache +26 -0
- package/security/authorizedKeys/daemon/dist/repoFiles.ts.cache +114 -0
- package/security/authorizedKeys/daemon/dist/revocation.ts.cache +0 -0
- package/security/authorizedKeys/daemon/dist/sessions.ts.cache +147 -0
- package/security/authorizedKeys/daemon/dist/state.ts.cache +76 -0
- package/security/authorizedKeys/daemon/dist/trust.ts.cache +295 -0
- package/security/authorizedKeys/daemon/notify.ts +8 -2
- package/security/authorizedKeys/daemon/paths.ts +0 -3
- package/security/authorizedKeys/daemon/repoFiles.ts +118 -0
- package/security/authorizedKeys/daemon/revocation.ts +0 -0
- package/security/authorizedKeys/daemon/rootKeys.ts +77 -10
- package/security/authorizedKeys/daemon/state.ts +21 -7
- package/security/authorizedKeys/daemon/trust.ts +37 -16
- package/security/authorizedKeys/daemon/userKeys.ts +5 -4
- package/security/authorizedKeys/dist/authorizedKeys.ts.cache +37 -3
- package/security/authorizedKeys/dist/keysRepo.ts.cache +111 -0
- package/security/authorizedKeys/dist/revokeSource.ts.cache +17 -4
- package/security/authorizedKeys/dist/secureSSH.ts.cache +147 -71
- package/security/authorizedKeys/dist/sources.ts.cache +52 -8
- package/security/authorizedKeys/dist/unrevoke.ts.cache +159 -44
- package/security/authorizedKeys/keysRepo.ts +111 -0
- package/security/authorizedKeys/secureSSH.ts +66 -58
- package/security/authorizedKeys/unrevoke.ts +160 -36
- package/security/helpers/dist/remoteSSH.ts.cache +19 -9
- package/security/machines/addMachine.ts +84 -0
- package/security/machines/dist/addMachine.ts.cache +88 -0
- package/security/machines/dist/identity.ts.cache +176 -0
- package/security/machines/dist/machines.ts.cache +264 -0
- package/security/machines/dist/trustClient.ts.cache +48 -0
- package/security/machines/dist/trustState.ts.cache +57 -0
- package/security/machines/identity.ts +199 -0
- package/security/machines/machines.ts +304 -0
- package/security/machines/trustClient.ts +51 -0
- package/security/machines/trustServer.ts +87 -0
- package/security/notifications/discord.ts +14 -5
- package/security/notifications/dist/discord.ts.cache +17 -8
- package/security/notifications/dist/remoteWebhook.ts.cache +78 -0
- package/security/notifications/dist/setupNotify.ts.cache +39 -0
- package/security/signedFiles/dist/signFiles.ts.cache +8 -10
- package/security/signedFiles/signFiles.ts +6 -10
|
@@ -7,6 +7,7 @@ import { findSourceKey, KEYS_DIR_NAME, sourceKeyPath, sourceName, sourceRepoPath
|
|
|
7
7
|
import { deriveRevokeKey, REVOKE_KEY_LABEL, revokeRepoURL } from "./revokeSource";
|
|
8
8
|
import { legacySourceKeyPath } from "./sources";
|
|
9
9
|
import { revokedKeysInRepo } from "./unrevoke";
|
|
10
|
+
import { resolveKeysRepo } from "./keysRepo";
|
|
10
11
|
import { expandHome } from "../helpers/paths";
|
|
11
12
|
import { spawnPromise } from "../helpers/spawn";
|
|
12
13
|
import { describeHost, readRemoteFile, remoteCommandExists, runOverSSH, SUDO_PREAMBLE, THIS_MACHINE, writeRemoteFile } from "../helpers/remoteSSH";
|
|
@@ -155,38 +156,15 @@ async function cloneRepoForInspection(config: { repoURL: string; keyPath: string
|
|
|
155
156
|
return repoPath;
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
/** With no repo url given, the repo
|
|
159
|
-
|
|
159
|
+
/** With no repo url given, the keys repo this machine resolves to is used - the one we are
|
|
160
|
+
standing in when that holds keys, and this machine's own otherwise. */
|
|
160
161
|
async function resolveRepoURL(passedURL: string | undefined) {
|
|
161
162
|
if (passedURL) {
|
|
162
163
|
return normalizeRepoURL(passedURL);
|
|
163
164
|
}
|
|
164
|
-
let
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
let repoPath = topLevel.stdout.trim();
|
|
169
|
-
try {
|
|
170
|
-
await readRepoKeys(repoPath);
|
|
171
|
-
} catch (e) {
|
|
172
|
-
throw new Error(
|
|
173
|
-
`Expected a repo url, or the current repo (${repoPath}) to hold keys, it does not.\n${e}\n${USAGE}`
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
let origin = await runLocal({
|
|
177
|
-
command: "git",
|
|
178
|
-
args: ["remote", "get-url", "origin"],
|
|
179
|
-
cwd: repoPath,
|
|
180
|
-
allowFailure: true,
|
|
181
|
-
});
|
|
182
|
-
if (origin.status !== 0 || !origin.stdout.trim()) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
`Expected the current repo (${repoPath}) to have an origin remote, it has none.`
|
|
185
|
-
+ ` The host clones the source itself, so a local path is no use to it.`
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
let repoURL = normalizeRepoURL(origin.stdout.trim());
|
|
189
|
-
console.log(`No repo url given, using the current repo: ${repoURL}`);
|
|
165
|
+
let { repoPath, sourceURL } = await resolveKeysRepo();
|
|
166
|
+
let repoURL = normalizeRepoURL(sourceURL);
|
|
167
|
+
console.log(`No repo url given, using ${repoPath}: ${repoURL}`);
|
|
190
168
|
return repoURL;
|
|
191
169
|
}
|
|
192
170
|
|
|
@@ -449,6 +427,31 @@ async function addSource(config: { host: string; keyPath: string; repoURL: strin
|
|
|
449
427
|
console.log(`${describeHost(host)} already has ${repoURL}, refreshing its key and the daemon.`);
|
|
450
428
|
}
|
|
451
429
|
|
|
430
|
+
// Copied into place before anything else needs it. The checks below look this source's key up
|
|
431
|
+
// where the machine keeps it, not at the path it was handed on the command line, so a key that
|
|
432
|
+
// is still only sitting in the caller's directory reads as no key at all.
|
|
433
|
+
//
|
|
434
|
+
// Into the home of whoever the daemon runs as, which is where it looks. Asked of the host
|
|
435
|
+
// rather than worked out here: this may be running on a machine with no such user, and with a
|
|
436
|
+
// home directory in an entirely different shape.
|
|
437
|
+
let hostKeysDirectory = await daemonKeysDir(host);
|
|
438
|
+
await writeRemoteFile({
|
|
439
|
+
host,
|
|
440
|
+
filePath: `${hostKeysDirectory}/${sourceName(repoURL)}`,
|
|
441
|
+
contents: await fs.readFile(keyPath, "utf8"),
|
|
442
|
+
fileMode: "600",
|
|
443
|
+
directoryMode: "700",
|
|
444
|
+
});
|
|
445
|
+
// The public half too, so the deploy key that a repo was given can be looked up on the machine
|
|
446
|
+
// using it, rather than only in whatever github shows.
|
|
447
|
+
await writeRemoteFile({
|
|
448
|
+
host,
|
|
449
|
+
filePath: `${hostKeysDirectory}/${sourceName(repoURL)}.pub`,
|
|
450
|
+
contents: await publicKeyOf(keyPath),
|
|
451
|
+
fileMode: "644",
|
|
452
|
+
directoryMode: "700",
|
|
453
|
+
});
|
|
454
|
+
|
|
452
455
|
// The merged result is what root ends up with, so our own key has to be somewhere in it. On
|
|
453
456
|
// this machine there is no ssh session to preserve, so there is nothing to check.
|
|
454
457
|
let ourFingerprint = "";
|
|
@@ -490,27 +493,6 @@ async function addSource(config: { host: string; keyPath: string; repoURL: strin
|
|
|
490
493
|
let webhookURL = await requireRemoteWebhook(host);
|
|
491
494
|
console.log(`${describeHost(host)} notifies ${webhookURL}`);
|
|
492
495
|
|
|
493
|
-
// Into the home of whoever the daemon runs as, which is where it looks. Asked of the host
|
|
494
|
-
// rather than worked out here: this may be running on a machine with no such user, and with a
|
|
495
|
-
// home directory in an entirely different shape.
|
|
496
|
-
let hostKeysDirectory = await daemonKeysDir(host);
|
|
497
|
-
await writeRemoteFile({
|
|
498
|
-
host,
|
|
499
|
-
filePath: `${hostKeysDirectory}/${sourceName(repoURL)}`,
|
|
500
|
-
contents: await fs.readFile(keyPath, "utf8"),
|
|
501
|
-
fileMode: "600",
|
|
502
|
-
directoryMode: "700",
|
|
503
|
-
});
|
|
504
|
-
// The public half too, so the deploy key that a repo was given can be looked up on the machine
|
|
505
|
-
// using it, rather than only in whatever github shows.
|
|
506
|
-
await writeRemoteFile({
|
|
507
|
-
host,
|
|
508
|
-
filePath: `${hostKeysDirectory}/${sourceName(repoURL)}.pub`,
|
|
509
|
-
contents: await publicKeyOf(keyPath),
|
|
510
|
-
fileMode: "644",
|
|
511
|
-
directoryMode: "700",
|
|
512
|
-
});
|
|
513
|
-
|
|
514
496
|
let repoSources = remoteConfig.repoSources.filter(source => source !== repoURL);
|
|
515
497
|
repoSources.push(repoURL);
|
|
516
498
|
await installDaemon({ host, hostLabel: remoteConfig.hostLabel, repoSources });
|
|
@@ -634,32 +616,58 @@ function parseArgs(argv: string[]) {
|
|
|
634
616
|
host = positional[0];
|
|
635
617
|
positional = positional.slice(1);
|
|
636
618
|
}
|
|
619
|
+
|
|
620
|
+
// How many arguments each verb takes, checked here rather than where the verb is acted on, so
|
|
621
|
+
// that a command which was typed wrong says so before anything else is complained about.
|
|
622
|
+
if (verb === "list" && positional.length) {
|
|
623
|
+
throw new Error(`Expected nothing after list, was ${positional.length} argument(s)\n${USAGE}`);
|
|
624
|
+
}
|
|
625
|
+
if (verb === "update" && positional.length) {
|
|
626
|
+
throw new Error(`Expected nothing after update, was ${positional.length} argument(s)\n${USAGE}`);
|
|
627
|
+
}
|
|
628
|
+
if (verb === "add" && (!positional.length || positional.length > 2)) {
|
|
629
|
+
throw new Error(`Expected a private key and optionally a repo url, was ${positional.length} argument(s)\n${USAGE}`);
|
|
630
|
+
}
|
|
631
|
+
if (verb === "remove" && positional.length > 1) {
|
|
632
|
+
throw new Error(`Expected at most a repo url to remove, was ${positional.length} argument(s)\n${USAGE}`);
|
|
633
|
+
}
|
|
637
634
|
return { verb, host, rest: positional };
|
|
638
635
|
}
|
|
639
636
|
|
|
637
|
+
/** Everything here is root's: root's authorized_keys, root's keys directory, the sshd config, the
|
|
638
|
+
systemd unit. Run as anyone else it reads a different home and writes files the daemon will
|
|
639
|
+
never see, and it gets there several steps in rather than failing at the start. */
|
|
640
|
+
function requireRoot() {
|
|
641
|
+
let uid = process.getuid?.();
|
|
642
|
+
if (uid !== 0) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
`Expected to be running as root, is uid ${uid ?? "unknown"}.\n`
|
|
645
|
+
+ `Rerun this with sudo.`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
640
650
|
export async function main() {
|
|
651
|
+
// What was typed is checked before anything about this machine is. A command that does not
|
|
652
|
+
// make sense is worth saying so about whoever is running it, and being told to find sudo only
|
|
653
|
+
// to then be told the arguments were wrong is two trips for one mistake.
|
|
641
654
|
let { verb, host, rest } = parseArgs(process.argv.slice(2));
|
|
655
|
+
// With a host, everything root-owned happens on that host through $SUDO over ssh, so being root here proves nothing and would block running this from a workstation.
|
|
656
|
+
if (host === THIS_MACHINE) {
|
|
657
|
+
requireRoot();
|
|
658
|
+
}
|
|
642
659
|
|
|
643
660
|
if (verb === "list") {
|
|
644
661
|
await listSources(host);
|
|
645
662
|
return;
|
|
646
663
|
}
|
|
647
664
|
if (verb === "update") {
|
|
648
|
-
if (rest.length) {
|
|
649
|
-
throw new Error(`Expected nothing after update, was ${rest.length} argument(s)\n${USAGE}`);
|
|
650
|
-
}
|
|
651
665
|
await updateDaemon(host);
|
|
652
666
|
return;
|
|
653
667
|
}
|
|
654
668
|
if (verb === "add") {
|
|
655
|
-
if (!rest.length || rest.length > 2) {
|
|
656
|
-
throw new Error(`Expected a private key and optionally a repo url, was ${rest.length} argument(s)\n${USAGE}`);
|
|
657
|
-
}
|
|
658
669
|
await addSource({ host, keyPath: expandHome(rest[0]), repoURL: await resolveRepoURL(rest[1]) });
|
|
659
670
|
return;
|
|
660
671
|
}
|
|
661
|
-
if (rest.length > 1) {
|
|
662
|
-
throw new Error(`Expected at most a repo url to remove, was ${rest.length} argument(s)\n${USAGE}`);
|
|
663
|
-
}
|
|
664
672
|
await removeSource({ host, repoURL: await resolveRepoURL(rest[0]) });
|
|
665
673
|
}
|
|
@@ -2,13 +2,15 @@ import fs from "fs/promises";
|
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { runPromise } from "socket-function/src/runPromise";
|
|
5
|
-
import { keyFingerprint, normalizeKeys } from "./authorizedKeys";
|
|
5
|
+
import { allowAddresses, keyFingerprint, normalizeKeys, summarizeKey } from "./authorizedKeys";
|
|
6
6
|
import { deriveRevokeKey, revokeRepoURL } from "./revokeSource";
|
|
7
7
|
import { findSourceKey } from "./sources";
|
|
8
8
|
import { signRepo } from "../signedFiles/signFiles";
|
|
9
9
|
import { readRepoKeys } from "./authorizedKeys";
|
|
10
10
|
import { expandHome } from "../helpers/paths";
|
|
11
11
|
import { spawnPromise } from "../helpers/spawn";
|
|
12
|
+
import { getMachines, setMachines } from "../machines/machines";
|
|
13
|
+
import { resolveKeysRepo } from "./keysRepo";
|
|
12
14
|
|
|
13
15
|
const UNREVOKES_DIR = "unrevoked";
|
|
14
16
|
const REVOCATIONS_DIR = "revocations";
|
|
@@ -17,21 +19,41 @@ const COMMIT_MESSAGE = "unrevoke keys";
|
|
|
17
19
|
const USAGE = `Usage: yarn unrevoke [keys-repo] [${GIT_KEYWORD}]
|
|
18
20
|
|
|
19
21
|
Run this in a keys repo, or name one. It reads that repo's revoke repo and writes one unrevoke file
|
|
20
|
-
naming every revocation in it, so the keys are accepted again
|
|
21
|
-
|
|
22
|
+
naming every revocation in it, so the keys are accepted again as each machine picks it up. It signs
|
|
23
|
+
the result, and with ${GIT_KEYWORD} it commits and pushes it too.
|
|
22
24
|
|
|
23
25
|
Keys that were revoked should normally be deleted from the repo instead. Unrevoking only matters
|
|
24
26
|
for a key you still want.`;
|
|
25
27
|
|
|
28
|
+
/** One revocation event. Either an ssh key used from an address it is not allowed from, or a
|
|
29
|
+
machine that talked to us from one. Both are the same shape - an identity and an address - and
|
|
30
|
+
an unrevoke undoes either by allowing that same pair.
|
|
31
|
+
|
|
32
|
+
Which is why unrevoking never makes anything unrevokable: used from some other address, it is
|
|
33
|
+
revoked again, under a new id no existing unrevoke names. */
|
|
26
34
|
export type Revocation = {
|
|
27
35
|
revocationId: string;
|
|
28
|
-
fingerprint
|
|
36
|
+
// Exactly one of these. A key revocation names a fingerprint, a machine revocation a machineId.
|
|
37
|
+
fingerprint?: string;
|
|
38
|
+
machineId?: string;
|
|
39
|
+
ip?: string;
|
|
29
40
|
key?: string;
|
|
30
41
|
revokedAt?: string;
|
|
31
42
|
revokedBy?: string;
|
|
43
|
+
reason?: string;
|
|
32
44
|
attempt?: { ip?: string; user?: string; required?: string };
|
|
33
45
|
};
|
|
34
46
|
|
|
47
|
+
/** Older revocation files carry the address under attempt only. */
|
|
48
|
+
export function revocationIP(revocation: Revocation) {
|
|
49
|
+
return revocation.ip || revocation.attempt?.ip || "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What the revocation is about, which is what an unrevoke has to name to undo it. */
|
|
53
|
+
export function revocationIdentity(revocation: Revocation) {
|
|
54
|
+
return revocation.fingerprint || revocation.machineId || "";
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
/** Every revocation the revoke repo lists. Cloned read only into a temp directory.
|
|
36
58
|
|
|
37
59
|
A person running this has their own access to the repo, so their credentials are what is used.
|
|
@@ -86,6 +108,10 @@ export async function revokedKeysInRepo(config: { repoPath: string; sourceURL: s
|
|
|
86
108
|
continue;
|
|
87
109
|
}
|
|
88
110
|
let parsed = JSON.parse(await fs.readFile(path.join(directory, name), "utf8"));
|
|
111
|
+
for (let allowed of parsed.allowed || []) {
|
|
112
|
+
unrevoked.add(`${allowed.fingerprint || allowed.machineId} ${allowed.ip}`);
|
|
113
|
+
}
|
|
114
|
+
// Named ids, for the unrevokes written before this was about pairs.
|
|
89
115
|
for (let revocationId of parsed.revocationIds || []) {
|
|
90
116
|
unrevoked.add(revocationId);
|
|
91
117
|
}
|
|
@@ -93,40 +119,85 @@ export async function revokedKeysInRepo(config: { repoPath: string; sourceURL: s
|
|
|
93
119
|
} catch (e) {
|
|
94
120
|
// Nothing has been unrevoked.
|
|
95
121
|
}
|
|
96
|
-
let stillRevoked = revocations.filter(revocation => !unrevoked.has(revocation.revocationId)
|
|
122
|
+
let stillRevoked = revocations.filter(revocation => !unrevoked.has(revocation.revocationId)
|
|
123
|
+
&& !unrevoked.has(`${revocation.fingerprint} ${revocationIP(revocation)}`));
|
|
97
124
|
let keys = normalizeKeys(await fs.readFile(path.join(repoPath, "authorized_keys"), "utf8").catch(() => ""));
|
|
98
125
|
return stillRevoked
|
|
99
126
|
.filter(revocation => keys.some(key => keyFingerprint(key) === revocation.fingerprint))
|
|
100
127
|
.map(revocation => ({ revocation, key: keys.find(key => keyFingerprint(key) === revocation.fingerprint) || "" }));
|
|
101
128
|
}
|
|
102
129
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
let repoPath =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
+ `${(topLevel.stdout + topLevel.stderr).trim()}\n${USAGE}`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
130
|
+
/** Writes the addresses into the from= list of each key in the repo's authorized_keys, and says
|
|
131
|
+
which ones were actually new. Only the lines being changed are touched, so comments, ordering
|
|
132
|
+
and any key not involved come out of this byte for byte the same. */
|
|
133
|
+
async function allowAddressesInRepo(config: {
|
|
134
|
+
repoPath: string;
|
|
135
|
+
addressesByFingerprint: Map<string, string[]>;
|
|
136
|
+
}) {
|
|
137
|
+
let { repoPath, addressesByFingerprint } = config;
|
|
138
|
+
let added = new Map<string, string[]>();
|
|
139
|
+
let filePath = path.join(repoPath, "authorized_keys");
|
|
140
|
+
let contents;
|
|
117
141
|
try {
|
|
118
|
-
await
|
|
142
|
+
contents = await fs.readFile(filePath, "utf8");
|
|
119
143
|
} catch (e) {
|
|
120
|
-
|
|
144
|
+
// A repo holding its keys as separate .pub files. Nothing to edit in one place, so the
|
|
145
|
+
// caller's listing is all whoever ran this gets.
|
|
146
|
+
console.log(`No authorized_keys in ${repoPath}, so the addresses were not added to any key.`);
|
|
147
|
+
return added;
|
|
148
|
+
}
|
|
149
|
+
let lines = contents.split("\n");
|
|
150
|
+
for (let index = 0; index < lines.length; index++) {
|
|
151
|
+
let fingerprint = keyFingerprint(lines[index]);
|
|
152
|
+
let addresses = fingerprint && addressesByFingerprint.get(fingerprint);
|
|
153
|
+
if (!addresses || !addresses.length) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
let result = allowAddresses(lines[index], addresses);
|
|
157
|
+
lines[index] = result.keyLine;
|
|
158
|
+
added.set(fingerprint, [...(added.get(fingerprint) || []), ...result.added]);
|
|
159
|
+
}
|
|
160
|
+
if ([...added.values()].some(list => list.length)) {
|
|
161
|
+
await fs.writeFile(filePath, lines.join("\n"));
|
|
162
|
+
}
|
|
163
|
+
return added;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Puts each machine's addresses back into its own file, which is the machine equivalent of adding
|
|
167
|
+
an address to a key's from= list: without it the machine is allowed again in principle and
|
|
168
|
+
still rejected on sight, which revokes it once more. */
|
|
169
|
+
async function allowAddressesForMachines(config: { repoPath: string; revocations: Revocation[] }) {
|
|
170
|
+
let { repoPath, revocations } = config;
|
|
171
|
+
let added = new Map<string, string[]>();
|
|
172
|
+
// Read the whole list, change what these revocations are about, write it back. Setting is by
|
|
173
|
+
// the whole set, so writing one machine at a time would delete every other machine.
|
|
174
|
+
let machines = await getMachines(repoPath);
|
|
175
|
+
for (let revocation of revocations) {
|
|
176
|
+
let machineId = revocation.machineId;
|
|
177
|
+
let ip = revocationIP(revocation);
|
|
178
|
+
if (!machineId || !ip) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
let existing = machines.find(machine => machine.machineId === machineId);
|
|
182
|
+
if (!existing) {
|
|
183
|
+
// Removed from the repo since it was revoked, so there is nothing to allow. Deleting a
|
|
184
|
+
// machine is the stronger statement and it stands.
|
|
185
|
+
console.log(`machines/${machineId}.json is not in this repo, so ${ip} was not added to it`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (existing.ips.includes(ip)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
existing.ips = [...existing.ips, ip];
|
|
192
|
+
added.set(machineId, [...(added.get(machineId) || []), ip]);
|
|
121
193
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (origin.status !== 0 || !originURL) {
|
|
125
|
-
throw new Error(`Expected ${repoPath} to have an origin remote, it has none.\n${USAGE}`);
|
|
194
|
+
if (added.size) {
|
|
195
|
+
await setMachines({ repoPath, machines });
|
|
126
196
|
}
|
|
127
|
-
return
|
|
197
|
+
return added;
|
|
128
198
|
}
|
|
129
199
|
|
|
200
|
+
|
|
130
201
|
export async function main() {
|
|
131
202
|
let argv = process.argv.slice(2);
|
|
132
203
|
let pushToGit = argv.includes(GIT_KEYWORD);
|
|
@@ -134,7 +205,7 @@ export async function main() {
|
|
|
134
205
|
if (positional.length > 1) {
|
|
135
206
|
throw new Error(`Expected at most a keys repo, was ${positional.length} argument(s)\n${USAGE}`);
|
|
136
207
|
}
|
|
137
|
-
let { repoPath, originURL } = await resolveKeysRepo(positional[0]);
|
|
208
|
+
let { repoPath, sourceURL: originURL } = await resolveKeysRepo(positional[0]);
|
|
138
209
|
|
|
139
210
|
let revocations = await readRemoteRevocations({ sourceURL: originURL });
|
|
140
211
|
if (!revocations.length) {
|
|
@@ -149,36 +220,89 @@ export async function main() {
|
|
|
149
220
|
await fs.writeFile(path.join(directory, `${unrevokeId}.json`), JSON.stringify({
|
|
150
221
|
unrevokeId,
|
|
151
222
|
createdAt: new Date().toISOString(),
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
223
|
+
// What this file says: each of these keys is allowed from this address. Pairs, not keys,
|
|
224
|
+
// and not a blanket "allow everything again" - a key forgiven here for one address is
|
|
225
|
+
// still revoked the moment it is used from another.
|
|
226
|
+
allowed: revocations.map(revocation => ({
|
|
227
|
+
fingerprint: revocation.fingerprint,
|
|
228
|
+
machineId: revocation.machineId,
|
|
229
|
+
ip: revocationIP(revocation),
|
|
230
|
+
})),
|
|
155
231
|
revocations: revocations.map(revocation => ({
|
|
156
232
|
revocationId: revocation.revocationId,
|
|
157
233
|
fingerprint: revocation.fingerprint,
|
|
234
|
+
machineId: revocation.machineId,
|
|
235
|
+
ip: revocationIP(revocation),
|
|
158
236
|
revokedAt: revocation.revokedAt,
|
|
159
237
|
revokedBy: revocation.revokedBy,
|
|
160
238
|
attempt: revocation.attempt,
|
|
161
239
|
})),
|
|
162
240
|
}, undefined, 4) + "\n");
|
|
163
241
|
|
|
164
|
-
|
|
242
|
+
// Named the way the notifications name them. A fingerprint is not in authorized_keys and
|
|
243
|
+
// cannot be worked out by eye, so on its own it tells whoever is reading this nothing about
|
|
244
|
+
// which of their keys is being talked about.
|
|
245
|
+
let keysByFingerprint = new Map<string, string>();
|
|
246
|
+
for (let key of await readRepoKeys(repoPath).catch(() => [] as string[])) {
|
|
247
|
+
keysByFingerprint.set(keyFingerprint(key), key);
|
|
248
|
+
}
|
|
249
|
+
let describeKey = (revocation: Revocation) => {
|
|
250
|
+
let key = keysByFingerprint.get(revocation.fingerprint || "") || revocation.key || "";
|
|
251
|
+
return key && summarizeKey(key) || `a key no longer in this repo (${revocation.fingerprint})`;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
let addressesByIdentity = new Map<string, string[]>();
|
|
165
255
|
for (let revocation of revocations) {
|
|
166
|
-
|
|
256
|
+
let ip = revocationIP(revocation);
|
|
257
|
+
let identity = revocationIdentity(revocation);
|
|
258
|
+
let existing = addressesByIdentity.get(identity) || [];
|
|
259
|
+
if (ip && !existing.includes(ip)) {
|
|
260
|
+
existing.push(ip);
|
|
261
|
+
}
|
|
262
|
+
addressesByIdentity.set(identity, existing);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Cancelling the revocation on its own would leave sshd refusing that address, and the machines
|
|
266
|
+
// revoking all over again, so the addresses go into the key's from= list and into the machine's
|
|
267
|
+
// own file here. We are in the repo that owns both, so there is nothing to hand back to whoever
|
|
268
|
+
// ran this.
|
|
269
|
+
let added = await allowAddressesInRepo({ repoPath, addressesByFingerprint: addressesByIdentity });
|
|
270
|
+
let addedForMachines = await allowAddressesForMachines({ repoPath, revocations });
|
|
271
|
+
|
|
272
|
+
console.log("");
|
|
273
|
+
for (let [identity, addresses] of addressesByIdentity) {
|
|
274
|
+
let entries = revocations.filter(entry => revocationIdentity(entry) === identity);
|
|
275
|
+
let machineId = entries[0].machineId;
|
|
276
|
+
console.log(`Unfreezing ${machineId && `machine ${machineId}` || describeKey(entries[0])}`);
|
|
277
|
+
console.log(`Allowing IP ${addresses.join(", ") || "(no address recorded)"}`);
|
|
278
|
+
let addedHere = machineId && addedForMachines.get(machineId) || added.get(identity) || [];
|
|
279
|
+
if (addedHere.length) {
|
|
280
|
+
let where = machineId && `machines/${machineId}.json` || `that key's from= list in authorized_keys`;
|
|
281
|
+
console.log(` added to ${where}: ${addedHere.join(", ")}`);
|
|
282
|
+
} else {
|
|
283
|
+
console.log(` already allowed from ${addresses.join(", ") || "everywhere"}`);
|
|
284
|
+
}
|
|
285
|
+
for (let entry of entries) {
|
|
286
|
+
console.log(
|
|
287
|
+
` frozen by ${entry.revokedBy || "?"} ${entry.revokedAt || "at an unrecorded time"}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
console.log("");
|
|
167
291
|
}
|
|
292
|
+
console.log(`Every other address stays frozen, and any of these used from anywhere else is`);
|
|
293
|
+
console.log(`frozen again.`);
|
|
168
294
|
|
|
169
295
|
// Signed here rather than left as a step to remember. An unrevoke nobody signed does nothing at
|
|
170
296
|
// all, and the repo would sit there looking done while every machine ignored it.
|
|
171
297
|
await signRepo({ repoPath });
|
|
172
298
|
|
|
173
299
|
if (!pushToGit) {
|
|
174
|
-
console.log(`\nCommit and push it, and
|
|
175
|
-
console.log(`those keys work again:`);
|
|
300
|
+
console.log(`\nCommit and push it, and those keys work again as each machine picks it up:`);
|
|
176
301
|
console.log(`\`\`\`\ngit add -A\ngit commit -m "${COMMIT_MESSAGE}"\ngit push\n\`\`\``);
|
|
177
302
|
return;
|
|
178
303
|
}
|
|
179
304
|
await runPromise(`git add -A`, { cwd: repoPath });
|
|
180
305
|
await runPromise(`git commit -m "${COMMIT_MESSAGE}"`, { cwd: repoPath });
|
|
181
306
|
await runPromise(`git push`, { cwd: repoPath });
|
|
182
|
-
console.log(`\nCommitted and pushed.
|
|
183
|
-
console.log(`keys work again.`);
|
|
307
|
+
console.log(`\nCommitted and pushed. Those keys work again as each machine picks it up.`);
|
|
184
308
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
3
|
-
//exports.remoteCommandExists = exports.writeRemoteFile = exports.readRemoteFile = exports.runOverSSH = exports.SUDO_PREAMBLE = void 0;
|
|
3
|
+
//exports.remoteCommandExists = exports.writeRemoteFile = exports.readRemoteFile = exports.runOverSSH = exports.describeHost = exports.THIS_MACHINE = exports.SUDO_PREAMBLE = void 0;
|
|
4
4
|
const spawn_1 = require("./spawn");
|
|
5
5
|
const SSH_CONNECT_TIMEOUT = 10;
|
|
6
6
|
const EXISTS_MARKER = "__PORTSECURE_EXISTS__";
|
|
@@ -8,27 +8,37 @@ const MISSING_MARKER = "__PORTSECURE_MISSING__";
|
|
|
8
8
|
const MAX_ERROR_BODY_LENGTH = 500;
|
|
9
9
|
// /etc is root owned, so fall back to sudo whenever the SSH user is not root.
|
|
10
10
|
exports.SUDO_PREAMBLE = `SUDO=""; if [ "$(id -u)" -ne 0 ]; then SUDO="sudo -n"; fi`;
|
|
11
|
+
/** No host means this machine, so the very same script runs with nothing in front of it. Local and
|
|
12
|
+
remote then do exactly the same thing, rather than being two pieces of code that have to be kept
|
|
13
|
+
saying the same thing. */
|
|
14
|
+
exports.THIS_MACHINE = "";
|
|
15
|
+
function describeHost(host) {
|
|
16
|
+
return host || "this machine";
|
|
17
|
+
}
|
|
18
|
+
exports.describeHost = describeHost;
|
|
11
19
|
/** The host string is handed to ssh untouched. Users, keys and ports belong in the caller's ssh
|
|
12
20
|
config, so BatchMode makes a missing setup fail immediately instead of prompting. */
|
|
13
21
|
async function runOverSSH(config) {
|
|
14
22
|
let { host, script, input, allowFailure } = config;
|
|
15
23
|
let result = await (0, spawn_1.spawnPromise)({
|
|
16
|
-
command: "ssh",
|
|
17
|
-
args: [
|
|
24
|
+
command: host && "ssh" || "sh",
|
|
25
|
+
args: host && [
|
|
18
26
|
"-o", "BatchMode=yes",
|
|
19
27
|
"-o", `ConnectTimeout=${SSH_CONNECT_TIMEOUT}`,
|
|
20
28
|
host,
|
|
21
29
|
script,
|
|
22
|
-
],
|
|
30
|
+
] || ["-c", script],
|
|
23
31
|
input,
|
|
24
32
|
inheritStderr: !allowFailure,
|
|
25
33
|
});
|
|
26
34
|
if (result.error) {
|
|
27
|
-
throw new Error(`Expected
|
|
35
|
+
throw new Error(`Expected to run against ${describeHost(host)}, failed with ${result.error.message}`);
|
|
28
36
|
}
|
|
29
37
|
if (result.status !== 0 && !allowFailure) {
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
let advice = host
|
|
39
|
+
&& ` Non-interactive ssh access to ${host} has to work on its own - fix it in your ssh config.`
|
|
40
|
+
|| "";
|
|
41
|
+
throw new Error(`Expected the script on ${describeHost(host)} to exit 0, was ${result.status}.${advice}\n`
|
|
32
42
|
+ `${(result.stdout + result.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`);
|
|
33
43
|
}
|
|
34
44
|
return { stdout: result.stdout || "", stderr: result.stderr || "", status: result.status };
|
|
@@ -86,5 +96,5 @@ async function remoteCommandExists(config) {
|
|
|
86
96
|
return result.stdout.trim() === "yes";
|
|
87
97
|
}
|
|
88
98
|
exports.remoteCommandExists = remoteCommandExists;
|
|
89
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
90
|
-
/* _JS_SOURCE_HASH = "
|
|
99
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVtb3RlU1NILmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicmVtb3RlU1NILnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLG1DQUF1QztBQUV2QyxNQUFNLG1CQUFtQixHQUFHLEVBQUUsQ0FBQztBQUMvQixNQUFNLGFBQWEsR0FBRyx1QkFBdUIsQ0FBQztBQUM5QyxNQUFNLGNBQWMsR0FBRyx3QkFBd0IsQ0FBQztBQUNoRCxNQUFNLHFCQUFxQixHQUFHLEdBQUcsQ0FBQztBQUVsQyw4RUFBOEU7QUFDakUsUUFBQSxhQUFhLEdBQUcsMkRBQTJELENBQUM7QUFFekY7OzZCQUU2QjtBQUNoQixRQUFBLFlBQVksR0FBRyxFQUFFLENBQUM7QUFFL0IsU0FBZ0IsWUFBWSxDQUFDLElBQVk7SUFDckMsT0FBTyxJQUFJLElBQUksY0FBYyxDQUFDO0FBQ2xDLENBQUM7QUFGRCxvQ0FFQztBQUVEO3dGQUN3RjtBQUNqRixLQUFLLFVBQVUsVUFBVSxDQUFDLE1BQWdGO0lBQzdHLElBQUksRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxZQUFZLEVBQUUsR0FBRyxNQUFNLENBQUM7SUFDbkQsSUFBSSxNQUFNLEdBQUcsTUFBTSxJQUFBLG9CQUFZLEVBQUM7UUFDNUIsT0FBTyxFQUFFLElBQUksSUFBSSxLQUFLLElBQUksSUFBSTtRQUM5QixJQUFJLEVBQUUsSUFBSSxJQUFJO1lBQ1YsSUFBSSxFQUFFLGVBQWU7WUFDckIsSUFBSSxFQUFFLGtCQUFrQixtQkFBbUIsRUFBRTtZQUM3QyxJQUFJO1lBQ0osTUFBTTtTQUNULElBQUksQ0FBQyxJQUFJLEVBQUUsTUFBTSxDQUFDO1FBQ25CLEtBQUs7UUFDTCxhQUFhLEVBQUUsQ0FBQyxZQUFZO0tBQy9CLENBQUMsQ0FBQztJQUNILElBQUksTUFBTSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ2YsTUFBTSxJQUFJLEtBQUssQ0FBQywyQkFBMkIsWUFBWSxDQUFDLElBQUksQ0FBQyxpQkFBaUIs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LEdBQUcsTUFBTSxVQUFVLENBQUM7UUFDMUIsSUFBSTtRQUNKLE1BQU0sRUFBRSxlQUFlLE9BQU8sMkNBQTJDO1FBQ3pFLFlBQVksRUFBRSxJQUFJO0tBQ3JCLENBQUMsQ0FBQztJQUNILE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsS0FBSyxLQUFLLENBQUM7QUFDMUMsQ0FBQztBQVJELGtEQVFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgc3Bhd25Qcm9taXNlIH0gZnJvbSBcIi4vc3Bhd25cIjtcblxuY29uc3QgU1NIX0NPTk5FQ1RfVElNRU9VVCA9IDEwO1xuY29uc3QgRVhJU1RTX01BUktFUiA9IFwiX19QT1JUU0VDVVJFX0VYSVNUU19fXCI7XG5jb25zdCBNSVNTSU5HX01BUktFUiA9IFwiX19QT1JUU0VDVVJFX01JU1NJTkdfX1wiO1xuY29uc3QgTUFYX0VSUk9SX0JPRFlfTEVOR1RIID0gNTAwO1xuXG4vLyAvZXRjIGlzIHJvb3Qgb3duZWQsIHNvIGZhbGwgYmFjayB0byBzdWRvIHdoZW5ldmVyIHRoZSBTU0ggdXNlciBpcyBub3Qgcm9vdC5cbmV4cG9ydCBjb25zdCBTVURPX1BSRUFNQkxFID0gYFNVRE89XCJcIjsgaWYgWyBcIiQoaWQgLXUpXCIgLW5lIDAgXTsgdGhlbiBTVURPPVwic3VkbyAtblwiOyBmaWA7XG5cbi8qKiBObyBob3N0IG1lYW5zIHRoaXMgbWFjaGluZSwgc28gdGhlIHZlcnkgc2FtZSBzY3JpcHQgcnVucyB3aXRoIG5vdGhpbmcgaW4gZnJvbnQgb2YgaXQuIExvY2FsIGFuZFxuICAgIHJlbW90ZSB0aGVuIGRvIGV4YWN0bHkgdGhlIHNhbWUgdGhpbmcsIHJhdGhlciB0aGFuIGJlaW5nIHR3byBwaWVjZXMgb2YgY29kZSB0aGF0IGhhdmUgdG8gYmUga2VwdFxuICAgIHNheWluZyB0aGUgc2FtZSB0aGluZy4gKi9cbmV4cG9ydCBjb25zdCBUSElTX01BQ0hJTkUgPSBcIlwiO1xuXG5leHBvcnQgZnVuY3Rpb24gZGVzY3JpYmVIb3N0KGhvc3Q6IHN0cmluZykge1xuICAgIHJldHVybiBob3N0IHx8IFwidGhpcyBtYWNoaW5lXCI7XG59XG5cbi8qKiBUaGUgaG9zdCBzdHJpbmcgaXMgaGFuZGVkIHRvIHNzaCB1bnRvdWNoZWQuIFVzZXJzLCBrZXlzIGFuZCBwb3J0cyBiZWxvbmcgaW4gdGhlIGNhbGxlcidzIHNzaFxuICAgIGNvbmZpZywgc28gQmF0Y2hNb2RlIG1ha2VzIGEgbWlzc2luZyBzZXR1cCBmYWlsIGltbWVkaWF0ZWx5IGluc3RlYWQgb2YgcHJvbXB0aW5nLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHJ1bk92ZXJTU0goY29uZmlnOiB7IGhvc3Q6IHN0cmluZzsgc2NyaXB0OiBzdHJpbmc7IGlucHV0Pzogc3RyaW5nOyBhbGxvd0ZhaWx1cmU/OiBib29sZWFuIH0pIHtcbiAgICBsZXQgeyBob3N0LCBzY3JpcHQsIGlucHV0LCBhbGxvd0ZhaWx1cmUgfSA9IGNvbmZpZztcbiAgICBsZXQgcmVzdWx0ID0gYXdhaXQgc3Bhd25Qcm9taXNlKHtcbiAgICAgICAgY29tbWFuZDogaG9zdCAmJiBcInNzaFwiIHx8IFwic2hcIixcbiAgICAgICAgYXJnczogaG9zdCAmJiBbXG4gICAgICAgICAgICBcIi1vXCIsIFwiQmF0Y2hNb2RlPXllc1wiLFxuICAgICAgICAgICAgXCItb1wiLCBgQ29ubmVjdFRpbWVvdXQ9JHtTU0hfQ09OTkVDVF9USU1FT1VUfWAsXG4gICAgICAgICAgICBob3N0LFxuICAgICAgICAgICAgc2NyaXB0LFxuICAgICAgICBdIHx8IFtcIi1jXCIsIHNjcmlwdF0sXG4gICAgICAgIGlucHV0LFxuICAgICAgICBpbmhlcml0U3RkZXJyOiAhYWxsb3dGYWlsdXJlLFxuICAgIH0pO1xuICAgIGlmIChyZXN1bHQuZXJyb3IpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCB0byBydW4gYWdhaW5zdCAke2Rlc2NyaWJlSG9zdChob3N0KX0sIGZhaWxlZCB3aXRoICR7cmVzdWx0LmVycm9yLm1lc3NhZ2V9YCk7XG4gICAgfVxuICAgIGlmIChyZXN1bHQuc3RhdHVzICE9PSAwICYmICFhbGxvd0ZhaWx1cmUpIHtcbiAgICAgICAgbGV0IGFkdmljZSA9IGhvc3RcbiAgICAgICAgICAgICYmIGAgTm9uLWludGVyYWN0aXZlIHNzaCBhY2Nlc3MgdG8gJHtob3N0fSBoYXMgdG8gd29yayBvbiBpdHMgb3duIC0gZml4IGl0IGluIHlvdXIgc3NoIGNvbmZpZy5gXG4gICAgICAgICAgICB8fCBcIlwiO1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgICAgICAgICBgRXhwZWN0ZWQgdGhlIHNjcmlwdCBvbiAke2Rlc2NyaWJlSG9zdChob3N0KX0gdG8gZXhpdCAwLCB3YXMgJHtyZXN1bHQuc3RhdHVzfS4ke2FkdmljZX1cXG5gXG4gICAgICAgICAgICArIGAkeyhyZXN1bHQuc3Rkb3V0ICsgcmVzdWx0LnN0ZGVycikudHJpbSgpLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCl9YFxuICAgICAgICApO1xuICAgIH1cbiAgICByZXR1cm4geyBzdGRvdXQ6IHJlc3VsdC5zdGRvdXQgfHwgXCJcIiwgc3RkZXJyOiByZXN1bHQuc3RkZXJyIHx8IFwiXCIsIHN0YXR1czogcmVzdWx0LnN0YXR1cyB9O1xufVxuXG4vKiogUmV0dXJucyB1bmRlZmluZWQgd2hlbiB0aGUgZmlsZSBkb2VzIG5vdCBleGlzdCwgc28gYSBtaXNzaW5nIGZpbGUgcmVhZHMgZGlmZmVyZW50bHkgZnJvbSBhblxuICAgIGVtcHR5IG9uZS4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZWFkUmVtb3RlRmlsZShjb25maWc6IHsgaG9zdDogc3RyaW5nOyBmaWxlUGF0aDogc3RyaW5nIH0pIHtcbiAgICBsZXQgeyBob3N0LCBmaWxlUGF0aCB9ID0gY29uZmlnO1xuICAgIGxldCBvdXRwdXQgPSAoYXdhaXQgcnVuT3ZlclNTSCh7XG4gICAgICAgIGhvc3QsXG4gICAgICAgIHNjcmlwdDogYCR7U1VET19QUkVBTUJMRX1cbmlmICRTVURPIHRlc3QgLWYgXCIke2ZpbGVQYXRofVwiOyB0aGVuXG4gICAgZWNobyBcIiR7RVhJU1RTX01BUktFUn1cIlxuICAgICRTVURPIGNhdCBcIiR7ZmlsZVBhdGh9XCJcbmVsc2VcbiAgICBlY2hvIFwiJHtNSVNTSU5HX01BUktFUn1cIlxuZmlgLFxuICAgIH0pKS5zdGRvdXQ7XG4gICAgbGV0IG5ld2xpbmVJbmRleCA9IG91dHB1dC5pbmRleE9mKFwiXFxuXCIpO1xuICAgIGxldCBtYXJrZXIgPSBvdXRwdXQuc2xpY2UoMCwgbmV3bGluZUluZGV4KS50cmltKCk7XG4gICAgaWYgKG1hcmtlciA9PT0gTUlTU0lOR19NQVJLRVIpIHtcbiAgICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG4gICAgaWYgKG1hcmtlciAhPT0gRVhJU1RTX01BUktFUikge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkICR7RVhJU1RTX01BUktFUn0gb3IgJHtNSVNTSU5HX01BUktFUn0gZnJvbSAke2hvc3R9LCB3YXMgJHtvdXRwdXQuc2xpY2UoMCwgTUFYX0VSUk9SX0JPRFlfTEVOR1RIKX1gKTtcbiAgICB9XG4gICAgcmV0dXJuIG91dHB1dC5zbGljZShuZXdsaW5lSW5kZXggKyAxKTtcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHdyaXRlUmVtb3RlRmlsZShjb25maWc6IHtcbiAgICBob3N0OiBzdHJpbmc7XG4gICAgZmlsZVBhdGg6IHN0cmluZztcbiAgICBjb250ZW50czogc3RyaW5nO1xuICAgIGZpbGVNb2RlOiBzdHJpbmc7XG4gICAgZGlyZWN0b3J5TW9kZTogc3RyaW5nO1xufSkge1xuICAgIGxldCB7IGhvc3QsIGZpbGVQYXRoLCBjb250ZW50cywgZmlsZU1vZGUsIGRpcmVjdG9yeU1vZGUgfSA9IGNvbmZpZztcbiAgICBsZXQgZGlyZWN0b3J5ID0gZmlsZVBhdGguc2xpY2UoMCwgZmlsZVBhdGgubGFzdEluZGV4T2YoXCIvXCIpKTtcbiAgICBhd2FpdCBydW5PdmVyU1NIKHtcbiAgICAgICAgaG9zdCxcbiAgICAgICAgLy8gRGVwbG95aW5nIGZyb20gV2luZG93cyBtdXN0IG5vdCBjYXJyeSBDUkxGIG9udG8gdGhlIHRhcmdldCwgd2hlcmUgaXQgYnJlYWtzIHN5c3RlbWRcbiAgICAgICAgLy8gdW5pdHMgYW5kIG1ha2VzIHByaXZhdGUga2V5cyB1bnJlYWRhYmxlIHRvIHNzaC5cbiAgICAgICAgaW5wdXQ6IGNvbnRlbnRzLnJlcGxhY2UoL1xcclxcbi9nLCBcIlxcblwiKSxcbiAgICAgICAgc2NyaXB0OiBgJHtTVURPX1BSRUFNQkxFfVxuc2V0IC1lXG4kU1VETyBta2RpciAtcCBcIiR7ZGlyZWN0b3J5fVwiXG4kU1VETyBjaG1vZCAke2RpcmVjdG9yeU1vZGV9IFwiJHtkaXJlY3Rvcnl9XCJcbiRTVURPIHRlZSBcIiR7ZmlsZVBhdGh9XCIgPiAvZGV2L251bGxcbiRTVURPIGNobW9kICR7ZmlsZU1vZGV9IFwiJHtmaWxlUGF0aH1cImAsXG4gICAgfSk7XG59XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZW1vdGVDb21tYW5kRXhpc3RzKGNvbmZpZzogeyBob3N0OiBzdHJpbmc7IGNvbW1hbmQ6IHN0cmluZyB9KSB7XG4gICAgbGV0IHsgaG9zdCwgY29tbWFuZCB9ID0gY29uZmlnO1xuICAgIGxldCByZXN1bHQgPSBhd2FpdCBydW5PdmVyU1NIKHtcbiAgICAgICAgaG9zdCxcbiAgICAgICAgc2NyaXB0OiBgY29tbWFuZCAtdiBcIiR7Y29tbWFuZH1cIiA+IC9kZXYvbnVsbCAyPiYxICYmIGVjaG8geWVzIHx8IGVjaG8gbm9gLFxuICAgICAgICBhbGxvd0ZhaWx1cmU6IHRydWUsXG4gICAgfSk7XG4gICAgcmV0dXJuIHJlc3VsdC5zdGRvdXQudHJpbSgpID09PSBcInllc1wiO1xufVxuIl19
|
|
100
|
+
/* _JS_SOURCE_HASH = "50b1a08f2aa4b96f04f676f20c516bc6c2897a47ad7c12729c0f651735c71057"; */
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import { getOwnIPs } from "../../misc/ownIPs";
|
|
3
|
+
import { describeHost } from "../helpers/remoteSSH";
|
|
4
|
+
import { createRemoteIdentity, Identity, localDomains, localIdentity, remoteIdentity } from "./identity";
|
|
5
|
+
import { resolveKeysRepo } from "../authorizedKeys/keysRepo";
|
|
6
|
+
import { getMachines, setMachines } from "./machines";
|
|
7
|
+
|
|
8
|
+
const USAGE = `Usage: yarn addmachine <domain> [ip]
|
|
9
|
+
|
|
10
|
+
The domain is required: a machine has a different identity, and so a different machine id, under
|
|
11
|
+
every domain it runs under, so there is no such thing as "this machine" without one.
|
|
12
|
+
|
|
13
|
+
With no ip it adds this machine, with its own addresses. With an ip it asks that machine over ssh
|
|
14
|
+
for its identity under that domain, gives it one if it has none, and adds it under that address.
|
|
15
|
+
|
|
16
|
+
It edits the keys repo this machine reads, wherever that is, not whatever directory this was run
|
|
17
|
+
from. Nothing is signed or pushed here. Run "yarn signfiles git" in that repo afterwards, or no
|
|
18
|
+
machine will believe any of it.`;
|
|
19
|
+
|
|
20
|
+
export async function main() {
|
|
21
|
+
let argv = process.argv.slice(2);
|
|
22
|
+
if (!argv.length || argv.length > 2) {
|
|
23
|
+
throw new Error(`Expected a domain and optionally an ip, was ${argv.length} argument(s)\n${USAGE}`);
|
|
24
|
+
}
|
|
25
|
+
let [domain, host = ""] = argv;
|
|
26
|
+
// The same repo the acceptance check reads. Editing anything else would write a machine list
|
|
27
|
+
// nothing on this machine ever looks at.
|
|
28
|
+
let { repoPath } = await resolveKeysRepo();
|
|
29
|
+
|
|
30
|
+
let identity: Identity;
|
|
31
|
+
let addresses: string[];
|
|
32
|
+
if (!host) {
|
|
33
|
+
let found = await localIdentity(domain);
|
|
34
|
+
if (!found) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Expected an identity for ${domain} in ${os.homedir()}, this machine has none.\n`
|
|
37
|
+
+ `It has: ${(await localDomains()).join(", ") || "no identities at all"}\n`
|
|
38
|
+
+ `One is generated the first time something runs here under a domain, so run that`
|
|
39
|
+
+ ` first, or name another machine to add instead.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
identity = found;
|
|
43
|
+
addresses = await getOwnIPs();
|
|
44
|
+
if (!addresses.length) {
|
|
45
|
+
throw new Error(`Expected this machine to have an address anything else can see, it has none`);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// Nothing there under this domain means it gets one. Its own from this point on: the key
|
|
49
|
+
// is written there and nothing keeps a copy.
|
|
50
|
+
let found = await remoteIdentity(host, domain);
|
|
51
|
+
if (!found) {
|
|
52
|
+
console.log(`${describeHost(host)} has no identity for ${domain}, generating one`);
|
|
53
|
+
}
|
|
54
|
+
identity = found || await createRemoteIdentity({ host, domain });
|
|
55
|
+
// The address it was named by is the address it talks to us from, which is the one thing
|
|
56
|
+
// it cannot lie about.
|
|
57
|
+
addresses = [host];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The whole list is read and written back, because setting is by the set: writing this one
|
|
61
|
+
// machine alone would remove every other machine from the repo.
|
|
62
|
+
let machines = await getMachines(repoPath);
|
|
63
|
+
let existing = machines.find(machine => machine.machineId === identity.machineId);
|
|
64
|
+
let ips = [...(existing?.ips || [])];
|
|
65
|
+
let added = addresses.filter(address => !ips.includes(address));
|
|
66
|
+
if (existing) {
|
|
67
|
+
existing.ips = [...ips, ...added];
|
|
68
|
+
} else {
|
|
69
|
+
machines.push({ machineId: identity.machineId, ips: [...added], addedAt: "" });
|
|
70
|
+
}
|
|
71
|
+
await setMachines({ repoPath, machines });
|
|
72
|
+
|
|
73
|
+
console.log(`\nTrusting machine ${identity.machineId}`);
|
|
74
|
+
console.log(`Allowing IP ${[...ips, ...added].join(", ")}`);
|
|
75
|
+
if (added.length) {
|
|
76
|
+
console.log(` added to machines/${identity.machineId}.json: ${added.join(", ")}`);
|
|
77
|
+
} else {
|
|
78
|
+
console.log(` already allowed from ${addresses.join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
console.log(` identity ${identity.fullDomain}`);
|
|
81
|
+
console.log(` in ${repoPath}`);
|
|
82
|
+
console.log(`\nNothing believes this until it is signed. In that repo, run:`);
|
|
83
|
+
console.log(`\`\`\`\nyarn signfiles git\n\`\`\``);
|
|
84
|
+
}
|