wren-linux / tools / vps-client.sh
#!/bin/bash
# vps-client.sh: your computer's side of tools/setup-vps.sh. As you. Only
# --setup, --onion-only, --bridges and --domain run sudo here (for this
# computer's Tor, after asking); the rest prints the root commands it needs.
# --setup IP [--login NAME] [--user NAME] the whole first setup of a new
# server, every connection through Tor: installs Tor,
# netcat-openbsd and torsocks here (after asking), makes
# the keys that are missing, checks the server's key
# through three Tor paths, runs setup-vps.sh on the
# server as NAME (--login: the provider's user, root),
# confirms from a second login, gives this computer's
# Tor the onion client key, writes the ssh entry and
# the publish settings, then onion-only. --user: your
# user on the server (wren).
# --backup FOLDER [HOST] what this computer needs to reach the server again, in one
# file encrypted with a passphrase you choose, in FOLDER (on
# a USB stick): the SSH key, the onion client key, your user
# and the SSH onion address, the server's key, the publish
# settings, and your bridge lines (asked; sudo reads them)
# --reconnect --from FILE|FOLDER --reconnect, from that backup (in a FOLDER, its
# newest): puts the files back first
# --reconnect USER ONION [HOST] this computer again, for a server set up before
# (a reinstall, a new laptop): from the keys you put back
# (~/.ssh/wren-vps, ~/.config/wren/vps/wren-vps.key), Tor here
# (after asking), Tor given the onion client key, the ssh
# entry, and a login through the onion address. Nothing on the
# server changes. USER: yours there (wren); ONION: its SSH
# onion address (HostName in your backed-up ~/.ssh/config).
# --onion-only [HOST] logs in through the onion address, closes port 22 to
# the internet, and confirms from a second onion login
# --domain NAME [HOST] the site's domain, once it is ready (registered, its A
# record at the server): looked up through Tor, given to
# the server, confirmed; Caddy then gets its HTTPS
# certificate. setup leaves it out while it isn't ready.
# --origin [HOST] this git copy's server (origin) becomes the server's mirror,
# over ssh through Tor: git-push.sh and wren-release push
# there, and update.sh fetches from there
# --bridges the bridges this computer's Tor uses (to the server too):
# whether you need one, which kind, your bridge lines. A Tor
# installed by setup asks the same first.
# --keygen [NAME] the onion client key: only a computer that has its
# private half can even find the server's SSH onion
# address. Keeps that half in ~/.config/wren/vps/NAME.key
# (yours alone) and prints the public half, for
# setup-vps.sh's VPS_ONION_SSH_CLIENT.
# --ssh-key [--security-key] an SSH key for the server, ~/.ssh/wren-vps (it
# asks a passphrase for it), and prints its public
# line. --security-key: one that needs your hardware
# key and a touch for every login (extras/security-key.sh)
# --tor-auth ONION [NAME] Tor's client file for that onion address, and the
# root commands that give it to this computer's Tor
# --ssh-config USER ONION [HOST] the ~/.ssh/config entry that reaches the server
# through Tor (ssh HOST then works; HOST: wren-vps)
# --keygen-show [NAME] the public half of a key keygen made, again
# NAME is the server's name for the key files (default: wren-vps).
# Usage: bash tools/vps-client.sh --setup IP [--login NAME] [--user NAME] | --backup FOLDER
# | --reconnect --from FOLDER | --reconnect USER ONION | --onion-only | --bridges
# | --domain example.org | --origin
# | --keygen | --keygen-show | --ssh-key [--security-key] | --tor-auth ONION | --ssh-config USER ONION
_path=$PATH
# The shared helpers (say, ask, the bridge questions of lib/tor.sh), with your own PATH
# kept: ssh, nc and torsocks are the ones you run (and a test's pretend ones)
. "$(dirname -- "${BASH_SOURCE[0]}")/../lib.sh"
PATH=$_path
not_root
umask 077
DIR=${XDG_CONFIG_HOME:-$HOME/.config}/wren/vps
name_ok() { [[ $1 =~ ^[a-z][a-z0-9-]{0,30}$ ]] || die "a name is small letters, digits and dashes: \"$1\" isn't one."; }
onion_ok() { [[ $1 =~ ^[a-z2-7]{56}(\.onion)?$ ]] || die "\"$1\" isn't an onion address (56 letters and digits 2-7, then .onion)."; }
b32() { python3 -I -c 'import base64,sys; print(base64.b32encode(sys.stdin.buffer.read()[-32:]).decode().rstrip("="))'; }
keygen() {
local name=${1:-wren-vps} der pub
name_ok "$name"
command -v openssl >/dev/null || die "openssl is missing (sudo apt install openssl)."
mkdir -p "$DIR"
chmod 700 "$DIR"
[ ! -e "$DIR/$name.key" ] || die "$DIR/$name.key exists already; its public half: bash $0 --keygen-show $name"
der=$(mktemp)
openssl genpkey -algorithm X25519 -outform DER -out "$der" 2>/dev/null || { rm -f "$der"; die "openssl couldn't make the key."; }
b32 <"$der" >"$DIR/$name.key"
pub=$(openssl pkey -inform DER -in "$der" -pubout -outform DER | b32)
rm -f "$der"
printf '%s\n' "$pub" >"$DIR/$name.pub"
note "The private half is in $DIR/$name.key (yours alone): back it up with your other keys."
note "For setup-vps.sh, VPS_ONION_SSH_CLIENT:"
echo "$pub"
}
keygen_show() { local name=${1:-wren-vps}; name_ok "$name"; cat "$DIR/$name.pub" 2>/dev/null || die "no key called $name: bash $0 --keygen $name"; }
ssh_key() {
local f=$HOME/.ssh/wren-vps type=ed25519
case ${1:-} in
"") ;;
--security-key) type=ed25519-sk ;;
*) die "--ssh-key takes only --security-key." ;;
esac
if [ -f "$f" ] && [ "$type" = ed25519-sk ] && ! grep -q '^sk-ssh-ed25519@openssh.com ' "$f.pub"; then
die "$f is a plain key already; move it away first (and remove it from the server once the new one works)."
fi
if [ ! -f "$f" ]; then
command -v ssh-keygen >/dev/null || die "ssh-keygen is missing (sudo apt install openssh-client)."
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
if [ "$type" = ed25519-sk ]; then
note "Plug the key in and touch it when it blinks. The file is only a handle: every login"
note "needs the key too. A passphrase on the file is optional then (it asks twice):"
else
note "A passphrase protects the key if this computer is ever lost (it asks twice):"
fi
ssh-keygen -q -t "$type" -a 100 -C "wren-vps" -f "$f"
fi
note "For setup-vps.sh, VPS_SSH_KEY:"
cat "$f.pub"
}
tor_auth() {
local onion=${1:-} name=${2:-wren-vps}
onion_ok "$onion"
name_ok "$name"
onion=${onion%.onion}
[ -s "$DIR/$name.key" ] || die "no client key called $name yet: bash $0 --keygen $name"
printf '%s:descriptor:x25519:%s\n' "$onion" "$(cat "$DIR/$name.key")" >"$DIR/$name.auth_private"
note "Tor's client file: $DIR/$name.auth_private"
note "This computer's Tor (the system one, which ssh goes through) needs it. As your user, run:"
cat <<EOF
sudo apt install tor netcat-openbsd
sudo install -d -m 0700 -o debian-tor -g debian-tor /var/lib/tor/onion_auth
sudo install -m 0600 -o debian-tor -g debian-tor $DIR/$name.auth_private /var/lib/tor/onion_auth/
grep -qx 'ClientOnionAuthDir /var/lib/tor/onion_auth' /etc/tor/torrc || echo 'ClientOnionAuthDir /var/lib/tor/onion_auth' | sudo tee -a /etc/tor/torrc
sudo systemctl restart tor
EOF
}
ssh_config() {
local user=${1:-} onion=${2:-} host=${3:-wren-vps}
[[ $user =~ ^[a-z][a-z0-9-]{0,30}$ ]] || die "\"$user\" isn't a user name (the one setup-vps.sh made, e.g. wren)."
onion_ok "$onion"
name_ok "$host"
note "Add this to ~/.ssh/config, then: ssh $host"
ssh_entry "$user" "$onion" "$host"
}
ssh_entry() { # USER ONION HOST: the entry itself
cat <<EOF
Host $3
HostName ${2%.onion}.onion
User $1
IdentityFile ~/.ssh/wren-vps
IdentitiesOnly yes
# the server's key under this name: the one checked at its first login is known here too
HostKeyAlias $3
# through this computer's Tor (tor-auth sets it up)
ProxyCommand $PROXY
ServerAliveInterval 30
EOF
}
# ---- setup and onion-only: the whole way, through Tor ----
# Every connection to the server goes through this computer's Tor: the server,
# its provider and the network on the way never see this computer's address.
PROXY='nc -X 5 -x 127.0.0.1:9050 %h %p'
SSH_KEY=$HOME/.ssh/wren-vps
# This computer's Tor: the system one, shared with apt over Tor and remote access. The
# mark says this needs it (lib/tor.sh: OnionShare doesn't switch it off then); its
# bridges, if it has them from here, are between marks of their own in torrc.
MARK=${WREN_ETC:-/etc}/tor/.wren-vps-client
TORRC=${WREN_ETC:-/etc}/tor/torrc
BR_BEGIN='# >>> Wren Linux vps-client bridges >>>'
BR_END='# <<< Wren Linux vps-client bridges <<<'
# asks WHAT: in bold, what the next prompt wants (one secret typed into the other's prompt
# is the easy mistake, and over Tor a login doesn't wait long)
asks() { ding; printf '\033[1m It asks: %s\033[0m\n' "$*"; }
# A sound each time it waits for you, if asked for at the start of a run: the timer's gong
# (lib/alarm-sounds.py; the others are sharper, and none is offered without it), only with
# your own sound server (the sound extra), checked as the timer checks it; its first 3
# seconds, behind, so nothing waits for it
SOUND_DIR=${WREN_SOUND_DIR:-$HOME/.local/share/wren}
DING=
DING_ASKED=false
ding() { [ -z "$DING" ] || { timeout 3 pw-play "$DING" >/dev/null 2>&1 & }; }
offer_ding() {
local gong=$SOUND_DIR/alarms/gong.wav
$DING_ASKED && return 0
DING_ASKED=true
command -v pw-play >/dev/null || return 0
systemctl --user -q is-active pipewire.socket 2>/dev/null || systemctl --user -q is-active pipewire.service 2>/dev/null || return 0
[ -r "$gong" ] || return 0
ask "This takes a while. A gong each time it waits for you to type something (this run only)?" y && DING=$gong
return 0
}
KEYPASS="your SSH key's passphrase (for ~/.ssh/wren-vps)"
sudo_pass() { echo "$1's password on the server (for sudo)"; }
# login_asks [WHAT ELSE]: what a login asks: the key's passphrase (unless the agent of this
# run has the key), then what else
login_asks() {
if $KEY_IN_AGENT; then [ -z "${1:-}" ] || asks "$1"; else asks "$KEYPASS${1:+, then $1}"; fi
}
# run_agent: the key's passphrase once, at the start, rather than at each login (a login
# over Tor waits only about a minute for it). An ssh-agent of this run's own (one of yours
# is left alone), stopped when this ends, and holding the key 45 minutes at most should it
# not be. While it runs, a program of yours could log in with it to the server; typing the
# passphrase at each login holds nothing.
KEY_IN_AGENT=false
AGENT_ASKED=false
run_agent() {
$AGENT_ASKED && return 0
AGENT_ASKED=true
note "Every login asks your SSH key's passphrase, and over Tor a login waits only about a minute."
note "This can ask it once now instead: an ssh-agent of this run only holds the key, and is"
note "stopped when the setup ends (45 minutes at most). Meanwhile a program of yours could use"
note "it to log in to the server; typed at each login, it is held nowhere."
ask "Type your SSH key's passphrase once now, for this run (ssh-agent)?" y || return 0
if ! eval "$(ssh-agent -s -t 45m)" >/dev/null || [ -z "${SSH_AGENT_PID:-}" ]; then
warn "ssh-agent didn't start: each login asks the passphrase instead."
return 0
fi
# shellcheck disable=SC2064 # this agent's number, now
trap "kill $SSH_AGENT_PID 2>/dev/null" EXIT
asks "$KEYPASS"
if ssh-add "$SSH_KEY" 2>/dev/null; then
KEY_IN_AGENT=true
note "The key is in this run's agent: logins won't ask it again."
else
warn "The key wasn't taken (a wrong passphrase?): each login asks it instead."
fi
}
# wait_undo WHY CMD...: a change on the server waited to be confirmed, and wasn't. The server
# undoes it by itself within 10 minutes; a run before that finds it half done. Says so, counts
# the minutes down (sleep: a test's pretend one doesn't wait), then offers to run CMD again.
wait_undo() {
local why=$1 m
shift
stop_agent
warn "$why"
note "Nothing is lost. The server puts its old SSH and firewall settings back by itself within"
note "10 minutes: that is what keeps a mistake from locking you out. A run before then would find"
note "the server half changed, so this waits for it. (Ctrl+C stops waiting: then, after the 10"
note "minutes, run it again yourself: bash $0 $*)"
for m in 10 9 8 7 6 5 4 3 2 1; do
printf ' You can run it again in %d minute%s.\n' "$m" "$( [ "$m" = 1 ] || echo s)"
sleep 60
done
note "The old settings are back now."
ding
ask "Run it again now (bash $0 $*)?" y || exit 1
exec bash "$0" "$@"
}
stop_agent() { # the agent of this run goes, and the key with it
[ -n "${SSH_AGENT_PID:-}" ] && $KEY_IN_AGENT || return 0
kill "$SSH_AGENT_PID" 2>/dev/null || true
KEY_IN_AGENT=false
note "This run's ssh-agent is stopped: your key is in no agent of it any more."
}
mark() { echo 'Written by Wren Linux tools/vps-client.sh: ssh to your server goes through this tor.' | sudo tee "$MARK" >/dev/null; }
socks_up() { # SECONDS: waits for Tor's port to answer
local i
for i in $(seq 1 "$1"); do
if nc -z -w 2 127.0.0.1 9050 2>/dev/null; then return 0; fi
/bin/sleep 1
done
return 1
}
# tor_ready: Tor, netcat-openbsd and torsocks here, Tor running. A Tor new here asks
# about bridges first, as every Wren script does (TOR_NEW: it was installed just now).
TOR_NEW=false
tor_ready() {
local missing=() p help
for p in tor netcat-openbsd torsocks; do is_installed "$p" || missing+=("$p"); done
if [ ${#missing[@]} -gt 0 ]; then
note "Every connection to the server goes through Tor, and that needs ${missing[*]} here."
ask "Install ${missing[*]} now (sudo apt install ${missing[*]})?" y ||
die "Nothing was changed. Install them (sudo apt install ${missing[*]}), then: $AGAIN"
# (a tor another Wren script set up already has its bridges)
if ! is_installed tor && ! system_tor_wanted; then
say "Bridges"
if [ -n "$RESTORED_BRIDGE_TYPE" ]; then
BRIDGE_TYPE=$RESTORED_BRIDGE_TYPE BRIDGE_WANTED=true
BRIDGES=("${RESTORED_BRIDGES[@]}")
note "From your backup: $(bridge_summary)."
else
pick_bridges system-tor
no_bridge_ok
fi
TOR_NEW=true
# Debian starts tor the moment it is installed: held until its bridges are in
sudo systemctl mask tor.service tor@default.service >/dev/null
[ "$BRIDGE_TYPE" != webtunnel ] || { add_torproject_repo; sudo apt-get update -q; }
p=$(bridge_packages)
[ -z "$p" ] || missing+=("$p")
fi
sudo apt-get install -y "${missing[@]}" || die "They weren't installed (sudo apt install ${missing[*]} says why)."
if $TOR_NEW; then
write_bridges
mark
say "Starting Tor$( [ "$BRIDGE_TYPE" = none ] || echo ", through $(bridge_summary)")"
tor_service_on
tor_up || warn "Tor hasn't connected yet ($(bridge_summary)); the steps below wait for it a while."
fi
fi
# (read first: nc -h ends in an error, which pipefail would take for grep's answer)
help=$(nc -h 2>&1) || :
grep -q -- '-X proxy_protocol' <<<"$help" ||
die "nc is another netcat here, one that can't go through Tor. Choose OpenBSD's: sudo update-alternatives --set nc /bin/nc.openbsd"
if ! socks_up 3; then
ask "Tor isn't running here. Start it now (sudo systemctl enable --now tor)?" y ||
die "Nothing was done. Start it (sudo systemctl enable --now tor), then: $AGAIN"
sudo systemctl enable --now tor || die "Tor didn't start: sudo journalctl -u tor@default -n 30"
socks_up 30 || die "Tor started, but doesn't answer on 127.0.0.1:9050: sudo journalctl -u tor@default -n 30"
fi
}
# (--reconnect --from: the bridges in the backup, used if Tor is set up here now)
RESTORED_BRIDGE_TYPE='' RESTORED_BRIDGES=()
# no_bridge_ok: someone who said they need a bridge, but chose none, goes on only on yes
no_bridge_ok() {
$BRIDGE_WANTED && [ "$BRIDGE_TYPE" = none ] || return 0
warn "You said you need a bridge, but none was set up: Tor would connect to the Tor"
warn "network directly, and your network could see that you use Tor."
ask "Go on without a bridge?" n || die "Stopped before changing anything. With a bridge, run: $AGAIN"
}
# write_bridges: the chosen bridges into torrc, between this script's marks (none: the
# marks and what was between them go)
write_bridges() {
sudo sed -i "\\|^$BR_BEGIN\$|,\\|^$BR_END\$|d" "$TORRC"
[ "$BRIDGE_TYPE" != none ] || return 0
{ echo "$BR_BEGIN"; tor_bridge_config; echo "$BR_END"; } | sudo tee -a "$TORRC" >/dev/null
torrc_private "$TORRC"
}
# tor_up: waits for Tor to say it is connected (through a bridge it can take a few minutes)
tor_up() {
local i
for i in $(seq 1 90); do
if tor_connected "$TOR_SINCE"; then return 0; fi
/bin/sleep 2
done
return 1
}
# bridges_cmd: the bridges this computer's Tor uses, to the server too: asked again
bridges_cmd() {
local now
tor_ready
# (installed just now: its bridges were just asked)
if $TOR_NEW; then return 0; fi
# another Wren script's bridges: that script's to change, as the Tor is shared
if sudo sed "\\|^$BR_BEGIN\$|,\\|^$BR_END\$|d" "$TORRC" | grep -qE '^[[:space:]]*UseBridges[[:space:]]+1'; then
note "Your Tor uses bridges already, set up by another Wren Linux script (apt over Tor, or"
note "remote access). They are what ssh to your server goes through too: change them there,"
note "with that script (wrenctl install apt-tor, or wrenctl install remote-access). Nothing was changed."
return 0
fi
now=$(sudo sed -n "\\|^$BR_BEGIN\$|,\\|^$BR_END\$|p" "$TORRC" | grep -c '^Bridge ') || now=0
say "Bridges"
if [ "$now" -gt 0 ]; then note "Now: $now bridge line(s), set up here before."; else note "Now: no bridge (Tor connects directly)."; fi
pick_bridges system-tor
no_bridge_ok
[ "$BRIDGE_TYPE" != webtunnel ] || { add_torproject_repo; sudo apt-get update -q; }
now=$(bridge_packages)
[ -z "$now" ] || is_installed "$now" || sudo apt-get install -y "$now" || die "$now wasn't installed; nothing else was changed."
write_bridges
mark
say "Restarting Tor with $(bridge_summary)"
tor_service_on
if tor_up; then note "Tor is connected, with $(bridge_summary). ssh to your server goes through it."
else warn "Tor hasn't connected yet: sudo journalctl -u tor@default -n 30 says why (bash $0 --bridges changes them again)."; fi
}
# domain_cmd NAME [HOST]: the site's domain, given to the server once it is ready
domain_cmd() {
local d=${1:-} host=${2:-wren-vps} addr ips i code url
d=${d,,}
d=${d#www.}
[[ $d =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ ]] || die "Which domain? bash $0 --domain example.org"
name_ok "$host"
offer_ding
tor_ready
say "Is $d ready? (looked up through Tor: your network doesn't see the lookup)"
addr=$(tor-resolve "$d" 2>/dev/null) || addr=
[ -n "$addr" ] || die "$d has no address yet: still pending at the registrar, or no A record pointing at the
server yet (a new one can take an hour). Nothing was changed: then run $AGAIN"
note "$d points at $addr."
run_agent
login_asks
ips=$(ssh -n "$host" 'hostname -I') || die "Couldn't log in through the onion address: ssh -v $host true says why."
if ! grep -qwF -- "$addr" <<<"$ips"; then
warn "But the server's own addresses are: $ips"
warn "Is the A record at the registrar the server's address? Caddy can't get a certificate otherwise."
ask "Give the server this domain anyway?" n || die "Nothing was changed. Fix the A record, then run: $AGAIN"
fi
say "Giving the server its domain, $d"
note "Ports 80 and 443 open for the web; SSH stays onion only."
login_asks "$(sudo_pass "$(ssh -G "$host" 2>/dev/null | awk '$1 == "user" { print $2; exit }')"), then an email for the certificate's notices (Enter: none)"
# Send the current setup-vps.sh and run that, not the server's installed copy, so a fix to
# it (like reloading Tor instead of restarting it) reaches an already-set-up server; it
# reinstalls itself as wren-vps as it runs.
scp -q "$(dirname -- "$0")/setup-vps.sh" "$host:setup-vps.sh" || die "Couldn't copy setup-vps.sh to the server."
ssh -t "$host" "sudo env VPS_DOMAIN=$d bash setup-vps.sh" || die "The server's setup stopped (what it said is above)."
say "Confirming from a second, new onion login"
login_asks "$(sudo_pass "$(ssh -G "$host" 2>/dev/null | awk '$1 == "user" { print $2; exit }')")"
ssh -t "$host" sudo wren-vps --confirm ||
wait_undo "Not confirmed: the login closed (a prompt waits about a minute), or sudo refused the password." --domain "$d" "$host"
url=https://$d/
if command -v curl >/dev/null; then
say "Waiting for its HTTPS certificate (through Tor)"
for i in 1 2 3 4 5 6; do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 60 --socks5-hostname 127.0.0.1:9050 "$url") || code=
if [ "$code" = 200 ]; then note "$url answers, with its certificate."; break; fi
if [ "$i" = 6 ]; then warn "$url doesn't answer yet; Caddy keeps trying by itself. Look again in a while."; else /bin/sleep 20; fi
done
fi
stop_agent
# Wren Linux's own address, for new installs (git-helper.sh, the site's pages)
if [ -n "${WREN_GIT_URL:-}" ] && [[ $WREN_GIT_URL != "https://$d/"* ]]; then
note "If this is Wren Linux's own server: pins.sh names ${WREN_GIT_URL%/git/*} for new installs."
note "Change WREN_GIT_URL there to https://$d/git/wren-linux.git, in a commit of its own."
fi
cat <<EOF
Done. The site: $url (http and www.$d are sent there)
Add, at the registrar (docs/vps.md, section 4): CAA 0 issue "letsencrypt.org", and DNSSEC on.
EOF
}
# origin_cmd [HOST]: this git copy's server (origin) is the server's mirror, over ssh through
# Tor: tools/git-push.sh and wren-release push there, update.sh fetches from there (WREN_GIT_COPY:
# a test's copy)
origin_cmd() {
local host=${1:-wren-vps} copy=${WREN_GIT_COPY:-$WREN_DIR} url cur heads
name_ok "$host"
url=$host:/srv/wren-git/wren-linux.git
git -C "$copy" rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "$copy isn't a git copy."
cur=$(git -C "$copy" remote get-url origin 2>/dev/null) || cur=
if [ "$cur" = "$url" ]; then
note "This copy's server is already $url."
elif [ -n "$cur" ]; then
note "This copy's server (origin) is now: $(printable <<<"$cur")"
ask "Make it your own server's mirror instead ($url)?" y || die "Nothing was changed."
git -C "$copy" remote set-url origin "$url"
git -C "$copy" remote set-url --delete --push origin '.*' 2>/dev/null || true
else
git -C "$copy" remote add origin "$url"
fi
say "Looking at the server's mirror (through the onion address)"
run_agent
login_asks
heads=$(git -C "$copy" ls-remote origin refs/heads/main) || die "The mirror couldn't be reached: ssh -v $host true says why."
if [ -z "$heads" ]; then note "It has nothing yet: the first push sends all of this copy."; else note "Its main: ${heads:0:12}"; fi
stop_agent
cat <<EOF
Done. This copy's server is your own, through Tor: $url
Next: bash $WREN_DIR/tools/git-push.sh (or wren-release), which signs and pushes;
then: bash $WREN_DIR/tools/publish-site.sh, the site from that same commit.
EOF
}
# reach HOST PORT: waits until Tor reaches it (Tor may still be connecting)
reach() {
local i
for i in 1 2 3 4 5 6; do
if nc -z -w 30 -X 5 -x 127.0.0.1:9050 "$1" "$2" 2>/dev/null; then return 0; fi
[ "$i" = 6 ] || { note "(Tor doesn't reach $1 yet; trying again, $i of 5)"; /bin/sleep 10; }
done
return 1
}
# host_key IP ALIAS: the server's key, fetched through three separate Tor connections
# (torsocks -i: a circuit of their own, so most likely three different exits). Someone
# on one path handing out a false key would have to be on all three. When they agree,
# it goes into ~/.ssh/known_hosts under ALIAS, so no login asks about it again.
host_key() {
local ip=$1 alias=$2 i k keys=() fp kept kh=$HOME/.ssh/known_hosts
for i in 1 2 3; do
k=$(torsocks -i ssh-keyscan -T 60 -t ed25519 "$ip" 2>/dev/null | awk '$2 == "ssh-ed25519" { print $2, $3; exit }') || k=
[ -n "$k" ] || die "The server's key couldn't be fetched through Tor (try $i of 3). Is the server up? Nothing was sent to it."
keys+=("$k")
done
[ "${keys[0]}" = "${keys[1]}" ] && [ "${keys[0]}" = "${keys[2]}" ] ||
die "Three Tor paths gave the server's key differently: someone on one of them may be handing out a false
key. Nothing was sent to the server. Try again later, or ask the provider's support for its ED25519 fingerprint."
fp=$(ssh-keygen -lf - <<<"$ip ${keys[0]}" | awk '{ print $2 }')
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
# (what is kept for ALIAS, by ssh-keygen: Debian's ssh hashes the names it keeps, so the
# file itself doesn't show them)
kept=$(ssh-keygen -F "$alias" -f "$kh" 2>/dev/null | awk '!/^#/ { print $2, $3 }') || kept=
if [ -n "$kept" ]; then
grep -qxF "${keys[0]}" <<<"$kept" ||
die "Your ~/.ssh/known_hosts already has another key for $alias (an earlier server?). If that server is gone:
ssh-keygen -R $alias then run: $AGAIN"
else
echo "$alias ${keys[0]}" >>"$kh"
fi
note "The server's key, the same through three Tor paths: $fp"
note "It is kept for $alias: logins won't ask about it, and refuse a server that shows another."
note "(To be surer still, the provider's support can tell you its ED25519 fingerprint: compare.)"
}
# give_tor_key ONION NAME: this computer's Tor may then find the SSH onion address
give_tor_key() {
local f
tor_auth "$1" "$2" >/dev/null
f=$DIR/$2.auth_private
note "This computer's Tor needs your onion client key to find the server's SSH address."
ask "Give it to Tor now (sudo: into /var/lib/tor/onion_auth, a line in /etc/tor/torrc, Tor restarted)?" y ||
die "Stopped. To do it yourself: bash $0 --tor-auth $1 $2 (and run what it prints), then: bash $0 --onion-only"
sudo install -d -m 0700 -o debian-tor -g debian-tor /var/lib/tor/onion_auth
sudo install -m 0600 -o debian-tor -g debian-tor "$f" /var/lib/tor/onion_auth/
sudo grep -qx 'ClientOnionAuthDir /var/lib/tor/onion_auth' "$TORRC" ||
echo 'ClientOnionAuthDir /var/lib/tor/onion_auth' | sudo tee -a "$TORRC" >/dev/null
mark
sudo systemctl restart tor
socks_up 30 || die "Tor didn't come back after its restart: sudo journalctl -u tor@default -n 30"
}
# write_entry USER ONION HOST: the ssh entry into ~/.ssh/config, between marks of
# its own (an entry of that name without them is yours: then nothing is changed)
write_entry() {
local cfg=$HOME/.ssh/config begin="# >>> $3 (tools/vps-client.sh) >>>" end="# <<< $3 (tools/vps-client.sh) <<<"
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
[ -f "$cfg" ] || : >"$cfg"
if ! grep -qxF "$begin" "$cfg" && grep -qiE "^[[:space:]]*Host[[:space:]]+(.*[[:space:]])?$3([[:space:]]|$)" "$cfg"; then
die "Your ~/.ssh/config has a Host $3 of your own already, so it wasn't changed. Put this instead of it, then: bash $0 --onion-only
$(ssh_entry "$1" "$2" "$3")"
fi
cp -p "$cfg" "$cfg.wren-bak"
{ sed "\\|^$begin\$|,\\|^$end\$|d" "$cfg.wren-bak"; echo "$begin"; ssh_entry "$1" "$2" "$3"; echo "$end"; } >"$cfg.wren-new"
chmod 600 "$cfg.wren-new"
mv "$cfg.wren-new" "$cfg"
note "The entry is in ~/.ssh/config (what was there before: ~/.ssh/config.wren-bak)."
}
# onion_login HOST: one login through the onion address, tried a few times (a new
# onion address takes a few minutes to be found)
onion_login() { # HOST [SSH OPTION...]
local i
for i in 1 2 3 4 5; do
if ssh -n "${@:2}" "$1" true; then return 0; fi
[ "$i" = 5 ] || { note "(not found yet; trying again in 30 seconds, $i of 4)"; /bin/sleep 30; }
done
return 1
}
onion_only() {
local host=${1:-wren-vps}
name_ok "$host"
tor_ready
local u
offer_ding
u=$(ssh -G "$host" 2>/dev/null | awk '$1 == "user" { print $2; exit }') || u=
say "Logging in through the onion address (10 to 60 seconds; a new one can take minutes)"
run_agent
$KEY_IN_AGENT || asks "$KEYPASS, at each try"
onion_login "$host" || die "The onion address wasn't reached. Look at where it stops: ssh -v $host true
(Right after setup, try again in a few minutes: bash $0 --onion-only)"
say "Closing port 22 to the internet: SSH only through the onion address"
login_asks "$(sudo_pass "${u:-your user}")"
ssh -t "$host" sudo wren-vps --ssh-onion-only || die "Port 22 wasn't closed (what the server said is above)."
say "Confirming from a second, new onion login"
login_asks "$(sudo_pass "${u:-your user}")"
ssh -t "$host" sudo wren-vps --confirm ||
wait_undo "Not confirmed: the login closed (a prompt waits about a minute), or sudo refused the password." --onion-only "$host"
}
setup() {
local args=(--setup "$@")
local ip='' login=root user=wren name=wren-vps host=wren-vps dest client keyline out onion_ssh onion_site run p
while [ $# -gt 0 ]; do
case $1 in
--login) login=${2:-}; shift 2 || die "--login needs a name." ;;
--user) user=${2:-}; shift 2 || die "--user needs a name." ;;
-*) die "--setup takes IP [--login NAME] [--user NAME]." ;;
*) [ -z "$ip" ] || die "one server at a time."; ip=$1; shift ;;
esac
done
[[ $ip =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ || $ip =~ ^[0-9A-Fa-f:]+:[0-9A-Fa-f:]*$ ]] ||
die "Which server? bash $0 --setup IP (its address, from your provider)"
[[ $login =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "\"$login\" isn't a user name (the provider's: root, debian, ...)."
[[ $user =~ ^[a-z][a-z0-9-]{0,30}$ ]] && [ "$user" != root ] || die "\"$user\" can't be your user's name (small letters, digits, dashes)."
dest=$ip
[[ $ip != *:* ]] || dest="[$ip]"
for p in ssh scp python3 openssl; do command -v "$p" >/dev/null || die "$p is missing (sudo apt install openssh-client python3 openssl)."; done
cat <<EOF
This sets up your new server at $ip, the whole way, and every connection
it makes to the server goes through Tor: the server, its provider and your
network never see this computer's address. In order:
1. Tor, netcat-openbsd and torsocks on this computer: installed if missing
(it asks first; sudo asks your password; a Tor new here asks whether you
need a bridge, as your other Wren scripts do), and Tor started.
2. Your keys: ~/.ssh/wren-vps, the one you gave the provider (it must be
there already), and an onion client key, made if you have none.
3. The server's key, fetched through three separate Tor paths: they must
agree (someone on one path can't slip you a false one), and it is kept,
so ssh never asks about it. Then the first login, as $login, and
setup-vps.sh runs there. It asks the domain: only one that is ready
(registered, its A record at $ip); otherwise press Enter, and later
run: bash $0 --domain example.org. And a password for
your user there, $user.
4. A second login, as $user, confirms that the new settings let you in
(otherwise they undo themselves within 10 minutes: you can't be locked out).
5. This computer's Tor gets your onion client key (it asks first; sudo), and
~/.ssh/config an entry, $host, that goes to the server's onion address.
6. Through the onion address: port 22 is closed to the internet, and a
second onion login confirms it. From then on, SSH is only through Tor.
7. A summary: how to use ssh, scp and rsync with the server, and what to
back up.
It takes about 10 to 20 minutes.
EOF
printf '\n\033[1;31m DO NOT GO AFK: THE SETUP MAY FAIL.\033[0m\n'
printf '\033[1;33m%s\033[0m\n' \
" You will have one minute to answer each SSH key passphrase prompt." \
" You will have five minutes to answer each sudo password prompt." \
" Expect to be prompted multiple times before completion." \
" If a prompt times out, the script stops. If the server was already" \
" changed, it counts down ten minutes, then offers to try again."
ask "Continue?" y || { note "Nothing was done."; exit 0; }
offer_ding
say "Tor, on this computer"
tor_ready
say "Your keys"
[ -s "$SSH_KEY.pub" ] || die "There is no ~/.ssh/wren-vps: the server takes the key you gave the provider. Make it with
bash $0 --ssh-key, and give the provider its line (most let you add a key in their panel)."
keyline=$(head -n1 "$SSH_KEY.pub")
note "SSH key: ~/.ssh/wren-vps (its public line is the one you gave the provider)"
if [ ! -s "$DIR/$name.key" ]; then keygen "$name" >/dev/null; fi
client=$(keygen_show "$name")
note "Onion client key: $DIR/$name.key"
say "Reaching $ip through Tor"
reach "$ip" 22 || die "Tor doesn't reach $ip on port 22. Is the server up (the provider's panel)? Is the address right?
(If Tor can't connect at all here, a bridge helps: bash $(cd -- "$(dirname -- "$0")/.." && pwd)/extras/apt-tor.sh sets one up.)"
# the options of every login to the address: through Tor, this key only, and the
# server's key kept under the name the onion entry uses too
# (only the key host_key checked: a server showing another one is refused, never asked about)
local tor_ssh=(-o "ProxyCommand=$PROXY" -o IdentitiesOnly=yes -i "$SSH_KEY" -o HostKeyAlias="$host"
-o HostKeyAlgorithms=ssh-ed25519 -o StrictHostKeyChecking=yes -o ServerAliveInterval=30)
say "The server's key, checked through three Tor paths"
host_key "$ip" "$host"
run_agent
say "The first login, as $login"
login_asks
scp -q "${tor_ssh[@]}" "$(dirname -- "$0")/setup-vps.sh" "$login@$dest:setup-vps.sh" ||
die "setup-vps.sh couldn't be copied to $login@$ip (the right login name? --login NAME)."
say "setup-vps.sh, on the server"
note "It asks: the domain (only if it's ready: registered, its A record at $ip; if not, press"
note "Enter, and later: bash $0 --domain example.org), an email for its"
note "certificate (with a domain), and a password for $user (keep it in your password manager:"
note "you type it again in a minute, for sudo). At its end it says to log in again from a second"
note "terminal and to run vps-client.sh by hand: don't; this does both next, by itself."
login_asks "setup-vps.sh's questions"
run="VPS_ADMIN=$user VPS_SSH_KEY=$(printf %q "$keyline") VPS_ONION_SITE=yes VPS_ONION_SSH=yes VPS_ONION_SSH_CLIENT=$client bash setup-vps.sh"
[ "$login" = root ] || run="sudo env $run"
ssh -t "${tor_ssh[@]}" "$login@$ip" "$run" ||
die "setup-vps.sh stopped on the server (what it said is above). Nothing waits to be confirmed if it stopped
before \"SSH and the firewall\" (otherwise wait 10 minutes first); then run: $AGAIN"
say "Confirming from a second, new login, as $user"
login_asks "$(sudo_pass "$user"): the one you just gave it"
# (the onion addresses too: Tor makes them within a minute of its start)
# shellcheck disable=SC2016 # run on the server
out=$(ssh -t "${tor_ssh[@]}" "$user@$ip" 'sudo wren-vps --confirm && for i in $(seq 1 12); do sudo wren-vps --status | grep -q "^Onion SSH: " && break; sleep 10; done; sudo wren-vps --status' | tee /dev/stderr | tr -d '\r') ||
wait_undo "Not confirmed: the login closed (a prompt waits about a minute), or sudo refused the password." "${args[@]}"
onion_ssh=$(sed -n 's/^Onion SSH: //p' <<<"$out" | head -n1)
onion_site=$(sed -n 's/^Onion site: //p' <<<"$out" | head -n1)
[[ $onion_ssh =~ ^[a-z2-7]{56}\.onion$ ]] || die "Confirmed, but the server named no SSH onion address yet. In a few minutes:
ssh ${tor_ssh[*]} $user@$ip sudo wren-vps --status, then: bash $0 --tor-auth ONION, --ssh-config $user ONION, --onion-only"
say "This computer's Tor, and your ssh entry"
give_tor_key "$onion_ssh" "$name"
write_entry "$user" "$onion_ssh" "$host"
if [[ $onion_site =~ ^[a-z2-7]{56}\.onion$ ]]; then
printf 'VPS_HOST=%s\nVPS_SITE_ONION=%s\n' "$host" "$onion_site" >"$DIR/site.conf"
note "Publish settings: $DIR/site.conf (tools/publish-site.sh reads it)"
fi
onion_only "$host"
stop_agent
cat <<EOF
Done. The server takes SSH only through its onion address, and only your key.
Everything below goes through Tor:
ssh $host log in
scp FILE $host:PATH copy a file there scp $host:PATH . and back here
rsync -a FOLDER/ $host:PATH/ a whole folder (only what changed)
bash tools/publish-site.sh the site, from your newest signed commit
Server: $ip SSH onion: $onion_ssh
Site: http://$ip${onion_site:+ onion: http://$onion_site}
Back these up with your other keys (KeePassXC can hold files):
~/.ssh/wren-vps $DIR/$name.key
The domain, once it is ready (registered, its A record at $ip):
bash $0 --domain example.org
To push this copy of Wren Linux to the server, as its home: bash $0 --origin
If Tor is ever down: the provider's web console, then sudo wren-vps --ssh-public
EOF
}
# ---- backup: what this computer needs to reach the server again, in one encrypted file ----
BACKUP_PREFIX=wren-vps-backup-
# What a backup holds, and nothing else: the SSH key, the onion client key, the server (your
# user, its SSH onion address, the ssh name), its key's known_hosts lines, the publish
# settings, the bridges (TYPE=, then a line each)
BACKUP_FILES="wren-vps wren-vps.pub client.key client.pub server known_hosts site.conf bridges"
# secret VAR PROMPT: a line typed without being shown (in a test, the next answer)
secret() {
local _s=
if [ -n "${WREN_TEST_ANSWERS:-}" ]; then
tty_read _s "$2"
elif { : </dev/tty; } 2>/dev/null; then
IFS= read -rs -p "$2" _s </dev/tty || _s=
printf '\n' >&2
fi
printf -v "$1" '%s' "$_s"
}
# work_dir: a folder in memory (/dev/shm), never on a disk, for what is packed or unpacked;
# gone when this ends. Its gpg has a home of its own: nothing of your keyring is used.
work_dir() {
WORK=$(mktemp -d "${WREN_SHM:-/dev/shm}/wren-vps.XXXXXXXX") || die "Couldn't make a folder in /dev/shm (memory)."
# shellcheck disable=SC2064 # this folder, now
trap "gpgconf --homedir '$WORK/g' --kill all 2>/dev/null; rm -rf '$WORK'" EXIT
mkdir -m 700 "$WORK/g" "$WORK/b"
}
work_done() { gpgconf --homedir "$WORK/g" --kill all 2>/dev/null; rm -rf "$WORK"; trap - EXIT; }
# gpg_pass ARGS...: gpg with the passphrase in WORK/pass (AES-256 and gpg's slowest key
# derivation, as the signing key's backup)
gpg_pass() {
gpg --homedir "$WORK/g" --batch --no-tty --quiet --pinentry-mode loopback --passphrase-file "$WORK/pass" "$@"
}
backup_cmd() {
local dest=${1:-} host=${2:-wren-vps} name=wren-vps kh=$HOME/.ssh/known_hosts u o pass pass2 out kind f has=''
[ -n "$dest" ] && [ -d "$dest" ] && [ -w "$dest" ] ||
die "Usage: bash $0 --backup FOLDER (a folder on a USB stick you can write to; usb mount mounts one)"
dest=$(cd "$dest" && pwd)
# (as wren-key: a folder on this computer is asked about, no by default)
case $dest/ in
"$HOME"/* | /tmp/* | /var/tmp/* | /dev/shm/*)
warn "$dest is on this computer, not on a stick: a backup there goes with the computer."
ask "Put it there anyway (and copy it to a stick yourself)?" n ||
die "Nothing was written. Mount a stick (usb mount) and give its folder." ;;
esac
name_ok "$host"
for f in gpg gpgconf ssh ssh-keygen tar; do command -v "$f" >/dev/null || die "$f is missing (sudo apt install gnupg openssh-client tar)."; done
[ -s "$SSH_KEY" ] && [ -s "$SSH_KEY.pub" ] || die "There is no ~/.ssh/wren-vps here: nothing to back up."
[ -s "$DIR/$name.key" ] || die "There is no onion client key here ($DIR/$name.key): nothing to back up."
u=$(ssh -G "$host" 2>/dev/null | awk '$1 == "user" { print $2; exit }')
o=$(ssh -G "$host" 2>/dev/null | awk '$1 == "hostname" { print $2; exit }')
[[ $o =~ ^[a-z2-7]{56}\.onion$ ]] || die "Your ssh entry $host doesn't go to an onion address ($o): set up, or reconnect, first."
[[ $u =~ ^[a-z][a-z0-9-]{0,30}$ ]] || die "Your ssh entry $host names no user of yours on the server."
work_dir
cp "$SSH_KEY" "$WORK/b/wren-vps"
cp "$SSH_KEY.pub" "$WORK/b/wren-vps.pub"
cp "$DIR/$name.key" "$WORK/b/client.key"
[ ! -s "$DIR/$name.pub" ] || cp "$DIR/$name.pub" "$WORK/b/client.pub"
[ ! -s "$DIR/site.conf" ] || cp "$DIR/site.conf" "$WORK/b/site.conf"
printf 'USER=%s\nONION=%s\nHOST=%s\n' "$u" "$o" "$host" >"$WORK/b/server"
ssh-keygen -F "$host" -f "$kh" 2>/dev/null | grep -v '^#' >"$WORK/b/known_hosts" || true
[ -s "$WORK/b/known_hosts" ] || { rm -f "$WORK/b/known_hosts"; warn "Your known_hosts has no key for $host: the backup goes without it (the onion address vouches for the server)."; }
# The bridges: in Tor's own settings, which only root reads
if [ -e "$TORRC" ] && ask "Add the bridge lines this computer's Tor uses, if any (sudo reads $TORRC)?" y; then
# shellcheck disable=SC2024 # root reads it; the copy is this run's own, in memory
sudo cat "$TORRC" >"$WORK/torrc" || die "$TORRC couldn't be read."
if grep -qE '^[[:space:]]*UseBridges[[:space:]]+1' "$WORK/torrc"; then
kind=$(sed -nE 's/^[[:space:]]*ClientTransportPlugin[[:space:]]+([a-z0-9]+)[[:space:]].*/\1/p' "$WORK/torrc" | head -n1)
case $kind in
obfs4 | webtunnel | snowflake)
{ echo "TYPE=$kind"; [ "$kind" = snowflake ] || sed -nE 's/^[[:space:]]*Bridge[[:space:]]+//p' "$WORK/torrc"; } >"$WORK/b/bridges"
note "Bridges: $kind$( [ "$kind" = snowflake ] || echo ", $(($(wc -l <"$WORK/b/bridges") - 1)) lines")." ;;
*) warn "Tor here uses bridges of a kind this doesn't know ($kind): not in the backup." ;;
esac
else
note "This computer's Tor uses no bridges."
fi
rm -f "$WORK/torrc"
fi
[ ! -s "$WORK/b/known_hosts" ] || has+="the server's key, "
[ ! -s "$WORK/b/site.conf" ] || has+="the publish settings, "
[ ! -s "$WORK/b/bridges" ] || has+="your bridges, "
say "The backup's passphrase"
note "It is all that protects the backup. Write it on paper, kept apart from the stick."
secret pass " Passphrase (at least 20 characters): "
secret pass2 " The same again: "
[ "$pass" = "$pass2" ] || die "The two didn't match; nothing was written."
[ ${#pass} -ge 20 ] || die "At least 20 characters, please (six random words); nothing was written."
printf '%s\n' "$pass" >"$WORK/pass"
unset pass pass2
tar -C "$WORK/b" -cf "$WORK/backup.tar" .
out=$dest/$BACKUP_PREFIX$(date +%F-%H%M).gpg
[ ! -e "$out" ] || die "$out exists already: one backup a minute."
gpg_pass --symmetric --cipher-algo AES256 --s2k-mode 3 --s2k-digest-algo SHA512 --s2k-count 65011712 \
-o "$out.new" "$WORK/backup.tar" || { rm -f "$out.new"; die "gpg couldn't write the backup."; }
# read back from the stick before it counts
gpg_pass --decrypt "$out.new" 2>/dev/null | cmp -s - "$WORK/backup.tar" ||
{ rm -f "$out.new"; die "The backup didn't open again with that passphrase; nothing was kept."; }
chmod 600 "$out.new"
mv "$out.new" "$out"
sync
work_done
say "Backed up: $out"
note "In it: your SSH key, the onion client key, ${has}and $u at $o."
note "Copy it to a second stick, kept elsewhere. After a reinstall, put Wren Linux back, then:"
note " bash $0 --reconnect --from ${dest}"
wipe_reminder "$dest" "$out"
}
# wipe_reminder DEST BACKUP: in red, the rest that a reinstall wipes, as Wren Linux keeps it,
# each with the command that backs it up into DEST; and DEST/RESTORE.txt, how to put each back
# (plain text to read, not a script to run: a stick can be changed by whatever had it).
# KeePassXC's databases always, the rest where this computer has it. It looks for the data
# itself, not for whether an extra is installed (wallet files stay after their extra goes), and
# changes nothing here. Root's folders (an onion service's keys) only with sudo already given
# in this run (sudo -n: it never asks); what it can't see is listed anyway, "if": a backup
# forgotten is the mistake that matters, one listed for nothing isn't. What isn't encrypted by
# itself goes into a file gpg encrypts with a passphrase (it asks), never onto the stick as it
# is; a VM's disk is opened by nothing here (the VM wrote it: its files aren't read).
wipe_reminder() {
local dest=$1 backup=$2 q w copy=${WREN_GIT_COPY:-$WREN_DIR} d dirs=() svc rc kdbx=() f r i img
local items=() cmds=() rests=()
# (as the backup's own file: AES-256 and gpg's slowest key derivation, written out in full,
# so each command reads as it runs)
local sym="gpg --symmetric --cipher-algo AES256 --s2k-mode 3 --s2k-digest-algo SHA512 --s2k-count 65011712"
q=$(printf '%q' "$dest")
img=$HOME/.local/share/libvirt/images
wr_full() { [ -d "$1" ] && [ -n "$(find "$1" -mindepth 1 -print -quit 2>/dev/null)" ]; }
# wr_root DIR: 0 there, 1 not there, 2 can't tell
wr_root() {
if [ -e "$1" ]; then return 0; fi
sudo -n test -d "$1" 2>/dev/null && return 0
sudo -n true 2>/dev/null && return 1
return 2
}
# wr_add ITEM BACKUP-COMMAND RESTORE-COMMAND
wr_add() { items+=("$1"); cmds+=("$2"); rests+=("$3"); }
# wr_gpg NAME FOLDER...: folders of your home, packed and encrypted; and how they come back
wr_gpg() { local n=$1; shift; echo "tar -C ~ -c $* | $sym -o $q/$n.tar.gpg"; }
wr_ungpg() { echo "gpg -d $q/$1.tar.gpg | tar -C ~ -x"; }
mapfile -t kdbx < <(find "$HOME" -maxdepth 4 -name '*.kdbx' -type f -not -path "$HOME/.cache/*" 2>/dev/null | sort | head -n 20)
if [ ${#kdbx[@]} -gt 0 ]; then
w='' r=''
for f in "${kdbx[@]}"; do
w+=" $(printf '%q' "$f")"
r+="${r:+ && }cp $q/$(printf '%q' "${f##*/}") $(printf '%q' "$f")"
done
wr_add "your KeePassXC databases (encrypted by themselves), and their key files if you use them" "cp$w $q/" "$r"
else
wr_add "your KeePassXC databases (.kdbx files: none found in your home), and their key files if you use them" \
"cp PATH/TO/YOURS.kdbx $q/" "cp $q/YOURS.kdbx ~/ (then open it in KeePassXC)"
fi
[ ! -s "${XDG_DATA_HOME:-$HOME/.local/share}/wren/signing-key/fingerprint" ] ||
wr_add "Wren Linux's signing key: its backup and revocation certificate, on two sticks" \
"cp -r /media/$USER/STICK/wren-signing-key /media/$USER/SECOND-STICK/ (wren-key status: which key)" \
"wrenctl install signing-key, then: wren-key restore /media/$USER/STICK/wren-signing-key/wren-signing-key-backup.asc (just after a restart, network off: it checks)"
[ ! -f "$HOME/.local/share/wren-vault/gocryptfs.conf" ] ||
wr_add "your vault (encrypted by itself), closed first; remember its password" \
"vault close && cp -a ~/.local/share/wren-vault $q/" \
"wrenctl install vault, then: cp -a $q/wren-vault ~/.local/share/ (vault open: its own password)"
w=''
[ ! -e "$HOME/Wallets" ] || w+=" Wallets"
[ ! -d "$HOME/.electrum" ] || w+=" .electrum"
[ ! -d "$HOME/.sparrow" ] || w+=" .sparrow"
[ ! -d "${XDG_CONFIG_HOME:-$HOME/.config}/feather" ] || w+=" .config/feather"
[ -z "$w" ] || wr_add "your wallets: their seed words on paper, and their files (${w# })" "$(wr_gpg wallets "${w# }")" \
"$(wr_ungpg wallets) (after the wallet extras are installed again)"
if [ -e "$img/Whonix-Workstation.qcow2" ]; then
wr_add "the Whonix Workstation, with any wallet in it (Feather): its seed words on paper; or its whole disk, switched off (gigabytes)" \
"whonix stop && cp --sparse=always $(printf '%q' "$img")/Whonix-Workstation.qcow2 $q/" \
"mkdir -p ~/.local/share/libvirt/images && cp --sparse=always $q/Whonix-Workstation.qcow2 ~/.local/share/libvirt/images/, then: wrenctl install whonix (keep the disks when it asks)"
fi
! wr_full "$HOME/.gnupg/private-keys-v1.d" || wr_add "your gpg keys (~/.gnupg)" "$(wr_gpg gnupg .gnupg)" "$(wr_ungpg gnupg)"
[ -z "$(find "$HOME/.ssh" -maxdepth 1 -type f -name 'id_*' ! -name '*.pub' -print -quit 2>/dev/null)" ] ||
wr_add "your other SSH keys (~/.ssh)" "$(wr_gpg ssh .ssh)" "$(wr_ungpg ssh)"
for svc in "wren-onion-site:your onion site's keys:onion-site" "wren-ssh:remote access's onion keys:remote-access"; do
IFS=: read -r d w f <<<"$svc"
# (|| : set -e would end this at a 1 or a 2)
rc=0
wr_root "/var/lib/tor/$d" || rc=$?
[ "$rc" != 1 ] || continue
wr_add "$w$( [ "$rc" = 2 ] && echo ", if you set one up") (/var/lib/tor/$d, root's): its onion address goes with them" \
"sudo tar -C /var/lib/tor -c $d | $sym -o $q/$d.tar.gpg" \
"wrenctl install $f, then: sudo systemctl stop tor && gpg -d $q/$d.tar.gpg | sudo tar -C /var/lib/tor --no-same-owner -x && sudo chown -R debian-tor:debian-tor /var/lib/tor/$d && sudo chmod 700 /var/lib/tor/$d && sudo systemctl start tor"
done
[ ! -f "$HOME/.config/wren/backup.conf" ] ||
wr_add "your home, with the backup extra (its drive plugged in), and its password" "backup" \
"wrenctl install backup (the same drive), then: backup restore latest --target ~/restored"
if git -C "$copy" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
f="git -C $(printf '%q' "$copy") bundle create $q/${copy##*/}.bundle --all"
[ -z "$(git -C "$copy" status --porcelain 2>/dev/null)" ] ||
wr_add "$copy: changes not committed yet (commit them, then bundle it, below)" "git -C $(printf '%q' "$copy") status" "-"
r="git clone $q/${copy##*/}.bundle ~/${copy##*/}"
if ! git -C "$copy" rev-parse '@{upstream}' >/dev/null 2>&1; then
wr_add "$copy: it has no server, so none of its commits are anywhere else" "$f" "$r"
elif [ -n "$(git -C "$copy" log --oneline '@{upstream}..' 2>/dev/null)" ]; then
wr_add "$copy: commits not pushed yet" "$f (or push them)" "$r"
fi
fi
for d in Documents Pictures Downloads Music Videos; do ! wr_full "$HOME/$d" || dirs+=("$d"); done
if [ ${#dirs[@]} -gt 0 ]; then
w='' r=''
# shellcheck disable=SC2088 # a literal ~, in the commands shown to type
for d in "${dirs[@]}"; do w+="~/$d " r+="$q/$d "; done
# shellcheck disable=SC2088 # the same
wr_add "your files (~/$(printf '%s, ~/' "${dirs[@]}" | sed 's/, ~\/$//'))" "cp -a $w$q/" "cp -a $r~/"
fi
if wr_full "$HOME/.mozilla/firefox" || getent passwd firefox >/dev/null 2>&1; then
wr_add "Firefox's bookmarks" "in Firefox: Ctrl+Shift+O, Import and Backup, Export Bookmarks to HTML, then copy that file to $q" \
"in Firefox: Ctrl+Shift+O, Import and Backup, Import Bookmarks from HTML: the file in $q"
fi
printf '\n\033[1;31m%s\033[0m\n' " This backs up your server keys only. Before wiping this computer, also back up:"
for i in "${!items[@]}"; do
printf '\033[1;31m - %s\033[0m\n' "${items[i]}"
printf ' \033[1m$ %s\033[0m\n' "${cmds[i]}"
done
printf '\033[1;31m%s\033[0m\n' " And anything else of yours this doesn't know of. Check each backup opens before you wipe." \
" How to put each back: $dest/RESTORE.txt (on the stick, for the day you need it)." \
" (On a stick encrypted with Wren Linux's encrypt-usb.sh, all of it stays private even if the stick is lost.)"
{
echo "Restoring from this stick"
echo "(written by Wren Linux tools/vps-client.sh --backup, $(date '+%F %H:%M'))"
echo
echo "This file names what was backed up here, never a key or a passphrase. On a stick that"
echo "isn't encrypted it shows that much (that you have wallets, an onion site) to whoever finds it."
echo "It is text to read, not a script to run: read each command before you type it."
echo
echo "1. Install Wren Linux. Then, with this stick mounted, in your Wren Linux git copy:"
echo " bash tools/vps-client.sh --reconnect --from $dest"
echo " (your server keys: ${backup##*/}, and its passphrase)"
echo
echo "2. The rest, as they were listed when that backup was made (put back those you backed up):"
for i in "${!items[@]}"; do
[ "${rests[i]}" != - ] || continue
echo
echo " - ${items[i]}"
echo " backed up with: ${cmds[i]}"
echo " put back with: ${rests[i]}"
done
echo
echo "A file ending .tar.gpg asks the passphrase it was made with. Check each opens before you wipe:"
echo " gpg -d FILE.tar.gpg | tar -t"
} >"$dest/RESTORE.txt"
}
# restore_from FILE|FOLDER: the files of a backup (in a FOLDER, its newest) back in place, for
# reconnect; sets R_USER, R_ONION, R_HOST and the restored bridges. A file of yours already in
# place and different is never replaced: it stops instead.
restore_from() {
local src=${1:-} f pass k kh=$HOME/.ssh/known_hosts kept new line
[ -n "$src" ] || die "Usage: bash $0 --reconnect --from FOLDER (the stick's folder, or the backup file in it)"
if [ -d "$src" ]; then
f=$(find "$src" -maxdepth 1 -type f -name "$BACKUP_PREFIX*.gpg" -printf '%f\n' 2>/dev/null | sort | tail -n1)
[ -n "$f" ] || die "No backup ($BACKUP_PREFIX*.gpg) in $src. Mount the stick (usb mount) and give its folder."
f=$src/$f
note "The newest backup there: ${f##*/}"
else
f=$src
fi
[ -f "$f" ] || die "$f isn't there."
command -v gpg >/dev/null || die "gpg is missing (sudo apt install gnupg)."
work_dir
secret pass " The backup's passphrase: "
printf '%s\n' "$pass" >"$WORK/pass"
unset pass
gpg_pass --decrypt -o "$WORK/backup.tar" "$f" 2>/dev/null || die "The backup doesn't open with that passphrase (or isn't one)."
rm -f "$WORK/pass"
# Only what a backup holds: those names, plain files, nothing else
while IFS= read -r line; do
case $line in ./ | .) continue ;; esac
line=${line#./}
case " $BACKUP_FILES " in *" $line "*) ;; *) die "That isn't a backup this made (it holds \"$line\"): nothing was put back." ;; esac
done < <(tar -tf "$WORK/backup.tar")
tar -C "$WORK/b" --no-same-owner --no-same-permissions -xf "$WORK/backup.tar"
[ -z "$(find "$WORK/b" -mindepth 1 ! -type f)" ] || die "That isn't a backup this made (it holds more than files): nothing was put back."
for k in wren-vps wren-vps.pub client.key server; do [ -s "$WORK/b/$k" ] || die "The backup has no $k: nothing was put back."; done
R_USER=$(sed -n 's/^USER=//p' "$WORK/b/server")
R_ONION=$(sed -n 's/^ONION=//p' "$WORK/b/server")
R_HOST=$(sed -n 's/^HOST=//p' "$WORK/b/server")
[[ $R_USER =~ ^[a-z][a-z0-9-]{0,30}$ && $R_ONION =~ ^[a-z2-7]{56}\.onion$ && $R_HOST =~ ^[a-z][a-z0-9-]{0,30}$ ]] ||
die "The backup's server details don't read: nothing was put back."
# yours already here, and different: not replaced
for k in "wren-vps:$SSH_KEY" "wren-vps.pub:$SSH_KEY.pub" "client.key:$DIR/wren-vps.key"; do
if [ -e "${k#*:}" ] && ! cmp -s "$WORK/b/${k%%:*}" "${k#*:}"; then
die "${k#*:} is here already, and isn't the one in the backup. Move it away first (it may be a newer key): nothing was put back."
fi
done
kept=$(ssh-keygen -F "$R_HOST" -f "$kh" 2>/dev/null | awk '!/^#/ { print $2, $3 }') || kept=
if [ -n "$kept" ] && [ -s "$WORK/b/known_hosts" ]; then
new=$(awk '{ print $2, $3 }' "$WORK/b/known_hosts")
[ "$kept" = "$new" ] || die "Your known_hosts has another key for $R_HOST than the backup. If that server is gone: ssh-keygen -R $R_HOST, then run this again. Nothing was put back."
fi
# the bridges, checked as typed ones are
RESTORED_BRIDGE_TYPE='' RESTORED_BRIDGES=()
if [ -s "$WORK/b/bridges" ]; then
k=$(sed -n '1s/^TYPE=//p' "$WORK/b/bridges")
case $k in obfs4 | webtunnel | snowflake) ;; *) die "The backup's bridges don't read: nothing was put back." ;; esac
while IFS= read -r line; do
line=$(bridge_line_clean "$line")
[ -n "$line" ] || continue
bridge_line_ok "$k" "$line" || die "A bridge line in the backup isn't a $k line Tor takes: nothing was put back."
RESTORED_BRIDGES+=("$line")
done < <(tail -n +2 "$WORK/b/bridges")
[ "$k" = snowflake ] || [ ${#RESTORED_BRIDGES[@]} -gt 0 ] || die "The backup names $k bridges but has no lines: nothing was put back."
RESTORED_BRIDGE_TYPE=$k
fi
say "Putting your files back"
mkdir -p "$HOME/.ssh" "$DIR"
chmod 700 "$HOME/.ssh" "$DIR"
install -m 600 "$WORK/b/wren-vps" "$SSH_KEY"
install -m 644 "$WORK/b/wren-vps.pub" "$SSH_KEY.pub"
install -m 600 "$WORK/b/client.key" "$DIR/wren-vps.key"
[ ! -s "$WORK/b/client.pub" ] || install -m 644 "$WORK/b/client.pub" "$DIR/wren-vps.pub"
if [ -s "$WORK/b/site.conf" ] && [ ! -e "$DIR/site.conf" ]; then install -m 600 "$WORK/b/site.conf" "$DIR/site.conf"; fi
if [ -z "$kept" ] && [ -s "$WORK/b/known_hosts" ]; then
: >>"$kh"
chmod 600 "$kh"
cat "$WORK/b/known_hosts" >>"$kh"
fi
# shellcheck disable=SC2088 # a literal ~, as the other messages write it
note "~/.ssh/wren-vps, the onion client key$( [ -s "$WORK/b/site.conf" ] && echo ", the publish settings")$( [ -s "$WORK/b/known_hosts" ] && echo ", the server's key")$( [ -n "$RESTORED_BRIDGE_TYPE" ] && echo "; the bridges are used if Tor is set up here now")."
work_done
}
# reconnect USER ONION [HOST]: this computer again, for a server set up before (a reinstall,
# a new laptop), from the keys you backed up and put back. Nothing on the server changes.
reconnect() {
local user=${1:-} onion=${2:-} host=${3:-wren-vps} name=wren-vps kh=$HOME/.ssh/known_hosts kept fp out site p
[[ $user =~ ^[a-z][a-z0-9-]{0,30}$ ]] && [ "$user" != root ] ||
die "Usage: bash $0 --reconnect USER ONION [HOST] (USER: yours on the server, e.g. wren; ONION: its SSH onion address, the HostName in your backed-up ~/.ssh/config)"
onion_ok "$onion"
onion=${onion%.onion}.onion
name_ok "$host"
for p in ssh ssh-keygen; do command -v "$p" >/dev/null || die "$p is missing (sudo apt install openssh-client)."; done
[ -s "$SSH_KEY" ] && [ -s "$SSH_KEY.pub" ] ||
die "There is no ~/.ssh/wren-vps here. Put back the key you backed up, both halves
(~/.ssh/wren-vps and ~/.ssh/wren-vps.pub; then chmod 600 ~/.ssh/wren-vps), and run: $AGAIN"
[ -s "$DIR/$name.key" ] ||
die "There is no onion client key here ($DIR/$name.key). Put back the one you backed up
(mkdir -p $DIR, then chmod 600 the key), and run: $AGAIN
Without it this computer can't even find the server's SSH address. If it is lost, a new one
(bash $0 --keygen) has to be given to the server by its provider's web console."
chmod 600 "$SSH_KEY" "$DIR/$name.key"
chmod 700 "$DIR"
# the server's key, if your backup of known_hosts is back (Debian's ssh hashes the names)
kept=$(ssh-keygen -F "$host" -f "$kh" 2>/dev/null | awk '!/^#/ { print $2, $3 }') || kept=
cat <<EOF
This connects this computer again to your server at $onion, through Tor,
for $user. The server isn't changed. In order:
1. Tor, netcat-openbsd and torsocks on this computer: installed if missing
(it asks first; sudo asks your password; a Tor new here asks whether you
need a bridge, as your other Wren scripts do), and Tor started.
2. This computer's Tor gets your onion client key (it asks first; sudo), and
~/.ssh/config an entry, $host, that goes to the server's onion address.
3. A login through the onion address. $( [ -n "$kept" ] && echo "Your known_hosts has the server's key:
a server that shows another one is refused." || echo "Your known_hosts has no key for $host:
the one the server shows is kept. The onion address vouches for it (only
the server holds what answers there), so this is as safe as the address
you typed is right." )
4. The publish settings, if they weren't put back: the site's onion address,
from the server (it asks; sudo asks $user's password there).
It takes a few minutes.
EOF
ask "Continue?" y || { note "Nothing was done."; exit 0; }
offer_ding
say "Tor, on this computer"
tor_ready
say "This computer's Tor, and your ssh entry"
give_tor_key "$onion" "$name"
write_entry "$user" "$onion" "$host"
say "Logging in through the onion address (10 to 60 seconds)"
run_agent
$KEY_IN_AGENT || asks "$KEYPASS, at each try"
if [ -n "$kept" ]; then
onion_login "$host" -o StrictHostKeyChecking=yes ||
die "The server wasn't reached. If ssh said its host key changed, the server at that address shows a
different key than the one you backed up: don't log in; look at it from the provider's web console
(ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub). Otherwise look at where it stops: ssh -v $host true"
else
onion_login "$host" -o StrictHostKeyChecking=accept-new ||
die "The server wasn't reached. Is the onion address right (the HostName in your backed-up ~/.ssh/config)?
Look at where it stops: ssh -v $host true"
fi
fp=$(ssh-keygen -l -F "$host" -f "$kh" 2>/dev/null | awk '!/^#/ { print $3; exit }') || fp=
note "Logged in. The server's key, kept for $host: ${fp:-(not found in ~/.ssh/known_hosts)}"
[ -n "$kept" ] || note "(To be surer, compare it with the server's own: ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub there.)"
if [ -s "$DIR/site.conf" ]; then
note "Publish settings: $DIR/site.conf (put back from your backup)"
elif ask "Fetch the site's onion address from the server, for publish-site.sh?" y; then
login_asks "$(sudo_pass "$user")"
out=$(ssh -t "$host" sudo wren-vps --status | tee /dev/stderr | tr -d '\r') || out=
site=$(sed -n 's/^Onion site: //p' <<<"$out" | head -n1)
if [[ $site =~ ^[a-z2-7]{56}\.onion$ ]]; then
printf 'VPS_HOST=%s\nVPS_SITE_ONION=%s\n' "$host" "$site" >"$DIR/site.conf"
note "Publish settings: $DIR/site.conf (tools/publish-site.sh reads it)"
else
warn "The server named no site onion address: publish-site.sh will ask. Later: bash $0 --reconnect $user $onion again."
fi
fi
stop_agent
cat <<EOF
Done. This computer reaches your server again, only through Tor:
ssh $host log in
scp FILE $host:PATH copy a file there scp $host:PATH . and back here
bash tools/publish-site.sh the site, from your newest signed commit
A new git copy here pushes to the server once you run: bash $0 --origin
EOF
}
# the command as it was run, for the messages that say to run it again
AGAIN="bash $0${*:+ $*}"
case ${1:-} in
--keygen) keygen "${2:-}" ;;
--keygen-show) keygen_show "${2:-}" ;;
--ssh-key) ssh_key "${2:-}" ;;
--tor-auth) tor_auth "${2:-}" "${3:-}" ;;
--ssh-config) ssh_config "${2:-}" "${3:-}" "${4:-}" ;;
--setup) shift; setup "$@" ;;
--backup) backup_cmd "${2:-}" "${3:-}" ;;
--reconnect)
if [ "${2:-}" = --from ]; then
restore_from "${3:-}"
reconnect "$R_USER" "$R_ONION" "$R_HOST"
else
reconnect "${2:-}" "${3:-}" "${4:-}"
fi ;;
--onion-only) onion_only "${2:-}" ;;
--bridges) bridges_cmd ;;
--domain) domain_cmd "${2:-}" "${3:-}" ;;
--origin) origin_cmd "${2:-}" ;;
"") script_help "$0" ;;
*) die "unknown command \"$1\" (--help)." ;;
esac