1804 lines
75 KiB
Bash
Executable file
1804 lines
75 KiB
Bash
Executable file
#!/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 MONITOR_USER="ochenstarik-monitor"
|
|
readonly MONITOR_HOME="/var/lib/ochenstarik-monitor"
|
|
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 METRICS_SCRIPT="/usr/local/libexec/ochenstarik-smm-metrics"
|
|
readonly SUDOERS_FILE="/etc/sudoers.d/ochenstarik-smm-control"
|
|
readonly SYSCTL_FILE="/etc/sysctl.d/90-ochenstarik-smm-mesh.conf"
|
|
readonly SYSTEM_WG_CONFIG="/etc/wireguard/smm0.conf"
|
|
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.*$"
|
|
readonly COSIGN_VERSION="v3.1.3"
|
|
readonly COSIGN_SHA256_AMD64="4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71"
|
|
readonly COSIGN_SHA256_ARM64="c5d324e091826b0d7a78eb16fef316450b4eb9aaec045611c08ba06f5e73220a"
|
|
readonly COSIGN_INSTALL_PATH="/usr/local/bin/cosign"
|
|
|
|
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 uninstall-system-plan [--purge-data]
|
|
ochenstarik-server-monitor-manager.sh uninstall-system --confirm-uninstall [--purge-data --confirm-destroy-data]
|
|
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"
|
|
}
|
|
|
|
ensure_cosign() {
|
|
local existing architecture expected_sha256 download_url download_path actual_sha256
|
|
if existing="$(command -v cosign 2>/dev/null)"; then
|
|
"$existing" version >/dev/null 2>&1 \
|
|
|| fail "Existing cosign cannot run: $existing"
|
|
return 0
|
|
fi
|
|
|
|
require_root
|
|
require_command curl
|
|
require_command sha256sum
|
|
require_command mktemp
|
|
require_command install
|
|
|
|
case "$(uname -m)" in
|
|
x86_64)
|
|
architecture="amd64"
|
|
expected_sha256="$COSIGN_SHA256_AMD64"
|
|
;;
|
|
aarch64|arm64)
|
|
architecture="arm64"
|
|
expected_sha256="$COSIGN_SHA256_ARM64"
|
|
;;
|
|
*) fail "Cannot provision cosign $COSIGN_VERSION for unsupported architecture: $(uname -m)" ;;
|
|
esac
|
|
|
|
download_url="https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-${architecture}"
|
|
TEMP_DIR="$(mktemp -d -t smm-cosign.XXXXXXXX)"
|
|
chmod 700 "$TEMP_DIR"
|
|
download_path="$TEMP_DIR/cosign-linux-${architecture}"
|
|
|
|
log "Downloading cosign $COSIGN_VERSION for $architecture."
|
|
if ! curl --fail --silent --show-error --location --retry 3 \
|
|
--output "$download_path" "$download_url"; then
|
|
fail "Could not download cosign $COSIGN_VERSION. Expected SHA-256: $expected_sha256. Install path: $COSIGN_INSTALL_PATH. Download URL: $download_url. Install this exact binary manually and verify its checksum."
|
|
fi
|
|
|
|
actual_sha256="$(sha256sum "$download_path" | awk '{ print $1 }')"
|
|
if [[ "${actual_sha256,,}" != "${expected_sha256,,}" ]]; then
|
|
fail "cosign $COSIGN_VERSION checksum mismatch. Expected SHA-256: $expected_sha256. Actual SHA-256: $actual_sha256. Install path: $COSIGN_INSTALL_PATH."
|
|
fi
|
|
|
|
install -m 0755 "$download_path" "$COSIGN_INSTALL_PATH" \
|
|
|| fail "Could not install cosign $COSIGN_VERSION at $COSIGN_INSTALL_PATH after verifying SHA-256 $expected_sha256."
|
|
"$COSIGN_INSTALL_PATH" version >/dev/null 2>&1 \
|
|
|| fail "Installed cosign $COSIGN_VERSION cannot run at $COSIGN_INSTALL_PATH. Expected SHA-256: $expected_sha256."
|
|
rm -rf -- "$TEMP_DIR"
|
|
TEMP_DIR=""
|
|
log "Installed cosign $COSIGN_VERSION at $COSIGN_INSTALL_PATH."
|
|
}
|
|
|
|
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
|
|
ensure_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 {} +
|
|
repair_mesh_state_permissions
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
ensure_mesh_state() {
|
|
install -d -m 0770 -o root -g "$CONTROL_USER" "$MESH_DIR"
|
|
touch "$MESH_DIR/nodes.tsv"
|
|
chown root:"$CONTROL_USER" "$MESH_DIR/nodes.tsv"
|
|
chmod 0660 "$MESH_DIR/nodes.tsv"
|
|
}
|
|
|
|
repair_mesh_state_permissions() {
|
|
ensure_mesh_state
|
|
}
|
|
|
|
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" <<EOF
|
|
[Interface]
|
|
Address = $HUB_MESH_ADDRESS
|
|
ListenPort = $port
|
|
PrivateKey = $private_key
|
|
SaveConfig = false
|
|
EOF
|
|
if [[ -r "$MESH_DIR/nodes.tsv" ]]; then
|
|
while IFS=$'\t' read -r node_id address public_key status; do
|
|
[[ "$status" == "active" ]] || continue
|
|
cat >>"/etc/wireguard/smm0.conf" <<EOF
|
|
|
|
# Node: $node_id
|
|
[Peer]
|
|
PublicKey = $public_key
|
|
AllowedIPs = $address/32
|
|
EOF
|
|
done <"$MESH_DIR/nodes.tsv"
|
|
fi
|
|
chmod 0600 "/etc/wireguard/smm0.conf"
|
|
}
|
|
|
|
mesh_init() {
|
|
local public_endpoint="$1" port="${2:-51820}" hub_private hub_public
|
|
require_root
|
|
validate_platform
|
|
validate_port "$port"
|
|
if [[ "$public_endpoint" == *:* ]]; then
|
|
fail "Use an IPv4 address or DNS name without a port for PUBLIC_ENDPOINT."
|
|
fi
|
|
[[ "$public_endpoint" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$ ]] \
|
|
|| fail "Invalid WireGuard public endpoint."
|
|
ensure_system_user "$CONTROL_USER"
|
|
ensure_mesh_packages
|
|
install -d -m 0700 -o root -g root "$WG_DIR" /etc/wireguard
|
|
ensure_mesh_state
|
|
if [[ ! -f "$WG_DIR/hub.key" ]]; then
|
|
umask 077
|
|
wg genkey >"$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" <<EOF
|
|
HUB_ENDPOINT=$public_endpoint:$port
|
|
HUB_PUBLIC_KEY=$hub_public
|
|
MESH_NETWORK=$MESH_NETWORK
|
|
EOF
|
|
chmod 0644 "$ETC_DIR/mesh.env"
|
|
printf '%s\n' 'net.ipv4.ip_forward=1' >"/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
|
|
ensure_mesh_state
|
|
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" <<EOF
|
|
[Interface]
|
|
Address = $node_address/32
|
|
PrivateKey = $node_private
|
|
SaveConfig = false
|
|
|
|
[Peer]
|
|
PublicKey = $hub_public_key
|
|
Endpoint = $hub_endpoint
|
|
AllowedIPs = $MESH_NETWORK
|
|
PersistentKeepalive = 25
|
|
EOF
|
|
chmod 0600 "$WG_DIR/node.key" /etc/wireguard/smm0.conf
|
|
chmod 0644 "$WG_DIR/node.pub"
|
|
systemctl enable wg-quick@smm0.service
|
|
systemctl restart wg-quick@smm0.service
|
|
MESH_PEER_CODE="$(printf 'SMMPEER1.%s.%s.%s' \
|
|
"$(printf '%s' "$node_id" | base64url_encode)" \
|
|
"$(printf '%s' "$node_address" | base64url_encode)" \
|
|
"$(printf '%s' "$node_public" | base64url_encode)")"
|
|
}
|
|
|
|
create_control_certificates() {
|
|
local public_host="$1" ca_key ca_cert serial_file server_key server_csr ext_file san
|
|
ca_key="$TEMP_DIR/control-ca.key"
|
|
ca_cert="$TEMP_DIR/control-ca.crt"
|
|
serial_file="$TEMP_DIR/control-ca.srl"
|
|
server_key="$TEMP_DIR/control-server.key"
|
|
server_csr="$TEMP_DIR/control-server.csr"
|
|
ext_file="$TEMP_DIR/control-server.ext"
|
|
if [[ "$public_host" == *:* || "$public_host" =~ ^[0-9]+(\.[0-9]+){3}$ ]]; then
|
|
san="IP:$public_host"
|
|
else
|
|
[[ "$public_host" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$ ]] \
|
|
|| fail "Invalid public host name."
|
|
san="DNS:$public_host"
|
|
fi
|
|
|
|
openssl ecparam -name prime256v1 -genkey -noout -out "$ca_key"
|
|
openssl req -x509 -new -sha256 -days 3650 -key "$ca_key" -out "$ca_cert" \
|
|
-subj "/CN=Server Monitor Manager Control CA" \
|
|
-addext "basicConstraints=critical,CA:TRUE" \
|
|
-addext "keyUsage=critical,keyCertSign,cRLSign"
|
|
openssl ecparam -name prime256v1 -genkey -noout -out "$server_key"
|
|
openssl req -new -sha256 -key "$server_key" -out "$server_csr" \
|
|
-subj "/CN=$public_host"
|
|
printf '%s\n' \
|
|
"basicConstraints=critical,CA:FALSE" \
|
|
"keyUsage=critical,digitalSignature,keyEncipherment" \
|
|
"extendedKeyUsage=serverAuth" \
|
|
"subjectAltName=$san" >"$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" <<EOF
|
|
ASPNETCORE_URLS=https://0.0.0.0:$port
|
|
ASPNETCORE_Kestrel__Certificates__Default__Path=$ETC_DIR/control-server.pfx
|
|
Control__DatabasePath=$STATE_DIR/control/control.db
|
|
Control__CertificateAuthorityPath=$ETC_DIR/control-ca.pfx
|
|
Control__BackupDirectory=$STATE_DIR/control/backups
|
|
Control__HubHelperPath=$POLICY_HELPER
|
|
Control__PrivilegeEscalationPath=/usr/bin/sudo
|
|
Control__LinkReconciliationSeconds=300
|
|
Control__LinkRetentionDays=90
|
|
EOF
|
|
printf '%s\n' "https://$public_host:$port" >"$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" <<EOF
|
|
SMM_NodeId=$node_id
|
|
SMM_AgentUid=$(id -u "$AGENT_USER")
|
|
SMM_ControlUrl=${control_url%/}
|
|
SMM_StateDirectory=$STATE_DIR/agent
|
|
SMM_EnrollmentTokenDirectory=$ENROLLMENT_DIR
|
|
SMM_CertificateAuthorityPath=$ETC_DIR/control-ca.crt
|
|
EOF
|
|
chown root:"$AGENT_USER" "$ETC_DIR/agent.env"
|
|
chmod 0640 "$ETC_DIR/agent.env"
|
|
read_enrollment_token
|
|
token_file="$ENROLLMENT_DIR/enroll-token"
|
|
token_temp="$(mktemp "$ENROLLMENT_DIR/.enroll-token.XXXXXXXX")"
|
|
ENROLLMENT_TOKEN_TEMP="$token_temp"
|
|
if ! printf '%s' "$ENROLL_TOKEN" >"$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."; }
|
|
chown root:"$CONTROL_USER" "$tmp"
|
|
chmod 0660 "$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="$METRICS_SCRIPT"
|
|
local monitor_user="$MONITOR_USER"
|
|
local monitor_home="$MONITOR_HOME"
|
|
|
|
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="$MONITOR_USER"
|
|
local monitor_home="$MONITOR_HOME"
|
|
local metrics_script="$METRICS_SCRIPT"
|
|
|
|
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."
|
|
}
|
|
|
|
owned_unit_exists() {
|
|
local unit="$1"
|
|
[[ -e "/etc/systemd/system/$unit" || -L "/etc/systemd/system/$unit" ]] && return 0
|
|
systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q "^${unit}[[:space:]]"
|
|
}
|
|
|
|
path_has_content() {
|
|
local path="$1"
|
|
[[ -f "$path" || -L "$path" ]] && return 0
|
|
[[ -d "$path" ]] || return 1
|
|
find "$path" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null | grep -q .
|
|
}
|
|
|
|
uninstall_system_plan() {
|
|
local purge_data="${1:-}" found=0 unit user path
|
|
local -a units=(
|
|
"$CONTROL_UNIT" "$AGENT_UNIT" "$PROVISIONING_HELPER_UNIT"
|
|
"$FIREWALL_UNIT" wg-quick@smm0.service
|
|
)
|
|
local -a users=("$CONTROL_USER" "$AGENT_USER" "$MONITOR_USER")
|
|
local -a files=(
|
|
"$POLICY_HELPER" "$METRICS_SCRIPT" "$EMERGENCY_COMMAND" "$BOOTSTRAP_COMMAND"
|
|
"$SUDOERS_FILE" "$SYSCTL_FILE" "$SYSTEM_WG_CONFIG"
|
|
)
|
|
local -a program_paths=("$LIB_DIR" "$MONITOR_HOME" "$ENROLLMENT_DIR")
|
|
printf '%s\n' 'Server Monitor Manager objects found on this machine:'
|
|
for unit in "${units[@]}"; do
|
|
if owned_unit_exists "$unit"; then
|
|
printf ' unit: %s\n' "$unit"
|
|
found=1
|
|
fi
|
|
done
|
|
for user in "${users[@]}"; do
|
|
if id -u "$user" >/dev/null 2>&1; then
|
|
printf ' user/group: %s\n' "$user"
|
|
found=1
|
|
fi
|
|
done
|
|
if command -v nft >/dev/null 2>&1 \
|
|
&& nft list table inet ochenstarik_smm >/dev/null 2>&1; then
|
|
printf '%s\n' ' nftables table: inet ochenstarik_smm'
|
|
found=1
|
|
fi
|
|
if command -v ip >/dev/null 2>&1 && ip link show smm0 >/dev/null 2>&1; then
|
|
printf '%s\n' ' network interface: smm0'
|
|
found=1
|
|
fi
|
|
for path in "${files[@]}" "${program_paths[@]}"; do
|
|
if path_has_content "$path"; then
|
|
printf ' path: %s\n' "$path"
|
|
found=1
|
|
fi
|
|
done
|
|
for path in "$ETC_DIR" "$STATE_DIR"; do
|
|
if path_has_content "$path"; then
|
|
if [[ "$purge_data" == "--purge-data" ]]; then
|
|
printf ' data path: %s\n' "$path"
|
|
else
|
|
printf ' program files under: %s (Control database, backups, and CA are preserved)\n' "$path"
|
|
fi
|
|
found=1
|
|
fi
|
|
done
|
|
if [[ "$found" == "0" ]]; then
|
|
printf '%s\n' ' nothing installed'
|
|
fi
|
|
}
|
|
|
|
stop_owned_services() {
|
|
local unit
|
|
local -a units=(
|
|
"$CONTROL_UNIT" "$AGENT_UNIT" "$PROVISIONING_HELPER_UNIT"
|
|
"$FIREWALL_UNIT" wg-quick@smm0.service
|
|
)
|
|
for unit in "${units[@]}"; do
|
|
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
|
done
|
|
}
|
|
|
|
terminate_owned_processes() {
|
|
local user running=0
|
|
local -a users=("$CONTROL_USER" "$AGENT_USER" "$MONITOR_USER")
|
|
for user in "${users[@]}"; do
|
|
if id -u "$user" >/dev/null 2>&1 && pgrep -u "$user" >/dev/null 2>&1; then
|
|
pkill -TERM -u "$user" || true
|
|
running=1
|
|
fi
|
|
done
|
|
[[ "$running" == "0" ]] || sleep 1
|
|
for user in "${users[@]}"; do
|
|
if id -u "$user" >/dev/null 2>&1 && pgrep -u "$user" >/dev/null 2>&1; then
|
|
pkill -KILL -u "$user" || true
|
|
fi
|
|
done
|
|
for user in "${users[@]}"; do
|
|
if id -u "$user" >/dev/null 2>&1 && pgrep -u "$user" >/dev/null 2>&1; then
|
|
fail "Processes owned by $user are still running."
|
|
fi
|
|
done
|
|
}
|
|
|
|
remove_owned_accounts() {
|
|
local user
|
|
local -a users=("$CONTROL_USER" "$AGENT_USER" "$MONITOR_USER")
|
|
for user in "${users[@]}"; do
|
|
if id -u "$user" >/dev/null 2>&1; then
|
|
userdel "$user"
|
|
fi
|
|
done
|
|
for user in "${users[@]}"; do
|
|
if getent group "$user" >/dev/null 2>&1; then
|
|
groupdel "$user" || log "Group $user remains because it is still in use."
|
|
fi
|
|
done
|
|
}
|
|
|
|
remove_owned_network() {
|
|
if command -v ip >/dev/null 2>&1 && ip link show smm0 >/dev/null 2>&1; then
|
|
ip link delete smm0
|
|
fi
|
|
if command -v nft >/dev/null 2>&1 \
|
|
&& nft list table inet ochenstarik_smm >/dev/null 2>&1; then
|
|
nft delete table inet ochenstarik_smm
|
|
fi
|
|
}
|
|
|
|
remove_owned_files() {
|
|
local purge_data="$1"
|
|
rm -f -- \
|
|
"/etc/systemd/system/$CONTROL_UNIT" \
|
|
"/etc/systemd/system/$AGENT_UNIT" \
|
|
"/etc/systemd/system/$PROVISIONING_HELPER_UNIT" \
|
|
"/etc/systemd/system/$FIREWALL_UNIT" \
|
|
"$POLICY_HELPER" "$METRICS_SCRIPT" "$EMERGENCY_COMMAND" \
|
|
"$SUDOERS_FILE" "$SYSCTL_FILE" "$SYSTEM_WG_CONFIG"
|
|
rm -rf -- "$LIB_DIR" "$MONITOR_HOME" "$ENROLLMENT_DIR"
|
|
if [[ "$purge_data" == "--purge-data" ]]; then
|
|
rm -rf -- "$ETC_DIR" "$STATE_DIR"
|
|
else
|
|
rm -f -- \
|
|
"$ETC_DIR/agent.env" "$ETC_DIR/control.env" \
|
|
"$ETC_DIR/control-public-url" "$ETC_DIR/control-server.pfx" \
|
|
"$ETC_DIR/mesh.env" "$ETC_DIR/mesh.nft" \
|
|
"$ETC_DIR/installed-version-agent" "$ETC_DIR/installed-version-control"
|
|
rm -rf -- "$WG_DIR" "$STATE_DIR/agent" "$STATE_DIR/mesh" "$STATE_DIR/provisioning"
|
|
fi
|
|
rm -f -- "$BOOTSTRAP_COMMAND"
|
|
rmdir "$ETC_DIR" "$STATE_DIR" 2>/dev/null || true
|
|
systemctl daemon-reload
|
|
systemctl reset-failed >/dev/null 2>&1 || true
|
|
}
|
|
|
|
port_state() {
|
|
local port="$1"
|
|
if command -v ss >/dev/null 2>&1 \
|
|
&& ss -H -lntu "sport = :$port" 2>/dev/null | grep -q .; then
|
|
printf '%s' 'in-use'
|
|
else
|
|
printf '%s' 'free'
|
|
fi
|
|
}
|
|
|
|
show_uninstall_result() {
|
|
local unit user units=() users=()
|
|
printf 'port-7443: %s\n' "$(port_state 7443)"
|
|
printf 'port-51820: %s\n' "$(port_state 51820)"
|
|
if command -v ip >/dev/null 2>&1 \
|
|
&& ip route show 2>/dev/null | grep -Fq '10.77.0.'; then
|
|
printf '%s\n' 'mesh-routes: present'
|
|
else
|
|
printf '%s\n' 'mesh-routes: absent'
|
|
fi
|
|
while IFS= read -r unit; do
|
|
[[ -n "$unit" ]] && units+=("${unit%%[[:space:]]*}")
|
|
done < <(systemctl list-unit-files 'ochenstarik-smm-*' --no-legend 2>/dev/null || true)
|
|
if (( ${#units[@]} == 0 )); then
|
|
printf '%s\n' 'owned-units: absent'
|
|
else
|
|
printf 'owned-units: present (%s)\n' "${units[*]}"
|
|
fi
|
|
for user in "$CONTROL_USER" "$AGENT_USER" "$MONITOR_USER"; do
|
|
id -u "$user" >/dev/null 2>&1 && users+=("$user")
|
|
done
|
|
if (( ${#users[@]} == 0 )); then
|
|
printf '%s\n' 'owned-users: absent'
|
|
else
|
|
printf 'owned-users: present (%s)\n' "${users[*]}"
|
|
fi
|
|
}
|
|
|
|
uninstall_system() {
|
|
local confirmation="${1:-}" purge_data="${2:-}" data_confirmation="${3:-}"
|
|
[[ "$confirmation" == "--confirm-uninstall" ]] \
|
|
|| fail "System removal requires --confirm-uninstall"
|
|
if [[ -n "$purge_data" ]]; then
|
|
[[ "$purge_data" == "--purge-data" && "$data_confirmation" == "--confirm-destroy-data" ]] \
|
|
|| fail "Permanent data removal requires --purge-data --confirm-destroy-data"
|
|
fi
|
|
require_root
|
|
uninstall_system_plan "$purge_data"
|
|
stop_owned_services
|
|
terminate_owned_processes
|
|
remove_owned_network
|
|
remove_owned_files "$purge_data"
|
|
remove_owned_accounts
|
|
show_uninstall_result
|
|
log "System removal completed${purge_data:+, including Control data and CA}."
|
|
}
|
|
|
|
preflight() {
|
|
validate_platform
|
|
local command_name
|
|
for command_name in openssl sha256sum tar systemctl getent useradd groupadd; do
|
|
require_command "$command_name"
|
|
done
|
|
ensure_cosign
|
|
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" ;;
|
|
uninstall-system-plan) [[ $# -eq 0 || ( $# -eq 1 && "$1" == "--purge-data" ) ]] || fail "uninstall-system-plan accepts only [--purge-data]"; uninstall_system_plan "${1:-}" ;;
|
|
uninstall-system) [[ $# -eq 1 || $# -eq 3 ]] || fail "uninstall-system requires --confirm-uninstall [--purge-data --confirm-destroy-data]"; uninstall_system "$@" ;;
|
|
*) fail "Unknown action: $action (run with --help)" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|