Add SSH monitoring server installer #3

Open
ochenstarik-ui wants to merge 15 commits from agent/server-monitor-installer into main
5 changed files with 258 additions and 28 deletions
Showing only changes of commit 66b9c3730e - Show all commits

View file

@ -43,3 +43,6 @@ jobs:
- name: Test step 8 AI agent catalog - name: Test step 8 AI agent catalog
run: bash tests/test-step8-agent-catalog.sh run: bash tests/test-step8-agent-catalog.sh
- name: Test Server Monitor Manager enrollment
run: bash tests/test-monitor-manager-enrollment.sh

View file

@ -92,7 +92,7 @@ bash -n ochenstarik-server-monitor-manager.sh
sudo ./ochenstarik-server-monitor-manager.sh hub sudo ./ochenstarik-server-monitor-manager.sh hub
``` ```
Create a separate configuration code for each Node on the Hub, then run the same installer with `node` on the matching server: Create a separate 10-minute enrollment code for each Node on the Hub, then run the same installer with `node` on the matching server:
```bash ```bash
sudo ochenstarik-smm node-code ai-agent sudo ochenstarik-smm node-code ai-agent
@ -109,7 +109,7 @@ sudo ochenstarik-smm nodes
sudo ochenstarik-smm links sudo ochenstarik-smm links
``` ```
The current configuration code contains the Node private WireGuard key and is intended only for early testing. Do not store it in Git or messages. Local Node key generation and expiring one-time enrollment are tracked before production use. The Node installer creates its private WireGuard key locally and prints an `SMMREQ1` request. In a second terminal run `sudo ochenstarik-smm node-enroll` on the Hub, paste the request at the hidden prompt, and return the resulting `SMMACK1` code to the Node installer. The enrollment code expires after 10 minutes and is consumed by the first successful registration.
## Installation flow ## Installation flow

View file

@ -79,7 +79,7 @@ bash -n ochenstarik-server-monitor-manager.sh
sudo ./ochenstarik-server-monitor-manager.sh hub sudo ./ochenstarik-server-monitor-manager.sh hub
``` ```
На Hub создайте отдельный код для каждого Node, после чего на соответствующем сервере запустите тот же файл в режиме `node`: На Hub создайте отдельный enrollment-код на 10 минут для каждого Node, после чего на соответствующем сервере запустите тот же файл в режиме `node`:
```bash ```bash
sudo ochenstarik-smm node-code ai-agent sudo ochenstarik-smm node-code ai-agent
@ -96,7 +96,7 @@ sudo ochenstarik-smm nodes
sudo ochenstarik-smm links sudo ochenstarik-smm links
``` ```
Текущий код конфигурации содержит приватный WireGuard-ключ Node и предназначен только для раннего тестирования. Не сохраняйте его в Git или сообщениях. Локальная генерация ключа и одноразовая регистрация с ограниченным сроком будут добавлены до production-использования. Установщик Node создаст приватный WireGuard-ключ локально и покажет запрос `SMMREQ1`. Во втором терминале выполните на Hub `sudo ochenstarik-smm node-enroll`, вставьте запрос по скрытому приглашению и верните полученный код `SMMACK1` в установщик Node. Enrollment-код действует 10 минут и погашается при первой успешной регистрации.
## Требования ## Требования

View file

