The data root belongs to the app's uid, so the SSH user usually cannot write into it, and a site.env they did write would be unreadable by the app — the admin would stay disabled after provisioning. The script now writes the file through a container running as the app user, so ownership is right the first time and no playbook re-run is needed. Also checks the data root exists and tells you to run the playbook if it does not.
113 lines
3.5 KiB
Bash
Executable File
113 lines
3.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Writes the one file that never goes in git: the admin password hash and the
|
|
# session secret. Run this on the server, once per Site.
|
|
#
|
|
# scripts/provision-site-env.sh /var/lib/menu-app.hopsngrains.com
|
|
#
|
|
# The hash is produced by the app image itself, so the server needs no Node and
|
|
# the password is never visible in the process list.
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
usage: provision-site-env.sh <data-root> [image] [--force]
|
|
|
|
<data-root> directory the app mounts as MENU_ROOT, e.g.
|
|
/var/lib/menu-app.hopsngrains.com
|
|
[image] app image to borrow Node from (default menu-admin:latest)
|
|
--force overwrite an existing site.env
|
|
USAGE
|
|
}
|
|
|
|
DATA_ROOT="${1:-}"
|
|
if [[ -z "$DATA_ROOT" || "$DATA_ROOT" == "-h" || "$DATA_ROOT" == "--help" ]]; then
|
|
usage
|
|
exit 1
|
|
fi
|
|
shift || true
|
|
|
|
IMAGE="menu-admin:latest"
|
|
FORCE=0
|
|
for argument in "$@"; do
|
|
case "$argument" in
|
|
--force) FORCE=1 ;;
|
|
*) IMAGE="$argument" ;;
|
|
esac
|
|
done
|
|
|
|
ENV_FILE="$DATA_ROOT/site.env"
|
|
|
|
if [[ -f "$ENV_FILE" && "$FORCE" -ne 1 ]]; then
|
|
echo "$ENV_FILE already exists; pass --force to replace it" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "docker is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
|
|
echo "image $IMAGE is not present; pull or build it first" >&2
|
|
exit 1
|
|
fi
|
|
|
|
read -r -s -p "Admin password: " PASSWORD
|
|
echo
|
|
read -r -s -p "Repeat password: " REPEAT
|
|
echo
|
|
|
|
if [[ -z "$PASSWORD" ]]; then
|
|
echo "the password cannot be empty" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$PASSWORD" != "$REPEAT" ]]; then
|
|
echo "the passwords do not match" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ${#PASSWORD} -lt 10 ]]; then
|
|
echo "use at least 10 characters" >&2
|
|
exit 1
|
|
fi
|
|
|
|
HASH="$(printf '%s' "$PASSWORD" | docker run --rm -i --entrypoint node "$IMAGE" \
|
|
--input-type=module \
|
|
-e "let s='';process.stdin.setEncoding('utf8');process.stdin.on('data',c=>s+=c);process.stdin.on('end',async()=>{const {pathToFileURL}=await import('node:url');const m=await import(pathToFileURL('/app/src/auth.ts').href);process.stdout.write(await m.hashPassword(s))})")"
|
|
|
|
SECRET="$(docker run --rm --entrypoint node "$IMAGE" --input-type=module \
|
|
-e "import {randomBytes} from 'node:crypto';process.stdout.write(randomBytes(32).toString('base64url'))")"
|
|
|
|
if [[ -z "$HASH" || -z "$SECRET" ]]; then
|
|
echo "the image failed to produce the hash or the secret" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The file is written by a container running as the app's own uid, not by the
|
|
# SSH user: the data root belongs to the app user (0750), so the SSH user
|
|
# usually cannot write into it, and a file they did write would be unreadable by
|
|
# the app. This way the ownership is right the first time and there is no
|
|
# "re-run the playbook afterwards" step.
|
|
if [[ ! -d "$DATA_ROOT" ]]; then
|
|
echo "$DATA_ROOT does not exist; run the menu-admin playbook first" >&2
|
|
exit 1
|
|
fi
|
|
|
|
APP_UID="${APP_UID:-1000}"
|
|
APP_GID="${APP_GID:-1000}"
|
|
|
|
if ! printf 'ADMIN_PASSWORD_HASH=%s\nSESSION_SECRET=%s\n' "$HASH" "$SECRET" | docker run --rm -i \
|
|
--user "$APP_UID:$APP_GID" -v "$DATA_ROOT:/data" --entrypoint sh "$IMAGE" \
|
|
-c 'umask 077 && cat > /data/site.env'; then
|
|
echo "could not write $ENV_FILE as $APP_UID:$APP_GID" >&2
|
|
exit 1
|
|
fi
|
|
|
|
unset PASSWORD REPEAT HASH SECRET
|
|
|
|
docker run --rm --user "$APP_UID:$APP_GID" -v "$DATA_ROOT:/data" --entrypoint sh "$IMAGE" \
|
|
-c 'ls -l /data/site.env'
|
|
|
|
echo "wrote $ENV_FILE, owned by $APP_UID:$APP_GID"
|
|
echo "restart the app container to pick it up; changing the password later means running this again with --force"
|