diff --git a/.env.example b/.env.example index 44e5ea8..9a2e64b 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,6 @@ # Copy to .env and fill in. Generate the three secrets with: # -# openssl rand -hex 32 # # Host port the relay is published on, on 127.0.0.1 only. Change this rather -# than editing docker-compose.yml — 8080 is frequently already taken on a -# machine that self-hosts other things, and editing the compose file makes the -# next `git pull` conflict. -FN_HOST_PORT=8080 - -POSTGRES_PASSWORD +# openssl rand -hex 32 # POSTGRES_PASSWORD # openssl rand -base64 32 # FN_AT_REST_SECRET # # POSTGRES_PASSWORD is hex, not base64, and that is not a style choice: it is @@ -22,6 +16,12 @@ POSTGRES_PASSWORD # Keep issuer.pem. Losing it invalidates every token you have issued; leaking it # lets anyone mint tokens this relay will accept. +# Host port the relay is published on, on 127.0.0.1 only. Change this rather +# than editing docker-compose.yml — 8080 is frequently already taken on a +# machine that self-hosts other things, and editing the compose file makes the +# next `git pull` conflict. +FN_HOST_PORT=8080 + POSTGRES_PASSWORD= FN_AT_REST_SECRET= FN_ISSUER_PRIVATE_KEY= diff --git a/Resources/Views/self-hosting.leaf b/Resources/Views/self-hosting.leaf index 8e52112..261d299 100644 --- a/Resources/Views/self-hosting.leaf +++ b/Resources/Views/self-hosting.leaf @@ -3,7 +3,7 @@ + source-sha256: 0f36788aea97abd7 -->

Self-hosting

@@ -52,12 +52,24 @@ told.

cd flamenet-relay cp .env.example .env

Generate the three secrets .env asks for:

-
openssl rand -base64 32                    #("#") POSTGRES_PASSWORD
+
openssl rand -hex 32                       #("#") POSTGRES_PASSWORD
 openssl rand -base64 32                    #("#") FN_AT_REST_SECRET
 
 openssl genpkey -algorithm ed25519 -out issuer.pem
 openssl pkey -in issuer.pem -outform DER | tail -c 32 | base64          #("#") FN_ISSUER_PRIVATE_KEY
 openssl pkey -in issuer.pem -pubout -outform DER | tail -c 32 | base64  #("#") FN_ISSUER_PUBLIC_KEY
+

🔴 -hex for the database password, not +-base64, and this one is not cosmetic. It is +interpolated into +postgres://flamenet:<password>@db:5432/flamenet, +where base64's /, + and = are all +meaningful. A password containing / makes that URL invalid +and the relay dies at start-up with a bare Code=-1000 and +the connection string echoed back at you — which reads like the database +is unreachable rather than like the password needs escaping. +openssl rand -base64 32 produces one of those roughly +three times in four, so following the old instructions +usually failed.

Then create the proxy network the compose file attaches to, and start:

