Files
Gardomatic/remote/setup/provision-server.sh
T
2026-09-14 19:58:31 +02:00

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