Add SSH monitoring server installer #3
5 changed files with 258 additions and 28 deletions
3
.github/workflows/bash-syntax.yml
vendored
3
.github/workflows/bash-syntax.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 минут и погашается при первой успешной регистрации.
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
99
tests/test-monitor-manager-enrollment.sh
Normal file
99
tests/test-monitor-manager-enrollment.sh
Normal 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'
|
||||||
Loading…
Reference in a new issue