#!/usr/bin/env bash set -Eeuo pipefail IFS=$'\n\t' readonly PROGRAM="ochenstarik-server-monitor-manager" readonly PROGRAM_VERSION="0.2.0-dev" readonly ETC_DIR="/etc/ochenstarik-server-monitor-manager" readonly LIB_DIR="/usr/local/lib/ochenstarik-server-monitor-manager" readonly STATE_DIR="/var/lib/ochenstarik-server-monitor-manager" readonly ENROLLMENT_DIR="${STATE_DIR}-enrollment" readonly BACKUP_DIR="${STATE_DIR}/bootstrap-backups" readonly CONTROL_USER="ochenstarik-smm-control" readonly AGENT_USER="ochenstarik-smm-agent" readonly CONTROL_UNIT="ochenstarik-smm-control.service" readonly AGENT_UNIT="ochenstarik-smm-agent.service" readonly PROVISIONING_HELPER_UNIT="ochenstarik-smm-provisioning-helper.service" readonly POLICY_HELPER="/usr/local/libexec/ochenstarik-smm-policy-apply" readonly EMERGENCY_COMMAND="/usr/local/sbin/ochenstarik-smm-emergency" readonly BOOTSTRAP_COMMAND="/usr/local/sbin/ochenstarik-server-monitor-manager.sh" readonly SUDOERS_FILE="/etc/sudoers.d/ochenstarik-smm-control" readonly MESH_DIR="${STATE_DIR}/mesh" readonly WG_DIR="${ETC_DIR}/wireguard" readonly FIREWALL_UNIT="ochenstarik-smm-firewall.service" readonly MESH_NETWORK="10.77.0.0/24" readonly HUB_MESH_ADDRESS="10.77.0.1/24" # Trust anchors — see docs/release-policy.md for the full signing and identity contract. readonly COSIGN_ISSUER="https://token.actions.githubusercontent.com" readonly COSIGN_IDENTITY_REGEXP="^https://github.com/ochenstarik-ui/server-monitor-manager/\.github/workflows/linux-release\.yml@refs/tags/v.*$" TEMP_DIR="" MESH_PEER_CODE="" ENROLLMENT_TOKEN_FILE="" ENROLLMENT_TOKEN_TEMP="" CONTROL_UPDATE_BACKUP_ID="" CONTROL_UPDATE_RECOVERY_REQUIRED=0 CONTROL_UPDATE_LEGACY_ITEMS=() log() { printf '%s\n' "[$PROGRAM] $*"; } fail() { printf '%s\n' "[$PROGRAM] ERROR: $*" >&2; exit 1; } cleanup() { local status=$? trap - EXIT if [[ "$CONTROL_UPDATE_RECOVERY_REQUIRED" == "1" ]]; then log "Control update failed; restoring the pre-update state." if recover_control_update "$CONTROL_UPDATE_BACKUP_ID"; then log "Control recovery completed." else printf '%s\n' "[$PROGRAM] ERROR: Automatic Control recovery failed; manual recovery is required." >&2 status=1 fi fi if [[ -n "$ENROLLMENT_TOKEN_FILE" ]]; then rm -f -- "$ENROLLMENT_TOKEN_FILE" fi if [[ -n "$ENROLLMENT_TOKEN_TEMP" ]]; then rm -f -- "$ENROLLMENT_TOKEN_TEMP" fi if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then rm -rf -- "$TEMP_DIR" fi exit "$status" } trap cleanup EXIT usage() { cat <<'EOF' Server Monitor Manager Linux bootstrap Usage: ochenstarik-server-monitor-manager.sh preflight ochenstarik-server-monitor-manager.sh verify-release ARCHIVE ochenstarik-server-monitor-manager.sh install-control ARCHIVE PUBLIC_HOST [HTTPS_PORT] ochenstarik-server-monitor-manager.sh install-agent ARCHIVE NODE_ID CONTROL_URL CA_CERT ochenstarik-server-monitor-manager.sh install-node ARCHIVE ochenstarik-server-monitor-manager.sh verify-manifest MANIFEST SIGNATURE CERTIFICATE ochenstarik-server-monitor-manager.sh mesh-init PUBLIC_ENDPOINT [WG_PORT] ochenstarik-server-monitor-manager.sh peer-add SMMPEER1_CODE ochenstarik-server-monitor-manager.sh mesh-status ochenstarik-server-monitor-manager.sh update-control ARCHIVE ochenstarik-server-monitor-manager.sh update-agent ARCHIVE ochenstarik-server-monitor-manager.sh rollback control|agent [BACKUP_ID] ochenstarik-server-monitor-manager.sh node-code NODE_ID ochenstarik-server-monitor-manager.sh control-device-code DEVICE_ID ochenstarik-server-monitor-manager.sh node-token NODE_ID ochenstarik-server-monitor-manager.sh control-ca-fingerprint ochenstarik-server-monitor-manager.sh status ochenstarik-server-monitor-manager.sh uninstall-agent [--purge] ochenstarik-server-monitor-manager.sh uninstall-control --confirm-destroy-control ochenstarik-server-monitor-manager.sh version ARCHIVE must have a matching ARCHIVE.sha256 file. Agent enrollment reads the one-time token from SMM_ENROLL_TOKEN or from a hidden local prompt; it is never written to agent.env. EOF } base64url_encode() { base64 -w 0 | tr '+/' '-_' | tr -d '=' } base64url_decode() { local value="$1" remainder [[ "$value" =~ ^[A-Za-z0-9_-]+$ ]] || fail "Enrollment code contains invalid base64url data." remainder=$(( ${#value} % 4 )) case "$remainder" in 0) ;; 2) value+="==" ;; 3) value+="=" ;; *) fail "Enrollment code contains invalid base64url length." ;; esac printf '%s' "$value" | tr '_-' '/+' | base64 -d } require_root() { [[ ${EUID:-$(id -u)} -eq 0 ]] || fail "This action must run as root (use sudo)." } require_command() { command -v "$1" >/dev/null 2>&1 || fail "Required command is missing: $1" } validate_platform() { [[ -r /etc/os-release ]] || fail "/etc/os-release is missing." # shellcheck disable=SC1091 . /etc/os-release case "${ID:-}" in ubuntu) case "${VERSION_ID:-}" in 22.04|24.04) ;; *) fail "Unsupported Ubuntu version: ${VERSION_ID:-unknown}" ;; esac ;; debian) case "${VERSION_ID:-}" in 12|13) ;; *) fail "Unsupported Debian version: ${VERSION_ID:-unknown}" ;; esac ;; *) fail "Unsupported distribution: ${ID:-unknown}" ;; esac case "$(uname -m)" in x86_64|aarch64|arm64) ;; *) fail "Unsupported architecture: $(uname -m)" ;; esac [[ "$(ps -p 1 -o comm=)" == "systemd" ]] || fail "systemd must be PID 1." } # Release tags carry a leading "v" while some manifest fields and recorded values may not. normalize_version() { printf '%s' "${1#v}" } # True when the first version orders strictly before the second. Lexicographic comparison is # wrong here: "0.1.0-alpha.10" sorts before "0.1.0-alpha.9" as a string, which would reject # every release after the ninth as a downgrade. version_lt() { local left right first left="$(normalize_version "$1")" right="$(normalize_version "$2")" [[ "$left" != "$right" ]] || return 1 first="$(printf '%s\n%s\n' "$left" "$right" | sort -V | head -n1)" [[ "$first" == "$left" ]] } installed_version_file() { printf '%s' "$ETC_DIR/installed-version-$1" } read_installed_version() { local file file="$(installed_version_file "$1")" [[ -r "$file" ]] || return 0 tr -d '\r\n' <"$file" } record_installed_version() { local role="$1" version="$2" file [[ -n "$version" ]] || return 0 file="$(installed_version_file "$role")" install -d -m 0755 "$ETC_DIR" printf '%s\n' "$version" >"$file" chmod 0644 "$file" } manifest_version_field() { local manifest="$1" field="$2" [[ -r "$manifest" ]] || return 0 awk -F'"' -v key="$field" '$2 == key { print $4; exit }' "$manifest" || true } # Version recorded for an archive being installed, empty when the release predates manifests. archive_version() { manifest_version_field "$(dirname "$1")/server-monitor-manager-manifest.json" version } validate_node_id() { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]] \ || fail "Node id must contain 1-63 lowercase letters, digits, or hyphens." } validate_port() { [[ "$1" =~ ^[0-9]{1,5}$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 )) \ || fail "Port must be in range 1-65535." } validate_ipv4_literal() { local address="$1" octet local -a octets IFS=. read -r -a octets <<<"$address" (( ${#octets[@]} == 4 )) || fail "Control URL contains an invalid IPv4 host." for octet in "${octets[@]}"; do [[ "$octet" =~ ^[0-9]{1,3}$ ]] && (( 10#$octet <= 255 )) \ || fail "Control URL contains an invalid IPv4 host." done } validate_control_url() { local authority host port="" label left right compressed=0 ipv4_groups=0 group_count last_group_index local -a groups labels right_groups [[ "$1" == https://* ]] || fail "Control URL must be an https URL without a path or credentials." authority="${1#https://}" authority="${authority%/}" [[ -n "$authority" && "$authority" != *['/?#@']* ]] \ || fail "Control URL must be an https URL without a path or credentials." if [[ "$authority" == \[* ]]; then [[ "$authority" =~ ^\[([0-9A-Fa-f:.]+)\](:([0-9]+))?$ ]] \ || fail "Control URL contains an invalid bracketed IPv6 authority." host="${BASH_REMATCH[1]}" port="${BASH_REMATCH[3]:-}" [[ "$host" == *:* ]] || fail "Control URL contains an invalid bracketed IPv6 authority." [[ "$host" != *:::* ]] || fail "Control URL contains an invalid bracketed IPv6 authority." [[ "$host" != :* || "$host" == ::* ]] \ || fail "Control URL contains an invalid bracketed IPv6 authority." [[ "$host" != *: || "$host" == *:: ]] \ || fail "Control URL contains an invalid bracketed IPv6 authority." groups=() if [[ "$host" == *::* ]]; then compressed=1 [[ "${host/::/}" != *::* ]] \ || fail "Control URL contains an invalid bracketed IPv6 authority." left="${host%%::*}" right="${host#*::}" if [[ -n "$left" ]]; then IFS=: read -r -a groups <<<"$left" fi if [[ -n "$right" ]]; then IFS=: read -r -a right_groups <<<"$right" groups+=("${right_groups[@]}") fi else IFS=: read -r -a groups <<<"$host" fi if (( ${#groups[@]} > 0 )) && [[ "${groups[${#groups[@]}-1]}" == *.* ]]; then last_group_index=$(( ${#groups[@]} - 1 )) validate_ipv4_literal "${groups[$last_group_index]}" unset "groups[$last_group_index]" ipv4_groups=2 fi for label in "${groups[@]}"; do [[ "$label" =~ ^[0-9A-Fa-f]{1,4}$ ]] \ || fail "Control URL contains an invalid bracketed IPv6 authority." done group_count=$(( ${#groups[@]} + ipv4_groups )) if (( compressed == 1 )); then (( group_count < 8 )) || fail "Control URL contains an invalid bracketed IPv6 authority." else (( group_count == 8 )) || fail "Control URL contains an invalid bracketed IPv6 authority." fi else [[ "$authority" != *:*:* ]] \ || fail "Control URL IPv6 authorities must use balanced brackets." if [[ "$authority" == *:* ]]; then host="${authority%%:*}" port="${authority#*:}" [[ -n "$port" ]] || fail "Control URL contains an invalid port." else host="$authority" fi [[ "$host" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ ]] \ || fail "Control URL contains an invalid DNS or IPv4 host." [[ ${#host} -le 253 && "$host" != *..* ]] \ || fail "Control URL contains an invalid DNS or IPv4 host." IFS=. read -r -a labels <<<"$host" for label in "${labels[@]}"; do [[ ${#label} -le 63 && "$label" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$ ]] \ || fail "Control URL contains an invalid DNS or IPv4 host." done if [[ "$host" =~ ^[0-9.]+$ ]]; then validate_ipv4_literal "$host" fi fi [[ -n "$host" ]] || fail "Control URL host is empty." [[ -z "$port" ]] || validate_port "$port" } verify_manifest() { local manifest="$1" signature="$2" certificate="$3" if [[ "${SMM_ALLOW_UNSIGNED:-0}" == "1" ]]; then log "WARNING: Signature verification skipped due to SMM_ALLOW_UNSIGNED=1." return 0 fi require_command cosign [[ -f "$manifest" ]] || fail "Manifest not found: $manifest" [[ -f "$signature" ]] || fail "Signature not found: $signature" log "Verifying manifest signature..." local verify_args if [[ -n "${SMM_TEST_PUBKEY:-}" ]]; then verify_args=(--key "$SMM_TEST_PUBKEY" --insecure-ignore-tlog) log "WARNING: Using test public key for verification. This must NOT happen in production." else [[ -f "$certificate" ]] || fail "Certificate not found: $certificate" verify_args=(--certificate "$certificate" --certificate-oidc-issuer "$COSIGN_ISSUER" --certificate-identity-regexp "$COSIGN_IDENTITY_REGEXP") fi if ! cosign verify-blob "${verify_args[@]}" \ --signature "$signature" "$manifest" >/dev/null 2>&1; then fail "Manifest signature verification failed." fi log "Manifest signature is valid." } verify_archive() { local archive="$1" expected actual entry manifest signature certificate [[ -f "$archive" ]] || fail "Archive not found: $archive" manifest="$(dirname "$archive")/server-monitor-manager-manifest.json" signature="$(dirname "$archive")/server-monitor-manager-manifest.sig" certificate="$(dirname "$archive")/server-monitor-manager-manifest.pem" if [[ -f "$manifest" && -f "$signature" && -f "$certificate" ]]; then verify_manifest "$manifest" "$signature" "$certificate" local archive_basename archive_basename="$(basename "$archive")" expected="$(awk -F'"' -v name="$archive_basename" '$2 == name {print $4}' "$manifest" || true)" if [[ -z "$expected" && "$archive_basename" == ochenstarik-* ]]; then expected="$(awk -F'"' -v name="${archive_basename#ochenstarik-}" '$2 == name {print $4}' "$manifest" || true)" fi [[ -n "$expected" ]] || fail "Could not extract archive hash from manifest." else if [[ "${SMM_ALLOW_UNSIGNED:-0}" == "1" ]]; then log "WARNING: Manifest, signature, or certificate not found; falling back to .sha256 file due to SMM_ALLOW_UNSIGNED=1." local checksum_file="${archive}.sha256" [[ -f "$checksum_file" ]] || fail "Checksum file not found: $checksum_file" expected="$(awk 'NR == 1 { print $1 }' "$checksum_file")" [[ "$expected" =~ ^[0-9a-fA-F]{64}$ ]] || fail "Invalid checksum file: $checksum_file" else fail "Manifest, signature, and certificate are required for archive verification. Set SMM_ALLOW_UNSIGNED=1 to bypass." fi fi actual="$(sha256sum "$archive" | awk '{ print $1 }')" [[ "${actual,,}" == "${expected,,}" ]] || fail "Archive checksum mismatch." while IFS= read -r entry; do [[ -n "$entry" ]] || continue [[ "$entry" != /* && "$entry" != *".."* ]] || fail "Unsafe archive entry: $entry" case "$entry" in agent|agent/*|control|control/*|provisioning-helper|provisioning-helper/*|deploy|deploy/*|bootstrap|bootstrap/*) ;; *) fail "Unexpected archive entry: $entry" ;; esac done < <(tar -tzf "$archive") } extract_archive() { local archive="$1" verify_archive "$archive" TEMP_DIR="$(mktemp -d -t smm-bootstrap.XXXXXXXX)" chmod 700 "$TEMP_DIR" tar -xzf "$archive" -C "$TEMP_DIR" --no-same-owner --no-same-permissions [[ -f "$TEMP_DIR/deploy/$CONTROL_UNIT" ]] || fail "Control systemd unit is missing from archive." [[ -f "$TEMP_DIR/deploy/$AGENT_UNIT" ]] || fail "Agent systemd unit is missing from archive." [[ -f "$TEMP_DIR/deploy/$PROVISIONING_HELPER_UNIT" ]] || fail "Provisioning helper systemd unit is missing from archive." [[ -f "$TEMP_DIR/deploy/$FIREWALL_UNIT" ]] || fail "Mesh firewall systemd unit is missing from archive." [[ -x "$TEMP_DIR/deploy/ochenstarik-smm-policy-apply" ]] || fail "Policy helper is missing from archive." [[ -x "$TEMP_DIR/deploy/ochenstarik-smm-emergency" ]] || fail "Emergency command is missing from archive." } verify_release_payload() { local archive="$1" require_command sha256sum require_command tar extract_archive "$archive" [[ -x "$TEMP_DIR/control/ochenstarik-smm-control" ]] || fail "Control binary is missing." [[ -x "$TEMP_DIR/agent/ochenstarik-smm-agent" ]] || fail "Agent binary is missing." [[ -x "$TEMP_DIR/provisioning-helper/ochenstarik-smm-provisioning-helper" ]] || fail "Provisioning helper binary is missing." [[ -x "$TEMP_DIR/deploy/ochenstarik-smm-policy-apply" ]] || fail "Policy helper is missing." [[ -x "$TEMP_DIR/deploy/ochenstarik-smm-emergency" ]] || fail "Emergency recovery command is missing." [[ -f "$TEMP_DIR/deploy/$FIREWALL_UNIT" ]] || fail "Mesh firewall unit is missing." [[ -x "$TEMP_DIR/bootstrap/ochenstarik-server-monitor-manager.sh" ]] || fail "Packaged bootstrap is missing." log "Release archive and checksum are valid." } ensure_system_user() { local user="$1" if ! getent group "$user" >/dev/null; then groupadd --system "$user" fi if ! id "$user" >/dev/null 2>&1; then useradd --system --gid "$user" --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin "$user" fi } ensure_mesh_packages() { local missing=0 command_name for command_name in wg wg-quick nft ip; do command -v "$command_name" >/dev/null 2>&1 || missing=1 done (( missing == 0 )) && return require_command apt-get log "Installing WireGuard/nftables dependencies." DEBIAN_FRONTEND=noninteractive apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ wireguard-tools nftables iproute2 } install_tree_atomic() { local source="$1" destination="$2" owner="$3" staging [[ -d "$source" ]] || fail "Release payload is missing: $source" staging="${destination}.new.$$" rm -rf -- "$staging" install -d -m 0755 "$staging" cp -a -- "$source/." "$staging/" chown -R "$owner" "$staging" find "$staging" -type d -exec chmod 0755 {} + find "$staging" -type f -exec chmod 0644 {} + find "$staging" -type f -name 'ochenstarik-smm-*' -exec chmod 0755 {} + rm -rf -- "$destination" mv -- "$staging" "$destination" } create_backup() { local role="$1" backup_id archive list=() backup_id="$(date -u +%Y%m%dT%H%M%SZ)-${role}-$RANDOM" install -d -m 0700 "$BACKUP_DIR" archive="$BACKUP_DIR/${backup_id}.tar.gz" case "$role" in control) list=( "usr/local/lib/ochenstarik-server-monitor-manager/control" "etc/ochenstarik-server-monitor-manager/control.env" "etc/ochenstarik-server-monitor-manager/control-ca.pfx" "etc/ochenstarik-server-monitor-manager/control-server.pfx" "etc/systemd/system/$CONTROL_UNIT" "usr/local/libexec/ochenstarik-smm-policy-apply" "etc/sudoers.d/ochenstarik-smm-control" ) ;; agent) list=( "usr/local/lib/ochenstarik-server-monitor-manager/agent" "usr/local/lib/ochenstarik-server-monitor-manager/provisioning-helper" "etc/ochenstarik-server-monitor-manager/agent.env" "etc/ochenstarik-server-monitor-manager/control-ca.crt" "etc/systemd/system/$AGENT_UNIT" "etc/systemd/system/$PROVISIONING_HELPER_UNIT" ) ;; *) fail "Unknown backup role: $role" ;; esac local existing=() item for item in "${list[@]}"; do [[ -e "/$item" ]] && existing+=("$item") done if (( ${#existing[@]} == 0 )); then printf '%s\n' "empty" >"$BACKUP_DIR/${backup_id}.empty" else tar -C / -czf "$archive" -- "${existing[@]}" chmod 0600 "$archive" fi printf '%s\n' "$backup_id" } install_unit() { local source="$1" unit="$2" install -m 0644 "$source" "/etc/systemd/system/$unit" systemctl daemon-reload } install_bootstrap_command() { local source="$TEMP_DIR/bootstrap/ochenstarik-server-monitor-manager.sh" staging [[ -x "$source" ]] || fail "Packaged bootstrap is missing." install -d -m 0755 "$(dirname "$BOOTSTRAP_COMMAND")" staging="$(mktemp "$(dirname "$BOOTSTRAP_COMMAND")/.ochenstarik-server-monitor-manager.XXXXXXXX")" if ! install -m 0755 -o root -g root "$source" "$staging"; then rm -f -- "$staging" fail "Could not stage the system bootstrap command." fi if ! mv -fT -- "$staging" "$BOOTSTRAP_COMMAND"; then rm -f -- "$staging" fail "Could not publish the system bootstrap command." fi } validate_control_state_migration() { local name for name in control.db control.db-wal control.db-shm; do [[ ! -e "$STATE_DIR/$name" || ! -e "$STATE_DIR/control/$name" ]] \ || fail "Both legacy and role-isolated Control state exist: $name" done [[ ! -e "$STATE_DIR/backups" || ! -e "$STATE_DIR/control/backups" ]] \ || fail "Both legacy and role-isolated Control backup directories exist." } record_control_legacy_state() { local name CONTROL_UPDATE_LEGACY_ITEMS=() for name in control.db control.db-wal control.db-shm; do [[ ! -e "$STATE_DIR/$name" ]] || CONTROL_UPDATE_LEGACY_ITEMS+=("$name") done [[ ! -e "$STATE_DIR/backups" ]] || CONTROL_UPDATE_LEGACY_ITEMS+=(backups) } prepare_control_state() { local name install -d -m 0700 -o "$CONTROL_USER" -g "$CONTROL_USER" "$STATE_DIR/control" for name in control.db control.db-wal control.db-shm; do if [[ -e "$STATE_DIR/$name" ]]; then mv -- "$STATE_DIR/$name" "$STATE_DIR/control/$name" fi done if [[ -e "$STATE_DIR/backups" ]]; then mv -- "$STATE_DIR/backups" "$STATE_DIR/control/backups" fi install -d -m 0700 -o "$CONTROL_USER" -g "$CONTROL_USER" "$STATE_DIR/control/backups" chown -R "$CONTROL_USER:$CONTROL_USER" "$STATE_DIR/control" find "$STATE_DIR/control" -type d -exec chmod 0700 {} + find "$STATE_DIR/control" -type f -exec chmod 0600 {} + } reverse_control_state_migration() { local name source destination for name in "${CONTROL_UPDATE_LEGACY_ITEMS[@]}"; do source="$STATE_DIR/control/$name" destination="$STATE_DIR/$name" if [[ -e "$destination" ]]; then [[ ! -e "$source" ]] || return 1 continue fi [[ ! -e "$source" ]] || mv -- "$source" "$destination" || return 1 done rmdir "$STATE_DIR/control/backups" 2>/dev/null || true rmdir "$STATE_DIR/control" 2>/dev/null || true } restore_control_update_backup() { local archive="$1" restore_root="${2:-/}" [[ -f "$archive" ]] || return 1 tar -C "$restore_root" -xzf "$archive" } restore_control_binary_from_archive() { local archive="$1" restore_root="${2:-/}" [[ -f "$archive" ]] || return 1 tar -C "$restore_root" -xzf "$archive" \ usr/local/lib/ochenstarik-server-monitor-manager/control } recover_control_update() { local backup_id="$1" restore_root="${2:-/}" local archive="$BACKUP_DIR/${backup_id}.tar.gz" systemctl stop "$CONTROL_UNIT" 2>/dev/null || true reverse_control_state_migration || return 1 restore_control_update_backup "$archive" "$restore_root" || return 1 systemctl daemon-reload || return 1 systemctl start "$CONTROL_UNIT" || return 1 systemctl is-active --quiet "$CONTROL_UNIT" } validate_control_environment_migration() { local env_file="$ETC_DIR/control.env" database_count backup_count database_value backup_value [[ -f "$env_file" && ! -L "$env_file" ]] \ || fail "Control environment is missing or unsafe." database_count="$(grep -c '^Control__DatabasePath=' "$env_file" || true)" backup_count="$(grep -c '^Control__BackupDirectory=' "$env_file" || true)" [[ "$database_count" == 1 && "$backup_count" == 1 ]] \ || fail "Control environment contains missing or conflicting state paths." database_value="$(grep '^Control__DatabasePath=' "$env_file")" backup_value="$(grep '^Control__BackupDirectory=' "$env_file")" case "$database_value" in "Control__DatabasePath=$STATE_DIR/control.db"|"Control__DatabasePath=$STATE_DIR/control/control.db") ;; *) fail "Control environment contains an unsupported database path." ;; esac case "$backup_value" in "Control__BackupDirectory=$STATE_DIR/backups"|"Control__BackupDirectory=$STATE_DIR/control/backups") ;; *) fail "Control environment contains an unsupported backup path." ;; esac } rewrite_control_environment() { local env_file="$ETC_DIR/control.env" staging line staging="$(mktemp "$ETC_DIR/.control.env.XXXXXXXX")" if ! while IFS= read -r line || [[ -n "$line" ]]; do case "$line" in Control__DatabasePath=*) printf 'Control__DatabasePath=%s/control/control.db\n' "$STATE_DIR" ;; Control__BackupDirectory=*) printf 'Control__BackupDirectory=%s/control/backups\n' "$STATE_DIR" ;; *) printf '%s\n' "$line" ;; esac done <"$env_file" >"$staging"; then rm -f -- "$staging" fail "Could not rewrite the Control environment." fi chown root:"$CONTROL_USER" "$staging" chmod 0640 "$staging" if ! mv -fT -- "$staging" "$env_file"; then rm -f -- "$staging" fail "Could not publish the Control environment." fi } write_mesh_firewall() { cat >"$ETC_DIR/mesh.nft" <<'EOF' table inet ochenstarik_smm { chain links { ct state established,related accept counter drop } chain mesh_forward { type filter hook forward priority filter; policy accept; iifname "smm0" oifname "smm0" jump links } } EOF chmod 0644 "$ETC_DIR/mesh.nft" if ! nft list table inet ochenstarik_smm >/dev/null 2>&1; then nft --check -f "$ETC_DIR/mesh.nft" fi } read_mesh_value() { local key="$1" [[ -r "$ETC_DIR/mesh.env" ]] || fail "Mesh Hub is not initialized." awk -F '=' -v key="$key" '$1 == key { print substr($0, index($0, "=") + 1); exit }' "$ETC_DIR/mesh.env" } render_hub_wireguard_config() { local private_key endpoint port node_id address public_key status private_key="$(cat "$WG_DIR/hub.key")" endpoint="$(read_mesh_value HUB_ENDPOINT)" port="${endpoint##*:}" cat >"/etc/wireguard/smm0.conf" <>"/etc/wireguard/smm0.conf" <"$WG_DIR/hub.key" fi hub_private="$(cat "$WG_DIR/hub.key")" hub_public="$(printf '%s' "$hub_private" | wg pubkey)" printf '%s\n' "$hub_public" >"$WG_DIR/hub.pub" chmod 0600 "$WG_DIR/hub.key" chmod 0644 "$WG_DIR/hub.pub" cat >"$ETC_DIR/mesh.env" <"/etc/sysctl.d/90-ochenstarik-smm-mesh.conf" sysctl --system >/dev/null write_mesh_firewall [[ -f "$LIB_DIR/control/ochenstarik-smm-control" ]] \ || log "Warning: Control is not installed yet; mesh peer codes require Control enrollment." if [[ -f "${TEMP_DIR:-}/deploy/$FIREWALL_UNIT" ]]; then install_unit "$TEMP_DIR/deploy/$FIREWALL_UNIT" "$FIREWALL_UNIT" elif [[ -f "$LIB_DIR/bootstrap/$FIREWALL_UNIT" ]]; then install_unit "$LIB_DIR/bootstrap/$FIREWALL_UNIT" "$FIREWALL_UNIT" else fail "Mesh firewall systemd unit is unavailable; reinstall Control from the current release." fi systemctl enable "$FIREWALL_UNIT" systemctl restart "$FIREWALL_UNIT" render_hub_wireguard_config systemctl enable wg-quick@smm0.service systemctl restart wg-quick@smm0.service log "Mesh Hub initialized at $public_endpoint:$port with $MESH_NETWORK." log "WireGuard public key: $hub_public" } reserve_node_address() { local node_id="$1" existing host address install -d -m 0700 "$MESH_DIR" touch "$MESH_DIR/nodes.tsv" chmod 0600 "$MESH_DIR/nodes.tsv" existing="$(awk -F '\t' -v node="$node_id" '$1 == node { print $2; exit }' "$MESH_DIR/nodes.tsv")" if [[ -n "$existing" ]]; then printf '%s\n' "$existing" return fi for host in $(seq 2 254); do address="10.77.0.$host" if ! awk -F '\t' -v address="$address" '$2 == address { found=1 } END { exit found ? 0 : 1 }' "$MESH_DIR/nodes.tsv"; then printf '%s\t%s\t-\treserved\n' "$node_id" "$address" >>"$MESH_DIR/nodes.tsv" printf '%s\n' "$address" return fi done fail "Mesh address pool is exhausted." } configure_node_wireguard() { local node_id="$1" node_address="$2" hub_endpoint="$3" hub_public_key="$4" node_private node_public ensure_mesh_packages install -d -m 0700 "$WG_DIR" /etc/wireguard if [[ ! -f "$WG_DIR/node.key" ]]; then umask 077 wg genkey >"$WG_DIR/node.key" fi node_private="$(cat "$WG_DIR/node.key")" node_public="$(printf '%s' "$node_private" | wg pubkey)" printf '%s\n' "$node_public" >"$WG_DIR/node.pub" cat >"/etc/wireguard/smm0.conf" <"$ext_file" openssl x509 -req -sha256 -days 825 -in "$server_csr" -CA "$ca_cert" -CAkey "$ca_key" \ -CAserial "$serial_file" -CAcreateserial -out "$TEMP_DIR/control-server.crt" -extfile "$ext_file" openssl pkcs12 -export -out "$ETC_DIR/control-ca.pfx" -inkey "$ca_key" -in "$ca_cert" -passout pass: openssl pkcs12 -export -out "$ETC_DIR/control-server.pfx" -inkey "$server_key" \ -in "$TEMP_DIR/control-server.crt" -certfile "$ca_cert" -passout pass: install -m 0644 "$ca_cert" "$ETC_DIR/control-ca.crt" chown root:"$CONTROL_USER" "$ETC_DIR/control-ca.pfx" "$ETC_DIR/control-server.pfx" chmod 0640 "$ETC_DIR/control-ca.pfx" "$ETC_DIR/control-server.pfx" } install_control() { local archive="$1" public_host="$2" port="${3:-7443}" backup_id require_root validate_platform validate_port "$port" require_command openssl require_command sha256sum require_command tar require_command systemctl require_command sudo require_command visudo extract_archive "$archive" [[ -x "$TEMP_DIR/control/ochenstarik-smm-control" ]] || fail "Control binary is missing." backup_id="$(create_backup control)" ensure_system_user "$CONTROL_USER" validate_control_state_migration systemctl stop "$CONTROL_UNIT" 2>/dev/null || true install -d -m 0711 -o root -g root "$ETC_DIR" "$STATE_DIR" prepare_control_state install_tree_atomic "$TEMP_DIR/control" "$LIB_DIR/control" "root:root" install_bootstrap_command if [[ ! -f "$ETC_DIR/control-ca.pfx" || ! -f "$ETC_DIR/control-server.pfx" ]]; then create_control_certificates "$public_host" fi cat >"$ETC_DIR/control.env" <"$ETC_DIR/control-public-url" chown root:"$CONTROL_USER" "$ETC_DIR/control.env" chmod 0640 "$ETC_DIR/control.env" chmod 0644 "$ETC_DIR/control-public-url" install -d -m 0755 "$(dirname "$POLICY_HELPER")" install -m 0755 "$TEMP_DIR/deploy/ochenstarik-smm-policy-apply" "$POLICY_HELPER" install -d -m 0755 "$(dirname "$EMERGENCY_COMMAND")" install -m 0755 "$TEMP_DIR/deploy/ochenstarik-smm-emergency" "$EMERGENCY_COMMAND" install -d -m 0755 "$LIB_DIR/bootstrap" install -m 0644 "$TEMP_DIR/deploy/$FIREWALL_UNIT" "$LIB_DIR/bootstrap/$FIREWALL_UNIT" printf '%s\n' "$CONTROL_USER ALL=(root) NOPASSWD: $POLICY_HELPER *" >"$SUDOERS_FILE" chmod 0440 "$SUDOERS_FILE" visudo -cf "$SUDOERS_FILE" >/dev/null install_unit "$TEMP_DIR/deploy/$CONTROL_UNIT" "$CONTROL_UNIT" systemctl enable --now "$CONTROL_UNIT" systemctl is-active --quiet "$CONTROL_UNIT" || { systemctl status --no-pager "$CONTROL_UNIT" >&2 || true fail "Control service failed; backup is $backup_id" } record_installed_version control "$(archive_version "$archive")" log "Control installed. Backup: $backup_id" log "CA fingerprint: $(openssl x509 -in "$ETC_DIR/control-ca.crt" -noout -fingerprint -sha256 | cut -d= -f2)" } read_enrollment_token() { if [[ -n "${SMM_ENROLL_TOKEN:-}" ]]; then ENROLL_TOKEN="$SMM_ENROLL_TOKEN" unset SMM_ENROLL_TOKEN return fi [[ -t 0 ]] || fail "Set SMM_ENROLL_TOKEN or run from an interactive local terminal." read -r -s -p "One-time enrollment token: " ENROLL_TOKEN printf '\n' [[ -n "$ENROLL_TOKEN" ]] || fail "Enrollment token is empty." } install_agent() { local archive="$1" node_id="$2" control_url="$3" ca_cert="$4" backup_id token_file token_temp require_root validate_platform validate_node_id "$node_id" validate_control_url "$control_url" [[ -f "$ca_cert" ]] || fail "Control CA certificate not found: $ca_cert" require_command sha256sum require_command tar require_command systemctl require_command runuser require_command openssl openssl x509 -in "$ca_cert" -noout >/dev/null 2>&1 || fail "Invalid Control CA certificate." extract_archive "$archive" [[ -x "$TEMP_DIR/agent/ochenstarik-smm-agent" ]] || fail "Agent binary is missing." [[ -x "$TEMP_DIR/provisioning-helper/ochenstarik-smm-provisioning-helper" ]] || fail "Provisioning helper binary is missing." backup_id="$(create_backup agent)" ensure_system_user "$AGENT_USER" install -d -m 0711 -o root -g root "$ETC_DIR" "$STATE_DIR" install -d -m 0700 -o "$AGENT_USER" -g "$AGENT_USER" "$STATE_DIR/agent" install -d -m 0700 -o "$AGENT_USER" -g "$AGENT_USER" "$ENROLLMENT_DIR" install -d -m 0700 -o root -g root "$STATE_DIR/provisioning/rollback" install_tree_atomic "$TEMP_DIR/agent" "$LIB_DIR/agent" "root:root" install_tree_atomic "$TEMP_DIR/provisioning-helper" "$LIB_DIR/provisioning-helper" "root:root" install_bootstrap_command install -d -m 0755 "$(dirname "$EMERGENCY_COMMAND")" install -m 0755 "$TEMP_DIR/deploy/ochenstarik-smm-emergency" "$EMERGENCY_COMMAND" if [[ "$(realpath "$ca_cert")" != "$(realpath -m "$ETC_DIR/control-ca.crt")" ]]; then install -m 0600 -o "$AGENT_USER" -g "$AGENT_USER" "$ca_cert" "$ETC_DIR/control-ca.crt" else chown "$AGENT_USER:$AGENT_USER" "$ETC_DIR/control-ca.crt" chmod 0600 "$ETC_DIR/control-ca.crt" fi cat >"$ETC_DIR/agent.env" <"$token_temp"; then ENROLL_TOKEN="" fail "Could not write the enrollment token file." fi chown "$AGENT_USER:$AGENT_USER" "$token_temp" chmod 0400 "$token_temp" ENROLLMENT_TOKEN_FILE="$token_file" mv -fT -- "$token_temp" "$token_file" ENROLLMENT_TOKEN_TEMP="" ENROLL_TOKEN="" if ! runuser -u "$AGENT_USER" -- env \ "SMM_NodeId=$node_id" \ "SMM_ControlUrl=${control_url%/}" \ "SMM_StateDirectory=$STATE_DIR/agent" \ "SMM_EnrollmentTokenDirectory=$ENROLLMENT_DIR" \ "SMM_CertificateAuthorityPath=$ETC_DIR/control-ca.crt" \ "SMM_EnrollTokenFile=$token_file" \ "$LIB_DIR/agent/ochenstarik-smm-agent"; then rm -f -- "$token_file" fail "Agent enrollment failed." fi rm -f -- "$token_file" ENROLLMENT_TOKEN_FILE="" chown root:"$AGENT_USER" "$ETC_DIR/control-ca.crt" chmod 0640 "$ETC_DIR/control-ca.crt" install_unit "$TEMP_DIR/deploy/$AGENT_UNIT" "$AGENT_UNIT" install_unit "$TEMP_DIR/deploy/$PROVISIONING_HELPER_UNIT" "$PROVISIONING_HELPER_UNIT" systemctl enable --now "$PROVISIONING_HELPER_UNIT" systemctl enable --now "$AGENT_UNIT" systemctl is-active --quiet "$AGENT_UNIT" || { systemctl status --no-pager "$AGENT_UNIT" >&2 || true fail "Agent service failed; backup is $backup_id" } record_installed_version agent "$(archive_version "$archive")" log "Agent $node_id installed and enrolled. Backup: $backup_id" } read_enrollment_code() { if [[ -n "${SMM_ENROLL_CODE:-}" ]]; then ENROLL_CODE="$SMM_ENROLL_CODE" unset SMM_ENROLL_CODE return fi [[ -t 0 ]] || fail "Set SMM_ENROLL_CODE or run from an interactive local terminal." read -r -s -p "SMMNODE enrollment code: " ENROLL_CODE printf '\n' [[ -n "$ENROLL_CODE" ]] || fail "Enrollment code is empty." } confirm_ca_fingerprint() { local ca_file="$1" answer log "Control CA fingerprint: $(openssl x509 -in "$ca_file" -noout -fingerprint -sha256 | cut -d= -f2)" if [[ "${SMM_ACCEPT_CA_FINGERPRINT:-}" == "1" ]]; then return fi [[ -t 0 ]] || fail "Set SMM_ACCEPT_CA_FINGERPRINT=1 only after verifying the fingerprint out of band." read -r -p "Type 'yes' after comparing this fingerprint with the Hub: " answer [[ "$answer" == "yes" ]] || fail "Control CA fingerprint was not confirmed." } install_node_from_code() { local archive="$1" prefix control_part ca_part node_part token_part local endpoint_part hub_key_part address_part network_part extra local control_url node_id token ca_file hub_endpoint hub_public_key node_address mesh_network require_root require_command base64 require_command openssl read_enrollment_code IFS='.' read -r prefix control_part ca_part node_part token_part endpoint_part \ hub_key_part address_part network_part extra <<<"$ENROLL_CODE" ENROLL_CODE="" [[ "$prefix" == "SMMNODE1" || "$prefix" == "SMMNODE2" ]] \ || fail "Unsupported SMMNODE enrollment code version." [[ -n "$control_part" && -n "$ca_part" && -n "$node_part" && -n "$token_part" \ && -z "${extra:-}" ]] || fail "Invalid SMMNODE enrollment code." if [[ "$prefix" == "SMMNODE1" ]]; then [[ -z "${endpoint_part:-}${hub_key_part:-}${address_part:-}${network_part:-}" ]] \ || fail "Invalid SMMNODE1 enrollment code." else [[ -n "${endpoint_part:-}" && -n "${hub_key_part:-}" \ && -n "${address_part:-}" && -n "${network_part:-}" ]] \ || fail "Invalid SMMNODE2 mesh enrollment code." fi control_url="$(base64url_decode "$control_part")" node_id="$(base64url_decode "$node_part")" token="$(base64url_decode "$token_part")" if [[ "$prefix" == "SMMNODE2" ]]; then hub_endpoint="$(base64url_decode "$endpoint_part")" hub_public_key="$(base64url_decode "$hub_key_part")" node_address="$(base64url_decode "$address_part")" mesh_network="$(base64url_decode "$network_part")" [[ "$hub_endpoint" =~ ^[A-Za-z0-9.-]+:[0-9]{1,5}$ ]] || fail "Invalid Hub WireGuard endpoint." validate_port "${hub_endpoint##*:}" [[ "$hub_public_key" =~ ^[A-Za-z0-9+/]{43}=$ ]] || fail "Invalid Hub WireGuard public key." [[ "$node_address" =~ ^10\.77\.0\.([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])$ ]] \ || fail "Invalid reserved mesh address." [[ "$mesh_network" == "$MESH_NETWORK" ]] || fail "Unsupported mesh network." fi ca_file="$(mktemp -t smm-control-ca.XXXXXXXX.crt)" chmod 0600 "$ca_file" base64url_decode "$ca_part" >"$ca_file" confirm_ca_fingerprint "$ca_file" SMM_ENROLL_TOKEN="$token" token="" install_agent "$archive" "$node_id" "$control_url" "$ca_file" rm -f -- "$ca_file" if [[ "$prefix" == "SMMNODE2" ]]; then configure_node_wireguard "$node_id" "$node_address" "$hub_endpoint" "$hub_public_key" log "Node mesh configured. Copy this public peer code back to the Hub:" printf '%s\n' "$MESH_PEER_CODE" MESH_PEER_CODE="" fi } refresh_agent_uid() { local env_file="$ETC_DIR/agent.env" agent_uid temp line found=0 [[ -f "$env_file" && ! -L "$env_file" ]] \ || fail "Agent environment is missing or unsafe: $env_file" agent_uid="$(id -u "$AGENT_USER")" temp="$(mktemp "$ETC_DIR/.agent.env.XXXXXXXX")" if ! while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" == SMM_AgentUid=* ]]; then printf 'SMM_AgentUid=%s\n' "$agent_uid" found=1 else printf '%s\n' "$line" fi done <"$env_file" >"$temp"; then rm -f -- "$temp" fail "Could not refresh SMM_AgentUid in agent.env." fi if [[ "$found" == "0" ]]; then printf 'SMM_AgentUid=%s\n' "$agent_uid" >>"$temp" fi chown root:"$AGENT_USER" "$temp" chmod 0640 "$temp" mv -fT -- "$temp" "$env_file" } update_role() { local role="$1" archive="$2" binary unit user backup_id require_root validate_platform extract_archive "$archive" case "$role" in control) binary="ochenstarik-smm-control"; unit="$CONTROL_UNIT"; user="root:root" ;; agent) binary="ochenstarik-smm-agent"; unit="$AGENT_UNIT"; user="root:root" ;; *) fail "Unknown role: $role" ;; esac [[ -x "$TEMP_DIR/$role/$binary" ]] || fail "$role binary is missing." local manifest new_version m_control m_agent installed peer_role peer_expected manifest="$(dirname "$archive")/server-monitor-manager-manifest.json" if [[ -f "$manifest" ]]; then new_version="$(manifest_version_field "$manifest" version)" m_control="$(manifest_version_field "$manifest" control)" m_agent="$(manifest_version_field "$manifest" agent)" if [[ -n "$new_version" ]]; then # Compared against the version recorded when this role was installed, not against # PROGRAM_VERSION: that constant describes the bootstrap source tree and never the # deployed component, so it can neither detect a downgrade nor match a release tag. installed="$(read_installed_version "$role")" if [[ -n "$installed" ]] \ && version_lt "$new_version" "$installed" \ && [[ "${SMM_ALLOW_DOWNGRADE:-0}" != "1" ]]; then fail "Downgrade of $role from $installed to $new_version is not allowed. Set SMM_ALLOW_DOWNGRADE=1 to bypass." fi # Cross-role compatibility: the peer component already on this host must run the # version this archive expects of it. An unknown peer version is reported but not # treated as failure, because installations predating version recording have # nothing to compare against. case "$role" in control) peer_role="agent"; peer_expected="$m_agent" ;; agent) peer_role="control"; peer_expected="$m_control" ;; *) peer_role="" ;; esac if [[ -n "$peer_role" ]] \ && systemctl list-unit-files 2>/dev/null | grep -q "^ochenstarik-smm-${peer_role}.service"; then local peer_actual peer_actual="$(read_installed_version "$peer_role")" if [[ -z "$peer_actual" ]]; then log "Warning: installed $peer_role version is unknown; compatibility check skipped." elif [[ -n "$peer_expected" ]] \ && [[ "$(normalize_version "$peer_actual")" != "$(normalize_version "$peer_expected")" ]]; then fail "Incompatible versions: installed $peer_role is $peer_actual, but this archive expects $peer_role $peer_expected. Update rejected." fi fi log "Updating $role to version $new_version" fi fi if [[ "$role" == "control" ]]; then validate_control_state_migration validate_control_environment_migration record_control_legacy_state else refresh_agent_uid fi backup_id="$(create_backup "$role")" if [[ "$role" == "control" ]]; then CONTROL_UPDATE_BACKUP_ID="$backup_id" CONTROL_UPDATE_RECOVERY_REQUIRED=1 fi systemctl stop "$unit" if [[ "$role" == "agent" ]]; then systemctl stop "$PROVISIONING_HELPER_UNIT" 2>/dev/null || true fi if [[ "$role" == "control" ]]; then install -d -m 0711 -o root -g root "$ETC_DIR" "$STATE_DIR" prepare_control_state rewrite_control_environment install_bootstrap_command install_unit "$TEMP_DIR/deploy/$CONTROL_UNIT" "$CONTROL_UNIT" systemctl enable "$CONTROL_UNIT" install_tree_atomic "$TEMP_DIR/control" "$LIB_DIR/control" "$user" else install_tree_atomic "$TEMP_DIR/$role" "$LIB_DIR/$role" "$user" [[ -x "$TEMP_DIR/provisioning-helper/ochenstarik-smm-provisioning-helper" ]] \ || fail "Provisioning helper binary is missing." install_tree_atomic "$TEMP_DIR/provisioning-helper" "$LIB_DIR/provisioning-helper" "root:root" install_unit "$TEMP_DIR/deploy/$AGENT_UNIT" "$AGENT_UNIT" install_unit "$TEMP_DIR/deploy/$PROVISIONING_HELPER_UNIT" "$PROVISIONING_HELPER_UNIT" systemctl enable --now "$PROVISIONING_HELPER_UNIT" fi systemctl restart "$unit" if ! systemctl is-active --quiet "$unit"; then if [[ "$role" == "agent" ]]; then log "Update failed; restoring backup $backup_id" restore_backup "$role" "$backup_id" fi fail "$role update was rolled back." fi if [[ "$role" == "control" ]]; then CONTROL_UPDATE_RECOVERY_REQUIRED=0 CONTROL_UPDATE_BACKUP_ID="" CONTROL_UPDATE_LEGACY_ITEMS=() fi record_installed_version "$role" "$(archive_version "$archive")" log "$role updated. Backup: $backup_id" } latest_backup_id() { local role="$1" path path="$(find "$BACKUP_DIR" -maxdepth 1 -type f \( -name "*-${role}-*.tar.gz" -o -name "*-${role}-*.empty" \) -printf '%f\n' 2>/dev/null | sort | tail -n1)" [[ -n "$path" ]] || fail "No backup found for $role." printf '%s\n' "${path%.tar.gz}" | sed 's/\.empty$//' } restore_backup() { local role="$1" backup_id="$2" archive="$BACKUP_DIR/${backup_id}.tar.gz" unit case "$role" in control) unit="$CONTROL_UNIT" ;; agent) unit="$AGENT_UNIT" ;; *) fail "Unknown role: $role" ;; esac [[ -f "$archive" ]] || fail "Backup archive not found: $backup_id" systemctl stop "$unit" || true if [[ "$role" == "agent" ]]; then systemctl stop "$PROVISIONING_HELPER_UNIT" || true fi if [[ "$role" == "control" ]]; then restore_control_binary_from_archive "$archive" else tar -C / -xzf "$archive" fi systemctl daemon-reload if [[ "$role" == "agent" ]]; then systemctl start "$PROVISIONING_HELPER_UNIT" fi systemctl start "$unit" systemctl is-active --quiet "$unit" || fail "Rollback restored files but service is not active." log "$role restored from $backup_id" } rollback_role() { local role="$1" backup_id="${2:-}" require_root [[ -n "$backup_id" ]] || backup_id="$(latest_backup_id "$role")" restore_backup "$role" "$backup_id" } show_status() { local unit for unit in "$CONTROL_UNIT" "$AGENT_UNIT"; do if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q "^$unit"; then printf '%s: %s\n' "$unit" "$(systemctl is-active "$unit" 2>/dev/null || true)" else printf '%s: not-installed\n' "$unit" fi done if [[ -f "$ETC_DIR/control-ca.crt" ]] && command -v openssl >/dev/null; then printf 'control-ca: %s\n' "$(openssl x509 -in "$ETC_DIR/control-ca.crt" -noout -fingerprint -sha256 | cut -d= -f2)" fi } run_control_cli() { local command_name="$1" identifier="$2" require_root validate_node_id "$identifier" [[ -x "$LIB_DIR/control/ochenstarik-smm-control" ]] || fail "Control is not installed." [[ -f "$ETC_DIR/control.env" ]] || fail "Control environment is missing." require_command systemd-run systemd-run --wait --pipe --quiet --collect \ --uid="$CONTROL_USER" \ --gid="$CONTROL_USER" \ -p "EnvironmentFile=$ETC_DIR/control.env" \ "$LIB_DIR/control/ochenstarik-smm-control" "$command_name" "$identifier" } create_node_code() { local node_id="$1" token control_url ca_pem node_address hub_endpoint hub_public_key mesh_network require_root validate_node_id "$node_id" [[ -r "$ETC_DIR/control-public-url" ]] || fail "Control public URL is missing; reinstall Control with PUBLIC_HOST." [[ -r "$ETC_DIR/control-ca.crt" ]] || fail "Control CA certificate is missing." require_command base64 control_url="$(tr -d '\r\n' <"$ETC_DIR/control-public-url")" validate_control_url "$control_url" token="$(run_control_cli token-create "$node_id")" [[ -n "$token" && "$token" != *$'\n'* ]] || fail "Control returned an invalid enrollment token." ca_pem="$(cat "$ETC_DIR/control-ca.crt")" if [[ -r "$ETC_DIR/mesh.env" && -r "$WG_DIR/hub.pub" ]]; then node_address="$(reserve_node_address "$node_id")" hub_endpoint="$(read_mesh_value HUB_ENDPOINT)" hub_public_key="$(read_mesh_value HUB_PUBLIC_KEY)" mesh_network="$(read_mesh_value MESH_NETWORK)" printf 'SMMNODE2.%s.%s.%s.%s.%s.%s.%s.%s\n' \ "$(printf '%s' "$control_url" | base64url_encode)" \ "$(printf '%s' "$ca_pem" | base64url_encode)" \ "$(printf '%s' "$node_id" | base64url_encode)" \ "$(printf '%s' "$token" | base64url_encode)" \ "$(printf '%s' "$hub_endpoint" | base64url_encode)" \ "$(printf '%s' "$hub_public_key" | base64url_encode)" \ "$(printf '%s' "$node_address" | base64url_encode)" \ "$(printf '%s' "$mesh_network" | base64url_encode)" else printf 'SMMNODE1.%s.%s.%s.%s\n' \ "$(printf '%s' "$control_url" | base64url_encode)" \ "$(printf '%s' "$ca_pem" | base64url_encode)" \ "$(printf '%s' "$node_id" | base64url_encode)" \ "$(printf '%s' "$token" | base64url_encode)" fi token="" } create_device_code() { local device_id="$1" token control_url ca_der require_root validate_node_id "$device_id" [[ -r "$ETC_DIR/control-public-url" ]] || fail "Control public URL is missing; reinstall Control with PUBLIC_HOST." [[ -r "$ETC_DIR/control-ca.crt" ]] || fail "Control CA certificate is missing." require_command base64 require_command openssl control_url="$(tr -d '\r\n' <"$ETC_DIR/control-public-url")" validate_control_url "$control_url" token="$(run_control_cli device-token-create "$device_id")" [[ "$token" =~ ^[A-Za-z0-9_-]{43}$ ]] \ || fail "Control returned an invalid device enrollment token." ca_der="$(openssl x509 -in "$ETC_DIR/control-ca.crt" -outform DER | base64 -w 0)" printf 'SMMDEV1-' printf 'VERSION=1\nDEVICE=%s\nTOKEN=%s\nURL=%s\nCA=%s\n' \ "$device_id" "$token" "$control_url" "$ca_der" | base64url_encode printf '\n' token="" } add_mesh_peer() { local code="$1" prefix node_part address_part key_part extra local node_id address public_key current tmp require_root require_command base64 require_command wg [[ -r "$ETC_DIR/mesh.env" && -r "$MESH_DIR/nodes.tsv" ]] || fail "Mesh Hub is not initialized." IFS='.' read -r prefix node_part address_part key_part extra <<<"$code" [[ "$prefix" == "SMMPEER1" && -n "$node_part" && -n "$address_part" \ && -n "$key_part" && -z "${extra:-}" ]] || fail "Invalid SMMPEER1 code." node_id="$(base64url_decode "$node_part")" address="$(base64url_decode "$address_part")" public_key="$(base64url_decode "$key_part")" validate_node_id "$node_id" [[ "$address" =~ ^10\.77\.0\.([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])$ ]] \ || fail "Invalid peer mesh address." [[ "$public_key" =~ ^[A-Za-z0-9+/]{43}=$ ]] || fail "Invalid peer WireGuard public key." current="$(awk -F '\t' -v node="$node_id" '$1 == node { print $2; exit }' "$MESH_DIR/nodes.tsv")" [[ "$current" == "$address" ]] || fail "Peer address does not match the Hub reservation." if awk -F '\t' -v address="$address" -v node="$node_id" '$2 == address && $1 != node { found=1 } END { exit found ? 0 : 1 }' "$MESH_DIR/nodes.tsv"; then fail "Peer mesh address is already assigned." fi tmp="$(mktemp -p "$MESH_DIR" nodes.tsv.XXXXXXXX)" awk -F '\t' -v OFS='\t' -v node="$node_id" -v address="$address" -v key="$public_key" \ '$1 == node { print node, address, key, "active"; found=1; next } { print } END { if (!found) exit 1 }' \ "$MESH_DIR/nodes.tsv" >"$tmp" || { rm -f -- "$tmp"; fail "Peer reservation is missing."; } chmod 0600 "$tmp" mv -- "$tmp" "$MESH_DIR/nodes.tsv" render_hub_wireguard_config systemctl restart wg-quick@smm0.service systemctl is-active --quiet wg-quick@smm0.service || fail "WireGuard failed after peer registration." log "Mesh peer $node_id activated at $address." } show_mesh_status() { require_root [[ -r "$ETC_DIR/mesh.env" ]] || fail "Mesh Hub is not initialized." printf 'endpoint: %s\n' "$(read_mesh_value HUB_ENDPOINT)" printf 'network: %s\n' "$(read_mesh_value MESH_NETWORK)" if [[ -r "$MESH_DIR/nodes.tsv" ]]; then printf '%-24s %-15s %-10s %s\n' NODE ADDRESS STATUS HANDSHAKE while IFS=$'\t' read -r node_id address public_key status; do [[ -n "$node_id" ]] || continue local handshake="-" if [[ "$status" == "active" ]]; then handshake="$(wg show smm0 latest-handshakes 2>/dev/null | awk -v key="$public_key" '$1 == key { print $2; exit }')" [[ -n "$handshake" && "$handshake" != "0" ]] || handshake="never" fi printf '%-24s %-15s %-10s %s\n' "$node_id" "$address" "$status" "$handshake" done <"$MESH_DIR/nodes.tsv" fi } show_ca_fingerprint() { [[ -f "$ETC_DIR/control-ca.crt" ]] || fail "Control CA certificate is not installed." require_command openssl openssl x509 -in "$ETC_DIR/control-ca.crt" -noout -fingerprint -sha256 } role_is_installed() { case "$1" in control) [[ -d "$LIB_DIR/control" || -f "$ETC_DIR/control.env" \ || -f "/etc/systemd/system/${CONTROL_UNIT:-ochenstarik-smm-control.service}" ]] ;; agent) [[ -d "$LIB_DIR/agent" || -f "$ETC_DIR/agent.env" \ || -f "/etc/systemd/system/${AGENT_UNIT:-ochenstarik-smm-agent.service}" ]] ;; *) return 1 ;; esac } remove_shared_ca_if_unused() { if ! role_is_installed control && ! role_is_installed agent; then rm -f -- "$ETC_DIR/control-ca.crt" fi } uninstall_agent() { local purge="${1:-}" require_root systemctl disable --now "$AGENT_UNIT" 2>/dev/null || true systemctl disable --now "$PROVISIONING_HELPER_UNIT" 2>/dev/null || true rm -f -- "/etc/systemd/system/$AGENT_UNIT" "/etc/systemd/system/$PROVISIONING_HELPER_UNIT" "$ETC_DIR/agent.env" rm -rf -- "$LIB_DIR/agent" "$LIB_DIR/provisioning-helper" if [[ "$purge" == "--purge" ]]; then rm -rf -- "$STATE_DIR/agent" "$ENROLLMENT_DIR" remove_shared_ca_if_unused fi systemctl daemon-reload log "Agent removed${purge:+ ($purge)}." } install_monitor() { local public_key="$1" require_root validate_platform local metrics_script="/usr/local/libexec/ochenstarik-smm-metrics" local monitor_user="ochenstarik-monitor" local monitor_home="/var/lib/ochenstarik-monitor" if ! id -u "$monitor_user" >/dev/null 2>&1; then useradd -r -s /usr/sbin/nologin -d "$monitor_home" -M "$monitor_user" fi install -d -m 0755 -o "$monitor_user" -g "$monitor_user" "$monitor_home" install -d -m 0755 "/usr/local/libexec" cat >"$metrics_script" <<'EOF' #!/usr/bin/env bash set -euo pipefail # ochenstarik-smm-metrics echo "PROTOCOL=1" echo "HOSTNAME=$(hostname)" UPTIME=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo "0") echo "UPTIME_SECONDS=${UPTIME}" LOAD1=$(awk '{print $1}' /proc/loadavg 2>/dev/null || echo "0.00") echo "LOAD1=${LOAD1}" CPU_COUNT=$(nproc 2>/dev/null || echo "1") echo "CPU_COUNT=${CPU_COUNT}" MEM_TOTAL=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo "0") echo "MEM_TOTAL_KB=${MEM_TOTAL}" MEM_AVAIL=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo 2>/dev/null || echo "0") if [ "$MEM_AVAIL" = "0" ] || [ -z "$MEM_AVAIL" ]; then MEM_FREE=$(awk '/^MemFree:/ {print $2}' /proc/meminfo 2>/dev/null || echo "0") MEM_CACHED=$(awk '/^Cached:/ {print $2}' /proc/meminfo 2>/dev/null || echo "0") MEM_AVAIL=$((MEM_FREE + MEM_CACHED)) fi echo "MEM_AVAILABLE_KB=${MEM_AVAIL}" SWAP_TOTAL=$(awk '/^SwapTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo "0") echo "SWAP_TOTAL_KB=${SWAP_TOTAL}" SWAP_FREE=$(awk '/^SwapFree:/ {print $2}' /proc/meminfo 2>/dev/null || echo "0") echo "SWAP_FREE_KB=${SWAP_FREE}" DF_OUT=$(df -k / 2>/dev/null | awk 'NR==2 {print $2, $4}' || echo "0 0") DISK_TOTAL=$(echo "$DF_OUT" | awk '{print $1}') DISK_AVAIL=$(echo "$DF_OUT" | awk '{print $2}') echo "DISK_TOTAL_KB=${DISK_TOTAL}" echo "DISK_AVAILABLE_KB=${DISK_AVAIL}" DF_INODES=$(df -i / 2>/dev/null | awk 'NR==2 {print $2, $4}' || echo "0 0") INODES_TOTAL=$(echo "$DF_INODES" | awk '{print $1}') INODES_FREE=$(echo "$DF_INODES" | awk '{print $2}') echo "DISK_INODES_TOTAL=${INODES_TOTAL}" echo "DISK_INODES_FREE=${INODES_FREE}" NET_RX=$(awk 'NR>2 {rx+=$1} END {print rx}' /proc/net/dev 2>/dev/null || echo "0") NET_TX=$(awk 'NR>2 {tx+=$9} END {print tx}' /proc/net/dev 2>/dev/null || echo "0") echo "NETWORK_RX_BYTES=${NET_RX}" echo "NETWORK_TX_BYTES=${NET_TX}" KERNEL=$(uname -r 2>/dev/null || echo "unknown") echo "KERNEL=${KERNEL}" SYSTEMD_SSH=$(systemctl is-active ssh.service 2>/dev/null || true) echo "SYSTEMD_SSH=${SYSTEMD_SSH:-unknown}" SYSTEMD_WIREGUARD=$(systemctl is-active wg-quick@smm0.service 2>/dev/null || true) echo "SYSTEMD_WIREGUARD=${SYSTEMD_WIREGUARD:-unknown}" EOF chown root:root "$metrics_script" chmod 0755 "$metrics_script" install -d -m 0700 -o "$monitor_user" -g "$monitor_user" "$monitor_home/.ssh" local auth_keys="$monitor_home/.ssh/authorized_keys" # We idempotently add the key local key_entry="command=\"$metrics_script\",restrict,no-pty,no-agent-forwarding,no-port-forwarding,no-X11-forwarding $public_key" if [[ -f "$auth_keys" ]] && grep -qF "$public_key" "$auth_keys"; then log "Monitor key already installed." else echo "$key_entry" >> "$auth_keys" chown "$monitor_user:$monitor_user" "$auth_keys" chmod 0600 "$auth_keys" log "Monitor key installed." fi } uninstall_monitor() { require_root local monitor_user="ochenstarik-monitor" local monitor_home="/var/lib/ochenstarik-monitor" local metrics_script="/usr/local/libexec/ochenstarik-smm-metrics" if id -u "$monitor_user" >/dev/null 2>&1; then userdel -f "$monitor_user" || true fi rm -rf -- "$monitor_home" rm -f -- "$metrics_script" log "Monitor removed." } uninstall_control() { [[ "${1:-}" == "--confirm-destroy-control" ]] || fail "Control removal requires --confirm-destroy-control" require_root systemctl disable --now "$CONTROL_UNIT" 2>/dev/null || true rm -f -- "/etc/systemd/system/$CONTROL_UNIT" "$ETC_DIR/control.env" \ "$ETC_DIR/control-ca.pfx" "$ETC_DIR/control-server.pfx" \ "$POLICY_HELPER" "$SUDOERS_FILE" rm -rf -- "$LIB_DIR/control" "$STATE_DIR/control" \ "$STATE_DIR/control.db" "$STATE_DIR/control.db-wal" \ "$STATE_DIR/control.db-shm" "$STATE_DIR/backups" remove_shared_ca_if_unused systemctl daemon-reload log "Control role and its state were removed." } preflight() { validate_platform local command_name for command_name in openssl sha256sum tar systemctl getent useradd groupadd; do require_command "$command_name" done log "Supported platform: $(. /etc/os-release; printf '%s %s' "$ID" "$VERSION_ID"), $(uname -m)" } main() { local action="${1:-help}" shift || true case "$action" in help|-h|--help) [[ $# -eq 0 ]] || fail "$action takes no arguments"; usage ;; version|--version) [[ $# -eq 0 ]] || fail "$action takes no arguments"; printf '%s %s\n' "$PROGRAM" "$PROGRAM_VERSION" ;; preflight) [[ $# -eq 0 ]] || fail "preflight takes no arguments"; preflight ;; verify-release) [[ $# -eq 1 ]] || fail "verify-release requires ARCHIVE"; verify_release_payload "$1" ;; install-control) [[ $# -ge 2 && $# -le 3 ]] || fail "install-control requires ARCHIVE PUBLIC_HOST [HTTPS_PORT]"; install_control "$@" ;; install-agent) [[ $# -eq 4 ]] || fail "install-agent requires ARCHIVE NODE_ID CONTROL_URL CA_CERT"; install_agent "$@" ;; install-node) [[ $# -eq 1 ]] || fail "install-node requires ARCHIVE"; install_node_from_code "$1" ;; verify-manifest) [[ $# -eq 3 ]] || fail "verify-manifest requires MANIFEST SIGNATURE CERTIFICATE"; verify_manifest "$1" "$2" "$3" ;; install-monitor) [[ $# -eq 1 ]] || fail "install-monitor requires PUBLIC_KEY"; install_monitor "$1" ;; uninstall-monitor) [[ $# -eq 0 ]] || fail "uninstall-monitor takes no arguments"; uninstall_monitor ;; mesh-init) [[ $# -ge 1 && $# -le 2 ]] || fail "mesh-init requires PUBLIC_ENDPOINT [WG_PORT]"; mesh_init "$@" ;; peer-add) [[ $# -eq 1 ]] || fail "peer-add requires SMMPEER1_CODE"; add_mesh_peer "$1" ;; mesh-status) [[ $# -eq 0 ]] || fail "mesh-status takes no arguments"; show_mesh_status ;; update-control) [[ $# -eq 1 ]] || fail "update-control requires ARCHIVE"; update_role control "$1" ;; update-agent) [[ $# -eq 1 ]] || fail "update-agent requires ARCHIVE"; update_role agent "$1" ;; rollback) [[ $# -ge 1 && $# -le 2 ]] || fail "rollback requires control|agent [BACKUP_ID]"; rollback_role "$@" ;; node-code) [[ $# -eq 1 ]] || fail "node-code requires NODE_ID"; create_node_code "$1" ;; control-device-code) [[ $# -eq 1 ]] || fail "control-device-code requires DEVICE_ID"; create_device_code "$1" ;; node-token) [[ $# -eq 1 ]] || fail "node-token requires NODE_ID"; run_control_cli token-create "$1" ;; control-ca-fingerprint) [[ $# -eq 0 ]] || fail "control-ca-fingerprint takes no arguments"; show_ca_fingerprint ;; status) [[ $# -eq 0 ]] || fail "status takes no arguments"; show_status ;; uninstall-agent) [[ $# -eq 0 || ( $# -eq 1 && "$1" == "--purge" ) ]] || fail "uninstall-agent accepts only [--purge]"; uninstall_agent "${1:-}" ;; uninstall-control) [[ $# -eq 1 ]] || fail "uninstall-control requires confirmation"; uninstall_control "$1" ;; *) fail "Unknown action: $action (run with --help)" ;; esac } main "$@"