repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

6.1 KB · 198 lines · Bash Raw History
  1#!/bin/sh
  2# Create and inspect the two ntfy accounts the alert path runs on. Run these
  3# from the repo root, where each one is a make target:
  4#
  5#   make ntfy          create both accounts and their topic access
  6#   make ntfy-token    mint the publishers' token, into their .env files
  7#   make ntfy-status   what exists right now
  8#   make ntfy-passwd   change the reading account's password
  9#
 10# ntfy is deny-all and there are three accounts: isaac reads every topic from
 11# the phone, orchard writes the alert topics from status and logging, and
 12# orchard-auth writes the auth topic and nothing else. That third one is
 13# separate because the token in status' and logging' .env files can publish to
 14# their topics, and sharing it would let a copy of either mint its own login
 15# codes. ntfy cannot restrict an account by source address, so Caddy also
 16# refuses the publish routes on the public hostname.
 17#
 18# All ntfy state lives in the orchard-ntfy-data volume, never a bind mount. The
 19# Docker CLI here talks to Docker Desktop on the Windows host, whose daemon
 20# cannot see this filesystem, so a bind mount silently resolves to an empty
 21# directory.
 22#
 23# Every docker command goes through sudo, because the socket in the webdev
 24# container is root:root mode 660 and being in the docker group does not help.
 25# On a host where docker needs no sudo:  make ntfy SUDO=
 26set -e
 27
 28# Resolved before the cd below, since $0 is relative to the caller's
 29# directory and stops resolving the moment we leave it.
 30SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
 31
 32# Run from this script's own directory, whatever the caller's was, since the
 33# token step writes into ../sites/*/.env.
 34cd "$(dirname "$0")"
 35
 36SUDO=${SUDO-sudo}
 37DOCKER_BIN=$(command -v docker) || { echo "no docker on PATH" >&2; exit 1; }
 38# An absolute path, because `command` is a shell builtin and sudo cannot exec
 39# one, and because sudo's secure_path is not this shell's PATH.
 40docker() { ${SUDO} "$DOCKER_BIN" "$@"; }
 41
 42CONTAINER=orchard-ntfy
 43READER=isaac
 44WRITER=orchard
 45AUTH_WRITER=orchard-auth
 46ALERT_TOPICS="status logging"
 47AUTH_TOPIC="auth"
 48TOPICS="$ALERT_TOPICS $AUTH_TOPIC"
 49
 50# ntfy reads auth-file out of the config baked into the image, so every command
 51# here runs inside the container rather than against the volume directly.
 52running() {
 53	docker ps --filter "name=^${CONTAINER}$" --format '{{.Names}}' 2>/dev/null | grep -q .
 54}
 55
 56require_running() {
 57	running || {
 58		echo "$CONTAINER is not running. from the repo root:" >&2
 59		echo "" >&2
 60		echo "  make up" >&2
 61		exit 1
 62	}
 63}
 64
 65ntfy() { docker exec -i "$CONTAINER" ntfy "$@"; }
 66
 67user_exists() {
 68	ntfy user list 2>/dev/null | grep -q "^user $1 "
 69}
 70
 71# 32 characters of /dev/urandom in groups of eight. Long enough that nobody is
 72# going to type it by hand, grouped so it can be read back off a screen.
 73gen_password() {
 74	LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom \
 75		| head -c 32 \
 76		| sed 's/.\{8\}/&-/g; s/-$//'
 77	echo
 78}
 79
 80case "${1:-}" in
 81up)
 82	require_running
 83
 84	# These passwords exist nowhere else, so they are generated and printed
 85	# rather than asked for. An account that already exists keeps the one it
 86	# has, because ntfy will not hand a password back and printing a new one
 87	# that was never applied is worse than printing nothing.
 88	made=""
 89	for user in "$READER" "$WRITER" "$AUTH_WRITER"; do
 90		if user_exists "$user"; then
 91			echo "$user exists already, and its password is untouched"
 92			continue
 93		fi
 94		pw=$(gen_password)
 95		# Over stdin, not NTFY_PASSWORD. sudo runs with env_reset and strips
 96		# the variable, and forwarding it through sudo would put the password
 97		# in the command line for anyone running ps.
 98		printf '%s\n%s\n' "$pw" "$pw" | ntfy user add "$user" >/dev/null
 99		made="$made$user $pw
100"
101	done
102
103	# isaac reads everything from the phone. The two writers are scoped to
104	# their own topics and cannot read any of them back.
105	for topic in $TOPICS; do
106		ntfy access "$READER" "$topic" read-only
107	done
108	for topic in $ALERT_TOPICS; do
109		ntfy access "$WRITER" "$topic" write-only
110	done
111	ntfy access "$AUTH_WRITER" "$AUTH_TOPIC" write-only
112
113	if [ -n "$made" ]; then
114		echo
115		echo "created, and these are the only copies:"
116		echo
117		printf '%s' "$made" | while read -r u p; do
118			printf '  %-10s %s\n' "$u" "$p"
119		done
120		echo
121		echo "$READER is the account the phone logs in with. put both in 1Password."
122	fi
123
124	echo
125	echo "now mint the publishers' token:"
126	echo
127	echo "  make ntfy-token"
128	;;
129
130token)
131	require_running
132
133	# Written straight into the .env files rather than printed, so a token
134	# never gets pasted between terminals and into a shell history.
135	mint() {
136		ntfy token add -l "$2" "$1" | grep -o 'tk_[A-Za-z0-9]*' | head -1
137	}
138
139	# Two tokens, because the auth topic has its own writer. Sharing the
140	# publishers' token would mean a copy of status' or logging' .env could
141	# publish a login code.
142	token=$(mint "$WRITER" "orchard site publishers")
143	auth_token=$(mint "$AUTH_WRITER" "orchard auth publisher")
144	if [ -z "$token" ] || [ -z "$auth_token" ]; then
145		echo "no token came back; is $CONTAINER healthy?" >&2
146		exit 1
147	fi
148
149	write_var() {
150		site=$1
151		var=$2
152		value=$3
153		if [ ! -f "$site/.env" ]; then
154			echo "no $site/.env yet; create it from .env.example first" >&2
155			exit 1
156		fi
157		# Passed to awk as a variable, so the token never appears on a
158		# command line where `ps` could read it.
159		tmp="$site/.env.tmp"
160		awk -v name="$var" -v tok="$value" \
161			'$0 ~ "^" name "=" { print name "=" tok; found=1; next } { print }
162			 END { if (!found) print name "=" tok }' \
163			"$site/.env" > "$tmp"
164		chmod 600 "$tmp"
165		mv "$tmp" "$site/.env"
166		echo "wrote $var into $(basename "$site")/.env"
167	}
168
169	write_var ../sites/status.bythewood.me NTFY_TOKEN "$token"
170	write_var ../sites/logging.bythewood.me NTFY_TOKEN "$token"
171	write_var ../sites/auth.bythewood.me AUTH_NTFY_TOKEN "$auth_token"
172
173	echo
174	echo "now, from the repo root, to hand it to the running sites:"
175	echo
176	echo "  make up"
177	;;
178
179status)
180	require_running
181	echo "--- access ---"
182	ntfy access
183	echo "--- tokens ---"
184	ntfy token list
185	;;
186
187passwd)
188	require_running
189	ntfy user change-pass "$READER"
190	echo "changed. update the password in the ntfy app on the phone."
191	;;
192
193*)
194	awk 'NR>1 && /^#/ { sub(/^#[ ]?/, ""); print; next } NR>1 { exit }' "$SELF"
195	exit 1
196	;;
197esac