clavarium-relay 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,15 @@
1
1
  # clavarium-relay
2
2
 
3
- The maintenance program of a Clavarium relay: status, repair, updates, key rotation.
3
+ The maintenance program of a Clavarium relay, a small root program in bash:
4
4
 
5
- Not built yet: this version only keeps the name. Running it prints this and exits; it runs nothing else and reaches no network.
5
+ clavarium-relay status | repair | update-os [--reboot] | update-relay X.Y.Z | rotate-key tunnel|keeper KEY
6
+
7
+ It is installed by `relay.sh` (or by `sudo clavarium online --relay HOST`) as
8
+ `/usr/local/sbin/clavarium-relay`. The home machine reaches it through the
9
+ account `clavarium-keeper`, which may run this program and nothing else.
10
+
11
+ `update-relay` is told a version number only: it fetches this package from the
12
+ npm registry itself and checks the registry's sha512 before installing it.
6
13
 
7
14
  - Site: https://clavarium.org
8
15
  - Source: https://github.com/clavarium/clavarium (folder relay/)
@@ -0,0 +1,282 @@
1
+ #!/bin/bash
2
+ # ---------------------------------------------------------------------------
3
+ # THE MAINTENANCE PROGRAM OF A CLAVARIUM RELAY. Runs as root, on the relay.
4
+ #
5
+ # clavarium-relay status what is set up, what runs, what is due
6
+ # clavarium-relay repair writes the rules and the port service again
7
+ # clavarium-relay update-os [--reboot] Ubuntu's updates now (and a restart if one is due)
8
+ # clavarium-relay update-relay X.Y.Z this program at exactly that version, from npm
9
+ # clavarium-relay rotate-key tunnel|keeper "ssh-ed25519 AAAA... name"
10
+ # clavarium-relay setup --key LINE [--keeper-key LINE] (relay.sh runs it once)
11
+ # clavarium-relay version
12
+ #
13
+ # WHO RUNS IT. Root, by hand; or the home machine, through the maintenance
14
+ # account "clavarium-keeper": that account's SSH key leads to "door" (below)
15
+ # and nowhere else, and sudo lets it run this one program, nothing else.
16
+ #
17
+ # IT NEVER TAKES CODE FROM THE HOME MACHINE. update-relay is told a version
18
+ # number only; it fetches the package "clavarium-relay" of that version from
19
+ # the npm registry itself, checks it against the registry's sha512, and only
20
+ # then replaces this file.
21
+ #
22
+ # Exit codes: 0 done (status: all is well); 1 something is wrong (said);
23
+ # 64 bad parameters; 78 refused.
24
+ # ---------------------------------------------------------------------------
25
+ set -euo pipefail
26
+
27
+ VERSION_OF_THIS="0.1.0"
28
+ # Tests run it on a folder that stands for "/": nothing outside it is touched.
29
+ ROOT="${CLAVARIUM_RELAY_ROOT:-}"
30
+ REGISTRY="https://registry.npmjs.org"
31
+ [ -z "$ROOT" ] || REGISTRY="${CLAVARIUM_RELAY_REGISTRY:-$REGISTRY}"
32
+
33
+ TUNNEL="clavarium-tunnel"
34
+ KEEPER="clavarium-keeper"
35
+ TLS_PORT="8443"
36
+ HTTP_PORT="8080"
37
+ PROGRAM="/usr/local/sbin/clavarium-relay"
38
+ KEYS="/etc/ssh/clavarium-relay"
39
+ SSHD_FILE="$ROOT/etc/ssh/sshd_config.d/10-clavarium-relay.conf"
40
+ RULES="$ROOT/etc/clavarium-relay.nft"
41
+ UNIT="$ROOT/etc/systemd/system/clavarium-relay-ports.service"
42
+ SUDOERS="$ROOT/etc/sudoers.d/clavarium-relay"
43
+ UPGRADES="$ROOT/etc/apt/apt.conf.d/52clavarium-relay"
44
+ KEY_RE='^ssh-ed25519 [A-Za-z0-9+/]{68}( [A-Za-z0-9@._-]{1,64})?$'
45
+ VERSION_RE='^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,6}(-[0-9A-Za-z.-]{1,40})?$'
46
+
47
+ bad() { echo "clavarium-relay: $*" >&2; echo "usage: clavarium-relay status | repair | update-os [--reboot] | update-relay X.Y.Z | rotate-key tunnel|keeper KEY | version" >&2; exit 64; }
48
+ refuse() { echo "clavarium-relay: REFUSED: $*" >&2; exit 78; }
49
+ as_root() { [ -n "$ROOT" ] || [ "$(id -u)" -eq 0 ] || refuse "run it as root (sudo)"; }
50
+ key_ok() { case "$1" in *"PRIVATE KEY"*) return 1 ;; esac; [[ "$1" =~ $KEY_RE ]]; }
51
+
52
+ # --- fetch_relay: shared, word for word, with deploy/relay.sh (a test says so) ---
53
+ # fetch_relay VERSION DIR: the package clavarium-relay@VERSION from the registry,
54
+ # checked against the registry's sha512, unpacked in DIR (DIR/package/...).
55
+ fetch_relay() {
56
+ local version="$1" dir="$2" meta integrity got proto="=https"
57
+ case "$REGISTRY" in file://*) proto="=file" ;; esac
58
+ meta="$(curl --proto "$proto" -fsSL --max-time 60 "$REGISTRY/clavarium-relay/$version")" || { echo "the registry did not answer for clavarium-relay@$version" >&2; return 1; }
59
+ printf '%s' "$meta" | grep -Eq "\"version\" *: *\"${version//./\\.}\"" || { echo "the registry has no clavarium-relay@$version" >&2; return 1; }
60
+ integrity="$(printf '%s' "$meta" | grep -Eo '"integrity" *: *"sha512-[A-Za-z0-9+/=]+"' | head -1 | sed 's/.*"\(sha512-[^"]*\)"$/\1/')"
61
+ [ -n "$integrity" ] || { echo "the registry gave no sha512 for clavarium-relay@$version" >&2; return 1; }
62
+ curl --proto "$proto" -fsSL --max-time 120 -o "$dir/relay.tgz" "$REGISTRY/clavarium-relay/-/clavarium-relay-$version.tgz" || { echo "the package could not be downloaded" >&2; return 1; }
63
+ got="sha512-$(openssl dgst -sha512 -binary "$dir/relay.tgz" | base64 -w0)"
64
+ [ "$got" = "$integrity" ] || { echo "the package is not the one the registry describes (sha512)" >&2; return 1; }
65
+ tar -xzf "$dir/relay.tgz" -C "$dir" --no-same-owner package/clavarium-relay package/package.json || { echo "the package does not hold the program" >&2; return 1; }
66
+ grep -Eq "\"name\" *: *\"clavarium-relay\"" "$dir/package/package.json" && grep -Eq "\"version\" *: *\"${version//./\\.}\"" "$dir/package/package.json" || { echo "the package is not clavarium-relay@$version" >&2; return 1; }
67
+ bash -n "$dir/package/clavarium-relay" || { echo "the program in the package does not parse" >&2; return 1; }
68
+ }
69
+ # --- end of fetch_relay ---
70
+
71
+ write_key() { # write_key ACCOUNT LINE: the one key of that account, in a file root owns
72
+ local account="$1" key="$2" options
73
+ key_ok "$key" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
74
+ if [ "$account" = "$TUNNEL" ]; then options="restrict,port-forwarding,permitlisten=\"$TLS_PORT\",permitlisten=\"$HTTP_PORT\""; else options="restrict"; fi
75
+ install -d -m 0755 "$ROOT$KEYS"
76
+ printf '%s %s\n' "$options" "$key" > "$ROOT$KEYS/$account.new"
77
+ chmod 0644 "$ROOT$KEYS/$account.new"
78
+ mv -f -- "$ROOT$KEYS/$account.new" "$ROOT$KEYS/$account"
79
+ }
80
+
81
+ do_repair() {
82
+ as_root
83
+ [ -s "$ROOT$KEYS/$TUNNEL" ] || refuse "this machine has no tunnel key: set it up with relay.sh --key ..."
84
+ export DEBIAN_FRONTEND=noninteractive NEEDRESTART_SUSPEND=1
85
+ local missing=""
86
+ command -v nft >/dev/null 2>&1 || missing="nftables"
87
+ [ -e "$ROOT/usr/bin/unattended-upgrade" ] || missing="$missing unattended-upgrades"
88
+ if [ -n "$missing" ]; then
89
+ # shellcheck disable=SC2086
90
+ apt-get update -qq && apt-get install -y -qq $missing >/dev/null || refuse "could not install:$missing"
91
+ fi
92
+ id "$TUNNEL" >/dev/null 2>&1 || useradd --system --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin "$TUNNEL"
93
+ # The keeper's commands go through its shell to "door": it needs one, and
94
+ # has no password and no other command.
95
+ if [ -s "$ROOT$KEYS/$KEEPER" ]; then id "$KEEPER" >/dev/null 2>&1 || useradd --system --home-dir /nonexistent --no-create-home --shell /bin/sh "$KEEPER"; fi
96
+
97
+ install -d "$(dirname "$SSHD_FILE")"
98
+ cat > "$SSHD_FILE" <<SSHD
99
+ # Clavarium relay: what its two accounts may do. Written by clavarium-relay.
100
+ Match User $TUNNEL
101
+ AuthorizedKeysFile $KEYS/%u
102
+ PasswordAuthentication no
103
+ KbdInteractiveAuthentication no
104
+ AllowTcpForwarding remote
105
+ GatewayPorts clientspecified
106
+ PermitListen $TLS_PORT $HTTP_PORT
107
+ PermitOpen none
108
+ PermitTTY no
109
+ X11Forwarding no
110
+ AllowAgentForwarding no
111
+ PermitTunnel no
112
+ ForceCommand /bin/false
113
+ # A tunnel that died without saying so gives its ports back within minutes.
114
+ ClientAliveInterval 30
115
+ ClientAliveCountMax 3
116
+ Match User $KEEPER
117
+ AuthorizedKeysFile $KEYS/%u
118
+ PasswordAuthentication no
119
+ KbdInteractiveAuthentication no
120
+ AllowTcpForwarding no
121
+ PermitTTY no
122
+ X11Forwarding no
123
+ AllowAgentForwarding no
124
+ PermitTunnel no
125
+ ForceCommand $PROGRAM door
126
+ # What follows this file in the server's settings is for everybody again.
127
+ Match all
128
+ SSHD
129
+ if ! sshd -t; then rm -f -- "$SSHD_FILE"; refuse "the SSH server refused these settings; they were taken out again"; fi
130
+ systemctl reload ssh.service 2>/dev/null || systemctl reload sshd.service
131
+
132
+ install -d "$(dirname "$SUDOERS")"
133
+ printf '# Clavarium relay: the maintenance account may run ONE program as root.\n%s ALL=(root) NOPASSWD: %s\n' "$KEEPER" "$PROGRAM" > "$SUDOERS.new"
134
+ visudo -cf "$SUDOERS.new" >/dev/null || { rm -f -- "$SUDOERS.new"; refuse "the sudo rule did not pass its check"; }
135
+ chmod 0440 "$SUDOERS.new"; mv -f -- "$SUDOERS.new" "$SUDOERS"
136
+
137
+ cat > "$RULES" <<NFT
138
+ # Clavarium relay: what arrives on 443 and 80 goes to the tunnel. Written by clavarium-relay.
139
+ table inet clavarium_relay
140
+ delete table inet clavarium_relay
141
+ table inet clavarium_relay {
142
+ chain arriving {
143
+ type nat hook prerouting priority dstnat;
144
+ tcp dport 443 redirect to :$TLS_PORT
145
+ tcp dport 80 redirect to :$HTTP_PORT
146
+ }
147
+ }
148
+ NFT
149
+ install -d "$(dirname "$UNIT")"
150
+ cat > "$UNIT" <<UNITFILE
151
+ [Unit]
152
+ Description=Clavarium relay - 443 and 80 go to the tunnel
153
+ After=network-pre.target
154
+ Before=network.target
155
+
156
+ [Service]
157
+ Type=oneshot
158
+ RemainAfterExit=yes
159
+ ExecStart=/usr/sbin/nft -f /etc/clavarium-relay.nft
160
+ ExecStop=/usr/sbin/nft delete table inet clavarium_relay
161
+
162
+ [Install]
163
+ WantedBy=multi-user.target
164
+ UNITFILE
165
+ systemctl daemon-reload
166
+ systemctl enable --quiet clavarium-relay-ports.service
167
+ systemctl restart clavarium-relay-ports.service
168
+ systemctl is-active --quiet clavarium-relay-ports.service || refuse "the port rule could not be loaded: journalctl -u clavarium-relay-ports"
169
+
170
+ # Ubuntu's security updates, by themselves, every day.
171
+ install -d "$(dirname "$UPGRADES")"
172
+ cat > "$UPGRADES" <<'APT'
173
+ // Clavarium relay: security updates install by themselves. Written by clavarium-relay.
174
+ APT::Periodic::Update-Package-Lists "1";
175
+ APT::Periodic::Unattended-Upgrade "1";
176
+ APT::Periodic::AutocleanInterval "7";
177
+ APT
178
+ systemctl enable --quiet --now unattended-upgrades.service 2>/dev/null || true
179
+ echo "repaired: ssh rules, sudo rule, port rule, automatic updates"
180
+ }
181
+
182
+ do_status() {
183
+ local bad_count=0 line
184
+ say() { printf '%-15s %s\n' "$1" "$2"; }
185
+ check() { if eval "$2"; then say "$1" "${3:-ok}"; else say "$1" "${4:-missing}"; bad_count=$((bad_count + 1)); fi; }
186
+ say version "$VERSION_OF_THIS"
187
+ check tunnel-account "id $TUNNEL >/dev/null 2>&1"
188
+ check tunnel-key "[ -s '$ROOT$KEYS/$TUNNEL' ]"
189
+ check keeper-account "id $KEEPER >/dev/null 2>&1"
190
+ check ssh-rules "[ -s '$SSHD_FILE' ]"
191
+ check sudo-rule "[ -s '$SUDOERS' ]"
192
+ check port-rules "systemctl is-active --quiet clavarium-relay-ports.service" ok "not loaded"
193
+ check os-updates "[ -s '$UPGRADES' ]" automatic off
194
+ line="waiting for home"
195
+ ss -ltnH "sport = :$TLS_PORT" 2>/dev/null | grep -q . && line="connected"
196
+ say tunnel "$line"
197
+ if [ -e "$ROOT/var/run/reboot-required" ]; then say reboot needed; else say reboot no; fi
198
+ [ "$bad_count" -eq 0 ]
199
+ }
200
+
201
+ do_update_os() {
202
+ as_root
203
+ export DEBIAN_FRONTEND=noninteractive NEEDRESTART_SUSPEND=1
204
+ apt-get update -qq
205
+ apt-get -y -qq -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade >/dev/null
206
+ apt-get -y -qq autoremove >/dev/null || true
207
+ if [ -e "$ROOT/var/run/reboot-required" ]; then
208
+ if [ "${1:-}" = "--reboot" ]; then echo "updated; restarting now"; systemctl reboot; else echo "updated; a restart is due (update-os --reboot)"; fi
209
+ else echo "updated"; fi
210
+ }
211
+
212
+ do_update_relay() {
213
+ as_root
214
+ local version="${1:-}" dir
215
+ [[ "$version" =~ $VERSION_RE ]] || bad "update-relay takes an exact version, like 0.1.0"
216
+ dir="$(mktemp -d)"
217
+ if ! fetch_relay "$version" "$dir"; then rm -rf -- "$dir"; refuse "clavarium-relay@$version was not installed (above: why); this version stays"; fi
218
+ install -m 0755 "$dir/package/clavarium-relay" "$ROOT$PROGRAM.new"
219
+ mv -f -- "$ROOT$PROGRAM.new" "$ROOT$PROGRAM"
220
+ rm -rf -- "$dir"
221
+ echo "clavarium-relay $version installed"
222
+ # The new version writes its own rules.
223
+ exec "$ROOT$PROGRAM" repair
224
+ }
225
+
226
+ do_rotate_key() {
227
+ as_root
228
+ local who="${1:-}"; shift || true
229
+ local key="$*"
230
+ case "$who" in tunnel) who="$TUNNEL" ;; keeper) who="$KEEPER" ;; *) bad "rotate-key tunnel|keeper KEY" ;; esac
231
+ write_key "$who" "$key"
232
+ echo "the key of $who was replaced"
233
+ }
234
+
235
+ do_setup() {
236
+ as_root
237
+ local key="" keeper=""
238
+ while [ $# -gt 0 ]; do
239
+ case "$1" in
240
+ --key) key="${2:-}"; shift 2 || bad "--key needs a value" ;;
241
+ --keeper-key) keeper="${2:-}"; shift 2 || bad "--keeper-key needs a value" ;;
242
+ *) bad "unknown parameter: $1" ;;
243
+ esac
244
+ done
245
+ write_key "$TUNNEL" "$key"
246
+ [ -z "$keeper" ] || write_key "$KEEPER" "$keeper"
247
+ do_repair
248
+ }
249
+
250
+ # THE DOOR: what the maintenance account's SSH key leads to. It runs as that
251
+ # account, takes the command it was asked for, lets through only the verbs
252
+ # above with parameters of the expected shape, and asks sudo for this program.
253
+ do_door() {
254
+ local -a words
255
+ read -r -a words <<< "${SSH_ORIGINAL_COMMAND:-}"
256
+ local verb="${words[0]:-}"
257
+ local sudo=(sudo -n "$PROGRAM")
258
+ case "$verb" in
259
+ status|repair|version) [ "${#words[@]}" -eq 1 ] || bad "$verb takes nothing"; exec "${sudo[@]}" "$verb" ;;
260
+ update-os) [ "${#words[@]}" -eq 1 ] || { [ "${#words[@]}" -eq 2 ] && [ "${words[1]}" = "--reboot" ]; } || bad "update-os [--reboot]"; exec "${sudo[@]}" "${words[@]}" ;;
261
+ update-relay) [ "${#words[@]}" -eq 2 ] && [[ "${words[1]}" =~ $VERSION_RE ]] || bad "update-relay X.Y.Z"; exec "${sudo[@]}" update-relay "${words[1]}" ;;
262
+ rotate-key)
263
+ case "${words[1]:-}" in tunnel|keeper) : ;; *) bad "rotate-key tunnel|keeper KEY" ;; esac
264
+ local key="${words[*]:2}"
265
+ key_ok "$key" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
266
+ exec "${sudo[@]}" rotate-key "${words[1]}" "$key" ;;
267
+ *) bad "not a verb of this door: ${verb:-nothing}" ;;
268
+ esac
269
+ }
270
+
271
+ verb="${1:-}"; shift || true
272
+ case "$verb" in
273
+ status) do_status ;;
274
+ repair) do_repair ;;
275
+ update-os) do_update_os "$@" ;;
276
+ update-relay) do_update_relay "$@" ;;
277
+ rotate-key) do_rotate_key "$@" ;;
278
+ setup) do_setup "$@" ;;
279
+ door) do_door ;;
280
+ version|--version) echo "$VERSION_OF_THIS" ;;
281
+ *) bad "unknown verb: ${verb:-none}" ;;
282
+ esac
package/package.json CHANGED
@@ -1,13 +1,11 @@
1
1
  {
2
2
  "name": "clavarium-relay",
3
- "version": "0.0.1",
4
- "description": "The maintenance program of a Clavarium relay (status, repair, updates). Not built yet: this version keeps the name.",
3
+ "version": "0.1.0",
4
+ "description": "The maintenance program of a Clavarium relay: status, repair, updates, key rotation.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://clavarium.org",
7
7
  "repository": { "type": "git", "url": "git+https://github.com/clavarium/clavarium.git", "directory": "relay" },
8
- "type": "module",
9
- "bin": { "clavarium-relay": "keep.mjs" },
10
- "files": ["keep.mjs", "README.md"],
11
- "dependencies": {},
8
+ "bin": { "clavarium-relay": "clavarium-relay" },
9
+ "files": ["clavarium-relay", "README.md"],
12
10
  "publishConfig": { "access": "public" }
13
11
  }
package/keep.mjs DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- /* The relay's maintenance program is not built yet: this version only keeps
3
- * the name. It runs nothing else, installs nothing and reaches no network. */
4
- process.stderr.write('clavarium-relay is not built yet: it will be the maintenance program of a Clavarium relay.\nhttps://github.com/clavarium/clavarium\n');
5
- process.exitCode = 1;