server-setup/setup.sh

256 lines
10 KiB
Bash
Executable file

#!/bin/bash
set -e
test_ca_list=()
case "$1" in
--test)
ln -sf test.env .env
test_ca_list=(test/certs/cur-root.crt test/certs/pebble.minica.pem)
;;
--production)
ln -sf production.env .env
;;
*)
echo "usage: $0 --test|--production" >&2
exit 1
;;
esac
. .env
subdomains=('' mail git forum)
function fatal()
{
echo "fatal: $*" >&2
exit 1
}
function random_passwd()
{
tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 16
}
function wait_for_server()
{
local i
echo "waiting for server to start up..."
for i in {1..30}; do
sleep 1
if curl "http://$BASE_DOMAIN_NAME/" >/dev/null; then
echo "server up"
return
fi
done
fatal "server failed to start up after $i attempts"
}
function retry_if_failed()
{
local retry error_args=": $*"
if [[ "$1" == "-q" ]]; then
error_args=""
shift
fi
for retry in {1..3}; do
if "$@"; then
return
fi
echo "command failed (retry $retry)$error_args" >&2
sleep 2
done
fatal "command failed$error_args"
}
function forgejo()
{
docker run --rm codeberg.org/forgejo/forgejo:7 forgejo "$@"
}
function write_config()
{
local src="" dest="" vars="" new_vars=() mode="644" owner="root:root"
while (($#)); do
case "$1" in
--src)
src="$2"
shift 2
;;
--mode)
mode="$2"
shift 2
;;
--owner)
owner="$2"
shift 2
;;
--dest)
dest="$2"
shift 2
;;
--var)
[[ "$2" =~ ^([A-Za-z0-9_]+)= ]] || fatal "invalid --var argument"
vars+="\$${BASH_REMATCH[1]} "
new_vars+=("$2")
shift 2
;;
*)
fatal "write_config: unrecognized argument: $1"
;;
esac
done
: "${src:?missing --src argument}"
local dest_dir temp
dest_dir="$(dirname "${dest:?missing --dest argument}")"
temp="$(umask 577 && mktemp -p "$dest_dir")"
# printf '%q ' env "${new_vars[@]}" envsubst "$vars";
# echo "<" "$src" ">" "$temp"
env "${new_vars[@]}" envsubst "$vars" < "$src" > "$temp" || { rm -f "$temp"; exit 1; }
chmod "$mode" "$temp" || { rm -f "$temp"; exit 1; }
chown "$owner" "$temp" || { rm -f "$temp"; exit 1; }
if [[ ! -f "$dest" ]] && mv -v -T "$temp" "$dest"; then
return 0
fi
if diff -u --label="expanded $src" "$temp" "$dest"; then
rm -f "$temp"
return 0
else
rm -f "$temp"
fatal "config file doesn't match generated config for $dest expanded from $src"
fi
}
if [[ "$(id -u)" != 0 ]]; then
fatal "must be ran as root"
fi
apt-get remove -y -q docker.io docker-doc docker-compose podman-docker containerd runc
mkdir -p /var/lib/stalwart-mail
apt-get update -y -q
apt-get install ca-certificates curl jq gettext-base diffutils -y -q
# force using overlay2 driver so btrfs snapshots will snapshot the entire system and not miss all the docker stuff
mkdir -p /etc/docker
write_config --src templates/etc/docker/daemon.json --dest /etc/docker/daemon.json
write_config --src templates/etc/apt/sources.list.d/docker.list \
--dest /etc/apt/sources.list.d/docker.list \
--var dpkg_arch="$(dpkg --print-architecture)" \
--var VERSION_CODENAME="$(. /etc/os-release && echo "$VERSION_CODENAME")"
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
apt-get update -y -q
apt-get install certbot docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo openssl crudini git ssl-cert -y -q
if ((${#test_ca_list[@]})); then
install -m 644 "${test_ca_list[0]}" /usr/local/share/ca-certificates/test-root.crt
install -m 644 "${test_ca_list[1]}" /usr/local/share/ca-certificates/test-root2.crt
update-ca-certificates
fi
addgroup --gid=1000 git || true
[[ "$(getent group 1000)" =~ ^'git:x:1000:' ]] ||
fatal "some other group has gid 1000, which is needed for the git group"
adduser --system --shell=/bin/bash --gecos 'Git Version Control' --uid=1000 --ingroup=git --disabled-password --home=/var/lib/forgejo git || true
[[ "$(getent passwd 1000)" == 'git:x:1000:1000:Git Version Control,,,:/var/lib/forgejo:/bin/bash' ]] ||
fatal "some other user has gid 1000, which is needed for the git user"
[[ -f ~git/.ssh/id_ed25519 ]] || sudo -u git ssh-keygen -f ~git/.ssh/id_ed25519 -t ed25519 -C "Forgejo Host Key" -N ""
[[ -f ~git/.ssh/authorized_keys ]] || sudo -u git cat ~git/.ssh/id_ed25519.pub | sudo -u git tee ~git/.ssh/authorized_keys
sudo -u git chmod 600 ~git/.ssh/authorized_keys
write_config --src templates/usr/local/bin/gitea --dest /usr/local/bin/gitea --mode 755
mkdir -p /etc/forgejo
rm -rf /var/www/html
mkdir -p /var/www/html
chown git:git /var/www/html
chmod 775 /var/www/html
(cd /var/www/html && sudo -u git git init --no-initial-branch)
(cd /var/www/html && sudo -u git git remote add -t heads/rendered --mirror=fetch origin /data/git/repositories/libre-chip/website.git)
chown root:git /etc/forgejo
chmod 770 /etc/forgejo
if [[ ! -f /etc/forgejo/app.ini ]]; then
write_config --src templates/etc/forgejo/app.ini \
--dest /etc/forgejo/app.ini --mode 640 --owner root:git \
--var BASE_DOMAIN_NAME="$BASE_DOMAIN_NAME" \
--var SECRET_KEY="$(forgejo generate secret SECRET_KEY)" \
--var INTERNAL_TOKEN="$(forgejo generate secret INTERNAL_TOKEN)" \
--var MAIL_PASSWD="$(random_passwd)" \
--var JWT_SECRET="$(forgejo generate secret JWT_SECRET)" \
--var LFS_JWT_SECRET="$(forgejo generate secret LFS_JWT_SECRET)"
fi
mkdir -p /var/lib/stalwart-mail/etc
mail_passwd=""
mail_passwd_hash=""
if [[ ! -f /var/lib/stalwart-mail/etc/config.toml ]]; then
mail_passwd="$(random_passwd)"
write_config --src templates/var/lib/stalwart-mail/cli.sh \
--dest /var/lib/stalwart-mail/cli.sh --mode 400 \
--var wd="$(pwd)" --var mail_passwd="$mail_passwd"
mail_passwd_hash="$(echo -n "$mail_passwd" | openssl passwd -6 -stdin)"
write_config --src templates/var/lib/stalwart-mail/etc/config.toml \
--dest /var/lib/stalwart-mail/etc/config.toml --mode 600 \
--var mail_passwd_hash="$mail_passwd_hash" \
--var BASE_DOMAIN_NAME="$BASE_DOMAIN_NAME"
fi
. /var/lib/stalwart-mail/cli.sh
if [[ ! -f /var/discourse/containers/app.yml ]]; then
if [[ ! -d /var/discourse/containers ]]; then
git clone https://github.com/discourse/discourse_docker.git /var/discourse
chmod 700 /var/discourse/containers
fi
forum_smtp_passwd="$(random_passwd)"
write_config --src templates/var/discourse/containers/app.yml \
--dest /var/discourse/containers/app.yml --mode 400 \
--var BASE_DOMAIN_NAME="$BASE_DOMAIN_NAME" \
--var forum_smtp_passwd="$forum_smtp_passwd" \
--var mail_passwd="$mail_passwd"
fi
wd="$(pwd)"
if ! [[ "$wd" =~ ^/[-/a-zA-Z0-9_]*$ ]]; then
fatal "invalid characters in current directory: $wd"
fi
nginx_container="$(docker create --rm -v /var/www/.well-known/acme-challenge:/var/www/.well-known/acme-challenge:ro -v "$wd"/http_only_nginx_templates:/etc/nginx/templates:ro -p 80:80 nginx:bookworm)"
docker start "$nginx_container"
trap 'docker stop "$nginx_container"' EXIT
echo "waiting for server to come up..."
for _ in {0..30}; do
sleep 1
if curl "http://$BASE_DOMAIN_NAME/" >/dev/null; then
break
fi
done
echo "server up"
certbot_args=(certonly -n --email "postmaster@$BASE_DOMAIN_NAME" "--server=$ACME_SERVER_URL" --cert-name server --agree-tos --webroot --webroot-path /var/www)
certbot_args+=(--disable-hook-validation --post-hook "cd '$wd' && docker compose -p server restart")
for subdomain in "${subdomains[@]}"; do
if [[ -n "$subdomain" ]]; then
subdomain+=.
fi
certbot_args+=(-d "$subdomain$BASE_DOMAIN_NAME")
certbot_args+=(-d "$subdomain$ALT_BASE_DOMAIN_NAME")
done
retry_if_failed certbot "${certbot_args[@]}"
trap EXIT
docker stop "$nginx_container"
DOCKER_BUILDKIT=1 docker compose -p server up -d
sleep 10
if [[ -n "$mail_passwd_hash" ]]; then
forgejo_smtp_passwd="$(crudini --get /etc/forgejo/app.ini mailer PASSWD)"
stalwart-cli domain create "$BASE_DOMAIN_NAME"
curl -u "admin:$mail_passwd" "https://mail.$BASE_DOMAIN_NAME/api/dkim" --data-binary '{"id":null,"algorithm":"Ed25519","domain":"'"$BASE_DOMAIN_NAME"'","selector":null}' > /dev/null
curl -u "admin:$mail_passwd" "https://mail.$BASE_DOMAIN_NAME/api/dkim" --data-binary '{"id":null,"algorithm":"Rsa","domain":"'"$BASE_DOMAIN_NAME"'","selector":null}' > /dev/null
stalwart-cli account create -d 'Admin Account' -i true -a "postmaster@$BASE_DOMAIN_NAME" 'admin' "$mail_passwd"
stalwart-cli account create -d 'Forgejo Server' -i false -a "forgejo@$BASE_DOMAIN_NAME" 'forgejo' "$forgejo_smtp_passwd"
add_postmaster=(docker compose -p server exec -T -u git forgejo forgejo admin user create --admin --username postmaster --password "$mail_passwd" --email "postmaster@$BASE_DOMAIN_NAME")
retry_if_failed -q "${add_postmaster[@]}"
forum_smtp_passwd="$(sed 's/^ *DISCOURSE_SMTP_PASSWORD: "*\([^"]*\)"$/\1/p; d' < /var/discourse/containers/app.yml)"
[[ -n "$forum_smtp_passwd" ]] || fatal "can't parse smtp password out of /var/discourse/containers/app.yml"
stalwart-cli account create -d 'Forum Replies' -i false -a "@$BASE_DOMAIN_NAME" 'forum' "$forum_smtp_passwd"
stalwart-cli account create -d 'Forum Notifications' -i false -a "forum-noreply@$BASE_DOMAIN_NAME" 'forum-noreply' "$forum_smtp_passwd"
forgejo_api=(retry_if_failed -q curl --fail-with-body -u "postmaster:$mail_passwd" -H 'Accept: application/json' -H 'Content-Type: application/json')
"${forgejo_api[@]}" -X 'POST' "https://git.$BASE_DOMAIN_NAME/api/v1/orgs" -d '{"username": "libre-chip"}' > /dev/null
"${forgejo_api[@]}" -X 'POST' "https://git.$BASE_DOMAIN_NAME/api/v1/orgs/libre-chip/repos" -d '{"name": "website"}' > /dev/null
post_receive_hook="$(jq -csR '{content:.}' < website_git_post_receive_hook.sh)"
"${forgejo_api[@]}" -X 'PATCH' "https://git.$BASE_DOMAIN_NAME/api/v1/repos/libre-chip/website/hooks/git/post-receive" -d "$post_receive_hook" > /dev/null
fi
(
cd /var/discourse
# must run after starting mail server since it validates POP3
./launcher bootstrap app
./launcher start app
)