Add firstboot onboarding and prep/check tooling
This commit is contained in:
10
systemd/pikit-certgen.service
Normal file
10
systemd/pikit-certgen.service
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Generate Pi-Kit TLS certs if missing
|
||||
Before=nginx.service dietpi-dashboard-frontend.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/pikit-certgen.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=nginx.service dietpi-dashboard-frontend.service
|
||||
101
systemd/pikit-certgen.sh
Executable file
101
systemd/pikit-certgen.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate Pi-Kit TLS CA + server cert if missing (idempotent).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CERT_DIR="/etc/pikit/certs"
|
||||
WEB_ASSETS="/var/www/pikit-web/assets"
|
||||
CA_CRT="$CERT_DIR/pikit-ca.crt"
|
||||
CA_KEY="$CERT_DIR/pikit-ca.key"
|
||||
CA_SRL="$CERT_DIR/pikit-ca.srl"
|
||||
SRV_KEY="$CERT_DIR/pikit.local.key"
|
||||
SRV_CRT="$CERT_DIR/pikit.local.crt"
|
||||
SRV_CSR="$CERT_DIR/pikit.local.csr"
|
||||
CERT_GROUP="pikit-cert"
|
||||
|
||||
log() {
|
||||
printf '[pikit-certgen] %s\n' "$*"
|
||||
}
|
||||
|
||||
ensure_group() {
|
||||
if ! getent group "$CERT_GROUP" >/dev/null 2>&1; then
|
||||
groupadd "$CERT_GROUP" || true
|
||||
fi
|
||||
for u in www-data dietpi-dashboard-frontend; do
|
||||
if id -u "$u" >/dev/null 2>&1; then
|
||||
usermod -a -G "$CERT_GROUP" "$u" || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
fix_perms() {
|
||||
ensure_group
|
||||
if [ -d "$CERT_DIR" ]; then
|
||||
chgrp "$CERT_GROUP" "$CERT_DIR" || true
|
||||
chmod 750 "$CERT_DIR" || true
|
||||
fi
|
||||
for f in "$CA_CRT" "$CA_KEY" "$SRV_CRT" "$SRV_KEY"; do
|
||||
if [ -e "$f" ]; then
|
||||
chgrp "$CERT_GROUP" "$f" || true
|
||||
fi
|
||||
done
|
||||
[ -e "$CA_KEY" ] && chmod 640 "$CA_KEY"
|
||||
[ -e "$SRV_KEY" ] && chmod 640 "$SRV_KEY"
|
||||
[ -e "$CA_CRT" ] && chmod 644 "$CA_CRT"
|
||||
[ -e "$SRV_CRT" ] && chmod 644 "$SRV_CRT"
|
||||
}
|
||||
|
||||
if [ -s "$CA_CRT" ] && [ -s "$CA_KEY" ] && [ -s "$SRV_KEY" ] && [ -s "$SRV_CRT" ]; then
|
||||
mkdir -p "$WEB_ASSETS"
|
||||
if [ ! -s "$WEB_ASSETS/pikit-ca.crt" ]; then
|
||||
cp "$CA_CRT" "$WEB_ASSETS/pikit-ca.crt"
|
||||
chmod 644 "$WEB_ASSETS/pikit-ca.crt"
|
||||
log "Copied CA to web assets."
|
||||
fi
|
||||
fix_perms
|
||||
log "TLS certs already present; skipping generation."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
log "openssl not installed; cannot generate certs."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Generating TLS certs..."
|
||||
mkdir -p "$CERT_DIR" "$WEB_ASSETS"
|
||||
ensure_group
|
||||
chgrp "$CERT_GROUP" "$CERT_DIR" || true
|
||||
chmod 750 "$CERT_DIR"
|
||||
|
||||
rm -f "$CA_KEY" "$CA_CRT" "$CA_SRL" "$SRV_KEY" "$SRV_CRT" "$SRV_CSR" || true
|
||||
|
||||
openssl genrsa -out "$CA_KEY" 2048
|
||||
openssl req -x509 -new -nodes -key "$CA_KEY" -sha256 -days 3650 \
|
||||
-out "$CA_CRT" -subj "/CN=Pi-Kit CA"
|
||||
|
||||
openssl genrsa -out "$SRV_KEY" 2048
|
||||
openssl req -new -key "$SRV_KEY" -out "$SRV_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 "$SRV_CSR" -CA "$CA_CRT" -CAkey "$CA_KEY" \
|
||||
-CAcreateserial -out "$SRV_CRT" -days 825 -sha256 -extfile "$SAN_CFG"
|
||||
|
||||
rm -f "$SAN_CFG" "$SRV_CSR"
|
||||
fix_perms
|
||||
|
||||
cp "$CA_CRT" "$WEB_ASSETS/pikit-ca.crt"
|
||||
chmod 644 "$WEB_ASSETS/pikit-ca.crt"
|
||||
|
||||
log "TLS certs generated."
|
||||
250
systemd/pikit-firstboot.sh
Executable file
250
systemd/pikit-firstboot.sh
Executable file
@@ -0,0 +1,250 @@
|
||||
#!/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"
|
||||
|
||||
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
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*"
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
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 systemctl >/dev/null 2>&1; then
|
||||
systemctl reload nginx || systemctl restart nginx
|
||||
fi
|
||||
finish_step 2
|
||||
|
||||
begin_step 3
|
||||
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
|
||||
finish_step 3
|
||||
|
||||
begin_step 4
|
||||
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
|
||||
10
systemd/pikit-ready.service
Normal file
10
systemd/pikit-ready.service
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Pi-Kit ready flag helper
|
||||
ConditionPathExists=/var/lib/pikit/firstboot/firstboot.done
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/pikit-ready.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
11
systemd/pikit-ready.sh
Executable file
11
systemd/pikit-ready.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Touches /var/run/pikit-ready on boot when firstboot is complete.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DONE_FILE="/var/lib/pikit/firstboot/firstboot.done"
|
||||
READY_FILE="/var/run/pikit-ready"
|
||||
|
||||
if [ -f "$DONE_FILE" ]; then
|
||||
touch "$READY_FILE"
|
||||
fi
|
||||
10
systemd/pikit-ssh-keygen.service
Normal file
10
systemd/pikit-ssh-keygen.service
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Generate SSH host keys if missing
|
||||
Before=ssh.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/ssh-keygen -A
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user