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:
@@ -39,7 +39,8 @@ Project constraints (do not regress):
|
|||||||
serve.*.tailnet.json + compose.tailnet*.yml are the same stacks without
|
serve.*.tailnet.json + compose.tailnet*.yml are the same stacks without
|
||||||
AllowFunnel (tailnet-only, no public exposure). Images are pushed to the
|
AllowFunnel (tailnet-only, no public exposure). Images are pushed to the
|
||||||
Gitea registry with deploy/push_*.sh (multi-arch, :latest + :sha, GIT_HASH
|
Gitea registry with deploy/push_*.sh (multi-arch, :latest + :sha, GIT_HASH
|
||||||
baked in for the version pill).
|
baked in for the version pill); deploy/gen_env.sh generates .env with
|
||||||
|
openssl secrets (--update keeps SECRET_KEY, --force rotates it).
|
||||||
- Security/permission tests live in backend/apps/core/tests and need a
|
- Security/permission tests live in backend/apps/core/tests and need a
|
||||||
one-time grant: GRANT ALL ON `test_j621`.* TO 'j621'@'%';
|
one-time grant: GRANT ALL ON `test_j621`.* TO 'j621'@'%';
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -37,6 +37,8 @@ docker compose -f compose.tailnet.yml up -d
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Project names are `…-tailnet` so both sets can be installed side by side.
|
- 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).
|
- 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
|
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
|
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
|
- Switching a host from funnel to tailnet (or back) is just starting the other
|
||||||
compose file with the same `.env`.
|
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
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd deploy
|
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
|
docker compose up -d # default: everything on one host, public funnel
|
||||||
# or
|
# or
|
||||||
docker compose -f compose.frontend.yml up -d
|
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