deploy/gen_env.sh: generate .env with openssl secrets
Builds deploy/.env from .env.example, generating SECRET_KEY and both database passwords with openssl (base64/hex only, so nothing needs quoting in the env file or the compose parser). Derives TS_HOSTNAME and ALLOWED_HOSTS from the tailnet hostname, optionally sets CORS_ALLOWED_ORIGINS/CSRF_TRUSTED_ORIGINS for split deployments, forces DEBUG=False and writes the file with mode 600. Modes: --no-prompt (defaults only), --update (refresh hostnames/auth key while keeping the existing secrets, reading the stored FQDN from ALLOWED_HOSTS), --force (rotate everything, with the SECRET_KEY warning in the docs). Refuses to overwrite an existing file otherwise. Verified: all modes, updated FQDN preservation, mode 600, and docker compose config accepting the generated file.
This commit is contained in:
+22
-1
@@ -37,6 +37,8 @@ docker compose -f compose.tailnet.yml up -d
|
||||
|
||||
Notes:
|
||||
- Project names are `…-tailnet` so both sets can be installed side by side.
|
||||
- Serve needs no tailnet policy change (Funnel requires the `funnel` node
|
||||
attribute in your ACLs), so this set works as soon as the sidecar joins.
|
||||
- Both sets use the same `deploy/data` directory (library, database, logs).
|
||||
Run one set per data directory — two MariaDB instances on one data dir would
|
||||
corrupt it. Giving the tailnet set its own `TS_HOSTNAME` (or running it on a
|
||||
@@ -44,11 +46,30 @@ Notes:
|
||||
- Switching a host from funnel to tailnet (or back) is just starting the other
|
||||
compose file with the same `.env`.
|
||||
|
||||
## Environment file
|
||||
|
||||
`gen_env.sh` builds `deploy/.env` from `.env.example`, generating `SECRET_KEY`
|
||||
and both database passwords with `openssl`:
|
||||
|
||||
```bash
|
||||
./gen_env.sh # asks for the Tailscale auth key + hostname(s)
|
||||
./gen_env.sh --no-prompt # secrets and defaults only; fill TS_AUTHKEY later
|
||||
./gen_env.sh --update # refresh hostnames/authkey, keep the existing secrets
|
||||
./gen_env.sh --force # regenerate everything, including SECRET_KEY
|
||||
```
|
||||
|
||||
It derives `TS_HOSTNAME` and `ALLOWED_HOSTS` from the tailnet hostname you
|
||||
give it, and can set `CORS_ALLOWED_ORIGINS`/`CSRF_TRUSTED_ORIGINS` when you
|
||||
provide the frontend's hostname for a split deployment. `--update` is the safe
|
||||
way to add hostnames later; `--force` rotates SECRET_KEY, which invalidates
|
||||
signed media URLs and stored e621 API keys. The file is written with mode 600
|
||||
and is git-ignored.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cd deploy
|
||||
cp .env.example .env # fill in TS_AUTHKEY, SECRET_KEY, ALLOWED_HOSTS, ...
|
||||
./gen_env.sh # or: cp .env.example .env and edit it yourself
|
||||
docker compose up -d # default: everything on one host, public funnel
|
||||
# or
|
||||
docker compose -f compose.frontend.yml up -d
|
||||
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/bin/bash
|
||||
# Generate deploy/.env from .env.example with fresh secrets.
|
||||
#
|
||||
# ./gen_env.sh # interactive: asks for the auth key/hostnames
|
||||
# ./gen_env.sh --no-prompt # secrets + defaults only
|
||||
# ./gen_env.sh --update # refresh hostnames/authkey, KEEP existing secrets
|
||||
# ./gen_env.sh --force # regenerate everything (new SECRET_KEY!)
|
||||
#
|
||||
# SECRET_KEY and the database passwords come from openssl. Rotating
|
||||
# SECRET_KEY invalidates signed media URLs and stored e621 API keys, which is
|
||||
# why --update keeps it.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
FORCE=0
|
||||
UPDATE=0
|
||||
NO_PROMPT=0
|
||||
TS_AUTHKEY=""
|
||||
FQDN=""
|
||||
FRONTEND=""
|
||||
DEFAULT_FQDN="j621.rainbow-herring.ts.net"
|
||||
|
||||
usage() {
|
||||
sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--force) FORCE=1 ;;
|
||||
--update) UPDATE=1 ;;
|
||||
--no-prompt) NO_PROMPT=1 ;;
|
||||
--ts-authkey) TS_AUTHKEY="${2:?missing value}"; shift ;;
|
||||
--hostname) FQDN="${2:?missing value}"; shift ;;
|
||||
--frontend) FRONTEND="${2:?missing value}"; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
command -v openssl >/dev/null || { echo "openssl is required." >&2; exit 1; }
|
||||
command -v python3 >/dev/null || { echo "python3 is required." >&2; exit 1; }
|
||||
[ -f .env.example ] || { echo "Run me from the deploy/ directory." >&2; exit 1; }
|
||||
|
||||
if [ -f .env ] && [ "$FORCE" -eq 0 ] && [ "$UPDATE" -eq 0 ]; then
|
||||
echo "deploy/.env already exists."
|
||||
echo " --update keeps the existing secrets and just refreshes the rest"
|
||||
echo " --force regenerates everything, including SECRET_KEY and DB passwords"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
existing() { grep -E "^$1=" .env 2>/dev/null | head -1 | cut -d= -f2- || true; }
|
||||
|
||||
gen_key() { openssl rand -base64 48 | tr -d '\n'; }
|
||||
gen_password() { openssl rand -hex 24; }
|
||||
|
||||
if [ "$UPDATE" -eq 1 ] && [ -f .env ]; then
|
||||
SECRET_KEY="$(existing SECRET_KEY)"
|
||||
DB_PASSWORD="$(existing DB_PASSWORD)"
|
||||
DB_ROOT_PASSWORD="$(existing DB_ROOT_PASSWORD)"
|
||||
TS_AUTHKEY="${TS_AUTHKEY:-$(existing TS_AUTHKEY)}"
|
||||
# TS_HOSTNAME is the short label; the full FQDN lives in ALLOWED_HOSTS.
|
||||
EXISTING_HOSTS="$(existing ALLOWED_HOSTS)"
|
||||
FQDN="${FQDN:-${EXISTING_HOSTS%%,*}}"
|
||||
fi
|
||||
|
||||
# Fill whatever is still missing.
|
||||
SECRET_KEY="${SECRET_KEY:-$(gen_key)}"
|
||||
DB_PASSWORD="${DB_PASSWORD:-$(gen_password)}"
|
||||
DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-$(gen_password)}"
|
||||
|
||||
if [ "$NO_PROMPT" -eq 0 ]; then
|
||||
if [ -z "$TS_AUTHKEY" ]; then
|
||||
read -rp "Tailscale auth key (tskey-..., Enter to fill in later): " TS_AUTHKEY
|
||||
fi
|
||||
if [ -z "$FQDN" ]; then
|
||||
read -rp "Tailnet hostname of this deployment [$DEFAULT_FQDN]: " FQDN
|
||||
fi
|
||||
FQDN="${FQDN:-$DEFAULT_FQDN}"
|
||||
if [ -z "$FRONTEND" ]; then
|
||||
read -rp "Frontend hostname for the split deploys (optional, Enter to skip): " FRONTEND
|
||||
fi
|
||||
fi
|
||||
FQDN="${FQDN:-$DEFAULT_FQDN}"
|
||||
|
||||
# Derive the rest from the tailnet hostname.
|
||||
TS_HOSTNAME="${FQDN%%.*}"
|
||||
ALLOWED_HOSTS="$FQDN,localhost,127.0.0.1"
|
||||
CORS_ALLOWED_ORIGINS="${FRONTEND:+https://$FRONTEND}"
|
||||
CSRF_TRUSTED_ORIGINS="${FRONTEND:+https://$FRONTEND}"
|
||||
|
||||
J621_SECRET_KEY="$SECRET_KEY" \
|
||||
J621_DB_PASSWORD="$DB_PASSWORD" \
|
||||
J621_DB_ROOT_PASSWORD="$DB_ROOT_PASSWORD" \
|
||||
J621_TS_AUTHKEY="$TS_AUTHKEY" \
|
||||
J621_TS_HOSTNAME="$TS_HOSTNAME" \
|
||||
J621_ALLOWED_HOSTS="$ALLOWED_HOSTS" \
|
||||
J621_CORS_ALLOWED_ORIGINS="$CORS_ALLOWED_ORIGINS" \
|
||||
J621_CSRF_TRUSTED_ORIGINS="$CSRF_TRUSTED_ORIGINS" \
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
text = Path(".env.example").read_text()
|
||||
|
||||
def apply(name):
|
||||
value = os.environ.get(f"J621_{name}")
|
||||
if not value:
|
||||
return text
|
||||
line = f"{name}={value}"
|
||||
pattern = re.compile(rf"^(?:# )?{re.escape(name)}=.*$", re.M)
|
||||
if pattern.search(text):
|
||||
return pattern.sub(line, text, count=1)
|
||||
return text + f"\n{line}\n"
|
||||
|
||||
for name in (
|
||||
"SECRET_KEY",
|
||||
"TS_AUTHKEY",
|
||||
"TS_HOSTNAME",
|
||||
"ALLOWED_HOSTS",
|
||||
"CORS_ALLOWED_ORIGINS",
|
||||
"CSRF_TRUSTED_ORIGINS",
|
||||
"DB_PASSWORD",
|
||||
"DB_ROOT_PASSWORD",
|
||||
):
|
||||
text = apply(name)
|
||||
|
||||
text = re.sub(r"^DEBUG=.*$", "DEBUG=False", text, count=1, flags=re.M)
|
||||
Path(".env").write_text(text)
|
||||
PY
|
||||
|
||||
chmod 600 .env
|
||||
|
||||
echo
|
||||
echo "Wrote deploy/.env (mode 600):"
|
||||
echo " SECRET_KEY $([ "$UPDATE" -eq 1 ] && echo 'kept from the existing file' || echo 'generated with openssl')"
|
||||
echo " DB passwords $([ "$UPDATE" -eq 1 ] && echo 'kept from the existing file' || echo 'generated with openssl')"
|
||||
echo " TS_HOSTNAME $TS_HOSTNAME"
|
||||
echo " ALLOWED_HOSTS $ALLOWED_HOSTS"
|
||||
[ -n "$CORS_ALLOWED_ORIGINS" ] && echo " cross-origin $CORS_ALLOWED_ORIGINS"
|
||||
[ -z "$TS_AUTHKEY" ] && echo " TS_AUTHKEY still empty - paste your Tailscale auth key before starting"
|
||||
echo
|
||||
echo "Next: docker compose -f compose.yml up -d (or a compose.tailnet*.yml variant)"
|
||||
Reference in New Issue
Block a user