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 183 additions and 47 deletions
Showing only changes of commit 78e650a35b - Show all commits

View file

@ -46,3 +46,6 @@ jobs:
- name: Test Server Monitor Manager enrollment
run: bash tests/test-monitor-manager-enrollment.sh
- name: Test Server Monitor Manager Link policies
run: bash tests/test-monitor-manager-links.sh

View file

@ -103,8 +103,8 @@ sudo ./ochenstarik-server-monitor-manager.sh node
Links are directional. The reverse direction requires its own rule:
```bash
sudo ochenstarik-smm link-connect ai-agent home
sudo ochenstarik-smm link-disconnect ai-agent home
sudo ochenstarik-smm link-connect ai-agent home tcp 22 120
sudo ochenstarik-smm link-disconnect ai-agent home tcp 22
sudo ochenstarik-smm nodes
sudo ochenstarik-smm links
```

View file

@ -90,8 +90,8 @@ sudo ./ochenstarik-server-monitor-manager.sh node
Links являются направленными:
```bash
sudo ochenstarik-smm link-connect ai-agent home
sudo ochenstarik-smm link-disconnect ai-agent home
sudo ochenstarik-smm link-connect ai-agent home tcp 22 120
sudo ochenstarik-smm link-disconnect ai-agent home tcp 22
sudo ochenstarik-smm nodes
sudo ochenstarik-smm links
```

View file

