From d91f2d380c5604118455f59d792d0d43cce45ff7 Mon Sep 17 00:00:00 2001 From: Ochenstarik Date: Sun, 19 Jul 2026 13:22:11 +0700 Subject: [PATCH] Add single-code Node enrollment --- deploy/ochenstarik-server-monitor-manager.sh | 91 ++++++++++++++++++++ docs/linux-bootstrap.md | 24 +++--- tests/bootstrap/test-bootstrap-contract.sh | 2 + 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/deploy/ochenstarik-server-monitor-manager.sh b/deploy/ochenstarik-server-monitor-manager.sh index d556f3c..d27dd4d 100755 --- a/deploy/ochenstarik-server-monitor-manager.sh +++ b/deploy/ochenstarik-server-monitor-manager.sh @@ -35,9 +35,11 @@ Usage: ochenstarik-server-monitor-manager.sh preflight 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 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 node-token NODE_ID ochenstarik-server-monitor-manager.sh control-ca-fingerprint ochenstarik-server-monitor-manager.sh status @@ -51,6 +53,23 @@ 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)." } @@ -264,8 +283,10 @@ Control__BackupDirectory=$STATE_DIR/backups Control__HubHelperPath=$POLICY_HELPER Control__PrivilegeEscalationPath=/usr/bin/sudo 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 -m 0755 "$TEMP_DIR/deploy/ochenstarik-smm-policy-apply" "$POLICY_HELPER" printf '%s\n' "$CONTROL_USER ALL=(root) NOPASSWD: $POLICY_HELPER *" >"$SUDOERS_FILE" chmod 0440 "$SUDOERS_FILE" @@ -346,6 +367,54 @@ EOF 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 "SMMNODE1 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 extra + local control_url node_id token ca_file + require_root + require_command base64 + require_command openssl + read_enrollment_code + IFS='.' read -r prefix control_part ca_part node_part token_part extra <<<"$ENROLL_CODE" + ENROLL_CODE="" + [[ "$prefix" == "SMMNODE1" && -n "$control_part" && -n "$ca_part" \ + && -n "$node_part" && -n "$token_part" && -z "${extra:-}" ]] \ + || fail "Invalid SMMNODE1 enrollment code." + control_url="$(base64url_decode "$control_part")" + node_id="$(base64url_decode "$node_part")" + token="$(base64url_decode "$token_part")" + 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" +} + update_role() { local role="$1" archive="$2" binary unit user backup_id require_root @@ -423,6 +492,26 @@ run_control_cli() { "$LIB_DIR/control/ochenstarik-smm-control" "$command_name" "$identifier" } +create_node_code() { + local node_id="$1" token control_url ca_pem + 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")" + 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)" + token="" +} + show_ca_fingerprint() { [[ -f "$ETC_DIR/control-ca.crt" ]] || fail "Control CA certificate is not installed." require_command openssl @@ -471,9 +560,11 @@ main() { preflight) preflight ;; 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" ;; 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" ;; 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) show_status ;; diff --git a/docs/linux-bootstrap.md b/docs/linux-bootstrap.md index f84795b..0bf3d52 100644 --- a/docs/linux-bootstrap.md +++ b/docs/linux-bootstrap.md @@ -33,34 +33,30 @@ sudo ./ochenstarik-server-monitor-manager.sh install-control \ Bootstrap создаёт собственный Control CA и HTTPS certificate, системного пользователя, root-only configuration, SQLite directory и hardened systemd unit. В конце он показывает SHA-256 fingerprint CA. -Создайте десятиминутный token для Node: +Создайте десятиминутный самодостаточный код `SMMNODE1` для Node: ```bash -sudo ./ochenstarik-server-monitor-manager.sh node-token home +sudo ./ochenstarik-server-monitor-manager.sh node-code home sudo ./ochenstarik-server-monitor-manager.sh control-ca-fingerprint ``` -Скопируйте на Node только публичный файл `/etc/ochenstarik-server-monitor-manager/control-ca.crt`. Приватный CA PFX остаётся на Hub. +Код содержит URL Control, Node ID, одноразовый token и только публичный CA certificate. Приватный CA PFX остаётся на Hub. ## Установка Agent -`CONTROL_URL` обязан использовать то же DNS-имя/IP, которое было передано при установке Control. Token вводится скрыто в локальном терминале и не сохраняется в `agent.env`: +Вставьте код `SMMNODE1` в скрытый prompt. Bootstrap покажет fingerprint CA и потребует сравнить его с Hub до создания локального Agent key и CSR. Token не сохраняется в `agent.env`: ```bash -sudo ./ochenstarik-server-monitor-manager.sh install-agent \ - ./server-monitor-manager-linux-x64.tar.gz \ - home \ - https://hub.example.com:7443 \ - ./control-ca.crt +sudo ./ochenstarik-server-monitor-manager.sh install-node \ + ./server-monitor-manager-linux-x64.tar.gz ``` -Для автоматизированного теста token можно передать только в окружении одного процесса: +Для автоматизированного теста код и явное принятие уже сверенного fingerprint можно передать только в окружении одного процесса: ```bash -sudo SMM_ENROLL_TOKEN='one-time-token' \ - ./ochenstarik-server-monitor-manager.sh install-agent \ - ./server-monitor-manager-linux-x64.tar.gz home \ - https://hub.example.com:7443 ./control-ca.crt +sudo SMM_ENROLL_CODE='SMMNODE1....' SMM_ACCEPT_CA_FINGERPRINT=1 \ + ./ochenstarik-server-monitor-manager.sh install-node \ + ./server-monitor-manager-linux-x64.tar.gz ``` ## Жизненный цикл diff --git a/tests/bootstrap/test-bootstrap-contract.sh b/tests/bootstrap/test-bootstrap-contract.sh index 28afcae..42f5ec4 100755 --- a/tests/bootstrap/test-bootstrap-contract.sh +++ b/tests/bootstrap/test-bootstrap-contract.sh @@ -11,7 +11,9 @@ version_output="$(bash "$bootstrap" --version)" grep -Fq "install-control ARCHIVE PUBLIC_HOST" <<<"$help_output" grep -Fq "install-agent ARCHIVE NODE_ID CONTROL_URL CA_CERT" <<<"$help_output" +grep -Fq "install-node ARCHIVE" <<<"$help_output" grep -Fq "SMM_ENROLL_TOKEN" <<<"$help_output" +grep -Fq "node-code NODE_ID" <<<"$help_output" grep -Fq "node-token NODE_ID" <<<"$help_output" grep -Eq '^ochenstarik-server-monitor-manager [0-9]+\.[0-9]+\.[0-9]+-' <<<"$version_output"