docker network create web
diff --git a/Scripts/selfhost-smoke.sh b/Scripts/selfhost-smoke.sh
new file mode 100755
index 0000000..bf73a15
--- /dev/null
+++ b/Scripts/selfhost-smoke.sh
@@ -0,0 +1,145 @@
+#!/usr/bin/env bash
+# Run the published self-hosting quick start, exactly as SELF-HOSTING.md gives it.
+#
+# 🔴 **The quick start broke, silently, and stayed broken for a day.** Three
+# separate faults at once — and the one that survived longest was a missing
+# `docker network create web`, which fails only at `up` time, after the images
+# have been pulled. Nothing in this repository ran those instructions, so
+# nothing said so. The people it failed for were self-hosters, at the exact
+# moment they were deciding whether this project works.
+#
+# ⚠️ **It has to actually BOOT.** `docker compose config` renders that compose
+# file happily with no `web` network in existence: the external-network check
+# happens at `up`. A job that only validated the YAML would have passed
+# throughout the outage it exists to prevent.
+#
+#   Scripts/selfhost-smoke.sh            # published image, the documented path
+#   Scripts/selfhost-smoke.sh --build    # build from source, the ARM path
+#
+# 🔴 **Not wired into CI, and not for want of trying.** This project's runner is
+# a docker executor with `privileged = false` and no socket mounted, so a job
+# here gets an alpine container with no bash, no docker CLI and no way to reach
+# a daemon — `env: can't execute bash` is as far as it got. Running it in CI
+# needs a runner change (privileged, a socket mount, or a shell executor), which
+# is a decision about a machine that also serves production.
+#
+# ⚠️ Until then this is a RELEASE-TIME check somebody has to run, on any machine
+# with docker:
+#
+#     ssh  "cd /srv/prod/messenger/src && ./Scripts/selfhost-smoke.sh"
+#
+# A check that only runs when remembered is weaker than one that cannot be
+# forgotten. It is still worth having: on its first two runs it found the
+# missing `docker network create web` AND a documented password that broke the
+# relay three times in four.
+#
+# Leaves nothing behind: a scratch directory, its own compose project name, and
+# a trap that tears down volumes as well as containers.
+set -euo pipefail
+
+here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+mode="${1:-}"
+project="selfhost-smoke-$$"
+# ⚠️ Its own network name, not `web`. A shared name would collide with a real
+# proxy network on a machine that has one — and this script deletes it at the
+# end, which on the production box would be a very bad afternoon.
+network="${project}-web"
+work="$(mktemp -d)"
+
+cleanup() {
+  docker compose -p "$project" --project-directory "$work" \
+    -f "$work/docker-compose.yml" -f "$work/docker-compose.override.yml" \
+    down -v --remove-orphans >/dev/null 2>&1 || true
+  docker network rm "$network" >/dev/null 2>&1 || true
+  rm -rf "$work"
+}
+trap cleanup EXIT
+
+echo "SELFHOST: laying out a fresh checkout the way a self-hoster would"
+cp "$here/docker-compose.yml" "$work/docker-compose.yml"
+cp "$here/docker-compose.build.yml" "$work/docker-compose.build.yml" 2>/dev/null || true
+cp "$here/.env.example" "$work/.env"
+# The build path needs the sources the Dockerfile copies.
+if [ "$mode" = "--build" ]; then
+  cp -R "$here/Sources" "$here/Tests" "$here/Resources" "$here/Public" \
+        "$here/Package.swift" "$here/Package.resolved" "$here/Dockerfile" "$work/" 2>/dev/null || true
+fi
+
+echo "SELFHOST: generating the secrets .env asks for"
+# The same four commands the documentation prints. Generated rather than
+# hardcoded, so a change to what the compose file demands shows up here.
+pem="$work/issuer.pem"
+openssl genpkey -algorithm ed25519 -out "$pem" 2>/dev/null
+set_env() {
+  # ⚠️ In-place edit that tolerates an empty value on the right of `=`, which is
+  # what .env.example ships. Anchored to the line start so a variable whose name
+  # is a suffix of another one cannot be hit.
+  python3 - "$work/.env" "$1" "$2" <<'PY'
+import sys, re
+path, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
+text = open(path, encoding='utf-8').read()
+new, n = re.subn(rf'^{re.escape(key)}=.*$', f'{key}={val}', text, count=1, flags=re.M)
+if n != 1:
+    sys.exit(f'{key} is not in .env.example — the documented steps have drifted')
+open(path, 'w', encoding='utf-8').write(new)
+PY
+}
+# 🔴 **hex, not base64, and this is the documented command for a reason.**
+# `openssl rand -base64 32` emits `/` and `+` about 73% of the time, and the
+# password goes into `postgres://user:PASSWORD@db/...` unencoded — a `/` ends
+# the authority and the relay dies with `NSURLErrorDomain Code=-1000`, which
+# names the URL and says nothing about passwords. This script generates what
+# SELF-HOSTING.md tells people to generate; if that drifts back to base64, this
+# fails again and says why.
+set_env POSTGRES_PASSWORD "$(openssl rand -hex 32)"
+set_env FN_AT_REST_SECRET "$(openssl rand -base64 32 | tr -d '\n')"
+set_env FN_ISSUER_PRIVATE_KEY "$(openssl pkey -in "$pem" -outform DER 2>/dev/null | tail -c 32 | base64 | tr -d '\n')"
+set_env FN_ISSUER_PUBLIC_KEY "$(openssl pkey -in "$pem" -pubout -outform DER 2>/dev/null | tail -c 32 | base64 | tr -d '\n')"
+
+# A port nobody else is on, so this cannot collide with a relay already running
+# on the machine — which on the deployment box is the production one.
+port=18973
+set_env FN_HOST_PORT "$port"
+
+echo "SELFHOST: docker network create (the step that was missing)"
+docker network create "$network" >/dev/null
+# The compose file names the external network `web`; point it at ours for the
+# duration without editing the file a self-hoster would actually use.
+cat > "$work/docker-compose.override.yml" <