clavarium 0.0.2 → 0.1.0-next.2
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/LICENSE +21 -0
- package/README.md +265 -6
- package/config/tools.json +172 -0
- package/deploy/appliance.sh +83 -0
- package/deploy/backup.sh +97 -0
- package/deploy/clavarium-host +720 -0
- package/deploy/clavarium.mjs +30 -0
- package/deploy/git-read +85 -0
- package/deploy/install.sh +659 -0
- package/deploy/move-to-mnt.sh +142 -0
- package/deploy/parley-archive.mjs +47 -0
- package/deploy/relay.sh +156 -0
- package/deploy/restore.sh +147 -0
- package/deploy/updater.mjs +600 -0
- package/npm-shrinkwrap.json +1951 -0
- package/package.json +53 -8
- package/server/alerts.mjs +83 -0
- package/server/answerer.mjs +326 -0
- package/server/api.mjs +458 -0
- package/server/bridge.mjs +271 -0
- package/server/capacity.mjs +220 -0
- package/server/conversations.mjs +895 -0
- package/server/dataformat.mjs +59 -0
- package/server/devices.mjs +257 -0
- package/server/door.mjs +235 -0
- package/server/files.mjs +455 -0
- package/server/gate.mjs +54 -0
- package/server/gitaccess.mjs +295 -0
- package/server/gitchecks.mjs +126 -0
- package/server/gitkeeper.mjs +719 -0
- package/server/gitpush.mjs +585 -0
- package/server/gitrunner.mjs +540 -0
- package/server/grants.mjs +110 -0
- package/server/guide.mjs +278 -0
- package/server/health.mjs +207 -0
- package/server/imap.mjs +75 -0
- package/server/mailbox.mjs +165 -0
- package/server/main.mjs +420 -0
- package/server/names.mjs +10 -0
- package/server/network.mjs +85 -0
- package/server/parley.mjs +62 -0
- package/server/resources.mjs +396 -0
- package/server/seats.mjs +24 -0
- package/server/secrets.mjs +118 -0
- package/server/setup.mjs +768 -0
- package/server/shelf.mjs +260 -0
- package/server/shortcuts.mjs +62 -0
- package/server/signin.mjs +185 -0
- package/server/smoke.mjs +26 -0
- package/server/store.mjs +48 -0
- package/server/talk.mjs +514 -0
- package/server/tmux.mjs +54 -0
- package/server/toolbox.mjs +168 -0
- package/server/tools.mjs +109 -0
- package/server/updates.mjs +116 -0
- package/server/vault.mjs +37 -0
- package/server/vm.mjs +139 -0
- package/server/voice.mjs +90 -0
- package/server/wakeups.mjs +320 -0
- package/web/app.js +4779 -0
- package/web/device.html +149 -0
- package/web/device.js +152 -0
- package/web/icon-192.png +0 -0
- package/web/icon-512.png +0 -0
- package/web/icon.svg +1 -0
- package/web/icons.js +9 -0
- package/web/index.html +784 -0
- package/web/links.js +995 -0
- package/web/manifest.webmanifest +30 -0
- package/web/prism-manual.js +3 -0
- package/web/style.css +1006 -0
- package/web/sw.js +26 -0
- package/web/voice-core.js +107 -0
- package/web/voice.js +56 -0
- package/keep.mjs +0 -8
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# INSTALLS CLAVARIUM ON THIS MACHINE. One command, parameters only: it asks
|
|
4
|
+
# nothing, and can be run again to upgrade.
|
|
5
|
+
#
|
|
6
|
+
# sudo deploy/install.sh --domain clav.example.com --operator you@example.com
|
|
7
|
+
#
|
|
8
|
+
# --domain NAME the address people will use (https://NAME)
|
|
9
|
+
# --operator EMAIL who may use it; repeat for several people
|
|
10
|
+
# --port N the loopback port of the server (default 7717)
|
|
11
|
+
# --relay HOST this machine is at home, with no public address: it
|
|
12
|
+
# dials out to a relay (see relay.sh) and is reached
|
|
13
|
+
# through it. Nothing here then listens to the outside.
|
|
14
|
+
# --relay-port N the SSH port of the relay (default 22)
|
|
15
|
+
# --core-only the server only: no edge in front. For a machine that
|
|
16
|
+
# has its own authenticating proxy.
|
|
17
|
+
# --local TO TRY IT, on a machine of your own, with no domain: no
|
|
18
|
+
# edge, no sign-in, no name. The page answers on this
|
|
19
|
+
# machine only (http://127.0.0.1:PORT), and whoever can
|
|
20
|
+
# open a page on this machine is its operator. Not for a
|
|
21
|
+
# machine other people use. Takes no --domain, --operator
|
|
22
|
+
# or --relay.
|
|
23
|
+
# --version X.Y.Z install exactly this version of the package "clavarium"
|
|
24
|
+
# from npm, checked (integrity, provenance from
|
|
25
|
+
# github.com/clavarium/clavarium, npm audit signatures).
|
|
26
|
+
# The default when this script comes from the package
|
|
27
|
+
# (npx clavarium install): its own version.
|
|
28
|
+
# --from-dir DIR FOR DEVELOPMENT: install the code of a working tree,
|
|
29
|
+
# as it is (not checked). The default when this script
|
|
30
|
+
# runs from a git working tree.
|
|
31
|
+
# --updates MODE auto (default): updates itself in the window below;
|
|
32
|
+
# asap: as soon as a version is out; ask: says so in
|
|
33
|
+
# Settings and waits; off. Changed later in Settings.
|
|
34
|
+
# --update-window HH:MM-HH:MM when "auto" may update (default 02:00-05:00)
|
|
35
|
+
# --channel C latest (default) or next (versions with a "-": tests)
|
|
36
|
+
# --dry-run check the parameters, say what would be done, do nothing
|
|
37
|
+
#
|
|
38
|
+
# RUN AGAIN on an installation, it installs the version asked for beside the
|
|
39
|
+
# one that runs and switches to it. An installation made before releases
|
|
40
|
+
# existed (code copied into /opt/clavarium) is moved to them; the old copy is
|
|
41
|
+
# removed only once the new one answers.
|
|
42
|
+
#
|
|
43
|
+
# WHAT IT SETS UP, and why it is safe to leave running:
|
|
44
|
+
# - an account, "clavarium", that owns its data and NOTHING ELSE: no shell,
|
|
45
|
+
# no password, not in any privileged group;
|
|
46
|
+
# - the ONE program it may run as root (clavarium-host), named alone in the
|
|
47
|
+
# rule that allows it. Everything that needs root goes through it;
|
|
48
|
+
# - the code in /opt/clavarium, owned by root: the server cannot rewrite
|
|
49
|
+
# itself. One folder per version (releases/<version>), the one that runs
|
|
50
|
+
# named by a link (current); the last two are kept to go back to. A timer
|
|
51
|
+
# looks for new versions and updates (deploy/updater.mjs), as --updates
|
|
52
|
+
# and Settings say;
|
|
53
|
+
# - the data in /home/clavarium, the account's home; the directories whose
|
|
54
|
+
# content gets mounted into VMs belong to root (see clavarium-host).
|
|
55
|
+
# UNDER /home ON PURPOSE: the VM manager is confined and can only share
|
|
56
|
+
# folders from there. Anywhere else, a folder "does not exist" for it
|
|
57
|
+
# (found on a real machine: nothing could be attached from /var/lib);
|
|
58
|
+
# - the key of the vault APART from the data (/var/lib/clavarium-vault): a
|
|
59
|
+
# copy of the data alone does not open the secrets;
|
|
60
|
+
# - THE EDGE, unless --core-only: the only thing that listens to the outside
|
|
61
|
+
# (TLS), the sign-in in front of everything, and a small identity provider
|
|
62
|
+
# that knows the operators. Each is a pinned release, checked against a
|
|
63
|
+
# checksum written here, and runs as its own account. Every operator gets
|
|
64
|
+
# a password made on this machine, SHOWN ONCE at the end.
|
|
65
|
+
#
|
|
66
|
+
# Exit codes: 0 done; 64 bad parameters; 78 this machine is refused.
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
set -euo pipefail
|
|
69
|
+
|
|
70
|
+
SUPPORTED="24.04 26.04"
|
|
71
|
+
ACCOUNT="clavarium"
|
|
72
|
+
CODE="/opt/clavarium"
|
|
73
|
+
DATA="/home/clavarium"
|
|
74
|
+
# What the machines are given -- the shared folders, their read-only views,
|
|
75
|
+
# the work folders and their disks, what was put aside: under /home too (the
|
|
76
|
+
# VM manager shares from there only), but on a chain of directories that ROOT
|
|
77
|
+
# owns from / down, so that the server cannot swap a path between a check and
|
|
78
|
+
# a mount (see clavarium-host, "held"). The server's own data stays in $DATA.
|
|
79
|
+
MNT="/home/clavarium-mnt"
|
|
80
|
+
VAULT="/var/lib/clavarium-vault"
|
|
81
|
+
# The git reader's sandbox: an empty root for it, and its one script.
|
|
82
|
+
RUNNER_ROOT="/var/lib/clavarium/runner-root"
|
|
83
|
+
GIT_READ="/usr/lib/clavarium/git-read"
|
|
84
|
+
ETC="/etc/clavarium"
|
|
85
|
+
HOST_PROGRAM="/usr/local/sbin/clavarium-host"
|
|
86
|
+
UNIT="/etc/systemd/system/clavarium.service"
|
|
87
|
+
UPDATE_CONF="$ETC/update.conf"
|
|
88
|
+
# The edge: exact releases, exact checksums (sha256 of what is downloaded).
|
|
89
|
+
CADDY_VERSION="2.11.4"
|
|
90
|
+
CADDY_SHA256="527fbf917c39189a1e3b31d34fa955601680b2d5c8055d2a87b8b9588dec7bb9"
|
|
91
|
+
PROXY_VERSION="v7.15.4"
|
|
92
|
+
PROXY_SHA256="4fbe902189aab713d9c0519b90a645032d4636ecb523dc36f5cc312d8ebef1e2"
|
|
93
|
+
# The identity provider publishes no archive, only a container image. What is
|
|
94
|
+
# fetched is the one layer of that image that holds the program, named by its
|
|
95
|
+
# checksum: no container tool is needed, and nothing else of the image is used.
|
|
96
|
+
DEX_VERSION="v2.45.1"
|
|
97
|
+
DEX_LAYER="680f1119fdbcdd7f0ad8985b2534ded871b08b5b77c40f337c38aebd062b59c2"
|
|
98
|
+
DEX_SHA256="217aa675738d1cbaf25fe8d7bc04cb00dbc60f91d8465b76b8d8af963d571337"
|
|
99
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
100
|
+
|
|
101
|
+
bad() { echo "install: $*" >&2; echo "usage: install.sh --domain NAME --operator EMAIL [--operator EMAIL...] [--port N] [--relay HOST [--relay-port N]] [--core-only] [--version X.Y.Z | --from-dir DIR] [--updates auto|asap|ask|off] [--update-window HH:MM-HH:MM] [--channel latest|next] [--dry-run]" >&2; echo " or: install.sh --local [--port N] [same options] [--dry-run] to try it on this machine, with no domain" >&2; exit 64; }
|
|
102
|
+
refuse() { echo "install: REFUSED: $*" >&2; exit 78; }
|
|
103
|
+
step() { echo "== $*"; }
|
|
104
|
+
|
|
105
|
+
DOMAIN=""; OPERATORS=""; PORT="7717"; CORE_ONLY="no"; DRY="no"; RELAY=""; RELAY_PORT="22"; LOCAL="no"
|
|
106
|
+
VERSION=""; FROM_DIR=""; UPDATES=""; WINDOW=""; CHANNEL=""
|
|
107
|
+
while [ $# -gt 0 ]; do
|
|
108
|
+
case "$1" in
|
|
109
|
+
--domain) DOMAIN="${2:-}"; shift 2 || bad "--domain needs a value" ;;
|
|
110
|
+
--operator) [ -n "${2:-}" ] || bad "--operator needs a value"; OPERATORS="${OPERATORS:+$OPERATORS,}$2"; shift 2 ;;
|
|
111
|
+
--port) PORT="${2:-}"; shift 2 || bad "--port needs a value" ;;
|
|
112
|
+
--relay) RELAY="${2:-}"; [ -n "$RELAY" ] || bad "--relay needs a value"; shift 2 ;;
|
|
113
|
+
--relay-port) RELAY_PORT="${2:-}"; shift 2 || bad "--relay-port needs a value" ;;
|
|
114
|
+
--core-only) CORE_ONLY="yes"; shift ;;
|
|
115
|
+
--local) LOCAL="yes"; shift ;;
|
|
116
|
+
--version) VERSION="${2:-}"; [ -n "$VERSION" ] || bad "--version needs a value"; shift 2 ;;
|
|
117
|
+
--from-dir) FROM_DIR="${2:-}"; [ -n "$FROM_DIR" ] || bad "--from-dir needs a value"; shift 2 ;;
|
|
118
|
+
--updates) UPDATES="${2:-}"; shift 2 || bad "--updates needs a value" ;;
|
|
119
|
+
--update-window) WINDOW="${2:-}"; shift 2 || bad "--update-window needs a value" ;;
|
|
120
|
+
--channel) CHANNEL="${2:-}"; shift 2 || bad "--channel needs a value" ;;
|
|
121
|
+
--dry-run) DRY="yes"; shift ;;
|
|
122
|
+
*) bad "unknown parameter: $1" ;;
|
|
123
|
+
esac
|
|
124
|
+
done
|
|
125
|
+
|
|
126
|
+
# Every value ends up in a unit file and on a command line: checked here.
|
|
127
|
+
if [ "$LOCAL" = "yes" ]; then
|
|
128
|
+
# To try it: nothing of the outside is set up, so nothing of it is asked.
|
|
129
|
+
[ -z "$DOMAIN" ] && [ -z "$OPERATORS" ] && [ -z "$RELAY" ] || bad "--local is this machine only: it takes no --domain, --operator or --relay"
|
|
130
|
+
CORE_ONLY="yes"
|
|
131
|
+
else
|
|
132
|
+
[[ "$DOMAIN" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}$ ]] || bad "--domain is a host name, lower case, like clav.example.com"
|
|
133
|
+
[ -n "$OPERATORS" ] || bad "at least one --operator is needed: without one, nobody could come in"
|
|
134
|
+
IFS=',' read -r -a each <<< "$OPERATORS"
|
|
135
|
+
for operator in "${each[@]}"; do
|
|
136
|
+
[[ "$operator" =~ ^[a-z0-9._+-]{1,64}@([a-z0-9-]{1,63}\.)+[a-z]{2,24}$ ]] || bad "--operator is an e-mail address, lower case: $operator"
|
|
137
|
+
done
|
|
138
|
+
fi
|
|
139
|
+
[[ "$PORT" =~ ^[0-9]{4,5}$ ]] && [ "$PORT" -ge 1024 ] && [ "$PORT" -le 65535 ] || bad "--port is a number from 1024 to 65535"
|
|
140
|
+
if [ -n "$RELAY" ]; then
|
|
141
|
+
[[ "$RELAY" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}$|^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || bad "--relay is the name or the address of the relay machine"
|
|
142
|
+
[[ "$RELAY_PORT" =~ ^[0-9]{1,5}$ ]] && [ "$RELAY_PORT" -ge 1 ] && [ "$RELAY_PORT" -le 65535 ] || bad "--relay-port is a port number"
|
|
143
|
+
[ "$CORE_ONLY" = "no" ] || bad "--relay comes with the edge: it cannot go with --core-only"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# WHAT IS INSTALLED: an exact version from npm, or a working tree (development).
|
|
147
|
+
[ -z "$VERSION" ] || [[ "$VERSION" =~ ^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,6}(-[0-9A-Za-z.-]{1,40})?$ ]] || bad "--version is an exact version, like 1.2.3 or 1.3.0-next.1"
|
|
148
|
+
[ -z "$VERSION" ] || [ -z "$FROM_DIR" ] || bad "--version (from npm) or --from-dir (a working tree), not both"
|
|
149
|
+
if [ -n "$FROM_DIR" ]; then
|
|
150
|
+
FROM_DIR="$(cd "$FROM_DIR" 2>/dev/null && pwd)" || bad "--from-dir is a folder that exists"
|
|
151
|
+
[ -f "$FROM_DIR/server/main.mjs" ] && [ -f "$FROM_DIR/package-lock.json" ] || bad "--from-dir is a working tree of Clavarium (server/main.mjs, package-lock.json)"
|
|
152
|
+
fi
|
|
153
|
+
if [ -z "$VERSION" ] && [ -z "$FROM_DIR" ]; then
|
|
154
|
+
if [ -e "$HERE/.git" ]; then FROM_DIR="$HERE"
|
|
155
|
+
else VERSION="$(sed -n 's/^ "version": "\([^"]*\)",$/\1/p' "$HERE/package.json" | head -1)"
|
|
156
|
+
[[ "$VERSION" =~ ^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,6}(-[0-9A-Za-z.-]{1,40})?$ ]] || refuse "the version of this package was not found in $HERE/package.json: give --version"
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
[ -z "$UPDATES" ] || [[ "$UPDATES" =~ ^(auto|asap|ask|off)$ ]] || bad "--updates is auto, asap, ask or off"
|
|
160
|
+
[ -z "$WINDOW" ] || { [[ "$WINDOW" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]$ ]] && [ "${WINDOW%-*}" != "${WINDOW#*-}" ]; } || bad "--update-window is two times, like 02:00-05:00"
|
|
161
|
+
[ -z "$CHANNEL" ] || [[ "$CHANNEL" =~ ^(latest|next)$ ]] || bad "--channel is latest or next"
|
|
162
|
+
# What the updates will be, as the settings file has them or as asked now.
|
|
163
|
+
had() { { sed -n "s/^$1=//p" "$UPDATE_CONF" 2>/dev/null || true; } | head -1; }
|
|
164
|
+
SHOW_MODE="${UPDATES:-$(had MODE)}"; SHOW_MODE="${SHOW_MODE:-auto}"
|
|
165
|
+
SHOW_WINDOW="${WINDOW:-$(had WINDOW)}"; SHOW_WINDOW="${SHOW_WINDOW:-02:00-05:00}"
|
|
166
|
+
case "$VERSION" in *-*) default_channel="next" ;; *) default_channel="latest" ;; esac
|
|
167
|
+
SHOW_CHANNEL="${CHANNEL:-$(had CHANNEL)}"; SHOW_CHANNEL="${SHOW_CHANNEL:-$default_channel}"
|
|
168
|
+
|
|
169
|
+
# The Node.js the server runs on: the same pinned version, with the same
|
|
170
|
+
# checksum, as the one put in the VMs. One place says which (server/tools.mjs).
|
|
171
|
+
NODE_VERSION="$(sed -n "s/^export const NODE_VERSION = '\(v[0-9.]*\)';$/\1/p" "$HERE/server/tools.mjs")"
|
|
172
|
+
NODE_SHA256="$(sed -n "s/^export const NODE_SHA256 = '\([0-9a-f]\{64\}\)';$/\1/p" "$HERE/server/tools.mjs")"
|
|
173
|
+
[ -n "$NODE_VERSION" ] && [ -n "$NODE_SHA256" ] || refuse "the pinned Node.js version was not found in $HERE/server/tools.mjs"
|
|
174
|
+
|
|
175
|
+
if [ "$DRY" = "yes" ]; then
|
|
176
|
+
cat <<PLAN
|
|
177
|
+
would install Clavarium:
|
|
178
|
+
address $([ "$LOCAL" = "yes" ] && echo "http://127.0.0.1:$PORT, from this machine only (--local)" || echo "https://$DOMAIN")
|
|
179
|
+
operators $([ "$LOCAL" = "yes" ] && echo "whoever can open a page on this machine: no sign-in" || echo "$OPERATORS")
|
|
180
|
+
server 127.0.0.1:$PORT, as the account "$ACCOUNT", Node.js $NODE_VERSION
|
|
181
|
+
release $([ -n "$VERSION" ] && echo "clavarium@$VERSION from npm, checked (integrity, provenance from github.com/clavarium/clavarium, npm audit signatures)" || echo "the working tree $FROM_DIR, as it is (development: not checked)")
|
|
182
|
+
updates $(case "$SHOW_MODE" in auto) echo "automatic, between ${SHOW_WINDOW%-*} and ${SHOW_WINDOW#*-}" ;; asap) echo "as soon as a version is out" ;; ask) echo "said in Settings, done when you say" ;; off) echo "off" ;; esac), channel $SHOW_CHANNEL
|
|
183
|
+
code $CODE/releases/<version>, $CODE/current (root) data $DATA folders $MNT (root's chain) vault key $VAULT
|
|
184
|
+
root rule $ACCOUNT may run $HOST_PROGRAM, and nothing else
|
|
185
|
+
edge $([ "$LOCAL" = "yes" ] && echo "none (--local): no address in https, no sign-in" || [ "$CORE_ONLY" = "yes" ] && echo "none (--core-only)" || echo "caddy $CADDY_VERSION (TLS), oauth2-proxy $PROXY_VERSION (sign-in), dex $DEX_VERSION (who the operators are)")
|
|
186
|
+
reached $([ "$LOCAL" = "yes" ] && echo "from this machine only; from another one of yours, through ssh -L" || [ -n "$RELAY" ] && echo "through the relay $RELAY (this machine dials out; nothing here listens to the outside)" || echo "directly: ports 443 and 80 of this machine")
|
|
187
|
+
PLAN
|
|
188
|
+
exit 0
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
[ "$(id -u)" -eq 0 ] || refuse "run it as root (sudo)"
|
|
192
|
+
# /etc/os-release is read in a subshell: it defines VERSION, NAME and more, which
|
|
193
|
+
# would overwrite this script's own (VERSION is the Clavarium version asked for).
|
|
194
|
+
# shellcheck disable=SC1091
|
|
195
|
+
OS_ID="$(. /etc/os-release 2>/dev/null; printf '%s' "${ID:-}")"
|
|
196
|
+
# shellcheck disable=SC1091
|
|
197
|
+
OS_VERSION_ID="$(. /etc/os-release 2>/dev/null; printf '%s' "${VERSION_ID:-}")"
|
|
198
|
+
# shellcheck disable=SC1091
|
|
199
|
+
OS_PRETTY="$(. /etc/os-release 2>/dev/null; printf '%s' "${PRETTY_NAME:-}")"
|
|
200
|
+
[ "$OS_ID" = "ubuntu" ] || refuse "this is ${OS_PRETTY:-not Ubuntu}; Clavarium installs on Ubuntu Server $SUPPORTED"
|
|
201
|
+
case " $SUPPORTED " in *" ${OS_VERSION_ID} "*) : ;; *) refuse "this is Ubuntu ${OS_VERSION_ID:-?}; supported: $SUPPORTED" ;; esac
|
|
202
|
+
[ "$(uname -m)" = "x86_64" ] || refuse "this machine is $(uname -m); only x86_64 is supported"
|
|
203
|
+
grep -Eq '(vmx|svm)' /proc/cpuinfo || echo "install: WARNING: this processor does not show virtualisation: VMs may not start." >&2
|
|
204
|
+
|
|
205
|
+
step "packages"
|
|
206
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
207
|
+
# Installing packages must not stop to ask which services to restart, nor fill the screen with it.
|
|
208
|
+
export NEEDRESTART_SUSPEND=1
|
|
209
|
+
apt-get update -qq
|
|
210
|
+
# bindfs: the read-only views. nftables: the network rules. build-essential
|
|
211
|
+
# and python3: one module of the server (the terminal) is compiled here.
|
|
212
|
+
apt-get install -y -qq bindfs nftables build-essential python3 curl ca-certificates xz-utils sudo snapd openssl git >/dev/null
|
|
213
|
+
snap list multipass >/dev/null 2>&1 || snap install multipass >/dev/null
|
|
214
|
+
|
|
215
|
+
step "account"
|
|
216
|
+
id "$ACCOUNT" >/dev/null 2>&1 || useradd --system --home-dir "$DATA" --no-create-home --shell /usr/sbin/nologin "$ACCOUNT"
|
|
217
|
+
[ "$(getent passwd "$ACCOUNT" | cut -d: -f6)" = "$DATA" ] || usermod --home "$DATA" "$ACCOUNT"
|
|
218
|
+
install -d -o "$ACCOUNT" -g "$ACCOUNT" -m 0750 "$DATA"
|
|
219
|
+
install -d -o "$ACCOUNT" -g "$ACCOUNT" -m 0700 "$VAULT"
|
|
220
|
+
# What gets mounted into VMs is arranged by root only: see clavarium-host.
|
|
221
|
+
install -d -o root -g root -m 0755 "$MNT" "$MNT/resources" "$MNT/views" "$MNT/removed" "$MNT/work" "$MNT/work-disks"
|
|
222
|
+
|
|
223
|
+
step "code"
|
|
224
|
+
# No update while this runs: the timer and its service wait.
|
|
225
|
+
systemctl stop clavarium-update.timer clavarium-update.service 2>/dev/null || true
|
|
226
|
+
install -d -o root -g root -m 0755 "$CODE" "$CODE/releases" "$CODE/nodes"
|
|
227
|
+
# An installation from before releases: its code sits in $CODE itself. It
|
|
228
|
+
# keeps running while the new release is made beside it, and is removed only
|
|
229
|
+
# once the new one answers (below).
|
|
230
|
+
LEGACY="no"; [ -d "$CODE/server" ] && [ ! -L "$CODE/current" ] && LEGACY="yes"
|
|
231
|
+
# The Node.js the updater runs on to make the release (each release then has
|
|
232
|
+
# the one its own code pins, most often this one).
|
|
233
|
+
NODE_DIR="$CODE/nodes/$NODE_VERSION"
|
|
234
|
+
if [ ! -x "$NODE_DIR/bin/node" ]; then
|
|
235
|
+
rm -rf -- "$NODE_DIR.partial"
|
|
236
|
+
if [ -x "$CODE/node/bin/node" ] && [ "$("$CODE/node/bin/node" --version)" = "$NODE_VERSION" ]; then
|
|
237
|
+
cp -a "$CODE/node" "$NODE_DIR.partial"
|
|
238
|
+
else
|
|
239
|
+
archive="$(mktemp)"
|
|
240
|
+
curl --proto '=https' --tlsv1.2 -fsSL --max-time 600 -o "$archive" "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.xz"
|
|
241
|
+
echo "$NODE_SHA256 $archive" | sha256sum -c - >/dev/null || refuse "the Node.js archive does not have the expected checksum"
|
|
242
|
+
install -d "$NODE_DIR.partial"
|
|
243
|
+
tar -xJf "$archive" -C "$NODE_DIR.partial" --strip-components=1 --no-same-owner
|
|
244
|
+
rm -f -- "$archive"
|
|
245
|
+
fi
|
|
246
|
+
mv -- "$NODE_DIR.partial" "$NODE_DIR"
|
|
247
|
+
fi
|
|
248
|
+
updater() { PATH="$NODE_DIR/bin:$PATH" "$NODE_DIR/bin/node" "$HERE/deploy/updater.mjs" "$@"; }
|
|
249
|
+
if [ -n "$VERSION" ]; then
|
|
250
|
+
echo " clavarium@$VERSION from npm: fetched, checked, built, tried (a few minutes)"
|
|
251
|
+
release="$(updater prepare --version "$VERSION")" || refuse "clavarium@$VERSION was not installed (above: why)"
|
|
252
|
+
else
|
|
253
|
+
echo " the working tree $FROM_DIR (development): copied, built, tried"
|
|
254
|
+
release="$(updater prepare --from-dir "$FROM_DIR")" || refuse "the working tree was not installed (above: why)"
|
|
255
|
+
fi
|
|
256
|
+
systemctl stop clavarium.service 2>/dev/null || true
|
|
257
|
+
updater switch "$release"
|
|
258
|
+
APP="$CODE/current/app"
|
|
259
|
+
NODE="$CODE/current/node/bin/node"
|
|
260
|
+
# The updater of the release now in place, for what follows and from now on.
|
|
261
|
+
updater() { PATH="$CODE/current/node/bin:$PATH" "$NODE" "$APP/deploy/updater.mjs" "$@"; }
|
|
262
|
+
|
|
263
|
+
step "the git reader"
|
|
264
|
+
# The one way the server reads an agent's repository: a sandbox around one
|
|
265
|
+
# fixed script (clavarium-host "git read"). Its root is empty and root's.
|
|
266
|
+
install -d -o root -g root -m 0755 /var/lib/clavarium /usr/lib/clavarium "$RUNNER_ROOT"
|
|
267
|
+
install -o root -g root -m 0755 "$APP/deploy/git-read" "$GIT_READ"
|
|
268
|
+
|
|
269
|
+
step "the one privileged program"
|
|
270
|
+
install -o root -g root -m 0755 "$APP/deploy/clavarium-host" "$HOST_PROGRAM"
|
|
271
|
+
install -d -o root -g root -m 0755 "$ETC"
|
|
272
|
+
printf 'DATA=%s\nMNT=%s\n' "$DATA" "$MNT" > "$ETC/host.conf"; chmod 0644 "$ETC/host.conf"
|
|
273
|
+
rule="$(mktemp)"
|
|
274
|
+
printf '# Clavarium: the server account may run ONE program as root, and nothing else.\n%s ALL=(root) NOPASSWD: %s\n' "$ACCOUNT" "$HOST_PROGRAM" > "$rule"
|
|
275
|
+
visudo -cf "$rule" >/dev/null || refuse "the sudo rule did not pass its check"
|
|
276
|
+
install -o root -g root -m 0440 "$rule" /etc/sudoers.d/clavarium; rm -f -- "$rule"
|
|
277
|
+
|
|
278
|
+
step "what the machines are given, on root's chain"
|
|
279
|
+
# ONCE, from an earlier version: resources/, views/, work/, work-disks/ and
|
|
280
|
+
# removed/ move from $DATA to $MNT, with the server stopped (above); the
|
|
281
|
+
# machines are given back what they had, from the new places, through the
|
|
282
|
+
# program just installed. See deploy/move-to-mnt.sh.
|
|
283
|
+
bash "$APP/deploy/move-to-mnt.sh" --data "$DATA" --mnt "$MNT" --host "$HOST_PROGRAM" --node "$NODE"
|
|
284
|
+
# Each component from / to each of them belongs to root, nobody else may
|
|
285
|
+
# write it, and no access list says otherwise (ls marks one with a '+').
|
|
286
|
+
for at in /home "$MNT" "$MNT/resources" "$MNT/views" "$MNT/removed" "$MNT/work" "$MNT/work-disks"; do
|
|
287
|
+
[ "$(stat -c %u "$at")" = "0" ] && [ -d "$at" ] && [ ! -L "$at" ] || refuse "$at must be a directory of root"
|
|
288
|
+
case "$(stat -c %A "$at")" in ?????w????|????????w?) refuse "$at may be written by others than root" ;; esac
|
|
289
|
+
mode="$(ls -ld -- "$at" | cut -d' ' -f1)"; [ "${#mode}" -eq 10 ] || refuse "$at has an access list (getfacl): only root may write it"
|
|
290
|
+
done
|
|
291
|
+
# Settings that are not parameters (the speech-to-text endpoint): kept across upgrades.
|
|
292
|
+
[ -e "$ETC/server.env" ] || install -o root -g root -m 0600 /dev/null "$ETC/server.env"
|
|
293
|
+
|
|
294
|
+
step "service"
|
|
295
|
+
# NO private mount namespace here (PrivateTmp, ProtectHome, ProtectSystem...):
|
|
296
|
+
# the read-only views are mounts made on behalf of this service, and the VM
|
|
297
|
+
# manager must see them. In a private namespace it would see empty folders.
|
|
298
|
+
cat > "$UNIT" <<UNITFILE
|
|
299
|
+
[Unit]
|
|
300
|
+
Description=Clavarium - conversations as VMs
|
|
301
|
+
After=network-online.target snap.multipass.multipassd.service
|
|
302
|
+
Wants=network-online.target
|
|
303
|
+
|
|
304
|
+
[Service]
|
|
305
|
+
User=$ACCOUNT
|
|
306
|
+
Group=$ACCOUNT
|
|
307
|
+
WorkingDirectory=$CODE/current/app
|
|
308
|
+
EnvironmentFile=$ETC/server.env
|
|
309
|
+
ExecStart=$CODE/current/node/bin/node server/main.mjs --host "sudo -n $HOST_PROGRAM" --data $DATA --vault-key $VAULT/vault.key --port $PORT$([ "$LOCAL" = "yes" ] || echo " --operators $OPERATORS --public-origin https://$DOMAIN")
|
|
310
|
+
Restart=on-failure
|
|
311
|
+
RestartSec=3
|
|
312
|
+
|
|
313
|
+
[Install]
|
|
314
|
+
WantedBy=multi-user.target
|
|
315
|
+
UNITFILE
|
|
316
|
+
|
|
317
|
+
# THE UPDATES: their settings (kept from an earlier run unless asked here),
|
|
318
|
+
# and the timer that looks for a new version every 15 minutes. What it does
|
|
319
|
+
# is the updater's (deploy/updater.mjs), as root, through this service only.
|
|
320
|
+
updater set port "$PORT" >/dev/null
|
|
321
|
+
updater set mode "$SHOW_MODE" >/dev/null
|
|
322
|
+
updater set window "$SHOW_WINDOW" >/dev/null
|
|
323
|
+
updater set channel "$SHOW_CHANNEL" >/dev/null
|
|
324
|
+
cat > /etc/systemd/system/clavarium-update.service <<UPDATEUNIT
|
|
325
|
+
[Unit]
|
|
326
|
+
Description=Clavarium - looks for a new version, and updates as its settings say
|
|
327
|
+
After=network-online.target
|
|
328
|
+
Wants=network-online.target
|
|
329
|
+
|
|
330
|
+
[Service]
|
|
331
|
+
Type=oneshot
|
|
332
|
+
ExecStart=$CODE/current/node/bin/node $CODE/current/app/deploy/updater.mjs run
|
|
333
|
+
TimeoutStartSec=3600
|
|
334
|
+
UPDATEUNIT
|
|
335
|
+
cat > /etc/systemd/system/clavarium-update.timer <<'UPDATETIMER'
|
|
336
|
+
[Unit]
|
|
337
|
+
Description=Clavarium - looks for a new version every 15 minutes
|
|
338
|
+
|
|
339
|
+
[Timer]
|
|
340
|
+
OnBootSec=10min
|
|
341
|
+
OnUnitActiveSec=15min
|
|
342
|
+
RandomizedDelaySec=3min
|
|
343
|
+
|
|
344
|
+
[Install]
|
|
345
|
+
WantedBy=timers.target
|
|
346
|
+
UPDATETIMER
|
|
347
|
+
systemctl daemon-reload
|
|
348
|
+
systemctl enable --quiet clavarium.service
|
|
349
|
+
systemctl enable --quiet clavarium-update.timer
|
|
350
|
+
systemctl restart clavarium.service
|
|
351
|
+
|
|
352
|
+
step "checks"
|
|
353
|
+
for _ in $(seq 1 20); do
|
|
354
|
+
curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/" && break
|
|
355
|
+
sleep 1
|
|
356
|
+
done
|
|
357
|
+
systemctl is-active --quiet clavarium.service || refuse "the service did not stay up: journalctl -u clavarium"
|
|
358
|
+
curl -s -o /dev/null --max-time 5 "http://127.0.0.1:$PORT/" || refuse "the server does not answer on 127.0.0.1:$PORT"
|
|
359
|
+
sudo -u "$ACCOUNT" sudo -n -l "$HOST_PROGRAM" >/dev/null || refuse "the account cannot reach its one program"
|
|
360
|
+
if sudo -u "$ACCOUNT" sudo -n -l /bin/sh >/dev/null 2>&1; then refuse "the account can run more than its one program as root"; fi
|
|
361
|
+
# It answers: the copy of an installation from before releases goes, and
|
|
362
|
+
# releases beyond the last two. Then the timer may look for updates.
|
|
363
|
+
if [ "$LEGACY" = "yes" ]; then
|
|
364
|
+
rm -rf -- "$CODE/server" "$CODE/web" "$CODE/config" "$CODE/node_modules" "$CODE/node" "$CODE/package.json" "$CODE/package-lock.json"
|
|
365
|
+
echo " the earlier copy of the code in $CODE was removed: it runs from $CODE/current"
|
|
366
|
+
fi
|
|
367
|
+
updater prune
|
|
368
|
+
systemctl start clavarium-update.timer
|
|
369
|
+
echo " running: $(sed -n 's/^ "version": "\([^"]*\)",$/\1/p' "$APP/package.json" | head -1) ($release)"
|
|
370
|
+
|
|
371
|
+
if [ "$LOCAL" = "yes" ]; then
|
|
372
|
+
echo "Clavarium is installed, to try it: http://127.0.0.1:$PORT (from this machine only)"
|
|
373
|
+
echo "From another computer of yours: ssh -L $PORT:127.0.0.1:$PORT <you>@<this machine>, then the same address there."
|
|
374
|
+
echo "There is no sign-in: whoever can open a page on this machine is its operator."
|
|
375
|
+
echo "For real use, run it again with --domain and --operator."
|
|
376
|
+
exit 0
|
|
377
|
+
fi
|
|
378
|
+
if [ "$CORE_ONLY" = "yes" ]; then
|
|
379
|
+
echo "Clavarium is installed: 127.0.0.1:$PORT, for $OPERATORS."
|
|
380
|
+
echo "It answers nobody yet: put an authenticating proxy in front, for https://$DOMAIN."
|
|
381
|
+
exit 0
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
# ------------------------------- THE EDGE ---------------------------------
|
|
385
|
+
# Downloads one file and refuses it unless it has the expected checksum.
|
|
386
|
+
fetch() { # <url> <sha256> <destination> [extra curl arguments...]
|
|
387
|
+
local url="$1" sum="$2" to="$3"; shift 3
|
|
388
|
+
curl --proto '=https' --tlsv1.2 -fsSL --max-time 600 "$@" -o "$to" "$url"
|
|
389
|
+
echo "$sum $to" | sha256sum -c - >/dev/null || refuse "not the expected checksum: $url"
|
|
390
|
+
}
|
|
391
|
+
account() { id "$1" >/dev/null 2>&1 || useradd --system --home-dir "$2" --no-create-home --shell /usr/sbin/nologin "$1"; }
|
|
392
|
+
|
|
393
|
+
step "edge: programs"
|
|
394
|
+
work="$(mktemp -d)"
|
|
395
|
+
if [ "$(/usr/local/bin/caddy version 2>/dev/null | cut -d' ' -f1)" != "v$CADDY_VERSION" ]; then
|
|
396
|
+
fetch "https://github.com/caddyserver/caddy/releases/download/v$CADDY_VERSION/caddy_${CADDY_VERSION}_linux_amd64.tar.gz" "$CADDY_SHA256" "$work/caddy.tgz"
|
|
397
|
+
tar -xzf "$work/caddy.tgz" -C "$work" --no-same-owner caddy
|
|
398
|
+
install -o root -g root -m 0755 "$work/caddy" /usr/local/bin/caddy
|
|
399
|
+
fi
|
|
400
|
+
if ! /usr/local/bin/oauth2-proxy --version 2>/dev/null | grep -q " $PROXY_VERSION "; then
|
|
401
|
+
fetch "https://github.com/oauth2-proxy/oauth2-proxy/releases/download/$PROXY_VERSION/oauth2-proxy-$PROXY_VERSION.linux-amd64.tar.gz" "$PROXY_SHA256" "$work/proxy.tgz"
|
|
402
|
+
tar -xzf "$work/proxy.tgz" -C "$work" --no-same-owner --strip-components=1
|
|
403
|
+
install -o root -g root -m 0755 "$work/oauth2-proxy" /usr/local/bin/oauth2-proxy
|
|
404
|
+
fi
|
|
405
|
+
if [ "$(sha256sum /usr/local/bin/dex 2>/dev/null | cut -d' ' -f1)" != "$DEX_SHA256" ]; then
|
|
406
|
+
token="$(curl --proto '=https' --tlsv1.2 -fsSL --max-time 60 'https://ghcr.io/token?scope=repository:dexidp/dex:pull' | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
|
|
407
|
+
[ -n "$token" ] || refuse "the registry gave no anonymous token for the identity provider"
|
|
408
|
+
fetch "https://ghcr.io/v2/dexidp/dex/blobs/sha256:$DEX_LAYER" "$DEX_LAYER" "$work/dex.tgz" -H "Authorization: Bearer $token"
|
|
409
|
+
tar -xzf "$work/dex.tgz" -C "$work" --no-same-owner usr/local/bin/dex
|
|
410
|
+
echo "$DEX_SHA256 $work/usr/local/bin/dex" | sha256sum -c - >/dev/null || refuse "the identity provider is not the expected program"
|
|
411
|
+
install -o root -g root -m 0755 "$work/usr/local/bin/dex" /usr/local/bin/dex
|
|
412
|
+
fi
|
|
413
|
+
rm -rf -- "$work"
|
|
414
|
+
|
|
415
|
+
step "edge: accounts and settings"
|
|
416
|
+
account caddy /var/lib/caddy; account dex /var/lib/dex; account oauth2-proxy /nonexistent
|
|
417
|
+
install -d -o caddy -g caddy -m 0700 /var/lib/caddy
|
|
418
|
+
install -d -o dex -g dex -m 0700 /var/lib/dex
|
|
419
|
+
install -d -o root -g root -m 0755 /etc/caddy /etc/oauth2-proxy
|
|
420
|
+
install -d -o root -g dex -m 0750 /etc/dex
|
|
421
|
+
|
|
422
|
+
# The identity file holds the operators and their passwords. It is written
|
|
423
|
+
# ONCE: running this script again keeps it, and the passwords with it.
|
|
424
|
+
PASSWORDS=""
|
|
425
|
+
if [ ! -e /etc/dex/config.yaml ]; then
|
|
426
|
+
client_secret="$(openssl rand -hex 32)"
|
|
427
|
+
cookie_secret="$(openssl rand -base64 32 | tr '+/' '-_')"
|
|
428
|
+
identity="$(mktemp)"
|
|
429
|
+
cat > "$identity" <<DEX
|
|
430
|
+
# Who may sign in to Clavarium on THIS machine. Written by install.sh.
|
|
431
|
+
issuer: https://$DOMAIN/dex
|
|
432
|
+
storage:
|
|
433
|
+
type: sqlite3
|
|
434
|
+
config:
|
|
435
|
+
file: /var/lib/dex/dex.db
|
|
436
|
+
web:
|
|
437
|
+
# Loopback only: the TLS front publishes it under /dex.
|
|
438
|
+
http: 127.0.0.1:5556
|
|
439
|
+
oauth2:
|
|
440
|
+
skipApprovalScreen: true
|
|
441
|
+
staticClients:
|
|
442
|
+
- id: clavarium
|
|
443
|
+
name: Clavarium
|
|
444
|
+
secret: $client_secret
|
|
445
|
+
redirectURIs:
|
|
446
|
+
- https://$DOMAIN/oauth2/callback
|
|
447
|
+
enablePasswordDB: true
|
|
448
|
+
staticPasswords:
|
|
449
|
+
DEX
|
|
450
|
+
for operator in "${each[@]}"; do
|
|
451
|
+
password="$(openssl rand -base64 18 | tr '+/' '-_')"
|
|
452
|
+
# On standard input: a password never sits on a command line.
|
|
453
|
+
hash="$(printf '%s\n' "$password" | /usr/local/bin/caddy hash-password)"
|
|
454
|
+
[[ "$hash" == \$2* ]] || refuse "the password could not be hashed"
|
|
455
|
+
printf ' - email: "%s"\n hash: "%s"\n username: "%s"\n userID: "%s"\n' \
|
|
456
|
+
"$operator" "$hash" "${operator%%@*}" "$(cat /proc/sys/kernel/random/uuid)" >> "$identity"
|
|
457
|
+
PASSWORDS="$PASSWORDS $operator $password"$'\n'
|
|
458
|
+
done
|
|
459
|
+
install -o root -g dex -m 0640 "$identity" /etc/dex/config.yaml; rm -f -- "$identity"
|
|
460
|
+
|
|
461
|
+
# The sign-in proxy talks to the identity provider on the loopback, by
|
|
462
|
+
# explicit addresses: it does not depend on this machine resolving its own
|
|
463
|
+
# public name, nor on the certificate being issued yet.
|
|
464
|
+
proxy="$(mktemp)"
|
|
465
|
+
cat > "$proxy" <<PROXY
|
|
466
|
+
provider = "oidc"
|
|
467
|
+
client_id = "clavarium"
|
|
468
|
+
client_secret = "$client_secret"
|
|
469
|
+
cookie_secret = "$cookie_secret"
|
|
470
|
+
oidc_issuer_url = "https://$DOMAIN/dex"
|
|
471
|
+
skip_oidc_discovery = true
|
|
472
|
+
login_url = "https://$DOMAIN/dex/auth"
|
|
473
|
+
redeem_url = "http://127.0.0.1:5556/dex/token"
|
|
474
|
+
oidc_jwks_url = "http://127.0.0.1:5556/dex/keys"
|
|
475
|
+
redirect_url = "https://$DOMAIN/oauth2/callback"
|
|
476
|
+
# Who is an operator is decided by the identity file, and again by the server.
|
|
477
|
+
email_domains = ["*"]
|
|
478
|
+
cookie_secure = true
|
|
479
|
+
cookie_samesite = "lax"
|
|
480
|
+
set_xauthrequest = true
|
|
481
|
+
reverse_proxy = true
|
|
482
|
+
skip_provider_button = true
|
|
483
|
+
# The operators were chosen by whoever installed this: no "grant access" page.
|
|
484
|
+
approval_prompt = "auto"
|
|
485
|
+
http_address = "127.0.0.1:4180"
|
|
486
|
+
upstreams = ["http://127.0.0.1:$PORT/"]
|
|
487
|
+
# A paired device does not sign in: its own door (/c/) checks its key. The
|
|
488
|
+
# identity headers are stripped there, so nobody can claim to be an operator.
|
|
489
|
+
skip_auth_routes = ["^/c/"]
|
|
490
|
+
skip_auth_strip_headers = true
|
|
491
|
+
PROXY
|
|
492
|
+
install -o root -g root -m 0600 "$proxy" /etc/oauth2-proxy/oauth2-proxy.cfg; rm -f -- "$proxy"
|
|
493
|
+
else
|
|
494
|
+
echo " the identity file exists: kept, with its operators and passwords."
|
|
495
|
+
echo " (to start again: remove /etc/dex/config.yaml and run this script again)"
|
|
496
|
+
sed -i "s#^upstreams = .*#upstreams = [\"http://127.0.0.1:$PORT/\"]#" /etc/oauth2-proxy/oauth2-proxy.cfg
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
# Behind a relay, the front listens on the loopback only: the tunnel is the
|
|
500
|
+
# one way in.
|
|
501
|
+
BIND=""; [ -z "$RELAY" ] || BIND=" bind 127.0.0.1"
|
|
502
|
+
cat > /etc/caddy/Caddyfile <<CADDY
|
|
503
|
+
# The only thing on this machine that takes what comes from the outside.
|
|
504
|
+
$DOMAIN {
|
|
505
|
+
$BIND
|
|
506
|
+
tls {
|
|
507
|
+
issuer acme {
|
|
508
|
+
disable_tlsalpn_challenge
|
|
509
|
+
}
|
|
510
|
+
issuer internal
|
|
511
|
+
}
|
|
512
|
+
handle /dex/* {
|
|
513
|
+
reverse_proxy 127.0.0.1:5556
|
|
514
|
+
}
|
|
515
|
+
handle {
|
|
516
|
+
reverse_proxy 127.0.0.1:4180
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
CADDY
|
|
520
|
+
|
|
521
|
+
step "edge: services"
|
|
522
|
+
cat > /etc/systemd/system/caddy.service <<'CADDYUNIT'
|
|
523
|
+
[Unit]
|
|
524
|
+
Description=Caddy - the TLS front of this machine
|
|
525
|
+
After=network-online.target
|
|
526
|
+
Wants=network-online.target
|
|
527
|
+
|
|
528
|
+
[Service]
|
|
529
|
+
User=caddy
|
|
530
|
+
Group=caddy
|
|
531
|
+
Environment=XDG_DATA_HOME=/var/lib/caddy XDG_CONFIG_HOME=/var/lib/caddy
|
|
532
|
+
ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
|
|
533
|
+
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile
|
|
534
|
+
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
535
|
+
Restart=on-failure
|
|
536
|
+
|
|
537
|
+
[Install]
|
|
538
|
+
WantedBy=multi-user.target
|
|
539
|
+
CADDYUNIT
|
|
540
|
+
cat > /etc/systemd/system/dex.service <<'DEXUNIT'
|
|
541
|
+
[Unit]
|
|
542
|
+
Description=Dex - who the operators of this machine are
|
|
543
|
+
After=network.target
|
|
544
|
+
|
|
545
|
+
[Service]
|
|
546
|
+
User=dex
|
|
547
|
+
ExecStart=/usr/local/bin/dex serve /etc/dex/config.yaml
|
|
548
|
+
NoNewPrivileges=yes
|
|
549
|
+
ProtectSystem=strict
|
|
550
|
+
ProtectHome=yes
|
|
551
|
+
PrivateTmp=yes
|
|
552
|
+
ReadWritePaths=/var/lib/dex
|
|
553
|
+
Restart=on-failure
|
|
554
|
+
|
|
555
|
+
[Install]
|
|
556
|
+
WantedBy=multi-user.target
|
|
557
|
+
DEXUNIT
|
|
558
|
+
cat > /etc/systemd/system/oauth2-proxy.service <<'PROXYUNIT'
|
|
559
|
+
[Unit]
|
|
560
|
+
Description=oauth2-proxy - the sign-in in front of Clavarium
|
|
561
|
+
After=network-online.target dex.service
|
|
562
|
+
Wants=network-online.target
|
|
563
|
+
StartLimitIntervalSec=0
|
|
564
|
+
|
|
565
|
+
[Service]
|
|
566
|
+
User=oauth2-proxy
|
|
567
|
+
Group=oauth2-proxy
|
|
568
|
+
LoadCredential=oauth2-proxy.cfg:/etc/oauth2-proxy/oauth2-proxy.cfg
|
|
569
|
+
ExecStart=/usr/local/bin/oauth2-proxy --config %d/oauth2-proxy.cfg
|
|
570
|
+
Restart=on-failure
|
|
571
|
+
RestartSec=5
|
|
572
|
+
NoNewPrivileges=yes
|
|
573
|
+
ProtectSystem=strict
|
|
574
|
+
ProtectHome=yes
|
|
575
|
+
PrivateTmp=yes
|
|
576
|
+
|
|
577
|
+
[Install]
|
|
578
|
+
WantedBy=multi-user.target
|
|
579
|
+
PROXYUNIT
|
|
580
|
+
systemctl daemon-reload
|
|
581
|
+
systemctl enable --quiet caddy.service dex.service oauth2-proxy.service
|
|
582
|
+
systemctl restart dex.service caddy.service oauth2-proxy.service
|
|
583
|
+
|
|
584
|
+
step "edge: checks"
|
|
585
|
+
local_get() { curl -sk -o /dev/null -w '%{http_code}' --max-time 5 --resolve "$DOMAIN:443:127.0.0.1" "https://$DOMAIN$1" || true; }
|
|
586
|
+
for _ in $(seq 1 30); do
|
|
587
|
+
[ "$(local_get /dex/.well-known/openid-configuration)" = "200" ] && [ "$(local_get /oauth2/sign_in)" = "200" ] && break
|
|
588
|
+
sleep 2
|
|
589
|
+
done
|
|
590
|
+
for service in caddy dex oauth2-proxy; do
|
|
591
|
+
systemctl is-active --quiet "$service.service" || refuse "$service did not stay up: journalctl -u $service"
|
|
592
|
+
done
|
|
593
|
+
[ "$(local_get /dex/.well-known/openid-configuration)" = "200" ] || refuse "the identity provider does not answer through the front"
|
|
594
|
+
# Not signed in: the server must be out of reach.
|
|
595
|
+
case "$(local_get /api/conversations)" in 302|401|403) : ;; *) refuse "the server answers somebody who has not signed in" ;; esac
|
|
596
|
+
|
|
597
|
+
# ------------------------------ THE TUNNEL --------------------------------
|
|
598
|
+
TUNNEL_KEY=""; RELAY_PRINT=""
|
|
599
|
+
if [ -n "$RELAY" ]; then
|
|
600
|
+
step "tunnel to the relay"
|
|
601
|
+
apt-get install -y -qq openssh-client >/dev/null
|
|
602
|
+
account clavarium-tunnel /nonexistent
|
|
603
|
+
install -d -o clavarium-tunnel -g clavarium-tunnel -m 0700 /var/lib/clavarium-tunnel
|
|
604
|
+
# The private key is made here and never leaves this machine.
|
|
605
|
+
[ -e /var/lib/clavarium-tunnel/key ] || sudo -u clavarium-tunnel ssh-keygen -q -t ed25519 -N '' -C clavarium-tunnel -f /var/lib/clavarium-tunnel/key
|
|
606
|
+
# The relay's identity is taken once and then held: a different machine
|
|
607
|
+
# answering later at the same address is refused.
|
|
608
|
+
if [ ! -s /var/lib/clavarium-tunnel/known_hosts ]; then
|
|
609
|
+
scanned="$(ssh-keyscan -T 15 -t ed25519 -p "$RELAY_PORT" "$RELAY" 2>/dev/null || true)"
|
|
610
|
+
[ -n "$scanned" ] || refuse "the relay $RELAY does not answer on port $RELAY_PORT"
|
|
611
|
+
printf '%s\n' "$scanned" > /var/lib/clavarium-tunnel/known_hosts
|
|
612
|
+
chown clavarium-tunnel:clavarium-tunnel /var/lib/clavarium-tunnel/known_hosts
|
|
613
|
+
fi
|
|
614
|
+
cat > /etc/systemd/system/clavarium-tunnel.service <<TUNNEL
|
|
615
|
+
[Unit]
|
|
616
|
+
Description=Clavarium - the outgoing tunnel to the relay
|
|
617
|
+
After=network-online.target caddy.service
|
|
618
|
+
Wants=network-online.target
|
|
619
|
+
StartLimitIntervalSec=0
|
|
620
|
+
|
|
621
|
+
[Service]
|
|
622
|
+
User=clavarium-tunnel
|
|
623
|
+
Group=clavarium-tunnel
|
|
624
|
+
# Only TLS (443) and the certificate challenge (80) are carried.
|
|
625
|
+
ExecStart=/usr/bin/ssh -N -T -o BatchMode=yes -o IdentitiesOnly=yes -o IdentityFile=/var/lib/clavarium-tunnel/key -o UserKnownHostsFile=/var/lib/clavarium-tunnel/known_hosts -o StrictHostKeyChecking=yes -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ConnectTimeout=15 -R *:8443:127.0.0.1:443 -R *:8080:127.0.0.1:80 -p $RELAY_PORT clavarium-tunnel@$RELAY
|
|
626
|
+
Restart=always
|
|
627
|
+
RestartSec=5
|
|
628
|
+
NoNewPrivileges=yes
|
|
629
|
+
ProtectSystem=strict
|
|
630
|
+
ProtectHome=yes
|
|
631
|
+
PrivateTmp=yes
|
|
632
|
+
|
|
633
|
+
[Install]
|
|
634
|
+
WantedBy=multi-user.target
|
|
635
|
+
TUNNEL
|
|
636
|
+
systemctl daemon-reload
|
|
637
|
+
systemctl enable --quiet clavarium-tunnel.service
|
|
638
|
+
systemctl restart clavarium-tunnel.service
|
|
639
|
+
TUNNEL_KEY="$(cat /var/lib/clavarium-tunnel/key.pub)"
|
|
640
|
+
RELAY_PRINT="$(ssh-keygen -l -f /var/lib/clavarium-tunnel/known_hosts | cut -d' ' -f2 | head -1)"
|
|
641
|
+
fi
|
|
642
|
+
|
|
643
|
+
echo
|
|
644
|
+
echo "Clavarium is installed: https://$DOMAIN"
|
|
645
|
+
if [ -n "$PASSWORDS" ]; then
|
|
646
|
+
echo "Sign in with (shown ONCE, kept nowhere in clear):"
|
|
647
|
+
printf '%s' "$PASSWORDS"
|
|
648
|
+
else
|
|
649
|
+
echo "Operators and passwords: unchanged."
|
|
650
|
+
fi
|
|
651
|
+
if [ -n "$RELAY" ]; then
|
|
652
|
+
echo
|
|
653
|
+
echo "ON THE RELAY ($RELAY), run this one command; the tunnel then comes up by itself:"
|
|
654
|
+
echo " sudo deploy/relay.sh --key \"$TUNNEL_KEY\""
|
|
655
|
+
echo "It will print this, and it must be the same: $RELAY_PRINT"
|
|
656
|
+
echo "The name $DOMAIN must lead to the relay."
|
|
657
|
+
else
|
|
658
|
+
echo "The name $DOMAIN must lead to this machine (ports 443 and 80)."
|
|
659
|
+
fi
|