@ -8,6 +8,7 @@ readonly MONITOR_COMMAND="/usr/local/libexec/ochenstarik-server-monitor"
readonly AUTHORIZED_KEYS="${MONITOR_HOME}/.ssh/authorized_keys" readonly AUTHORIZED_KEYS="${MONITOR_HOME}/.ssh/authorized_keys"
readonly MESH_DIR="/etc/${APP_NAME}" readonly MESH_DIR="/etc/${APP_NAME}"
readonly NODES_DIR="${MESH_DIR}/nodes" readonly NODES_DIR="${MESH_DIR}/nodes"
readonly TOKENS_DIR="${MESH_DIR}/tokens"
readonly HUB_CONFIG="${MESH_DIR}/hub.conf" readonly HUB_CONFIG="${MESH_DIR}/hub.conf"
readonly HUB_PRIVATE_KEY="${MESH_DIR}/hub.key" readonly HUB_PRIVATE_KEY="${MESH_DIR}/hub.key"
readonly LINKS_FILE="${MESH_DIR}/links" readonly LINKS_FILE="${MESH_DIR}/links"
@ -191,6 +192,7 @@ export LC_ALL=C
readonly STATE_DIR="/etc/ochenstarik-server-monitor-manager" readonly STATE_DIR="/etc/ochenstarik-server-monitor-manager"
readonly NODES_DIR="${STATE_DIR}/nodes" readonly NODES_DIR="${STATE_DIR}/nodes"
readonly TOKENS_DIR="${STATE_DIR}/tokens"
readonly HUB_CONFIG="${STATE_DIR}/hub.conf" readonly HUB_CONFIG="${STATE_DIR}/hub.conf"
readonly HUB_PRIVATE_KEY="${STATE_DIR}/hub.key" readonly HUB_PRIVATE_KEY="${STATE_DIR}/hub.key"
readonly LINKS_FILE="${STATE_DIR}/links" readonly LINKS_FILE="${STATE_DIR}/links"
@ -241,7 +243,7 @@ next_address() {
for host in $(seq 2 254); do for host in $(seq 2 254); do
address="10.77.0.${host}" address="10.77.0.${host}"
used=false used=false
for file in "$NODES_DIR"/*.node; do for file in "$NODES_DIR"/*.node "$TOKENS_DIR"/*.token*; do
[[ -e "$file" ]] || continue [[ -e "$file" ]] || continue
if [[ "$(config_value ADDRESS "$file")" == "$address" ]]; then if [[ "$(config_value ADDRESS "$file")" == "$address" ]]; then
used=true used=true
@ -257,29 +259,119 @@ base64url_encode() {
base64 -w0 | tr '+/' '-_' | tr -d '=' base64 -w0 | tr '+/' '-_' | tr -d '='
} }
base64url_decode() {
local data="$1" remainder
data="${data//-/+}"
data="${data//_/\/}"
remainder=$(( ${#data} % 4 ))
if (( remainder == 2 )); then data+='=='
elif (( remainder == 3 )); then data+='='
elif (( remainder == 1 )); then return 1
fi
printf '%s' "$data" | base64 -d
}
payload_value() {
local payload="$1" key="$2"
printf '%s\n' "$payload" | awk -F= -v key="$key" '$1 == key { sub(/^[^=]*=/, ""); print; exit }'
}
token_cleanup() {
local file expires now
now="$(date +%s)"
for file in "$TOKENS_DIR"/*.token; do
[[ -e "$file" ]] || continue
expires="$(config_value EXPIRES "$file")"
[[ "$expires" =~ ^[0-9]+$ ]] || { rm -f -- "$file"; continue; }
(( expires > now )) || rm -f -- "$file"
done
}
node_code() { node_code() {
local name="$1" private_key public_key address endpoint port payload code file local name="$1" address endpoint port payload code token token_hash expires file
valid_name "$name" || die "Имя: строчные латинские буквы, цифры и дефис, максимум 32 символа" valid_name "$name" || die "Имя: строчные латинские буквы, цифры и дефис, максимум 32 символа"
node_exists "$name" && die "Узел $name уже существует" node_exists "$name" && die "Узел $name уже существует"
private_key="$(wg genkey)" token_cleanup
public_key="$(printf '%s' "$private_key" | wg pubkey)" for file in "$TOKENS_DIR"/*.token; do
[[ -e "$file" ]] || continue
[[ "$(config_value NAME "$file")" != "$name" ]] \
|| die "Для узла $name уже существует действующий enrollment-код"
done
address="$(next_address)" address="$(next_address)"
endpoint="$(config_value HUB_ENDPOINT "$HUB_CONFIG")" endpoint="$(config_value HUB_ENDPOINT "$HUB_CONFIG")"
port="$(config_value WG_PORT "$HUB_CONFIG")" port="$(config_value WG_PORT "$HUB_CONFIG")"
file="$(node_file "$name")" token="$(openssl rand -hex 32)"
token_hash="$(printf '%s' "$token" | sha256sum | awk '{ print $1 }')"
expires=$(( $(date +%s) + 600 ))
file="${TOKENS_DIR}/${token_hash}.token"
umask 077 umask 077
{ {
printf 'NAME=%s\n' "$name" printf 'NAME=%s\n' "$name"
printf 'ADDRESS=%s\n' "$address" printf 'ADDRESS=%s\n' "$address"
printf 'PUBLIC_KEY=%s\n' "$public_key" printf 'EXPIRES=%s\n' "$expires"
} > "$file" } > "$file"
payload="$(printf 'VERSION=2\nNAME=%s\nADDRESS=%s/32\nTOKEN=%s\nEXPIRES=%s\nHUB_PUBLIC_KEY=%s\nENDPOINT=%s:%s\nALLOWED_IPS=10.77.0.0/24\n' \
"$name" "$address" "$token" "$expires" "$(wg pubkey < "$HUB_PRIVATE_KEY")" "$endpoint" "$port")"
code="SMM2-$(printf '%s' "$payload" | base64url_encode)"
unset token payload
printf '\nОдноразовый enrollment-код для узла %s (действует 10 минут):\n%s\n\n' "$name" "$code"
printf 'Приватный WireGuard-ключ будет создан только на Node. Не сохраняйте код в чатах или логах.\n'
}
node_enroll() {
local request="${SMM_ENROLL_REQUEST:-}" payload version token token_hash token_file consuming_file
local name address public_key stored_name stored_address expires now node_path ack
local hub_public_key endpoint allowed_ips
if [[ -z "$request" && -r /dev/tty ]]; then
IFS= read -r -s -p 'Вставьте request-код SMMREQ1: ' request < /dev/tty
printf '\n'
fi
[[ "$request" == SMMREQ1-* ]] || die "Ожидается request-код SMMREQ1-..."
payload="$(base64url_decode "${request#SMMREQ1-}")" || die "Не удалось декодировать request-код"
version="$(payload_value "$payload" VERSION)"
token="$(payload_value "$payload" TOKEN)"
name="$(payload_value "$payload" NAME)"
address="$(payload_value "$payload" ADDRESS)"
public_key="$(payload_value "$payload" PUBLIC_KEY)"
[[ "$version" == 1 ]] || die "Неподдерживаемая версия request-кода"
[[ "$token" =~ ^[a-f0-9]{64}$ ]] || die "Некорректный enrollment token"
valid_name "$name" || die "Некорректное имя Node"
[[ "$address" =~ ^10\.77\.0\.([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])/32$ ]] \
|| die "Некорректный адрес Node"
[[ "$(printf '%s' "$public_key" | base64 -d 2>/dev/null | wc -c)" -eq 32 ]] \
|| die "Некорректный публичный WireGuard-ключ Node"
token_hash="$(printf '%s' "$token" | sha256sum | awk '{ print $1 }')"
token_file="${TOKENS_DIR}/${token_hash}.token"
[[ -f "$token_file" ]] || die "Enrollment token не найден, уже использован или истёк"
stored_name="$(config_value NAME "$token_file")"
stored_address="$(config_value ADDRESS "$token_file")"
expires="$(config_value EXPIRES "$token_file")"
now="$(date +%s)"
[[ "$stored_name" == "$name" && "${address%/32}" == "$stored_address" ]] \
|| die "Параметры Node не совпадают с enrollment token"
[[ "$expires" =~ ^[0-9]+$ ]] && (( expires > now )) \
|| { rm -f -- "$token_file"; die "Enrollment token истёк"; }
node_exists "$name" && die "Узел $name уже зарегистрирован"
consuming_file="${token_file}.consuming"
mv -- "$token_file" "$consuming_file" \
|| die "Enrollment token уже обрабатывается"
node_path="$(node_file "$name")"
umask 077
{
printf 'NAME=%s\n' "$name"
printf 'ADDRESS=%s\n' "${address%/32}"
printf 'PUBLIC_KEY=%s\n' "$public_key"
} > "$node_path"
render_wireguard_config render_wireguard_config
payload="$(printf 'VERSION=1\nNAME=%s\nADDRESS=%s/32\nPRIVATE_KEY=%s\nHUB_PUBLIC_KEY=%s\nENDPOINT=%s:%s\nALLOWED_IPS=10.77.0.0/24\n' \ rm -f -- "$consuming_file"
"$name" "$address" "$private_key" "$(wg pubkey < "$HUB_PRIVATE_KEY")" "$endpoint" "$port")" hub_public_key="$(wg pubkey < "$HUB_PRIVATE_KEY")"
code="SMM1-$(printf '%s' "$payload" | base64url_encode)" endpoint="$(config_value HUB_ENDPOINT "$HUB_CONFIG"):$(config_value WG_PORT "$HUB_CONFIG")"
unset private_key payload allowed_ips="10.77.0.0/24"
printf '\nСекретный код конфигурации для узла %s:\n%s\n\n' "$name" "$code" ack="$(printf '%s|%s|%s|%s|%s|%s|%s' \
printf 'Код содержит приватный ключ узла. Используйте его только на одном целевом сервере и не сохраняйте в чатах или логах.\n' "$token" "$public_key" "$name" "$address" "$hub_public_key" "$endpoint" "$allowed_ips" \
| sha256sum | awk '{ print $1 }')"
unset token payload
printf '\nNode зарегистрирован. Верните этот код в установщик Node:\nSMMACK1-%s\n\n' "$ack"
} }
restore_firewall() { restore_firewall() {
@ -409,6 +501,7 @@ main() {
[[ -r "$HUB_CONFIG" ]] || die "Mesh Hub не установлен" [[ -r "$HUB_CONFIG" ]] || die "Mesh Hub не установлен"
case "$action" in case "$action" in
node-code) [[ $# -eq 2 ]] || die "Использование: $0 node-code NAME"; node_code "$2" ;; node-code) [[ $# -eq 2 ]] || die "Использование: $0 node-code NAME"; node_code "$2" ;;
node-enroll) [[ $# -eq 1 ]] || die "Использование: $0 node-enroll"; node_enroll ;;
node-remove) [[ $# -eq 2 ]] || die "Использование: $0 node-remove NAME"; remove_node "$2" ;; node-remove) [[ $# -eq 2 ]] || die "Использование: $0 node-remove NAME"; remove_node "$2" ;;
nodes) list_nodes ;; nodes) list_nodes ;;
links) list_links ;; links) list_links ;;
@ -417,7 +510,7 @@ main() {
render) render_wireguard_config ;; render) render_wireguard_config ;;
firewall-restore) restore_firewall ;; firewall-restore) restore_firewall ;;
status) status ;; status) status ;;
*) die "Команды: node-code, node-remove, nodes, links, link-connect, link-disconnect, status" ;; *) die "Команды: node-code, node-enroll, node-remove, nodes, links, link-connect, link-disconnect, status" ;;
esac esac
} }
@ -487,7 +580,7 @@ install_hub() {
install_monitoring install_monitoring
install_mesh_dependencies install_mesh_dependencies
read_hub_endpoint read_hub_endpoint
install -d -m 0700 -o root -g root "$MESH_DIR" "$NODES_DIR" install -d -m 0700 -o root -g root "$MESH_DIR" "$NODES_DIR" "$TOKENS_DIR"
install -d -m 0700 -o root -g root /etc/wireguard install -d -m 0700 -o root -g root /etc/wireguard
[[ -f "$HUB_PRIVATE_KEY" ]] || { umask 077; wg genkey > "$HUB_PRIVATE_KEY"; } [[ -f "$HUB_PRIVATE_KEY" ]] || { umask 077; wg genkey > "$HUB_PRIVATE_KEY"; }
chmod 0600 "$HUB_PRIVATE_KEY" chmod 0600 "$HUB_PRIVATE_KEY"
@ -521,6 +614,10 @@ EOF
log "Включить связь: sudo ochenstarik-smm link-connect ai-agent home" log "Включить связь: sudo ochenstarik-smm link-connect ai-agent home"
} }
base64url_encode() {
base64 -w0 | tr '+/' '-_' | tr -d '='
}
base64url_decode() { base64url_decode() {
local data="$1" remainder local data="$1" remainder
data="${data//-/+}" data="${data//-/+}"
@ -543,37 +640,68 @@ read_join_code() {
JOIN_CODE="$SMM_JOIN_CODE" JOIN_CODE="$SMM_JOIN_CODE"
elif [[ -r /dev/tty ]]; then elif [[ -r /dev/tty ]]; then
printf '\nНа главном сервере выполните: sudo ochenstarik-smm node-code ИМЯ\n' printf '\nНа главном сервере выполните: sudo ochenstarik-smm node-code ИМЯ\n'
IFS= read -r -s -p 'Вставьте секретный код SMM1: ' JOIN_CODE < /dev/tty IFS= read -r -s -p 'Вставьте одноразовый код SMM2: ' JOIN_CODE < /dev/tty
printf '\n' printf '\n'
else else
die "Передайте код через SMM_JOIN_CODE" die "Передайте код через SMM_JOIN_CODE"
fi fi
JOIN_CODE="${JOIN_CODE//$'\r'/}" JOIN_CODE="${JOIN_CODE//$'\r'/}"
[[ "$JOIN_CODE" == SMM1-* ]] || die "Ожидается код формата SMM1-..." [[ "$JOIN_CODE" == SMM2-* ]] || die "Ожидается код формата SMM2-..."
} }
install_node_mesh() { install_node_mesh() {
local payload version name address private_key hub_public_key endpoint allowed_ips derived_public tmp ssh_port local payload version name address token expires now private_key public_key
local hub_public_key endpoint allowed_ips request_payload request_code ack expected_ack tmp ssh_port
install_mesh_dependencies install_mesh_dependencies
read_join_code read_join_code
payload="$(base64url_decode "${JOIN_CODE#SMM1-}")" || die "Не удалось декодировать код" payload="$(base64url_decode "${JOIN_CODE#SMM2-}")" || die "Не удалось декодировать код"
version="$(payload_value "$payload" VERSION)" version="$(payload_value "$payload" VERSION)"
name="$(payload_value "$payload" NAME)" name="$(payload_value "$payload" NAME)"
address="$(payload_value "$payload" ADDRESS)" address="$(payload_value "$payload" ADDRESS)"
private_key="$(payload_value "$payload" PRIVATE_KEY)" token="$(payload_value "$payload" TOKEN)"
expires="$(payload_value "$payload" EXPIRES)"
hub_public_key="$(payload_value "$payload" HUB_PUBLIC_KEY)" hub_public_key="$(payload_value "$payload" HUB_PUBLIC_KEY)"
endpoint="$(payload_value "$payload" ENDPOINT)" endpoint="$(payload_value "$payload" ENDPOINT)"
allowed_ips="$(payload_value "$payload" ALLOWED_IPS)" allowed_ips="$(payload_value "$payload" ALLOWED_IPS)"
[[ "$version" == 1 ]] || die "Неподдерживаемая версия кода" [[ "$version" == 2 ]] || die "Неподдерживаемая версия кода"
[[ "$name" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]] || die "Некорректное имя узла" [[ "$name" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]] || die "Некорректное имя узла"
[[ "$address" =~ ^10\.77\.0\.([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])/32$ ]] \ [[ "$address" =~ ^10\.77\.0\.([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])/32$ ]] \
|| die "Некорректный внутренний адрес" || die "Некорректный внутренний адрес"
[[ "$endpoint" =~ ^[A-Za-z0-9.-]+:[0-9]{1,5}$ ]] || die "Некорректный endpoint" [[ "$endpoint" =~ ^[A-Za-z0-9.-]+:[0-9]{1,5}$ ]] || die "Некорректный endpoint"
[[ "$allowed_ips" == "$MESH_CIDR" ]] || die "Некорректная mesh-подсеть" [[ "$allowed_ips" == "$MESH_CIDR" ]] || die "Некорректная mesh-подсеть"
derived_public="$(printf '%s' "$private_key" | wg pubkey 2>/dev/null)" \ [[ "$token" =~ ^[a-f0-9]{64}$ ]] || die "Некорректный enrollment token"
|| die "Некорректный приватный WireGuard-ключ" now="$(date +%s)"
[[ "$expires" =~ ^[0-9]+$ ]] && (( expires > now )) || die "Enrollment-код истёк"
[[ "$(printf '%s' "$hub_public_key" | base64 -d 2>/dev/null | wc -c)" -eq 32 ]] \ [[ "$(printf '%s' "$hub_public_key" | base64 -d 2>/dev/null | wc -c)" -eq 32 ]] \
|| die "Некорректный публичный ключ Hub" || die "Некорректный публичный ключ Hub"
private_key="$(wg genkey)"
public_key="$(printf '%s' "$private_key" | wg pubkey)"
request_payload="$(printf 'VERSION=1\nTOKEN=%s\nNAME=%s\nADDRESS=%s\nPUBLIC_KEY=%s\n' \
"$token" "$name" "$address" "$public_key")"
request_code="SMMREQ1-$(printf '%s' "$request_payload" | base64url_encode)"
printf '\nПриватный WireGuard-ключ создан локально и не покидает Node.\n'
printf 'Request-код:\n\n%s\n\n' "$request_code"
printf 'На Hub выполните: sudo ochenstarik-smm node-enroll\n'
printf 'Вставьте request-код по скрытому запросу и верните полученный SMMACK1.\n\n'
expected_ack="$(printf '%s|%s|%s|%s|%s|%s|%s' \
"$token" "$public_key" "$name" "$address" "$hub_public_key" "$endpoint" "$allowed_ips" \
| sha256sum | awk '{ print $1 }')"
if [[ -n "${SMM_ENROLL_ACK:-}" ]]; then
ack="$SMM_ENROLL_ACK"
ack="${ack//$'\r'/}"
[[ "$ack" == "SMMACK1-${expected_ack}" ]] \
|| die "Hub не подтвердил регистрацию этого Node"
elif [[ -r /dev/tty ]]; then
while true; do
IFS= read -r -s -p 'Вставьте ответ SMMACK1 от Hub: ' ack < /dev/tty
printf '\n'
ack="${ack//$'\r'/}"
[[ "$ack" == "SMMACK1-${expected_ack}" ]] && break
warn "Код не подтверждает этот Node. Повторите вставку или нажмите Ctrl+C."
done
else
die "Передайте подтверждение через SMM_ENROLL_ACK"
fi
install -d -m 0700 -o root -g root /etc/wireguard "$MESH_DIR" install -d -m 0700 -o root -g root /etc/wireguard "$MESH_DIR"
tmp="$(mktemp)" tmp="$(mktemp)"
{ {
@ -584,9 +712,9 @@ install_node_mesh() {
install -m 0600 -o root -g root "$tmp" "$WG_CONFIG" install -m 0600 -o root -g root "$tmp" "$WG_CONFIG"
rm -f -- "$tmp" rm -f -- "$tmp"
printf 'ROLE=node\nNAME=%s\nADDRESS=%s\nPUBLIC_KEY=%s\n' \ printf 'ROLE=node\nNAME=%s\nADDRESS=%s\nPUBLIC_KEY=%s\n' \
"$name" "$address" "$derived_public" > "$MESH_DIR/node.conf" "$name" "$address" "$public_key" > "$MESH_DIR/node.conf"
chmod 0600 "$MESH_DIR/node.conf" chmod 0600 "$MESH_DIR/node.conf"
unset private_key payload JOIN_CODE SMM_JOIN_CODE unset private_key token payload request_payload JOIN_CODE SMM_JOIN_CODE SMM_ENROLL_ACK ack expected_ack
systemctl enable --now "wg-quick@${WG_INTERFACE}.service" systemctl enable --now "wg-quick@${WG_INTERFACE}.service"
ssh_port="$(sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }')" ssh_port="$(sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }')"
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q '^Status: active' && [[ -n "$ssh_port" ]]; then if command -v ufw >/dev/null 2>&1 && ufw status | grep -q '^Status: active' && [[ -n "$ssh_port" ]]; then

View file

@ -0,0 +1,99 @@
#!/usr/bin/env bash
set -Eeuo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
script="ochenstarik-server-monitor-manager.sh"
temp_dir="$(mktemp -d)"
trap 'rm -rf -- "$temp_dir"' EXIT
[[ -s "$script" ]]
grep -Fq 'code="SMM2-' "$script"
grep -Fq 'request_code="SMMREQ1-' "$script"
grep -Fq 'SMMACK1-' "$script"
grep -Fq 'expires=$(( $(date +%s) + 600 ))' "$script"
grep -Fq 'mv -- "$token_file" "$consuming_file"' "$script"
grep -Fq 'private_key="$(wg genkey)"' "$script"
if grep -Fq 'PRIVATE_KEY=%s' "$script"; then
echo 'Enrollment payload must not contain a private WireGuard key.' >&2
exit 1
fi
helper="$temp_dir/ochenstarik-smm-hub"
awk '
capture && /^EOF$/ { exit }
/cat > .*HUB_HELPER.*<</ { capture=1; next }
capture { print }
' "$script" > "$helper"
chmod 700 "$helper"
state="$temp_dir/state"
mock_bin="$temp_dir/bin"
mkdir -p "$state/nodes" "$state/tokens" "$mock_bin"
sed -i "s|^readonly STATE_DIR=.*|readonly STATE_DIR=\"$state\"|" "$helper"
sed -i "s|^readonly WG_CONFIG=.*|readonly WG_CONFIG=\"$temp_dir/smm0.conf\"|" "$helper"
sed -i 's/^require_root() .*/require_root() { :; }/' "$helper"
sed -i 's/ -o root -g root//g' "$helper"
public_key='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
cat > "$mock_bin/wg" <<EOF
#!/usr/bin/env bash
if [[ "\${1:-}" == pubkey ]]; then
printf '%s\n' '$public_key'
exit 0
fi
exit 0
EOF
cat > "$mock_bin/ip" <<'EOF'
#!/usr/bin/env bash
exit 1
EOF
chmod 700 "$mock_bin/wg" "$mock_bin/ip"
cat > "$state/hub.conf" <<'EOF'
HUB_ENDPOINT=hub.example.test
WG_PORT=51820
HUB_ADDRESS=10.77.0.1
MESH_CIDR=10.77.0.0/24
EOF
printf 'test-hub-private-key\n' > "$state/hub.key"
: > "$state/links"
output="$(PATH="$mock_bin:$PATH" "$helper" node-code test-node)"
join_code="$(printf '%s\n' "$output" | grep '^SMM2-')"
[[ -n "$join_code" ]]
decode_base64url() {
local data="$1"
data="${data//-/+}"
data="${data//_/\/}"
case $(( ${#data} % 4 )) in
2) data+='==' ;;
3) data+='=' ;;
esac
printf '%s' "$data" | base64 -d
}
payload="$(decode_base64url "${join_code#SMM2-}")"
token="$(printf '%s\n' "$payload" | awk -F= '$1 == "TOKEN" { print $2 }')"
name="$(printf '%s\n' "$payload" | awk -F= '$1 == "NAME" { print $2 }')"
address="$(printf '%s\n' "$payload" | awk -F= '$1 == "ADDRESS" { print $2 }')"
[[ "$name" == test-node ]]
[[ "$address" == 10.77.0.2/32 ]]
[[ "$token" =~ ^[a-f0-9]{64}$ ]]
[[ "$payload" != *PRIVATE_KEY* ]]
request_payload="$(printf 'VERSION=1\nTOKEN=%s\nNAME=%s\nADDRESS=%s\nPUBLIC_KEY=%s\n' \
"$token" "$name" "$address" "$public_key")"
request_code="SMMREQ1-$(printf '%s' "$request_payload" | base64 -w0 | tr '+/' '-_' | tr -d '=')"
enroll_output="$(PATH="$mock_bin:$PATH" SMM_ENROLL_REQUEST="$request_code" "$helper" node-enroll)"
grep -Eq '^SMMACK1-[a-f0-9]{64}$' <<< "$enroll_output"
grep -Fqx 'NAME=test-node' "$state/nodes/test-node.node"
grep -Fqx "PUBLIC_KEY=$public_key" "$state/nodes/test-node.node"
if PATH="$mock_bin:$PATH" SMM_ENROLL_REQUEST="$request_code" "$helper" node-enroll >/dev/null 2>&1; then
echo 'Enrollment token was accepted more than once.' >&2
exit 1
fi
printf 'Server Monitor Manager enrollment checks passed.\n'