Skip to main content

max / makenotwork

7.4 KB · 193 lines History Blame Raw
1 #!/usr/bin/env bash
2 # Idempotent bootstrap for a fresh MNW node (tier A/B/C deploy target).
3 #
4 # Run on the new node as root. After this finishes, sandod on the Sando host
5 # can rsync + deploy to <ssh_target>:/opt/mnw/.
6 #
7 # Required env:
8 # SANDO_PUBKEY — sando user's public key on the Sando host. Get it via:
9 # `ssh fw13 'sudo cat /srv/sando/.ssh/id_ed25519.pub'`
10 #
11 # Optional env:
12 # DEPLOY_ROOT — defaults to /opt/mnw
13 # BIN_NAME — primary binary name (matches sando-daemon.toml's
14 # bin_names[0]). Defaults to "makenotwork".
15 # SERVICE_NAME — systemd unit name. Defaults to "makenotwork.service".
16 # SERVICE_USER — runtime user for the binary. Defaults to "deploy".
17 # ENABLE_FIREWALL — "1" to set up UFW (22/80/443). Defaults to "1".
18 # INSTALL_CADDY — "1" to apt-install caddy (config is operator's job).
19 # Defaults to "1".
20 # INSTALL_POSTGRES — "1" to apt-install postgresql. Defaults to "1".
21 # INSTALL_TAILSCALE — "1" to apt-install tailscale (NOT authenticated;
22 # operator runs `tailscale up`). Defaults to "1".
23 #
24 # What this does NOT do (operator's job):
25 # - tailscale up (auth)
26 # - DNS records
27 # - Caddyfile content + Cloudflare origin certs + private keys
28 # - postgres role + db + .env / DATABASE_URL
29 # - any secrets
30
31 set -euo pipefail
32
33 if [[ $EUID -ne 0 ]]; then
34 echo "must run as root" >&2
35 exit 1
36 fi
37 if [[ -z "${SANDO_PUBKEY:-}" ]]; then
38 echo "SANDO_PUBKEY env var is required" >&2
39 exit 1
40 fi
41
42 DEPLOY_ROOT="${DEPLOY_ROOT:-/opt/mnw}"
43 # FHS-style sidecar paths the systemd unit references. Bootstrap creates the
44 # dirs but does not populate `ENV_FILE` — operator drops secrets in after the
45 # bootstrap finishes, before starting the service.
46 ETC_DIR="${ETC_DIR:-/etc/mnw}"
47 ENV_FILE="${ENV_FILE:-$ETC_DIR/makenotwork.env}"
48 STATE_DIR="${STATE_DIR:-/var/lib/mnw}"
49 BIN_NAME="${BIN_NAME:-makenotwork}"
50 SERVICE_NAME="${SERVICE_NAME:-makenotwork.service}"
51 SERVICE_USER="${SERVICE_USER:-deploy}"
52 ENABLE_FIREWALL="${ENABLE_FIREWALL:-1}"
53 INSTALL_CADDY="${INSTALL_CADDY:-1}"
54 INSTALL_POSTGRES="${INSTALL_POSTGRES:-1}"
55 INSTALL_TAILSCALE="${INSTALL_TAILSCALE:-1}"
56
57 export DEBIAN_FRONTEND=noninteractive
58
59 log() { echo "[bootstrap] $*"; }
60
61 log "1/8 base packages"
62 apt-get update -qq
63 apt-get install -y -qq curl gnupg ca-certificates rsync ufw fail2ban > /dev/null
64
65 if [[ "$INSTALL_POSTGRES" == "1" ]]; then
66 log "2/8 postgresql"
67 apt-get install -y -qq postgresql > /dev/null
68 else
69 log "2/8 skipping postgresql"
70 fi
71
72 if [[ "$INSTALL_TAILSCALE" == "1" ]]; then
73 log "3/8 tailscale (not authenticating)"
74 if ! command -v tailscale >/dev/null; then
75 # Ubuntu codename. tailscale's repo is published per-codename;
76 # noble (24.04) keys work on 24.04+ derivatives.
77 codename=$(. /etc/os-release && echo "$VERSION_CODENAME")
78 curl -fsSL "https://pkgs.tailscale.com/stable/ubuntu/${codename}.noarmor.gpg" \
79 > /usr/share/keyrings/tailscale-archive-keyring.gpg
80 curl -fsSL "https://pkgs.tailscale.com/stable/ubuntu/${codename}.tailscale-keyring.list" \
81 > /etc/apt/sources.list.d/tailscale.list
82 apt-get update -qq
83 apt-get install -y -qq tailscale > /dev/null
84 systemctl enable --now tailscaled
85 fi
86 else
87 log "3/8 skipping tailscale"
88 fi
89
90 if [[ "$INSTALL_CADDY" == "1" ]]; then
91 log "4/8 caddy (no Caddyfile — operator's job)"
92 if ! command -v caddy >/dev/null; then
93 curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key \
94 | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
95 curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt \
96 > /etc/apt/sources.list.d/caddy-stable.list
97 apt-get update -qq
98 apt-get install -y -qq caddy > /dev/null
99 fi
100 else
101 log "4/8 skipping caddy"
102 fi
103
104 log "5/8 deploy user + dirs"
105 if ! id "$SERVICE_USER" &>/dev/null; then
106 useradd -m -d "/home/$SERVICE_USER" -s /bin/bash "$SERVICE_USER"
107 fi
108 install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0700 "/home/$SERVICE_USER/.ssh"
109 if ! grep -qF "$SANDO_PUBKEY" "/home/$SERVICE_USER/.ssh/authorized_keys" 2>/dev/null; then
110 echo "$SANDO_PUBKEY" >> "/home/$SERVICE_USER/.ssh/authorized_keys"
111 fi
112 chown "$SERVICE_USER:$SERVICE_USER" "/home/$SERVICE_USER/.ssh/authorized_keys"
113 chmod 0600 "/home/$SERVICE_USER/.ssh/authorized_keys"
114 install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0755 "$DEPLOY_ROOT" "$DEPLOY_ROOT/releases"
115 # FHS sidecars: /etc/mnw owned root:service (so the service can read the env
116 # file but not edit it); /var/lib/mnw owned service:service for runtime
117 # state (backups, scan-spool, anything else the binary writes).
118 install -d -o root -g "$SERVICE_USER" -m 0750 "$ETC_DIR"
119 install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0750 "$STATE_DIR"
120
121 # If the git user exists (i.e. this host runs git SSH), grant it read access
122 # to the env file via ACL so mnw-admin git-auth can load DATABASE_URL. The git
123 # user is neither owner nor in the SERVICE_USER group, so without this the
124 # /etc/mnw/makenotwork.env is unreadable and every `git push` panics with
125 # "DATABASE_URL must be set". Conditional + idempotent.
126 if getent passwd git >/dev/null; then
127 setfacl -m u:git:x "$ETC_DIR"
128 if [ -f "$ENV_FILE" ]; then
129 setfacl -m u:git:r "$ENV_FILE"
130 fi
131 fi
132
133 log "6/8 sudoers (systemctl on $SERVICE_NAME for $SERVICE_USER)"
134 cat > "/etc/sudoers.d/${SERVICE_USER}-mnw" <<EOF
135 $SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl reload-or-restart $SERVICE_NAME, /bin/systemctl restart $SERVICE_NAME, /bin/systemctl status $SERVICE_NAME
136 EOF
137 chmod 0440 "/etc/sudoers.d/${SERVICE_USER}-mnw"
138 visudo -c -f "/etc/sudoers.d/${SERVICE_USER}-mnw" >/dev/null
139
140 log "7/8 systemd unit ($SERVICE_NAME) — points at $DEPLOY_ROOT/current/$BIN_NAME"
141 cat > "/etc/systemd/system/$SERVICE_NAME" <<EOF
142 [Unit]
143 Description=Makenotwork
144 After=network.target
145
146 [Service]
147 Type=simple
148 User=$SERVICE_USER
149 Group=$SERVICE_USER
150 WorkingDirectory=$DEPLOY_ROOT/current
151 ExecStart=$DEPLOY_ROOT/current/$BIN_NAME
152 # Secrets live outside the release dir so they survive deploys + rollbacks.
153 # Bootstrap creates ETC_DIR but not ENV_FILE — operator populates that.
154 EnvironmentFile=$ENV_FILE
155 # Runtime state (backups, spool, etc.) on FHS path; never inside the release
156 # dir or the deploy will erase it.
157 ReadWritePaths=$STATE_DIR
158 Restart=on-failure
159 RestartSec=30
160 # Exit 2 = migration failure (MNW server convention). Don't restart;
161 # operator must intervene before the next deploy.
162 RestartPreventExitStatus=2
163 StandardOutput=journal
164 StandardError=journal
165 SyslogIdentifier=$BIN_NAME
166
167 [Install]
168 WantedBy=multi-user.target
169 EOF
170 systemctl daemon-reload
171 systemctl enable "$SERVICE_NAME" >/dev/null 2>&1 || true
172
173 if [[ "$ENABLE_FIREWALL" == "1" ]]; then
174 log "8/8 firewall (UFW: 22/80/443 in, all else deny)"
175 ufw --force reset > /dev/null
176 ufw default deny incoming > /dev/null
177 ufw default allow outgoing > /dev/null
178 ufw allow 22/tcp > /dev/null
179 ufw allow 80/tcp > /dev/null
180 ufw allow 443/tcp > /dev/null
181 ufw --force enable > /dev/null
182 else
183 log "8/8 skipping firewall"
184 fi
185
186 echo
187 log "Done. Next steps for the operator:"
188 echo " - tailscale up (auth this node to the tailnet)"
189 echo " - DNS A/AAAA records for the domain you'll serve"
190 echo " - Install /etc/caddy/Caddyfile + Cloudflare Origin CA cert + key"
191 echo " - postgres: create role+db, drop secrets into $ENV_FILE (chmod 0640, chown root:$SERVICE_USER)"
192 echo " - Run a sando deploy from the Sando host: POST /promote/<tier>"
193