Executable
+242
@@ -0,0 +1,242 @@
|
||||
#!/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
|
||||
|
||||
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_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
|
||||
Reference in New Issue
Block a user