@ -108,17 +108,17 @@ case "$original_command" in
;;
"mesh connect "*)
[[ -x "$HUB_HELPER" ]] || { echo "Mesh Hub не установлен" >&2; exit 1; }
read -r prefix action source target extra <<< "$original_command"
[[ "$prefix" == mesh && "$action" == connect && -n "$source" && -n "$target" && -z "${extra:-}" ]] \
read -r prefix action source target protocol port ttl extra <<< "$original_command"
[[ "$prefix" == mesh && "$action" == connect && -n "$ttl" && -z "${extra:-}" ]] \
|| { echo "Некорректная команда" >&2; exit 2; }
exec sudo -n "$HUB_HELPER" link-connect "$source" "$target"
exec sudo -n "$HUB_HELPER" link-connect "$source" "$target" "$protocol" "$port" "$ttl"
;;
"mesh disconnect "*)
[[ -x "$HUB_HELPER" ]] || { echo "Mesh Hub не установлен" >&2; exit 1; }
read -r prefix action source target extra <<< "$original_command"
[[ "$prefix" == mesh && "$action" == disconnect && -n "$source" && -n "$target" && -z "${extra:-}" ]] \
read -r prefix action source target protocol port extra <<< "$original_command"
[[ "$prefix" == mesh && "$action" == disconnect && -n "$port" && -z "${extra:-}" ]] \
|| { echo "Некорректная команда" >&2; exit 2; }
exec sudo -n "$HUB_HELPER" link-disconnect "$source" "$target"
exec sudo -n "$HUB_HELPER" link-disconnect "$source" "$target" "$protocol" "$port"
;;
*)
echo "Разрешены только metrics и команды mesh" >&2
@ -374,68 +374,95 @@ node_enroll() {
printf '\nNode зарегистрирован. Верните этот код в установщик Node:\nSMMACK1-%s\n\n' "$ack"
}
restore_firewall() {
local tmp source target source_ip target_ip first=true
prune_links() {
local tmp source target cidr protocol port expires now target_ip
tmp="$(mktemp)"
now="$(date +%s)"
if [[ -f "$LINKS_FILE" ]]; then
while read -r source target cidr protocol port expires extra; do
[[ -z "${extra:-}" ]] || continue
valid_name "$source" && valid_name "$target" || continue
node_exists "$source" && node_exists "$target" || continue
target_ip="$(node_value "$target" ADDRESS)"
[[ "$cidr" == "${target_ip}/32" ]] || continue
[[ "$protocol" == tcp || "$protocol" == udp ]] || continue
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) || continue
[[ "$expires" =~ ^[0-9]+$ ]] || continue
(( expires == 0 || expires > now )) || continue
printf '%s %s %s %s %s %s\n' "$source" "$target" "$cidr" "$protocol" "$port" "$expires" >> "$tmp"
done < "$LINKS_FILE"
fi
sort -u -o "$tmp" "$tmp"
install -m 0600 -o root -g root "$tmp" "$LINKS_FILE"
rm -f -- "$tmp"
}
restore_firewall() {
local tmp source target cidr protocol port expires source_ip
prune_links
tmp="$(mktemp)"
nft delete table inet "$NFT_TABLE" >/dev/null 2>&1 || true
{
printf 'table inet %s {\n' "$NFT_TABLE"
printf ' set links {\n type ipv4_addr . ipv4_addr\n'
if [[ -s "$LINKS_FILE" ]]; then
printf ' elements = { '
while read -r source target; do
[[ -n "$source" && -n "$target" ]] || continue
node_exists "$source" && node_exists "$target" || continue
source_ip="$(node_value "$source" ADDRESS)"
target_ip="$(node_value "$target" ADDRESS)"
[[ "$first" == true ]] || printf ', '
printf '%s . %s' "$source_ip" "$target_ip"
first=false
done < "$LINKS_FILE"
printf ' }\n'
fi
printf ' }\n'
printf ' chain forward {\n'
printf ' type filter hook forward priority 10; policy accept;\n'
printf ' iifname "%s" oifname "%s" ct state established,related accept\n' "$WG_INTERFACE" "$WG_INTERFACE"
printf ' iifname "%s" oifname "%s" ip saddr . ip daddr @links accept\n' "$WG_INTERFACE" "$WG_INTERFACE"
while read -r source target cidr protocol port expires; do
[[ -n "$source" ]] || continue
source_ip="$(node_value "$source" ADDRESS)"
printf ' iifname "%s" oifname "%s" ip saddr %s ip daddr %s %s dport %s accept\n' \
"$WG_INTERFACE" "$WG_INTERFACE" "$source_ip" "$cidr" "$protocol" "$port"
done < "$LINKS_FILE"
printf ' iifname "%s" oifname "%s" drop\n' "$WG_INTERFACE" "$WG_INTERFACE"
printf ' }\n}\n'
} > "$tmp"
nft --check -f "$tmp"
nft delete table inet "$NFT_TABLE" >/dev/null 2>&1 || true
nft -f "$tmp"
rm -f -- "$tmp"
}
link_connect() {
local source="$1" target="$2" tmp
local source="$1" target="$2" protocol="$3" port="$4" ttl_minutes="$5"
local target_ip cidr expires tmp
valid_name "$source" && valid_name "$target" || die "Некорректное имя узла"
[[ "$source" != "$target" ]] || die "Источник и назначение совпадают"
node_exists "$source" || die "Узел $source не найден"
node_exists "$target" || die "Узел $target не найден"
if grep -Fxq "$source $target" "$LINKS_FILE" 2>/dev/null; then
log "Связь $source$target уже включена"
return
fi
[[ "$protocol" == tcp || "$protocol" == udp ]] || die "Протокол должен быть tcp или udp"
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) || die "Порт должен быть от 1 до 65535"
[[ "$ttl_minutes" =~ ^[0-9]+$ ]] && (( ttl_minutes <= 525600 )) \
|| die "TTL должен быть от 0 до 525600 минут"
target_ip="$(node_value "$target" ADDRESS)"
cidr="${target_ip}/32"
expires=0
(( ttl_minutes == 0 )) || expires=$(( $(date +%s) + ttl_minutes * 60 ))
tmp="$(mktemp)"
{ [[ ! -f "$LINKS_FILE" ]] || cat "$LINKS_FILE"; printf '%s %s\n' "$source" "$target"; } \
| sort -u > "$tmp"
if [[ -f "$LINKS_FILE" ]]; then
awk -v source="$source" -v target="$target" -v protocol="$protocol" -v port="$port" \
'!($1 == source && $2 == target && $4 == protocol && $5 == port)' "$LINKS_FILE" > "$tmp"
fi
printf '%s %s %s %s %s %s\n' "$source" "$target" "$cidr" "$protocol" "$port" "$expires" >> "$tmp"
sort -u -o "$tmp" "$tmp"
install -m 0600 -o root -g root "$tmp" "$LINKS_FILE"
rm -f -- "$tmp"
restore_firewall
log "Связь $source$target включена"
log "Связь $source$target: $protocol/$port включена"
}
link_disconnect() {
local source="$1" target="$2" tmp
local source="$1" target="$2" protocol="$3" port="$4" tmp
valid_name "$source" && valid_name "$target" || die "Некорректное имя узла"
[[ "$protocol" == tcp || "$protocol" == udp ]] || die "Протокол должен быть tcp или udp"
[[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) || die "Некорректный порт"
tmp="$(mktemp)"
if [[ -f "$LINKS_FILE" ]]; then
awk -v source="$source" -v target="$target" '!($1 == source && $2 == target)' "$LINKS_FILE" > "$tmp"
awk -v source="$source" -v target="$target" -v protocol="$protocol" -v port="$port" \
'!($1 == source && $2 == target && $4 == protocol && $5 == port)' "$LINKS_FILE" > "$tmp"
fi
install -m 0600 -o root -g root "$tmp" "$LINKS_FILE"
rm -f -- "$tmp"
restore_firewall
log "Связь $source$target отключена"
log "Связь $source$target: $protocol/$port отключена"
}
remove_node() {
@ -477,11 +504,13 @@ list_nodes() {
}
list_links() {
local source target
local source target cidr protocol port expires
prune_links
[[ -f "$LINKS_FILE" ]] || return 0
while read -r source target; do
while read -r source target cidr protocol port expires; do
[[ -n "$source" && -n "$target" ]] || continue
printf 'LINK=%s|%s|enabled\n' "$source" "$target"
printf 'LINK=%s|%s|%s|%s|%s|%s|enabled\n' \
"$source" "$target" "$cidr" "$protocol" "$port" "$expires"
done < "$LINKS_FILE"
}
@ -505,8 +534,8 @@ main() {
node-remove) [[ $# -eq 2 ]] || die "Использование: $0 node-remove NAME"; remove_node "$2" ;;
nodes) list_nodes ;;
links) list_links ;;
link-connect) [[ $# -eq 3 ]] || die "Использование: $0 link-connect SOURCE TARGET"; link_connect "$2" "$3" ;;
link-disconnect) [[ $# -eq 3 ]] || die "Использование: $0 link-disconnect SOURCE TARGET"; link_disconnect "$2" "$3" ;;
link-connect) [[ $# -eq 6 ]] || die "Использование: $0 link-connect SOURCE TARGET tcp|udp PORT TTL_MINUTES"; link_connect "$2" "$3" "$4" "$5" "$6" ;;
link-disconnect) [[ $# -eq 5 ]] || die "Использование: $0 link-disconnect SOURCE TARGET tcp|udp PORT"; link_disconnect "$2" "$3" "$4" "$5" ;;
render) render_wireguard_config ;;
firewall-restore) restore_firewall ;;
status) status ;;
@ -530,15 +559,30 @@ Wants=wg-quick@${WG_INTERFACE}.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${HUB_HELPER} firewall-restore
[Install]
WantedBy=multi-user.target
EOF
chmod 0644 /etc/systemd/system/ochenstarik-smm-firewall.service
cat > /etc/systemd/system/ochenstarik-smm-firewall.timer <<'EOF'
[Unit]
Description=Expire Server Monitor mesh policies
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
AccuracySec=10s
Persistent=true
Unit=ochenstarik-smm-firewall.service
[Install]
WantedBy=timers.target
EOF
chmod 0644 /etc/systemd/system/ochenstarik-smm-firewall.timer
systemctl daemon-reload
systemctl enable --now ochenstarik-smm-firewall.service
systemctl enable --now ochenstarik-smm-firewall.timer
}
configure_hub_sudo() {
@ -611,7 +655,7 @@ EOF
fi
log "Главный Mesh Hub установлен: ${HUB_ENDPOINT_VALUE}:${WG_PORT_VALUE}/udp"
log "Создать код узла: sudo ochenstarik-smm node-code ai-agent"
log "Включить связь: sudo ochenstarik-smm link-connect ai-agent home"
log "Включить SSH на 2 часа: sudo ochenstarik-smm link-connect ai-agent home tcp 22 120"
}
base64url_encode() {

View file

@ -0,0 +1,89 @@
#!/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
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"
nft_capture="$temp_dir/nft.conf"
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"
cat > "$mock_bin/nft" <<'EOF'
#!/usr/bin/env bash
previous=''
for argument in "$@"; do
if [[ "$previous" == -f ]]; then
cp -- "$argument" "$NFT_CAPTURE"
fi
previous="$argument"
done
exit 0
EOF
chmod 700 "$mock_bin/nft"
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"
cat > "$state/nodes/ai-agent.node" <<'EOF'
NAME=ai-agent
ADDRESS=10.77.0.2
PUBLIC_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
EOF
cat > "$state/nodes/home.node" <<'EOF'
NAME=home
ADDRESS=10.77.0.3
PUBLIC_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
EOF
printf 'legacy-policy home\n' > "$state/links"
PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \
"$helper" link-connect ai-agent home tcp 2222 120
read -r source target cidr protocol port expires < "$state/links"
[[ "$source" == ai-agent ]]
[[ "$target" == home ]]
[[ "$cidr" == 10.77.0.3/32 ]]
[[ "$protocol" == tcp ]]
[[ "$port" == 2222 ]]
(( expires > $(date +%s) ))
grep -Fq 'ip saddr 10.77.0.2 ip daddr 10.77.0.3/32 tcp dport 2222 accept' "$nft_capture"
if PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \
"$helper" link-connect ai-agent home tcp 70000 120 >/dev/null 2>&1; then
echo 'Invalid port was accepted.' >&2
exit 1
fi
printf 'ai-agent home 10.77.0.3/32 udp 53 1\n' >> "$state/links"
PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" "$helper" firewall-restore
[[ "$(wc -l < "$state/links")" -eq 1 ]]
PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \
"$helper" link-disconnect ai-agent home tcp 2222
[[ ! -s "$state/links" ]]
if grep -Fq 'dport 2222 accept' "$nft_capture"; then
echo 'Disconnected policy remained in nftables.' >&2
exit 1
fi
grep -Fq 'OnUnitActiveSec=1min' "$script"
printf 'Server Monitor Manager Link policy checks passed.\n'