Files
pi-kit/systemd/pikit-firstboot.sh

305 lines
8.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Install as /var/lib/dietpi/postboot.d/10-pikit-firstboot and chmod +x.
# Runs once on first boot to finalize device-unique setup.
set -euo pipefail
FIRSTBOOT_DIR="/var/lib/pikit/firstboot"
STATE_FILE="$FIRSTBOOT_DIR/state.json"
LOG_FILE="$FIRSTBOOT_DIR/firstboot.log"
ERROR_FILE="$FIRSTBOOT_DIR/firstboot.error"
DONE_FILE="$FIRSTBOOT_DIR/firstboot.done"
LOCK_FILE="$FIRSTBOOT_DIR/firstboot.lock"
CERT_DIR="/etc/pikit/certs"
WEB_ASSETS="/var/www/pikit-web/assets"
PROFILE_FILE="/etc/pikit/profile.json"
MOTD_FILE="/etc/motd"
FIRSTBOOT_CONF="/etc/pikit/firstboot.conf"
APT_UA_OVERRIDE="/etc/apt/apt.conf.d/51pikit-unattended.conf"
STEPS=(
"Preparing system"
"Generating security keys"
"Securing the dashboard"
"Updating software (this can take a while)"
"Final checks"
"Starting Pi-Kit"
)
STEP_STATUS=(pending pending pending pending pending pending)
CURRENT_STEP=""
CURRENT_INDEX=-1
PIKIT_FIRSTBOOT_UPDATES="${PIKIT_FIRSTBOOT_UPDATES:-1}"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*"
}
load_config() {
if [ -f "$FIRSTBOOT_CONF" ]; then
# shellcheck disable=SC1090
. "$FIRSTBOOT_CONF"
fi
PIKIT_FIRSTBOOT_UPDATES="${PIKIT_FIRSTBOOT_UPDATES:-1}"
}
skip_updates() {
case "${PIKIT_FIRSTBOOT_UPDATES,,}" in
0|false|no|off) return 0 ;;
esac
return 1
}
configure_unattended_defaults() {
if [ -f "$APT_UA_OVERRIDE" ]; then
log "Unattended-upgrades config already present; skipping defaults."
return
fi
if ! command -v python3 >/dev/null 2>&1; then
log "python3 missing; skipping unattended-upgrades defaults."
return
fi
PYTHONPATH=/usr/local/bin python3 - <<'PY'
import sys
try:
from pikit_api.auto_updates import set_updates_config
except Exception as e:
print(f"pikit_api unavailable: {e}")
sys.exit(0)
set_updates_config({"enable": True, "scope": "security"})
PY
log "Unattended-upgrades defaults applied (security-only)."
}
write_state() {
local state="$1"
local current="$2"
local steps_joined
local status_joined
steps_joined=$(IFS='|'; echo "${STEPS[*]}")
status_joined=$(IFS='|'; echo "${STEP_STATUS[*]}")
PIKIT_STATE_FILE="$STATE_FILE" PIKIT_STATE="$state" PIKIT_CURRENT_STEP="$current" PIKIT_STEPS="$steps_joined" PIKIT_STEP_STATUSES="$status_joined" \
python3 - <<'PY'
import json
import os
import pathlib
from datetime import datetime, timezone
state_path = pathlib.Path(os.environ["PIKIT_STATE_FILE"]) if "PIKIT_STATE_FILE" in os.environ else pathlib.Path("/var/lib/pikit/firstboot/state.json")
state = os.environ.get("PIKIT_STATE", "running")
current = os.environ.get("PIKIT_CURRENT_STEP") or None
steps = (os.environ.get("PIKIT_STEPS") or "").split("|")
statuses = (os.environ.get("PIKIT_STEP_STATUSES") or "").split("|")
if len(statuses) < len(steps):
statuses += ["pending"] * (len(steps) - len(statuses))
updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
state_path.parent.mkdir(parents=True, exist_ok=True)
state_path.write_text(json.dumps({
"state": state,
"current_step": current,
"steps": [{"label": label, "status": status} for label, status in zip(steps, statuses)],
"updated_at": updated_at,
}, indent=2))
PY
}
begin_step() {
local idx="$1"
if [ "$CURRENT_INDEX" -ge 0 ]; then
STEP_STATUS[$CURRENT_INDEX]="done"
fi
CURRENT_INDEX="$idx"
CURRENT_STEP="${STEPS[$idx]}"
STEP_STATUS[$idx]="current"
write_state "running" "$CURRENT_STEP"
log "--- $CURRENT_STEP ---"
}
finish_step() {
local idx="$1"
local state="${2:-running}"
local current="${3:-$CURRENT_STEP}"
STEP_STATUS[$idx]="done"
write_state "$state" "$current"
}
clear_motd_block() {
if [ -f "$MOTD_FILE" ]; then
sed -i '/^\[Pi-Kit firstboot\]/,/^\[\/Pi-Kit firstboot\]/d' "$MOTD_FILE" || true
fi
}
write_motd_error() {
clear_motd_block
cat >> "$MOTD_FILE" <<'TXT'
[Pi-Kit firstboot]
Pi-Kit setup needs attention.
Error log: sudo cat /var/lib/pikit/firstboot/firstboot.error
Full log: sudo cat /var/lib/pikit/firstboot/firstboot.log
If needed: sudo systemctl restart nginx pikit-api
[/Pi-Kit firstboot]
TXT
}
handle_error() {
local line="$1"
local msg="Firstboot failed at step: ${CURRENT_STEP:-unknown} (line $line)"
log "$msg"
STEP_STATUS[$CURRENT_INDEX]="error"
write_state "error" "${CURRENT_STEP:-}" || true
echo "$msg" > "$ERROR_FILE"
if [ -f "$LOG_FILE" ]; then
echo "--- recent log ---" >> "$ERROR_FILE"
tail -n 120 "$LOG_FILE" >> "$ERROR_FILE"
fi
write_motd_error
exit 1
}
mkdir -p "$FIRSTBOOT_DIR"
:> "$LOG_FILE"
exec >>"$LOG_FILE" 2>&1
log "Pi-Kit firstboot starting"
load_config
if [ -f "$DONE_FILE" ]; then
log "Firstboot already completed; exiting."
exit 0
fi
rm -f "$ERROR_FILE"
clear_motd_block
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
log "Another firstboot run is in progress; exiting."
exit 0
fi
trap 'handle_error $LINENO' ERR
begin_step 0
mkdir -p "$CERT_DIR" "$WEB_ASSETS"
if getent group pikit-cert >/dev/null 2>&1; then
chgrp pikit-cert "$CERT_DIR" || true
fi
chmod 750 "$CERT_DIR"
finish_step 0
begin_step 1
if [ -x /usr/local/bin/pikit-certgen.sh ]; then
/usr/local/bin/pikit-certgen.sh
else
if [ -s "$CERT_DIR/pikit-ca.crt" ] && [ -s "$CERT_DIR/pikit-ca.key" ] && [ -s "$CERT_DIR/pikit.local.crt" ] && [ -s "$CERT_DIR/pikit.local.key" ]; then
log "TLS certs already present; skipping generation."
else
if ! command -v openssl >/dev/null 2>&1; then
echo "openssl not installed" >&2
exit 1
fi
rm -f "$CERT_DIR/pikit-ca.key" "$CERT_DIR/pikit-ca.crt" "$CERT_DIR/pikit-ca.srl" || true
rm -f "$CERT_DIR/pikit.local.key" "$CERT_DIR/pikit.local.crt" "$CERT_DIR/pikit.local.csr" || true
openssl genrsa -out "$CERT_DIR/pikit-ca.key" 2048
openssl req -x509 -new -nodes -key "$CERT_DIR/pikit-ca.key" -sha256 -days 3650 \
-out "$CERT_DIR/pikit-ca.crt" -subj "/CN=Pi-Kit CA"
openssl genrsa -out "$CERT_DIR/pikit.local.key" 2048
openssl req -new -key "$CERT_DIR/pikit.local.key" -out "$CERT_DIR/pikit.local.csr" -subj "/CN=pikit.local"
SAN_CFG=$(mktemp)
cat > "$SAN_CFG" <<'CFG'
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = pikit.local
DNS.2 = pikit
CFG
openssl x509 -req -in "$CERT_DIR/pikit.local.csr" -CA "$CERT_DIR/pikit-ca.crt" -CAkey "$CERT_DIR/pikit-ca.key" \
-CAcreateserial -out "$CERT_DIR/pikit.local.crt" -days 825 -sha256 -extfile "$SAN_CFG"
rm -f "$SAN_CFG" "$CERT_DIR/pikit.local.csr"
chmod 600 "$CERT_DIR/pikit-ca.key" "$CERT_DIR/pikit.local.key"
chmod 644 "$CERT_DIR/pikit-ca.crt" "$CERT_DIR/pikit.local.crt"
fi
fi
finish_step 1
begin_step 2
cp "$CERT_DIR/pikit-ca.crt" "$WEB_ASSETS/pikit-ca.crt"
chmod 644 "$WEB_ASSETS/pikit-ca.crt"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$WEB_ASSETS/pikit-ca.crt" | awk '{print $1}' > "$WEB_ASSETS/pikit-ca.sha256"
elif command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 "$WEB_ASSETS/pikit-ca.crt" | awk '{print $2}' > "$WEB_ASSETS/pikit-ca.sha256"
fi
if [ -s "$WEB_ASSETS/pikit-ca.sha256" ]; then
chmod 644 "$WEB_ASSETS/pikit-ca.sha256"
fi
if command -v systemctl >/dev/null 2>&1; then
systemctl reload nginx || systemctl restart nginx
fi
finish_step 2
begin_step 3
if skip_updates; then
log "Skipping software updates (PIKIT_FIRSTBOOT_UPDATES=$PIKIT_FIRSTBOOT_UPDATES)."
else
export DEBIAN_FRONTEND=noninteractive
mkdir -p /var/cache/apt/archives/partial /var/lib/apt/lists/partial
chmod 755 /var/cache/apt/archives /var/cache/apt/archives/partial /var/lib/apt/lists /var/lib/apt/lists/partial
apt-get update
apt-get -y -o Dpkg::Options::=--force-confold full-upgrade
fi
finish_step 3
begin_step 4
configure_unattended_defaults
if [ -f "$PROFILE_FILE" ] && command -v ufw >/dev/null 2>&1; then
python3 - <<'PY' > /tmp/pikit-profile-ports.txt
import json
import sys
from pathlib import Path
profile = Path("/etc/pikit/profile.json")
try:
data = json.loads(profile.read_text())
except Exception:
data = {}
ports = data.get("firewall_ports") or []
for port in ports:
try:
port_int = int(port)
except Exception:
continue
print(port_int)
PY
while read -r port; do
[ -z "$port" ] && continue
for subnet in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 169.254.0.0/16; do
ufw allow from "$subnet" to any port "$port" || true
done
done < /tmp/pikit-profile-ports.txt
rm -f /tmp/pikit-profile-ports.txt
else
log "Profile firewall step skipped (no profile.json or ufw missing)"
fi
if [ ! -f "$WEB_ASSETS/pikit-ca.crt" ]; then
echo "CA bundle missing in web assets" >&2
exit 1
fi
finish_step 4
begin_step 5
touch "$DONE_FILE"
touch /var/run/pikit-ready
finish_step 5 "done" "${STEPS[5]}"
log "Pi-Kit firstboot complete"
exit 0