sliftutils 1.7.124 → 1.7.125
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/CLAUDE.md +3 -1
- package/bin/securessh.js +10 -0
- package/bin/setupnotify.js +9 -0
- package/bin/signfiles.js +10 -0
- package/package.json +9 -3
- package/security/README.md +77 -0
- package/security/authorizedKeys/authorizedKeys.ts +43 -0
- package/security/authorizedKeys/daemon/portsecure.service +18 -0
- package/security/authorizedKeys/daemon/portsecureDaemon.js +1032 -0
- package/security/authorizedKeys/secureSSH.ts +427 -0
- package/security/authorizedKeys/sources.ts +20 -0
- package/security/helpers/paths.ts +20 -0
- package/security/helpers/remoteSSH.ts +94 -0
- package/security/helpers/spawn.ts +32 -0
- package/security/notifications/discord.ts +190 -0
- package/security/notifications/remoteWebhook.ts +85 -0
- package/security/notifications/setupNotify.ts +29 -0
- package/security/signedFiles/manifest.ts +56 -0
- package/security/signedFiles/signFiles.ts +116 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +17 -20
package/CLAUDE.md
CHANGED
|
@@ -8,10 +8,12 @@ from `.cursor/rules/*.mdc` so Claude reads them automatically.
|
|
|
8
8
|
- The code automatically updates on save, so do not ever run commands to rerun the site.
|
|
9
9
|
- Don't run shell commands when you need to create or move small code files. Use tool calls. Use tool calls to make files within folders — you don't need to make the folder, just make the file, the folder will be created automatically.
|
|
10
10
|
- If you need to add a dependency, don't just edit `package.json`. Use `yarn add` so you get the latest version, unless the user specifies a version.
|
|
11
|
-
- Use tool calls to read files and directories
|
|
11
|
+
- Use tool calls to read and write files and directories, as opposed to shell commands running `ls`, `dir`, etc.
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
## Coding styles
|
|
14
15
|
|
|
16
|
+
- Do not use synchronous file IO, or synchronous child_process functions.
|
|
15
17
|
- Times should almost always be in milliseconds; assume milliseconds if not told otherwise.
|
|
16
18
|
- Don't make functions that will never be reused and are short. If under 5 lines and not reused, don't create it unless explicitly told to.
|
|
17
19
|
- Comments are used sparingly and only when required to explain what's being done. A comment that just restates the function name is forbidden.
|
package/bin/securessh.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Manages which repos a host takes its root authorized_keys from, and installs the daemon that
|
|
4
|
+
// keeps them applied.
|
|
5
|
+
require("typenode");
|
|
6
|
+
|
|
7
|
+
require("../security/authorizedKeys/secureSSH").main().catch(e => {
|
|
8
|
+
console.error(`${e}`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}).finally(() => process.exit());
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Sets a Discord webhook up on a remote host, so anything on it can raise notifications.
|
|
4
|
+
require("typenode");
|
|
5
|
+
|
|
6
|
+
require("../security/notifications/setupNotify").main().catch(e => {
|
|
7
|
+
console.error(`${e}`);
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
}).finally(() => process.exit());
|
package/bin/signfiles.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Signs the files of the repo in the current directory, so a machine pulling it can tell who
|
|
4
|
+
// published what it is about to trust.
|
|
5
|
+
require("typenode");
|
|
6
|
+
|
|
7
|
+
require("../security/signedFiles/signFiles").main().catch(e => {
|
|
8
|
+
console.error(`${e}`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}).finally(() => process.exit());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sliftutils",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.125",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
"notes": "mobx, preact, socket-function, typenode SHOULD be peerDependencies. But we want to use yarn (better dependency deduplication), so we can't use peerDependencies (as they aren't installed by default, which makes them a nightmare to use). If you want to override the versions, feel free to use overrides/resolutions.",
|
|
29
29
|
"test": "typenode ./test.ts",
|
|
30
30
|
"filehoster": "node ./bin/filehoster.js",
|
|
31
|
-
"autohost": "node ./bin/autohost.js"
|
|
31
|
+
"autohost": "node ./bin/autohost.js",
|
|
32
|
+
"setupnotify": "node ./bin/setupnotify.js",
|
|
33
|
+
"securessh": "node ./bin/securessh.js",
|
|
34
|
+
"signfiles": "node ./bin/signfiles.js"
|
|
32
35
|
},
|
|
33
36
|
"bin": {
|
|
34
37
|
"filehoster": "./bin/filehoster.js",
|
|
@@ -45,7 +48,10 @@
|
|
|
45
48
|
"slift-watch": "./builders/watchRun.js",
|
|
46
49
|
"sliftwatch": "./builders/watchRun.js",
|
|
47
50
|
"slift-setup": "./builders/setupRun.js",
|
|
48
|
-
"sliftsetup": "./builders/setupRun.js"
|
|
51
|
+
"sliftsetup": "./builders/setupRun.js",
|
|
52
|
+
"setupnotify": "./bin/setupnotify.js",
|
|
53
|
+
"securessh": "./bin/securessh.js",
|
|
54
|
+
"signfiles": "./bin/signfiles.js"
|
|
49
55
|
},
|
|
50
56
|
"dependencies": {
|
|
51
57
|
"@types/chrome": "^0.0.237",
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# security
|
|
2
|
+
|
|
3
|
+
Tools for locking down machines. One folder per kind of security concern.
|
|
4
|
+
|
|
5
|
+
## notifications
|
|
6
|
+
|
|
7
|
+
Discord notifications, so a machine can raise the alarm somewhere a person will see it.
|
|
8
|
+
|
|
9
|
+
The webhook lives on the machine that sends, in `/etc/portsecure/discord-webhook`, and nothing
|
|
10
|
+
starts without one. `configureDiscordNotifications` loads it and aborts the process if it is
|
|
11
|
+
missing, then watches the file and warns the old webhook if it is ever swapped.
|
|
12
|
+
|
|
13
|
+
yarn setupnotify <host> <discord-webhook-url> [replace]
|
|
14
|
+
|
|
15
|
+
`replace` is required to overwrite a host's existing webhook with a different one.
|
|
16
|
+
|
|
17
|
+
## authorizedKeys
|
|
18
|
+
|
|
19
|
+
Keeps root's `authorized_keys` equal to the merged contents of one or more git repos, and turns
|
|
20
|
+
off every other way in.
|
|
21
|
+
|
|
22
|
+
yarn securessh <host> add <repo-private-key> [repo-url]
|
|
23
|
+
yarn securessh <host> remove [repo-url]
|
|
24
|
+
yarn securessh <host> list
|
|
25
|
+
|
|
26
|
+
With no repo url, the repo you are standing in is used, as long as it holds keys and has an
|
|
27
|
+
origin to clone from.
|
|
28
|
+
|
|
29
|
+
Each source repo keeps its own deploy key. Both verbs refuse to run if the key you log in with
|
|
30
|
+
would not survive the change, since that would lock you out of the host permanently.
|
|
31
|
+
|
|
32
|
+
`daemon/portsecureDaemon.js` is what actually runs on the host, under systemd. It is plain
|
|
33
|
+
JavaScript on the Node built-ins alone so a target machine needs nothing installed. It:
|
|
34
|
+
|
|
35
|
+
- rewrites root's `authorized_keys` from the merged sources, archiving whatever it replaces
|
|
36
|
+
- reverts and reports edits made outside it, every 60 seconds
|
|
37
|
+
- reports changes to any other account's `authorized_keys`, without touching them
|
|
38
|
+
- polls every source every 5 minutes, and reports a repo whose history was rewritten
|
|
39
|
+
- disables password authentication, after checking `sshd` accepts the config
|
|
40
|
+
|
|
41
|
+
With no sources, or none readable, it leaves `authorized_keys` exactly as it is rather than
|
|
42
|
+
locking everyone out.
|
|
43
|
+
|
|
44
|
+
The daemon carries hand ports of several TypeScript files here, because it cannot import them.
|
|
45
|
+
Both copies are marked `PORTED CODE` and have to be changed together.
|
|
46
|
+
|
|
47
|
+
## signedFiles
|
|
48
|
+
|
|
49
|
+
Signs everything in a repo, so a machine pulling it can tell who published what it is about to
|
|
50
|
+
trust. Run it inside the repo you want signed.
|
|
51
|
+
|
|
52
|
+
yarn signfiles [signing-key] [git]
|
|
53
|
+
|
|
54
|
+
`signedfiles.json` lists every non-ignored file with its size and sha256, and
|
|
55
|
+
`signedfiles.json.sig` is an ssh signature over it. With no key given, a hardware backed
|
|
56
|
+
`ed25519-sk` key at `~/.ssh/signfiles_ed25519_sk` is used, and created if missing - a key on disk
|
|
57
|
+
is compromised the moment the machine is, so the hardware key is the point. `git` also commits
|
|
58
|
+
and pushes, after signing, so a failed push never costs a second touch of the key.
|
|
59
|
+
|
|
60
|
+
### How authorizedKeys uses it
|
|
61
|
+
|
|
62
|
+
The daemon records the signer it last accepted for each source, and the keys it accepted from
|
|
63
|
+
them, on disk. Those recorded values are the reference, not the repo, because the repo is what an
|
|
64
|
+
attacker would be rewriting.
|
|
65
|
+
|
|
66
|
+
When a source starts being signed by a different key, the daemon warns on Discord and keeps
|
|
67
|
+
applying the keys it last accepted. It applies the new ones only after 24 hours of that same new
|
|
68
|
+
signer. Any different signer restarts the wait, so publishing twice in a row gains nothing, and a
|
|
69
|
+
return to the accepted signer cancels it. Losing a signature entirely counts as a change too, so
|
|
70
|
+
stripping it does not get anything through faster.
|
|
71
|
+
|
|
72
|
+
A signature that does not verify, or a manifest that does not match the files on disk, is never
|
|
73
|
+
treated as an identity - that content is ignored and the last accepted keys stay.
|
|
74
|
+
|
|
75
|
+
## helpers
|
|
76
|
+
|
|
77
|
+
Shared plumbing: running commands over ssh, spawning child processes, and expanding `~`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
// PORTED CODE: security/authorizedKeys/daemon/portsecureDaemon.js contains a plain JS port of normalizeKeys,
|
|
5
|
+
// summarizeKey and readRepoKeys, so it can resolve the same keys with no dependencies. The two
|
|
6
|
+
// must agree on which keys a repo produces - if you change one, make the matching change in the
|
|
7
|
+
// other.
|
|
8
|
+
|
|
9
|
+
export function normalizeKeys(contents: string) {
|
|
10
|
+
return contents.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("#"));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Enough to recognise whose key this is without printing the whole blob. */
|
|
14
|
+
export function summarizeKey(keyLine: string) {
|
|
15
|
+
let parts = keyLine.trim().split(/\s+/);
|
|
16
|
+
let typeIndex = parts.findIndex(part => /^(ssh-|ecdsa-|sk-)/.test(part));
|
|
17
|
+
if (typeIndex < 0) {
|
|
18
|
+
return keyLine.slice(0, 60);
|
|
19
|
+
}
|
|
20
|
+
let type = parts[typeIndex];
|
|
21
|
+
let blob = parts[typeIndex + 1] || "";
|
|
22
|
+
let comment = parts.slice(typeIndex + 2).join(" ");
|
|
23
|
+
return `${type} ...${blob.slice(-12)}${comment && ` ${comment}` || ""}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Reads the authorized keys a repo checkout wants applied. Prefers a top level authorized_keys
|
|
27
|
+
file and otherwise concatenates every .pub at the top level. */
|
|
28
|
+
export async function readRepoKeys(repoPath: string) {
|
|
29
|
+
let combinedPath = path.join(repoPath, "authorized_keys");
|
|
30
|
+
let entries = await fs.readdir(repoPath);
|
|
31
|
+
if (entries.includes("authorized_keys")) {
|
|
32
|
+
return normalizeKeys(await fs.readFile(combinedPath, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
let pubFiles = entries.filter(name => name.endsWith(".pub")).sort();
|
|
35
|
+
if (!pubFiles.length) {
|
|
36
|
+
throw new Error(`Expected authorized_keys or at least one .pub file in ${repoPath}, found neither`);
|
|
37
|
+
}
|
|
38
|
+
let keys: string[] = [];
|
|
39
|
+
for (let name of pubFiles) {
|
|
40
|
+
keys.push(...normalizeKeys(await fs.readFile(path.join(repoPath, name), "utf8")));
|
|
41
|
+
}
|
|
42
|
+
return keys;
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=portsecure authorized keys daemon
|
|
3
|
+
Documentation=https://github.com/sliftist/portsecure
|
|
4
|
+
After=network-online.target
|
|
5
|
+
Wants=network-online.target
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=simple
|
|
9
|
+
ExecStart=/usr/bin/env node /opt/portsecure/portsecure-daemon.js
|
|
10
|
+
User=root
|
|
11
|
+
# Availability is the point of this daemon, so it always comes back.
|
|
12
|
+
Restart=always
|
|
13
|
+
RestartSec=10
|
|
14
|
+
StandardOutput=journal
|
|
15
|
+
StandardError=journal
|
|
16
|
+
|
|
17
|
+
[Install]
|
|
18
|
+
WantedBy=multi-user.target
|