feat: unified launcher, multi-OS hardening, login alerts & auto-updates

Restructure around a single entry point (automations.sh) with a Gum wizard and
a self-extracting bundle for repo-less installs. Add scripts/oslib.sh so the
provisioning scripts (setup-host, harden-ssh, harden-jumphost, sshuser) run on
Alpine/Debian/Alma; seed root keys from globals/.
- ntfy SSH-login alerts (user, source IP, key, region, jump target) via pam_exec
- daily auto-updates: AUTO_REBOOT=idle reboots only when no SSH active; opt-in
  Alpine stable-branch upgrades (ALLOW_RELEASE_UPGRADE)
- cloud-init: generic base/jumphost + per-deployment, which harden SSH by
  default on fresh VMs
- pocket-id: optional WebFinger block (BASE_DOMAIN), tag v2.8.0
- headscale: fix oidc.expiry schema for 0.28 so the container starts
- Gitea release workflow on tag (TOKEN_GITEA); repo URLs -> Gitea
- README/LICENSE/.gitignore/.gitattributes (force LF)
This commit is contained in:
2026-06-12 15:24:30 -05:00
parent 7faa9098de
commit 8fbeb8f6b0
15 changed files with 280 additions and 195 deletions
+2
View File
@@ -122,6 +122,8 @@ deployments/<name>/ # one folder per stack
is, then run the scripts.
- **Stand up a stack**: each deployment ships its own `cloud-init.yml` (e.g.
[`deployments/pocket-id/cloud-init.yml`](deployments/pocket-id/cloud-init.yml)).
These assume a fresh VM, so they **harden SSH first** (`harden-ssh.sh`,
`HARDEN_SSH=1` by default) and then deploy the stack.
Fill in `REPO_URL` and the values at the top of the `runcmd` block, paste as
instance user-data, and the host configures itself on first boot.
+3 -1
View File
@@ -74,7 +74,9 @@ ask_deployment_vars() {
case "$1" in
pocket-id)
ask POCKETID_DOMAIN "Public hostname (e.g. id.example.com)"
ask ACME_EMAIL "Let's Encrypt email" ;;
ask ACME_EMAIL "Let's Encrypt email"
ask BASE_DOMAIN "WebFinger base domain (blank = none / use webfinger deployment)" optional
if [[ -n "${BASE_DOMAIN:-}" ]]; then ask REDIRECT_URL "Redirect target for the base domain"; fi ;;
beszel)
ask BESZEL_DOMAIN "Public hostname (e.g. monitoring.example.com)"
ask ACME_EMAIL "Let's Encrypt email" ;;
+12 -7
View File
@@ -1,6 +1,6 @@
#cloud-config
#
# Beszel (monitoring hub) — unattended deploy on a fresh Alpine host.
# Beszel (monitoring hub) — harden SSH, then deploy, on a fresh Alpine host.
#
# Fill in REPO_URL and the values in the runcmd block, then paste this as the
# instance user-data. DNS for BESZEL_DOMAIN must point at this host and ports
@@ -16,15 +16,20 @@ runcmd:
set -e
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git
REPO_BRANCH=main
HARDEN_SSH=1 # harden SSH on this fresh VM (set 0 to skip)
SSH_PORT=22
ALLOWED_IP= # optional: whitelist your client IP in sshguard
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations
cd /opt/automations
# Optional: install shared admin SSH keys from globals/ for root before deploy
# . scripts/lib.sh && load_globals \
# && install -d -m700 /root/.ssh && resolve_ssh_keys >> /root/.ssh/authorized_keys
# Optional: harden SSH first (prints a fresh root key — capture it!)
# SKIP_PROMPTS=1 FORCE=1 bash scripts/harden-ssh.sh
# Harden SSH on this fresh VM: PQ KEX, key-only auth, sshguard. Seeds root
# from globals/authorized_keys (or SSH_KEYS_URL). NOTE: harden-ssh also
# prints a generated root key to the serial console — capture it, or rely
# on the seeded keys.
if [ "$HARDEN_SSH" = 1 ]; then
SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" SKIP_PROMPTS=1 FORCE=1 \
bash scripts/harden-ssh.sh
fi
BESZEL_DOMAIN=monitoring.example.com \
ACME_EMAIL=admin@example.com \
+12 -7
View File
@@ -1,6 +1,6 @@
#cloud-config
#
# Headscale (Tailscale control server) — unattended deploy on fresh Alpine.
# Headscale (Tailscale control server) — harden SSH, then deploy, on fresh Alpine.
#
# Fill in REPO_URL and the values in the runcmd block, then paste this as the
# instance user-data. DNS for HEADSCALE_DOMAIN must point at this host and
@@ -16,15 +16,20 @@ runcmd:
set -e
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git
REPO_BRANCH=main
HARDEN_SSH=1 # harden SSH on this fresh VM (set 0 to skip)
SSH_PORT=22
ALLOWED_IP= # optional: whitelist your client IP in sshguard
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations
cd /opt/automations
# Optional: install shared admin SSH keys from globals/ for root before deploy
# . scripts/lib.sh && load_globals \
# && install -d -m700 /root/.ssh && resolve_ssh_keys >> /root/.ssh/authorized_keys
# Optional: harden SSH first (prints a fresh root key — capture it!)
# SKIP_PROMPTS=1 FORCE=1 bash scripts/harden-ssh.sh
# Harden SSH on this fresh VM: PQ KEX, key-only auth, sshguard. Seeds root
# from globals/authorized_keys (or SSH_KEYS_URL). NOTE: harden-ssh also
# prints a generated root key to the serial console — capture it, or rely
# on the seeded keys.
if [ "$HARDEN_SSH" = 1 ]; then
SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" SKIP_PROMPTS=1 FORCE=1 \
bash scripts/harden-ssh.sh
fi
HEADSCALE_DOMAIN=hs.example.com \
ACME_EMAIL=admin@example.com \
+9 -6
View File
@@ -1,8 +1,8 @@
# headscale config.yaml -- 0.28.0
#
# Placeholders __VAR__ are substituted at deploy time by deploy.sh from the
# values in .env. Don't set them here directly unless you're running
# headscale outside of the deploy script.
# Placeholder tokens (UPPER_SNAKE wrapped in double underscores) are
# substituted at deploy time by deploy.sh from the values in .env. Don't set
# them here directly unless you're running headscale outside the deploy script.
server_url: https://__HEADSCALE_DOMAIN__
listen_addr: 0.0.0.0:8080
@@ -56,6 +56,7 @@ dns:
- 1.0.0.1
- 2606:4700:4700::1111
- 2606:4700:4700::1001
split: {}
search_domains: []
extra_records: []
@@ -80,6 +81,8 @@ oidc:
# - tailscale-admins
# allowed_domains:
# - example.com
expiry:
from_authentication_path: false
refresh_period: 180d
# How long a node stays authenticated before re-auth is required. This is a
# duration scalar (not a map). use_expiry_from_token overrides it with the
# OIDC token's own expiry when true.
expiry: 180d
use_expiry_from_token: false
+57 -54
View File
@@ -225,8 +225,9 @@ sed -i \
-e "s/__OIDC_CLIENT_SECRET__/$(sed_escape "$OIDC_CLIENT_SECRET")/g" \
"$STACK_DIR/config.yaml"
# Final sanity check
grep -q '__.*__' "$STACK_DIR/config.yaml" \
# Final sanity check (ignore comment lines so documentation tokens don't
# trip a false positive).
grep -v '^[[:space:]]*#' "$STACK_DIR/config.yaml" | grep -q '__[A-Za-z_]*__' \
&& warn "config.yaml still has un-substituted __PLACEHOLDER__ markers!" \
|| true
@@ -312,55 +313,57 @@ EOF
# __ARCHIVE_BELOW__ marker is the embedded tar.gz payload (base64).
exit 0
__ARCHIVE_BELOW__
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
+14 -3
View File
@@ -4,10 +4,10 @@
# ─── Public hostname ────────────────────────────────────────────────────────
# Bare hostname (no scheme) of the pocket-id deployment. Used by Caddy for
# TLS issuance, by anubis as the cookie domain, and to derive APP_URL.
POCKETID_DOMAIN=id.example.com
POCKETID_DOMAIN=
# Email for Let's Encrypt registration / expiry notifications.
ACME_EMAIL=admin@example.com
ACME_EMAIL=
# ─── Pocket-ID ──────────────────────────────────────────────────────────────
# APP_URL is set automatically from POCKETID_DOMAIN in compose; no need to
@@ -37,8 +37,19 @@ PGID=1000
# Only needed while the anubis-pid service is enabled in compose.
ANUBIS_PID_KEY=
# ─── WebFinger (optional) ───────────────────────────────────────────────────
# Leave BOTH blank unless the base domain has no other web server and you want
# THIS Caddy to also serve /.well-known/webfinger (OIDC discovery for Tailscale
# et al.) at the apex. When set, deploy.sh appends a second site block.
# Otherwise use the dedicated `webfinger` deployment.
# BASE_DOMAIN apex to serve webfinger from (e.g. example.com)
# REDIRECT_URL where non-webfinger requests to the apex are 301'd
# If you set one you must set both.
BASE_DOMAIN=
REDIRECT_URL=
# ─── Image tags ─────────────────────────────────────────────────────────────
# Pin for reproducible deploys.
POCKETID_TAG=v2
POCKETID_TAG=v2.8.0
CADDY_TAG=2-alpine
ANUBIS_TAG=latest
+6 -45
View File
@@ -1,9 +1,11 @@
# Caddyfile for pocket-id stack.
# Caddyfile for pocket-id stack (base).
#
# Two site blocks:
# $POCKETID_DOMAIN pocket-id (via anubis PoW gate)
# $BASE_DOMAIN /.well-known/webfinger for OIDC discovery; everything
# else redirects to $REDIRECT_URL
#
# An optional WebFinger site block (Caddyfile.webfinger) is appended by
# deploy.sh when BASE_DOMAIN is set -- for the case where the base domain has
# no other web server and you want this Caddy to serve OIDC discovery too.
# Otherwise use the dedicated `webfinger` deployment.
{
email {$ACME_EMAIL}
@@ -38,44 +40,3 @@
format console
}
}
# ----------------------------------------------------------------------------
# Base domain: WebFinger for OIDC discovery + redirect everything else.
# Required for Tailscale's custom-OIDC setup -- the email's domain must
# serve /.well-known/webfinger pointing at the issuer.
# ----------------------------------------------------------------------------
{$BASE_DOMAIN} {
encode zstd gzip
@webfinger path /.well-known/webfinger
handle @webfinger {
header Content-Type "application/jrd+json"
header Cache-Control "public, max-age=3600"
templates
respond `{
"subject": "{{.Req.URL.Query.Get "resource"}}",
"links": [
{
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": "https://{$POCKETID_DOMAIN}"
}
]
}` 200
}
handle {
redir {$REDIRECT_URL}{uri} permanent
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
log {
output stdout
format console
}
}
+42
View File
@@ -0,0 +1,42 @@
# ----------------------------------------------------------------------------
# OPTIONAL: appended to the deployed Caddyfile by deploy.sh when BASE_DOMAIN is
# set. Serves /.well-known/webfinger at the base domain (so Tailscale et al.
# can discover the OIDC issuer) and redirects everything else to $REDIRECT_URL.
# Needs BASE_DOMAIN + REDIRECT_URL in .env (passed to caddy via compose).
# ----------------------------------------------------------------------------
{$BASE_DOMAIN} {
encode zstd gzip
@webfinger path /.well-known/webfinger
handle @webfinger {
header Content-Type "application/jrd+json"
header Cache-Control "public, max-age=3600"
templates
respond `{
"subject": "{{.Req.URL.Query.Get "resource"}}",
"links": [
{
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": "https://{$POCKETID_DOMAIN}"
}
]
}` 200
}
handle {
redir {$REDIRECT_URL}{uri} permanent
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
log {
output stdout
format console
}
}
+5
View File
@@ -16,6 +16,11 @@ identity provider for the other stacks (headscale, beszel, webfinger).
`MAXMIND_LICENSE_KEY` (audit-log geolocation) is optional. See
[`.env.example`](.env.example) for the full list.
**Optional WebFinger:** set `BASE_DOMAIN` (and `REDIRECT_URL`) to have this
Caddy also serve `/.well-known/webfinger` at the apex — useful when the base
domain has no other web server. Leave both blank to run pocket-id only and use
the dedicated [`webfinger`](../webfinger/) deployment instead.
## Deploy
From the repo root via the launcher:
+5 -5
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
#
# build.sh -- (re)embed docker-compose.yml, Caddyfile, .env.example into
# deploy.sh as a base64-encoded tar.gz payload after the __ARCHIVE_BELOW__
# marker. Idempotent: strips any existing payload first.
# build.sh -- (re)embed docker-compose.yml, Caddyfile, Caddyfile.webfinger and
# .env.example into deploy.sh as a base64-encoded tar.gz payload after the
# __ARCHIVE_BELOW__ marker. Idempotent: strips any existing payload first.
#
# Run after editing any of the loose files. The resulting deploy.sh is
# self-contained and can be scp'd to the target box on its own.
@@ -14,13 +14,13 @@ SCRIPT="$DIR/deploy.sh"
MARKER="__ARCHIVE_BELOW__"
[[ -f "$SCRIPT" ]] || { echo "deploy.sh not found at $SCRIPT" >&2; exit 1; }
for f in docker-compose.yml Caddyfile .env.example; do
for f in docker-compose.yml Caddyfile Caddyfile.webfinger .env.example; do
[[ -f "$DIR/$f" ]] || { echo "Missing $DIR/$f" >&2; exit 1; }
done
# tar -> gzip -> base64. Files only (no leading ./), wrapped at 76 cols
# so the embedded blob is git-friendly.
PAYLOAD=$(tar -czf - -C "$DIR" docker-compose.yml Caddyfile .env.example | base64)
PAYLOAD=$(tar -czf - -C "$DIR" docker-compose.yml Caddyfile Caddyfile.webfinger .env.example | base64)
# Strip any existing payload (everything from MARKER to EOF), then append a
# fresh one. If MARKER isn't present, sed leaves the file unchanged.
+12 -7
View File
@@ -1,6 +1,6 @@
#cloud-config
#
# Pocket-ID (OIDC provider) — unattended deploy on a fresh Alpine host.
# Pocket-ID (OIDC provider) — harden SSH, then deploy, on a fresh Alpine host.
#
# Fill in REPO_URL and the values in the runcmd block, then paste this as the
# instance user-data. DNS for POCKETID_DOMAIN must point at this host and
@@ -15,15 +15,20 @@ runcmd:
set -e
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git
REPO_BRANCH=main
HARDEN_SSH=1 # harden SSH on this fresh VM (set 0 to skip)
SSH_PORT=22
ALLOWED_IP= # optional: whitelist your client IP in sshguard
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations
cd /opt/automations
# Optional: install shared admin SSH keys from globals/ for root before deploy
# . scripts/lib.sh && load_globals \
# && install -d -m700 /root/.ssh && resolve_ssh_keys >> /root/.ssh/authorized_keys
# Optional: harden SSH first (prints a fresh root key — capture it!)
# SKIP_PROMPTS=1 FORCE=1 bash scripts/harden-ssh.sh
# Harden SSH on this fresh VM: PQ KEX, key-only auth, sshguard. Seeds root
# from globals/authorized_keys (or SSH_KEYS_URL). NOTE: harden-ssh also
# prints a generated root key to the serial console — capture it, or rely
# on the seeded keys.
if [ "$HARDEN_SSH" = 1 ]; then
SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" SKIP_PROMPTS=1 FORCE=1 \
bash scripts/harden-ssh.sh
fi
POCKETID_DOMAIN=id.example.com \
ACME_EMAIL=admin@example.com \
+85 -53
View File
@@ -12,10 +12,11 @@
# 5. Enables docker on boot, runs `docker compose pull && up -d`.
# 6. Waits for healthchecks to go green.
#
# Self-contained: docker-compose.yml, Caddyfile, and .env.example are
# embedded at the bottom of this file as a base64-encoded tar.gz. The
# script extracts them at runtime, so this single file is all you need
# on the target box.
# Self-contained: docker-compose.yml, Caddyfile, Caddyfile.webfinger and
# .env.example are embedded at the bottom of this file as a base64-encoded
# tar.gz. The script extracts them at runtime, so this single file is all you
# need on the target box. The WebFinger block is appended only when BASE_DOMAIN
# is set in .env.
#
# To rebuild after editing the loose files: run ./build.sh in this dir.
#
@@ -39,6 +40,8 @@ set -euo pipefail
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
: "${POCKETID_DOMAIN:=}"
: "${ACME_EMAIL:=}"
: "${BASE_DOMAIN:=}" # optional: enables the WebFinger block (set with REDIRECT_URL)
: "${REDIRECT_URL:=}" # optional: where the base domain 301s non-webfinger traffic
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
@@ -121,7 +124,7 @@ else
die "No embedded archive found. Run build.sh to embed deployment files."
fi
for f in docker-compose.yml Caddyfile .env.example; do
for f in docker-compose.yml Caddyfile Caddyfile.webfinger .env.example; do
[[ -f "$SCRIPT_DIR/$f" ]] || die "Embedded archive missing $f"
done
@@ -154,7 +157,8 @@ open_web_ports
log "Setting up $STACK_DIR..."
install -d -m 0750 "$STACK_DIR"
install -m 0640 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml"
install -m 0640 "$SCRIPT_DIR/Caddyfile" "$STACK_DIR/Caddyfile"
# (Caddyfile is assembled below, after .env, so the optional WebFinger block
# can be appended when BASE_DOMAIN is set.)
ENV_FILE="$STACK_DIR/.env"
if [[ ! -f "$ENV_FILE" ]]; then
@@ -165,6 +169,8 @@ if [[ ! -f "$ENV_FILE" ]]; then
-e "s|^ACME_EMAIL=.*|ACME_EMAIL=${ACME_EMAIL}|" \
-e "s|^ENCRYPTION_KEY=.*|ENCRYPTION_KEY=$(openssl rand -base64 32)|" \
-e "s|^ANUBIS_PID_KEY=.*|ANUBIS_PID_KEY=$(openssl rand -hex 32)|" \
-e "s|^BASE_DOMAIN=.*|BASE_DOMAIN=${BASE_DOMAIN}|" \
-e "s|^REDIRECT_URL=.*|REDIRECT_URL=${REDIRECT_URL}|" \
"$ENV_FILE"
else
log ".env exists; leaving secrets alone."
@@ -177,6 +183,20 @@ for var in POCKETID_DOMAIN ACME_EMAIL ENCRYPTION_KEY ANUBIS_PID_KEY; do
done
(( ${#missing[@]} == 0 )) || die "Missing values in $ENV_FILE: ${missing[*]}"
# Assemble the deployed Caddyfile each run: base (pocket-id) + the optional
# WebFinger block when BASE_DOMAIN is set in .env. Regenerated every run so
# edits to .env propagate.
install -m 0640 "$SCRIPT_DIR/Caddyfile" "$STACK_DIR/Caddyfile"
bd="$(sed -n 's/^BASE_DOMAIN=//p' "$ENV_FILE")"
ru="$(sed -n 's/^REDIRECT_URL=//p' "$ENV_FILE")"
if [[ -n "$bd" ]]; then
[[ -n "$ru" ]] || die "BASE_DOMAIN is set but REDIRECT_URL is empty in $ENV_FILE; set both or neither."
log "WebFinger enabled -- serving /.well-known/webfinger at ${bd}."
cat "$SCRIPT_DIR/Caddyfile.webfinger" >> "$STACK_DIR/Caddyfile"
else
log "No BASE_DOMAIN -- pocket-id only (use the webfinger deployment for OIDC discovery)."
fi
# ----------------------------------------------------------------------------
# Bring up the stack
# ----------------------------------------------------------------------------
@@ -243,50 +263,62 @@ EOF
# __ARCHIVE_BELOW__ marker is the embedded tar.gz payload (base64).
exit 0
__ARCHIVE_BELOW__
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
2B7bY/v29m9v1GZbACgAAA==
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
+4
View File
@@ -35,6 +35,10 @@ services:
environment:
POCKETID_DOMAIN: "${POCKETID_DOMAIN}"
ACME_EMAIL: "${ACME_EMAIL}"
# Only used by the optional WebFinger block (appended by deploy.sh when
# BASE_DOMAIN is set). Empty otherwise -- harmless.
BASE_DOMAIN: "${BASE_DOMAIN:-}"
REDIRECT_URL: "${REDIRECT_URL:-}"
depends_on:
- pocket-id
healthcheck:
+12 -7
View File
@@ -1,6 +1,6 @@
#cloud-config
#
# WebFinger + redirect — unattended deploy on a fresh Alpine host.
# WebFinger + redirect — harden SSH, then deploy, on a fresh Alpine host.
#
# Fill in REPO_URL and the values in the runcmd block, then paste this as the
# instance user-data. DNS for BASE_DOMAIN must point at this host and ports
@@ -15,15 +15,20 @@ runcmd:
set -e
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git
REPO_BRANCH=main
HARDEN_SSH=1 # harden SSH on this fresh VM (set 0 to skip)
SSH_PORT=22
ALLOWED_IP= # optional: whitelist your client IP in sshguard
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations
cd /opt/automations
# Optional: install shared admin SSH keys from globals/ for root before deploy
# . scripts/lib.sh && load_globals \
# && install -d -m700 /root/.ssh && resolve_ssh_keys >> /root/.ssh/authorized_keys
# Optional: harden SSH first (prints a fresh root key — capture it!)
# SKIP_PROMPTS=1 FORCE=1 bash scripts/harden-ssh.sh
# Harden SSH on this fresh VM: PQ KEX, key-only auth, sshguard. Seeds root
# from globals/authorized_keys (or SSH_KEYS_URL). NOTE: harden-ssh also
# prints a generated root key to the serial console — capture it, or rely
# on the seeded keys.
if [ "$HARDEN_SSH" = 1 ]; then
SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" SKIP_PROMPTS=1 FORCE=1 \
bash scripts/harden-ssh.sh
fi
BASE_DOMAIN=example.com \
ISSUER_URL=https://auth.example.com \