251 lines
8.0 KiB
Bash
Executable File
251 lines
8.0 KiB
Bash
Executable File
#!/bin/bash
|
|
set -Eeuo pipefail
|
|
|
|
# Run this script as root or as an administrator with passwordless sudo.
|
|
# Configuration is read from a trusted shell-style .env file or standard input.
|
|
|
|
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
ENV_FILE="${SCRIPT_DIR}/.env"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: ./provision-server.sh [--env-file PATH|-]
|
|
|
|
Provision the Gardomatic server and install its runtime configuration.
|
|
The default configuration file is .env next to this script. Use - to stream the
|
|
configuration over SSH without storing the source file on the server.
|
|
EOF
|
|
}
|
|
|
|
die() {
|
|
printf 'Error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
while (($# > 0)); do
|
|
case "$1" in
|
|
--env-file)
|
|
(($# >= 2)) || die "--env-file requires a path"
|
|
ENV_FILE=$2
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
die "unknown argument: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$ENV_FILE" == - ]]; then
|
|
ENV_SOURCE=/dev/stdin
|
|
ENV_LABEL='standard input'
|
|
else
|
|
[[ -f "$ENV_FILE" ]] || die "configuration file not found: $ENV_FILE"
|
|
env_permissions=$(stat -c '%a' "$ENV_FILE")
|
|
(( (8#$env_permissions & 077) == 0 )) || die "$ENV_FILE must not be readable or writable by group or others (run: chmod 600 '$ENV_FILE')"
|
|
ENV_SOURCE=$ENV_FILE
|
|
ENV_LABEL=$ENV_FILE
|
|
fi
|
|
|
|
# The file is deliberately sourced so quoted values work. It must therefore be
|
|
# controlled by the administrator running this script.
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
source "$ENV_SOURCE"
|
|
set +a
|
|
|
|
require_variable() {
|
|
local name=$1
|
|
[[ -n "${!name:-}" ]] || die "required variable $name is missing in $ENV_LABEL"
|
|
}
|
|
|
|
require_identifier() {
|
|
local name=$1
|
|
local value=${!name:-}
|
|
[[ "$value" =~ ^[a-z_][a-z0-9_]*$ ]] || die "$name must be a lowercase PostgreSQL identifier"
|
|
}
|
|
|
|
require_variable GARDOMATIC_DB_PASSWORD
|
|
require_variable GARDOMATIC_DB_DSN
|
|
require_variable GARDOMATIC_WEB_BASE_URL
|
|
require_identifier GARDOMATIC_DB_NAME
|
|
require_identifier GARDOMATIC_DB_USER
|
|
|
|
GARDOMATIC_DEMO_RESET_ENABLED=${GARDOMATIC_DEMO_RESET_ENABLED:-false}
|
|
[[ "$GARDOMATIC_DEMO_RESET_ENABLED" == true || "$GARDOMATIC_DEMO_RESET_ENABLED" == false ]] || die "GARDOMATIC_DEMO_RESET_ENABLED must be true or false"
|
|
if [[ "$GARDOMATIC_DEMO_RESET_ENABLED" == true ]]; then
|
|
require_variable GARDOMATIC_DEMO_ACCOUNT_EMAIL
|
|
fi
|
|
|
|
case "${GARDOMATIC_SMTP_MODE:-file}" in
|
|
smtp)
|
|
require_variable GARDOMATIC_SMTP_HOST
|
|
require_variable GARDOMATIC_SMTP_USERNAME
|
|
require_variable GARDOMATIC_SMTP_PASSWORD
|
|
require_variable GARDOMATIC_SMTP_SENDER
|
|
;;
|
|
file)
|
|
require_variable GARDOMATIC_SMTP_FILE_PATH
|
|
;;
|
|
*)
|
|
die "GARDOMATIC_SMTP_MODE must be smtp or file"
|
|
;;
|
|
esac
|
|
|
|
GARDOMATIC_SETUP_TIMEZONE=${GARDOMATIC_SETUP_TIMEZONE:-Europe/Berlin}
|
|
GARDOMATIC_MIGRATE_VERSION=${GARDOMATIC_MIGRATE_VERSION:-4.19.1}
|
|
GARDOMATIC_REBOOT=${GARDOMATIC_REBOOT:-false}
|
|
[[ "$GARDOMATIC_REBOOT" == true || "$GARDOMATIC_REBOOT" == false ]] || die "GARDOMATIC_REBOOT must be true or false"
|
|
|
|
readonly GARDOMATIC_SERVICE_USER=gardomatic
|
|
|
|
run_as_root() {
|
|
if [[ $(id -u) -eq 0 ]]; then
|
|
"$@"
|
|
else
|
|
sudo -n "$@"
|
|
fi
|
|
}
|
|
|
|
run_as_postgres() {
|
|
if [[ $(id -u) -eq 0 ]]; then
|
|
runuser -u postgres -- "$@"
|
|
else
|
|
sudo -n -u postgres "$@"
|
|
fi
|
|
}
|
|
|
|
if [[ $(id -u) -ne 0 ]]; then
|
|
sudo -n true || die "the SSH administrator needs passwordless sudo"
|
|
fi
|
|
|
|
# Force consistent command output while locales are being installed.
|
|
export LC_ALL=en_US.UTF-8
|
|
|
|
run_as_root apt update
|
|
run_as_root apt install --yes software-properties-common locales curl rsync ufw
|
|
run_as_root add-apt-repository --yes universe
|
|
run_as_root apt update
|
|
run_as_root timedatectl set-timezone "$GARDOMATIC_SETUP_TIMEZONE"
|
|
run_as_root apt --yes install locales-all
|
|
|
|
# Gardomatic runs under a dedicated service account. It has no login shell, SSH
|
|
# keys, password or sudo privileges; deployments continue through the configured
|
|
# server administrator account.
|
|
if id "$GARDOMATIC_SERVICE_USER" >/dev/null 2>&1; then
|
|
if ! getent group "$GARDOMATIC_SERVICE_USER" >/dev/null 2>&1; then
|
|
run_as_root groupadd --system "$GARDOMATIC_SERVICE_USER"
|
|
fi
|
|
run_as_root usermod --lock --shell /usr/sbin/nologin "$GARDOMATIC_SERVICE_USER"
|
|
run_as_root usermod --gid "$GARDOMATIC_SERVICE_USER" "$GARDOMATIC_SERVICE_USER"
|
|
run_as_root deluser --quiet "$GARDOMATIC_SERVICE_USER" sudo >/dev/null 2>&1 || true
|
|
else
|
|
run_as_root useradd --system --user-group --create-home --home-dir /var/lib/gardomatic \
|
|
--shell /usr/sbin/nologin "$GARDOMATIC_SERVICE_USER"
|
|
fi
|
|
run_as_root install -d -m 0750 -o gardomatic -g gardomatic /var/lib/gardomatic
|
|
|
|
run_as_root ufw allow 22
|
|
run_as_root ufw allow 4040/tcp
|
|
run_as_root ufw --force enable
|
|
run_as_root apt --yes install fail2ban
|
|
|
|
# Install the migrate CLI for the host architecture.
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) migrate_arch=amd64 ;;
|
|
aarch64|arm64) migrate_arch=arm64 ;;
|
|
*) die "unsupported architecture for migrate: $(uname -m)" ;;
|
|
esac
|
|
|
|
download_dir=$(mktemp -d)
|
|
runtime_env=$(mktemp)
|
|
cleanup() {
|
|
rm -rf -- "$download_dir"
|
|
rm -f -- "$runtime_env"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
migrate_archive="$download_dir/migrate.tar.gz"
|
|
curl --fail --location --show-error \
|
|
"https://github.com/golang-migrate/migrate/releases/download/v${GARDOMATIC_MIGRATE_VERSION}/migrate.linux-${migrate_arch}.tar.gz" \
|
|
--output "$migrate_archive"
|
|
tar -xzf "$migrate_archive" -C "$download_dir"
|
|
run_as_root install -m 0755 "$download_dir/migrate" /usr/local/bin/migrate
|
|
|
|
run_as_root apt --yes install postgresql postgresql-contrib
|
|
|
|
if ! run_as_postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname = '${GARDOMATIC_DB_NAME}'" | grep -qx 1; then
|
|
run_as_postgres createdb "$GARDOMATIC_DB_NAME"
|
|
fi
|
|
run_as_postgres psql -d "$GARDOMATIC_DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS citext"
|
|
run_as_postgres psql -d "$GARDOMATIC_DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS pgcrypto"
|
|
if ! run_as_postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname = '${GARDOMATIC_DB_USER}'" | grep -qx 1; then
|
|
printf '%s\n' "CREATE ROLE \"${GARDOMATIC_DB_USER}\" WITH LOGIN PASSWORD :'db_password';" | \
|
|
run_as_postgres psql -v db_password="$GARDOMATIC_DB_PASSWORD"
|
|
else
|
|
printf '%s\n' "ALTER ROLE \"${GARDOMATIC_DB_USER}\" WITH LOGIN PASSWORD :'db_password';" | \
|
|
run_as_postgres psql -v db_password="$GARDOMATIC_DB_PASSWORD"
|
|
fi
|
|
run_as_postgres psql -c "ALTER DATABASE \"${GARDOMATIC_DB_NAME}\" OWNER TO \"${GARDOMATIC_DB_USER}\";"
|
|
|
|
# Generate a dedicated systemd environment file. Setup-only values such as the
|
|
# raw database password are intentionally not copied into the service environment.
|
|
write_environment_variable() {
|
|
local name=$1
|
|
local value=${!name:-}
|
|
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "$name must not contain newlines"
|
|
value=${value//\\/\\\\}
|
|
value=${value//\"/\\\"}
|
|
value=${value//\$/\\$}
|
|
value=${value//\`/\\\`}
|
|
printf '%s="%s"\n' "$name" "$value" >>"$runtime_env"
|
|
}
|
|
|
|
runtime_variables=(
|
|
GARDOMATIC_ENV
|
|
GARDOMATIC_DB_DSN
|
|
GARDOMATIC_DB_MAX_OPEN_CONNS
|
|
GARDOMATIC_DB_MAX_IDLE_CONNS
|
|
GARDOMATIC_DB_MAX_IDLE_TIME
|
|
GARDOMATIC_API_HOST
|
|
GARDOMATIC_API_PORT
|
|
GARDOMATIC_WEB_HOST
|
|
GARDOMATIC_WEB_PORT
|
|
GARDOMATIC_API_BASE_URL
|
|
GARDOMATIC_WEB_BASE_URL
|
|
GARDOMATIC_SESSION_COOKIE_NAME
|
|
GARDOMATIC_SESSION_LIFETIME
|
|
GARDOMATIC_SESSION_IDLE_TIMEOUT
|
|
GARDOMATIC_COOKIE_SECURE
|
|
GARDOMATIC_RATE_LIMIT_ENABLED
|
|
GARDOMATIC_RATE_LIMIT_RPS
|
|
GARDOMATIC_RATE_LIMIT_BURST
|
|
GARDOMATIC_CORS_TRUSTED_ORIGINS
|
|
GARDOMATIC_DEMO_RESET_ENABLED
|
|
GARDOMATIC_DEMO_ACCOUNT_EMAIL
|
|
GARDOMATIC_SMTP_MODE
|
|
GARDOMATIC_SMTP_HOST
|
|
GARDOMATIC_SMTP_PORT
|
|
GARDOMATIC_SMTP_USERNAME
|
|
GARDOMATIC_SMTP_PASSWORD
|
|
GARDOMATIC_SMTP_SENDER
|
|
GARDOMATIC_SMTP_FILE_PATH
|
|
)
|
|
for variable in "${runtime_variables[@]}"; do
|
|
write_environment_variable "$variable"
|
|
done
|
|
|
|
run_as_root install -D -m 0600 -o root -g root "$runtime_env" /etc/gardomatic/gardomatic.env
|
|
|
|
run_as_root apt --yes -o Dpkg::Options::="--force-confnew" upgrade
|
|
|
|
printf 'Server setup complete. Runtime configuration installed at /etc/gardomatic/gardomatic.env.\n'
|
|
if [[ "$GARDOMATIC_REBOOT" == true ]]; then
|
|
run_as_root reboot
|
|
else
|
|
printf 'Reboot skipped. Set GARDOMATIC_REBOOT=true in %s to reboot automatically.\n' "$ENV_LABEL"
|
|
fi
|