fix(enrollment): repair node enrollment path (#24)
* fix(enrollment): repair node enrollment path Make trimmed Agent configuration fail closed, harden Control URL validation, add device-code and bootstrap delivery, and make alpha.7 Control updates migration- and recovery-safe. * fix(enrollment): allow agent token cleanup --------- Co-authored-by: Ochenstarik <ochenstarik@inbox.ru>
This commit is contained in:
parent
1b798e7ee8
commit
80b4797f2a
10 changed files with 1136 additions and 92 deletions
53
.github/workflows/linux-control-agent.yml
vendored
53
.github/workflows/linux-control-agent.yml
vendored
|
|
@ -90,54 +90,11 @@ jobs:
|
|||
tar -C smoke -czf smoke-release.tar.gz agent control provisioning-helper deploy bootstrap
|
||||
sha256sum smoke-release.tar.gz > smoke-release.tar.gz.sha256
|
||||
|
||||
- name: Verify repeated systemd installation
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
smoke_log=/tmp/smm-systemd-smoke.log
|
||||
report_failure() {
|
||||
local title="$1" message main_pid
|
||||
sudo systemctl status --no-pager ochenstarik-smm-control.service >>"$smoke_log" 2>&1 || true
|
||||
sudo journalctl --no-pager -u ochenstarik-smm-control.service -n 40 >>"$smoke_log" 2>&1 || true
|
||||
sudo ss -ltnp >>"$smoke_log" 2>&1 || true
|
||||
main_pid="$(sudo systemctl show -p MainPID --value ochenstarik-smm-control.service 2>/dev/null || true)"
|
||||
if [[ "$main_pid" =~ ^[1-9][0-9]*$ ]]; then
|
||||
sudo sh -c "tr '\0' '\n' </proc/$main_pid/environ | grep '^ASPNETCORE_'" \
|
||||
>>"$smoke_log" 2>&1 || true
|
||||
sudo sh -c "printf 'wchan='; cat /proc/$main_pid/wchan" >>"$smoke_log" 2>&1 || true
|
||||
fi
|
||||
message="$(tail -c 6000 "$smoke_log" | sed ':a;N;$!ba;s/%/%25/g;s/\r/%0D/g;s/\n/%0A/g')"
|
||||
printf '::error title=%s::%s\n' "$title" "$message"
|
||||
exit 1
|
||||
}
|
||||
sudo deploy/ochenstarik-server-monitor-manager.sh install-control \
|
||||
smoke-release.tar.gz 127.0.0.1 7443 >"$smoke_log" 2>&1 \
|
||||
|| report_failure 'Initial Control installation failed'
|
||||
sudo test -x /usr/local/sbin/ochenstarik-smm-emergency \
|
||||
|| report_failure 'Emergency recovery command was not installed'
|
||||
sudo /usr/local/sbin/ochenstarik-smm-emergency status >>"$smoke_log" 2>&1 \
|
||||
|| report_failure 'Emergency recovery status failed'
|
||||
for _ in {1..30}; do
|
||||
sudo curl --fail --silent --cacert /etc/ochenstarik-server-monitor-manager/control-ca.crt \
|
||||
https://127.0.0.1:7443/healthz && break
|
||||
sleep 1
|
||||
done
|
||||
sudo curl --fail --silent --show-error \
|
||||
--cacert /etc/ochenstarik-server-monitor-manager/control-ca.crt \
|
||||
https://127.0.0.1:7443/healthz >>"$smoke_log" 2>&1 \
|
||||
|| report_failure 'Initial Control healthcheck failed'
|
||||
sudo deploy/ochenstarik-server-monitor-manager.sh install-control \
|
||||
smoke-release.tar.gz 127.0.0.1 7443 >"$smoke_log" 2>&1 \
|
||||
|| report_failure 'Repeated Control installation failed'
|
||||
sudo systemctl restart ochenstarik-smm-control.service \
|
||||
|| report_failure 'Control restart failed'
|
||||
sudo systemctl is-active --quiet ochenstarik-smm-control.service \
|
||||
|| report_failure 'Control is inactive after restart'
|
||||
sudo curl --fail --silent --show-error \
|
||||
--retry 10 --retry-all-errors --retry-delay 1 \
|
||||
--cacert /etc/ochenstarik-server-monitor-manager/control-ca.crt \
|
||||
https://127.0.0.1:7443/healthz >>"$smoke_log" 2>&1 \
|
||||
|| report_failure 'Repeated Control healthcheck failed'
|
||||
- name: Verify complete published enrollment with systemd
|
||||
run: bash tests/bootstrap/run-native-systemd-smoke.sh smoke-release.tar.gz deploy/ochenstarik-server-monitor-manager.sh
|
||||
|
||||
- name: Clean up systemd smoke installation
|
||||
if: always()
|
||||
run: sudo deploy/ochenstarik-server-monitor-manager.sh uninstall-control --confirm-destroy-control || true
|
||||
run: |
|
||||
sudo /usr/local/sbin/ochenstarik-server-monitor-manager.sh uninstall-agent --purge || true
|
||||
sudo /usr/local/sbin/ochenstarik-server-monitor-manager.sh uninstall-control --confirm-destroy-control || true
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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 SUDOERS_FILE="/etc/sudoers.d/ochenstarik-smm-control"
|
||||
readonly MESH_DIR="${STATE_DIR}/mesh"
|
||||
readonly WG_DIR="${ETC_DIR}/wireguard"
|
||||
|
|
@ -27,11 +28,25 @@ 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
|
||||
|
|
@ -41,6 +56,7 @@ cleanup() {
|
|||
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
|
||||
rm -rf -- "$TEMP_DIR"
|
||||
fi
|
||||
exit "$status"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
|
|
@ -61,6 +77,7 @@ Usage:
|
|||
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
|
||||
|
|
@ -125,13 +142,99 @@ validate_node_id() {
|
|||
}
|
||||
|
||||
validate_port() {
|
||||
[[ "$1" =~ ^[0-9]+$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 )) \
|
||||
[[ "$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() {
|
||||
[[ "$1" =~ ^https://[A-Za-z0-9._:\[\]-]+(:[0-9]{1,5})?/?$ ]] \
|
||||
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_archive() {
|
||||
|
|
@ -269,6 +372,137 @@ install_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 {} +
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -480,18 +714,21 @@ install_control() {
|
|||
[[ -x "$TEMP_DIR/control/ochenstarik-smm-control" ]] || fail "Control binary is missing."
|
||||
backup_id="$(create_backup control)"
|
||||
ensure_system_user "$CONTROL_USER"
|
||||
install -d -m 0750 -o root -g "$CONTROL_USER" "$ETC_DIR"
|
||||
install -d -m 0750 -o "$CONTROL_USER" -g "$CONTROL_USER" "$STATE_DIR" "$STATE_DIR/backups"
|
||||
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.db
|
||||
Control__DatabasePath=$STATE_DIR/control/control.db
|
||||
Control__CertificateAuthorityPath=$ETC_DIR/control-ca.pfx
|
||||
Control__BackupDirectory=$STATE_DIR/backups
|
||||
Control__BackupDirectory=$STATE_DIR/control/backups
|
||||
Control__HubHelperPath=$POLICY_HELPER
|
||||
Control__PrivilegeEscalationPath=/usr/bin/sudo
|
||||
Control__LinkReconciliationSeconds=300
|
||||
|
|
@ -550,12 +787,13 @@ install_agent() {
|
|||
[[ -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 0750 -o root -g "$AGENT_USER" "$ETC_DIR"
|
||||
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 0710 -o root -g "$AGENT_USER" "$ENROLLMENT_DIR"
|
||||
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
|
||||
|
|
@ -727,16 +965,32 @@ update_role() {
|
|||
*) fail "Unknown role: $role" ;;
|
||||
esac
|
||||
[[ -x "$TEMP_DIR/$role/$binary" ]] || fail "$role binary is missing."
|
||||
if [[ "$role" == "agent" ]]; then
|
||||
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
|
||||
backup_id="$(create_backup "$role")"
|
||||
install_tree_atomic "$TEMP_DIR/$role" "$LIB_DIR/$role" "$user"
|
||||
if [[ "$role" == "agent" ]]; then
|
||||
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"
|
||||
|
|
@ -744,12 +998,19 @@ update_role() {
|
|||
install_unit "$TEMP_DIR/deploy/$PROVISIONING_HELPER_UNIT" "$PROVISIONING_HELPER_UNIT"
|
||||
systemctl enable --now "$PROVISIONING_HELPER_UNIT"
|
||||
fi
|
||||
systemctl start "$unit"
|
||||
systemctl restart "$unit"
|
||||
if ! systemctl is-active --quiet "$unit"; then
|
||||
log "Update failed; restoring backup $backup_id"
|
||||
restore_backup "$role" "$backup_id"
|
||||
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
|
||||
log "$role updated. Backup: $backup_id"
|
||||
}
|
||||
|
||||
|
|
@ -768,7 +1029,11 @@ restore_backup() {
|
|||
if [[ "$role" == "agent" ]]; then
|
||||
systemctl stop "$PROVISIONING_HELPER_UNIT" || true
|
||||
fi
|
||||
tar -C / -xzf "$archive"
|
||||
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"
|
||||
|
|
@ -849,6 +1114,27 @@ create_node_code() {
|
|||
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
|
||||
|
|
@ -908,6 +1194,22 @@ show_ca_fingerprint() {
|
|||
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
|
||||
|
|
@ -915,8 +1217,10 @@ uninstall_agent() {
|
|||
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"
|
||||
[[ "$purge" == "--purge" ]] && rm -rf -- \
|
||||
"$STATE_DIR/agent" "$ENROLLMENT_DIR" "$ETC_DIR/control-ca.crt"
|
||||
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)}."
|
||||
}
|
||||
|
|
@ -926,10 +1230,12 @@ uninstall_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" "$ETC_DIR/control-ca.crt" \
|
||||
"$ETC_DIR/control-ca.pfx" "$ETC_DIR/control-server.pfx" \
|
||||
"$POLICY_HELPER" "$SUDOERS_FILE"
|
||||
rm -rf -- "$LIB_DIR/control" "$STATE_DIR/control.db" "$STATE_DIR/control.db-wal" \
|
||||
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."
|
||||
}
|
||||
|
|
@ -947,9 +1253,9 @@ main() {
|
|||
local action="${1:-help}"
|
||||
shift || true
|
||||
case "$action" in
|
||||
help|-h|--help) usage ;;
|
||||
version|--version) printf '%s %s\n' "$PROGRAM" "$PROGRAM_VERSION" ;;
|
||||
preflight) preflight ;;
|
||||
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 "$@" ;;
|
||||
|
|
@ -961,10 +1267,11 @@ main() {
|
|||
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) show_status ;;
|
||||
uninstall-agent) [[ $# -le 1 ]] || fail "uninstall-agent accepts only [--purge]"; uninstall_agent "${1:-}" ;;
|
||||
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" ;;
|
||||
*) fail "Unknown action: $action (run with --help)" ;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ ProtectControlGroups=true
|
|||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
ReadWritePaths=/var/lib/ochenstarik-server-monitor-manager
|
||||
UMask=0077
|
||||
ReadWritePaths=/var/lib/ochenstarik-server-monitor-manager/control
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
|||
68
src/ServerMonitorManager.Agent/AgentConfiguration.cs
Normal file
68
src/ServerMonitorManager.Agent/AgentConfiguration.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace ServerMonitorManager.Agent;
|
||||
|
||||
internal static class AgentConfiguration
|
||||
{
|
||||
internal const string BindingError =
|
||||
"Agent configuration binding failed; check SMM_ControlUrl and SMM_NodeId.";
|
||||
internal const string ControlUrlError =
|
||||
"Agent ControlUrl must be a secure HTTPS origin.";
|
||||
|
||||
internal static bool TryBind(
|
||||
IConfiguration configuration,
|
||||
string? environmentControlUrl,
|
||||
string? environmentNodeId,
|
||||
out AgentOptions options,
|
||||
out string? error)
|
||||
{
|
||||
try
|
||||
{
|
||||
options = configuration.Get<AgentOptions>() ?? new AgentOptions();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
options = new AgentOptions();
|
||||
error = BindingError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (environmentControlUrl is not null
|
||||
&& (!Uri.TryCreate(environmentControlUrl, UriKind.Absolute, out var expectedControlUrl)
|
||||
|| options.ControlUrl != expectedControlUrl))
|
||||
{
|
||||
error = "Agent configuration binding failed for SMM_ControlUrl.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (environmentNodeId is not null
|
||||
&& !string.Equals(options.NodeId, environmentNodeId, StringComparison.Ordinal))
|
||||
{
|
||||
error = "Agent configuration binding failed for SMM_NodeId.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsSecureControlOrigin(options.ControlUrl))
|
||||
{
|
||||
error = ControlUrlError;
|
||||
return false;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsSecureControlOrigin(Uri? controlUrl)
|
||||
{
|
||||
return controlUrl is not null
|
||||
&& controlUrl.IsAbsoluteUri
|
||||
&& string.Equals(controlUrl.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrWhiteSpace(controlUrl.Host)
|
||||
&& Uri.CheckHostName(controlUrl.IdnHost) != UriHostNameType.Unknown
|
||||
&& string.IsNullOrEmpty(controlUrl.UserInfo)
|
||||
&& string.IsNullOrEmpty(controlUrl.Query)
|
||||
&& string.IsNullOrEmpty(controlUrl.Fragment)
|
||||
&& controlUrl.AbsolutePath == "/"
|
||||
&& controlUrl.Port is >= 1 and <= 65535;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,17 +2,17 @@ namespace ServerMonitorManager.Agent;
|
|||
|
||||
public sealed class AgentOptions
|
||||
{
|
||||
public string NodeId { get; init; } = Environment.MachineName.ToLowerInvariant();
|
||||
public Uri ControlUrl { get; init; } = new("https://127.0.0.1:7443");
|
||||
public string StateDirectory { get; init; } = "/var/lib/ochenstarik-server-monitor-manager/agent";
|
||||
public string CertificateAuthorityPath { get; init; } = "/etc/ochenstarik-server-monitor-manager/control-ca.crt";
|
||||
public string ProvisioningSocketPath { get; init; } = "/run/ochenstarik-server-monitor-manager/provisioning.sock";
|
||||
public string EnrollmentTokenDirectory { get; init; } = "/var/lib/ochenstarik-server-monitor-manager-enrollment";
|
||||
public string? EnrollTokenFile { get; init; }
|
||||
public int HeartbeatSeconds { get; init; } = 30;
|
||||
public int BufferMaxSamples { get; init; } = 720;
|
||||
public int BufferRecentSamples { get; init; } = 120;
|
||||
public int BufferDownsampleFactor { get; init; } = 4;
|
||||
public int UploadBatchSize { get; init; } = 20;
|
||||
public int MaxRetrySeconds { get; init; } = 300;
|
||||
public string NodeId { get; set; } = Environment.MachineName.ToLowerInvariant();
|
||||
public Uri ControlUrl { get; set; } = new("https://127.0.0.1:7443");
|
||||
public string StateDirectory { get; set; } = "/var/lib/ochenstarik-server-monitor-manager/agent";
|
||||
public string CertificateAuthorityPath { get; set; } = "/etc/ochenstarik-server-monitor-manager/control-ca.crt";
|
||||
public string ProvisioningSocketPath { get; set; } = "/run/ochenstarik-server-monitor-manager/provisioning.sock";
|
||||
public string EnrollmentTokenDirectory { get; set; } = "/var/lib/ochenstarik-server-monitor-manager-enrollment";
|
||||
public string? EnrollTokenFile { get; set; }
|
||||
public int HeartbeatSeconds { get; set; } = 30;
|
||||
public int BufferMaxSamples { get; set; } = 720;
|
||||
public int BufferRecentSamples { get; set; } = 120;
|
||||
public int BufferDownsampleFactor { get; set; } = 4;
|
||||
public int UploadBatchSize { get; set; } = 20;
|
||||
public int MaxRetrySeconds { get; set; } = 300;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,18 @@ var configuration = new ConfigurationBuilder()
|
|||
.AddEnvironmentVariables("SMM_")
|
||||
.AddCommandLine(args)
|
||||
.Build();
|
||||
var options = configuration.Get<AgentOptions>() ?? new AgentOptions();
|
||||
var environmentControlUrl = Environment.GetEnvironmentVariable("SMM_ControlUrl");
|
||||
var environmentNodeId = Environment.GetEnvironmentVariable("SMM_NodeId");
|
||||
if (!AgentConfiguration.TryBind(
|
||||
configuration,
|
||||
environmentControlUrl,
|
||||
environmentNodeId,
|
||||
out var options,
|
||||
out var bindingError))
|
||||
{
|
||||
Console.Error.WriteLine(bindingError);
|
||||
return 2;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(options.NodeId)
|
||||
|| !options.NodeId.All(character => character is >= 'a' and <= 'z' or >= '0' and <= '9' or '-'))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
|
||||
<Version>0.1.0</Version>
|
||||
<AssemblyName>ochenstarik-smm-agent</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using ServerMonitorManager.Agent;
|
||||
using Xunit;
|
||||
|
||||
namespace ServerMonitorManager.Control.Tests;
|
||||
|
||||
public sealed class AgentConfigurationTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryBindFailsClosedWhenNodeEnvironmentValueDidNotBind()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder().Build();
|
||||
|
||||
var success = AgentConfiguration.TryBind(
|
||||
configuration,
|
||||
environmentControlUrl: null,
|
||||
environmentNodeId: "expected-node",
|
||||
out _,
|
||||
out var error);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Contains("SMM_NodeId", error, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("expected-node", error, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBindFailsClosedWhenControlUrlEnvironmentValueDidNotBind()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder().Build();
|
||||
|
||||
var success = AgentConfiguration.TryBind(
|
||||
configuration,
|
||||
environmentControlUrl: "https://control.example:7443",
|
||||
environmentNodeId: null,
|
||||
out _,
|
||||
out var error);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Contains("SMM_ControlUrl", error, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("control.example", error, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBindAcceptsMatchingEnvironmentValues()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["NodeId"] = "expected-node",
|
||||
["ControlUrl"] = "https://control.example:7443"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var success = AgentConfiguration.TryBind(
|
||||
configuration,
|
||||
environmentControlUrl: "https://control.example:7443",
|
||||
environmentNodeId: "expected-node",
|
||||
out var options,
|
||||
out var error);
|
||||
|
||||
Assert.True(success, error);
|
||||
Assert.Equal("expected-node", options.NodeId);
|
||||
Assert.Equal(new Uri("https://control.example:7443"), options.ControlUrl);
|
||||
Assert.Null(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBindReturnsNonSecretErrorWhenControlUrlCannotBind()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ControlUrl"] = "not a URL"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var success = AgentConfiguration.TryBind(
|
||||
configuration,
|
||||
environmentControlUrl: "not a URL",
|
||||
environmentNodeId: null,
|
||||
out _,
|
||||
out var error);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Equal("Agent configuration binding failed for SMM_ControlUrl.", error);
|
||||
Assert.DoesNotContain("not a URL", error, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://control.example:7443")]
|
||||
[InlineData("https://user@control.example:7443")]
|
||||
[InlineData("https://control.example:7443/path")]
|
||||
[InlineData("https://control.example:7443?query=1")]
|
||||
[InlineData("https://control.example:7443#fragment")]
|
||||
[InlineData("https://control.example:0")]
|
||||
public void TryBindRejectsUnsafeControlUrlsWithoutDisclosingThem(string controlUrl)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ControlUrl"] = controlUrl
|
||||
})
|
||||
.Build();
|
||||
|
||||
var success = AgentConfiguration.TryBind(
|
||||
configuration,
|
||||
environmentControlUrl: controlUrl,
|
||||
environmentNodeId: null,
|
||||
out _,
|
||||
out var error);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Equal(AgentConfiguration.ControlUrlError, error);
|
||||
Assert.DoesNotContain(controlUrl, error, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://control.example")]
|
||||
[InlineData("https://control.example:7443")]
|
||||
[InlineData("https://127.0.0.1:7443/")]
|
||||
[InlineData("https://[2001:db8::1]:7443")]
|
||||
public void TryBindAcceptsStructurallySafeHttpsControlUrls(string controlUrl)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ControlUrl"] = controlUrl
|
||||
})
|
||||
.Build();
|
||||
|
||||
var success = AgentConfiguration.TryBind(
|
||||
configuration,
|
||||
environmentControlUrl: controlUrl,
|
||||
environmentNodeId: null,
|
||||
out _,
|
||||
out var error);
|
||||
|
||||
Assert.True(success, error);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,15 +5,22 @@ IFS=$'\n\t'
|
|||
archive="${1:?usage: run-native-systemd-smoke.sh ARCHIVE BOOTSTRAP}"
|
||||
bootstrap="${2:?usage: run-native-systemd-smoke.sh ARCHIVE BOOTSTRAP}"
|
||||
port="${SMM_SMOKE_PORT:-17443}"
|
||||
system_bootstrap="/usr/local/sbin/ochenstarik-server-monitor-manager.sh"
|
||||
probe_dir=""
|
||||
|
||||
cleanup() {
|
||||
sudo "$bootstrap" uninstall-control --confirm-destroy-control >/dev/null 2>&1 || true
|
||||
if [[ -n "$probe_dir" ]]; then
|
||||
rm -rf -- "$probe_dir"
|
||||
fi
|
||||
sudo "$system_bootstrap" uninstall-agent --purge >/dev/null 2>&1 || true
|
||||
sudo "$system_bootstrap" uninstall-control --confirm-destroy-control >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
sudo "$bootstrap" preflight
|
||||
sudo "$bootstrap" verify-release "$archive"
|
||||
sudo "$bootstrap" install-control "$archive" 127.0.0.1 "$port"
|
||||
sudo test -x "$system_bootstrap"
|
||||
sudo test -x /usr/local/sbin/ochenstarik-smm-emergency
|
||||
sudo /usr/local/sbin/ochenstarik-smm-emergency status
|
||||
|
||||
|
|
@ -29,9 +36,75 @@ sudo curl --fail --silent --show-error \
|
|||
--cacert /etc/ochenstarik-server-monitor-manager/control-ca.crt \
|
||||
"https://127.0.0.1:$port/healthz"
|
||||
|
||||
sudo "$bootstrap" install-control "$archive" 127.0.0.1 "$port"
|
||||
sudo systemctl restart ochenstarik-smm-control.service
|
||||
probe_dir="$(mktemp -d -t smm-agent-probe.XXXXXXXX)"
|
||||
tar -xzf "$archive" -C "$probe_dir" agent
|
||||
set +e
|
||||
probe_output="$(env SMM_NodeId=INVALID_UPPER \
|
||||
"$probe_dir/agent/ochenstarik-smm-agent" 2>&1)"
|
||||
probe_status=$?
|
||||
set -e
|
||||
[[ "$probe_status" -eq 2 ]] || {
|
||||
printf 'trimmed Agent invalid-environment probe returned %s, expected 2\n' "$probe_status" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -Fq 'NodeId must contain lowercase letters, digits, or hyphens.' <<<"$probe_output"
|
||||
if grep -Fq 'agent.pfx' <<<"$probe_output"; then
|
||||
printf '%s\n' 'trimmed Agent ignored SMM_NodeId and attempted to load agent.pfx' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set +e
|
||||
probe_output="$(env SMM_NodeId=smoke-probe SMM_ControlUrl=http://127.0.0.1:1 \
|
||||
"$probe_dir/agent/ochenstarik-smm-agent" 2>&1)"
|
||||
probe_status=$?
|
||||
set -e
|
||||
[[ "$probe_status" -eq 2 ]] || {
|
||||
printf 'trimmed Agent plaintext-ControlUrl probe returned %s, expected 2\n' "$probe_status" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -Fq 'Agent ControlUrl must be a secure HTTPS origin.' <<<"$probe_output"
|
||||
if grep -Eq 'agent\.pfx|Connection refused|127\.0\.0\.1' <<<"$probe_output"; then
|
||||
printf '%s\n' 'trimmed Agent plaintext-ControlUrl probe reached certificate or network access' >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf -- "$probe_dir"
|
||||
probe_dir=""
|
||||
|
||||
node_code="$(sudo "$system_bootstrap" node-code smoke-node)"
|
||||
[[ "$node_code" == SMMNODE1.* || "$node_code" == SMMNODE2.* ]]
|
||||
export SMM_ENROLL_CODE="$node_code"
|
||||
export SMM_ACCEPT_CA_FINGERPRINT=1
|
||||
sudo --preserve-env=SMM_ENROLL_CODE,SMM_ACCEPT_CA_FINGERPRINT \
|
||||
"$system_bootstrap" install-node "$archive"
|
||||
unset SMM_ENROLL_CODE SMM_ACCEPT_CA_FINGERPRINT
|
||||
node_code=""
|
||||
sudo test -s /var/lib/ochenstarik-server-monitor-manager/agent/agent.pfx
|
||||
sudo systemctl is-active --quiet ochenstarik-smm-agent.service
|
||||
sudo systemctl is-active --quiet ochenstarik-smm-control.service
|
||||
|
||||
[[ "$(sudo stat -c '%a:%U:%G' /var/lib/ochenstarik-server-monitor-manager)" == '711:root:root' ]]
|
||||
[[ "$(sudo stat -c '%a:%U:%G' /var/lib/ochenstarik-server-monitor-manager/control)" \
|
||||
== '700:ochenstarik-smm-control:ochenstarik-smm-control' ]]
|
||||
[[ "$(sudo stat -c '%a:%U:%G' /var/lib/ochenstarik-server-monitor-manager/agent)" \
|
||||
== '700:ochenstarik-smm-agent:ochenstarik-smm-agent' ]]
|
||||
if sudo -u ochenstarik-smm-agent test -r \
|
||||
/var/lib/ochenstarik-server-monitor-manager/control/control.db; then
|
||||
printf '%s\n' 'Agent can read the Control database' >&2
|
||||
exit 1
|
||||
fi
|
||||
if sudo -u ochenstarik-smm-agent id -nG | tr ' ' '\n' | \
|
||||
grep -Fxq ochenstarik-smm-control; then
|
||||
printf '%s\n' 'Agent unexpectedly belongs to the Control group' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
device_code="$(sudo "$system_bootstrap" control-device-code smoke-device)"
|
||||
[[ "$device_code" == SMMDEV1-* ]]
|
||||
device_code=""
|
||||
|
||||
sudo "$system_bootstrap" update-control "$archive"
|
||||
sudo systemctl is-active --quiet ochenstarik-smm-control.service
|
||||
sudo systemctl is-active --quiet ochenstarik-smm-agent.service
|
||||
sudo curl --fail --silent --show-error --retry 15 --retry-all-errors --retry-delay 1 \
|
||||
--cacert /etc/ochenstarik-server-monitor-manager/control-ca.crt \
|
||||
"https://127.0.0.1:$port/healthz"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ if grep -Fq 'SMM_EnrollToken=$ENROLL_TOKEN' "$bootstrap"; then
|
|||
exit 1
|
||||
fi
|
||||
grep -Fq 'readonly ENROLLMENT_DIR="${STATE_DIR}-enrollment"' "$bootstrap"
|
||||
grep -Fq 'install -d -m 0710 -o root -g "$AGENT_USER" "$ENROLLMENT_DIR"' "$bootstrap"
|
||||
grep -Fq 'install -d -m 0700 -o "$AGENT_USER" -g "$AGENT_USER" "$ENROLLMENT_DIR"' "$bootstrap"
|
||||
grep -Fq 'token_temp="$(mktemp "$ENROLLMENT_DIR/.enroll-token.XXXXXXXX")"' "$bootstrap"
|
||||
if grep -Fq '$STATE_DIR/enrollment' "$bootstrap"; then
|
||||
printf '%s\n' "enrollment directory is beneath Control-writable state" >&2
|
||||
|
|
@ -57,9 +57,495 @@ grep -Fq "peer-add SMMPEER1_CODE" <<<"$help_output"
|
|||
grep -Fq "mesh-status" <<<"$help_output"
|
||||
grep -Fq "SMM_ENROLL_TOKEN" <<<"$help_output"
|
||||
grep -Fq "node-code NODE_ID" <<<"$help_output"
|
||||
grep -Fq "control-device-code DEVICE_ID" <<<"$help_output"
|
||||
grep -Fq "verify-release ARCHIVE" <<<"$help_output"
|
||||
grep -Fq "node-token NODE_ID" <<<"$help_output"
|
||||
grep -Eq '^ochenstarik-server-monitor-manager [0-9]+\.[0-9]+\.[0-9]+-' <<<"$version_output"
|
||||
|
||||
extract_bootstrap_function() {
|
||||
local name="$1"
|
||||
awk -v signature="$name() {" '
|
||||
$0 == signature { emitting = 1 }
|
||||
emitting { print }
|
||||
emitting && $0 == "}" { exit }
|
||||
' "$bootstrap"
|
||||
}
|
||||
validate_port_definition="$(extract_bootstrap_function validate_port)"
|
||||
validate_ipv4_literal_definition="$(extract_bootstrap_function validate_ipv4_literal)"
|
||||
validate_control_url_definition="$(extract_bootstrap_function validate_control_url)"
|
||||
for accepted_url in \
|
||||
https://example.com \
|
||||
https://host.example:7443 \
|
||||
https://10.0.0.1:7443 \
|
||||
'https://[2001:db8::1]:7443' \
|
||||
'https://[::1]:7443' \
|
||||
'https://[2001:db8::]' \
|
||||
'https://[::]' \
|
||||
'https://[::ffff:192.0.2.128]' \
|
||||
'https://[2001:db8:3:4::192.0.2.33]:7443' \
|
||||
'https://[1:2:3:4:5:6:192.0.2.1]' \
|
||||
https://example.com/; do
|
||||
if ! (fail() { exit 1; }; source <(printf '%s\n%s\n%s\n' "$validate_port_definition" "$validate_ipv4_literal_definition" "$validate_control_url_definition"); validate_control_url "$accepted_url"); then
|
||||
printf 'valid Control URL was rejected: %s\n' "$accepted_url" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
for rejected_url in \
|
||||
http://example.com \
|
||||
https:// \
|
||||
https://:7443 \
|
||||
'https://[2001:db8::1' \
|
||||
'https://[::::]' \
|
||||
'https://[1:2:3]' \
|
||||
'https://[::ffff:192.0.2.999]' \
|
||||
'https://[::ffff:192.0.2]' \
|
||||
'https://[::ffff:192.0.2.1.5]' \
|
||||
'https://[::ffff:192.0.2.x]' \
|
||||
'https://[1:2:3:4:5:6:7:192.0.2.1]' \
|
||||
'https://[::ffff:192.0.2.1:]' \
|
||||
'https://[1:2:3:4:5:6:192.0.2.1:]' \
|
||||
'https://[1:2:3:4:5:6:7:8:]' \
|
||||
'https://[:1:2:3:4:5:6:7]' \
|
||||
'https://[::ffff:18446744073709551617.0.0.1]' \
|
||||
'https://[::1]:18446744073709551696' \
|
||||
'https://2001:db8::1:7443' \
|
||||
'https://example.com:7443:7444' \
|
||||
https://example..com \
|
||||
https://999.0.0.1 \
|
||||
https://18446744073709551617.0.0.1 \
|
||||
https://example.com/path \
|
||||
https://user@example.com \
|
||||
'https://example.com:0' \
|
||||
'https://example.com:65536' \
|
||||
'https://example.com:18446744073709551696' \
|
||||
'https://example.com?query=1' \
|
||||
'https://example.com#fragment'; do
|
||||
if (fail() { exit 1; }; source <(printf '%s\n%s\n%s\n' "$validate_port_definition" "$validate_ipv4_literal_definition" "$validate_control_url_definition"); validate_control_url "$rejected_url"); then
|
||||
printf 'invalid Control URL was accepted: %s\n' "$rejected_url" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
for action in status version --version preflight help -h --help; do
|
||||
if bash "$bootstrap" "$action" surplus >/dev/null 2>&1; then
|
||||
printf 'bootstrap action accepted surplus arguments: %s\n' "$action" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if ! bash "$bootstrap" >/dev/null 2>&1; then
|
||||
printf '%s\n' 'bootstrap no-argument help form failed' >&2
|
||||
exit 1
|
||||
fi
|
||||
for action in verify-release install-control install-agent install-node mesh-init peer-add \
|
||||
update-control update-agent rollback node-code control-device-code node-token uninstall-control; do
|
||||
if bash "$bootstrap" "$action" >/dev/null 2>&1; then
|
||||
printf 'bootstrap action accepted missing arguments: %s\n' "$action" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
base64url_encode_definition="$(extract_bootstrap_function base64url_encode)"
|
||||
create_device_code_definition="$(extract_bootstrap_function create_device_code)"
|
||||
device_fixture="$(mktemp -d -t smm-device-code.XXXXXXXX)"
|
||||
printf '%s\n' 'https://control.example:7443' >"$device_fixture/control-public-url"
|
||||
printf '%s\n' 'fixture-ca' >"$device_fixture/control-ca.crt"
|
||||
device_code="$(
|
||||
ETC_DIR="$device_fixture"
|
||||
require_root() { :; }
|
||||
require_command() { :; }
|
||||
validate_node_id() { [[ "$1" == 'desktop-device' ]]; }
|
||||
validate_control_url() { [[ "$1" == 'https://control.example:7443' ]]; }
|
||||
run_control_cli() {
|
||||
[[ "$1" == 'device-token-create' && "$2" == 'desktop-device' ]]
|
||||
printf '%043d\n' 0
|
||||
}
|
||||
openssl() { printf '%s' 'DER-fixture'; }
|
||||
source <(printf '%s\n%s\n' "$base64url_encode_definition" "$create_device_code_definition")
|
||||
create_device_code desktop-device
|
||||
)"
|
||||
[[ "$device_code" == SMMDEV1-* ]]
|
||||
device_payload_encoded="${device_code#SMMDEV1-}"
|
||||
device_payload_encoded="${device_payload_encoded//-/+}"
|
||||
device_payload_encoded="${device_payload_encoded//_/\/}"
|
||||
case $(( ${#device_payload_encoded} % 4 )) in
|
||||
0) ;;
|
||||
2) device_payload_encoded+='==' ;;
|
||||
3) device_payload_encoded+='=' ;;
|
||||
*) printf '%s\n' 'invalid generated SMMDEV1 base64url length' >&2; exit 1 ;;
|
||||
esac
|
||||
device_payload="$(printf '%s' "$device_payload_encoded" | base64 -d)"
|
||||
grep -Fxq 'VERSION=1' <<<"$device_payload"
|
||||
grep -Fxq 'DEVICE=desktop-device' <<<"$device_payload"
|
||||
grep -Fxq 'TOKEN=0000000000000000000000000000000000000000000' <<<"$device_payload"
|
||||
grep -Fxq 'URL=https://control.example:7443' <<<"$device_payload"
|
||||
grep -Fxq "CA=$(printf '%s' 'DER-fixture' | base64 -w 0)" <<<"$device_payload"
|
||||
rm -rf -- "$device_fixture"
|
||||
|
||||
grep -Fq 'readonly BOOTSTRAP_COMMAND="/usr/local/sbin/ochenstarik-server-monitor-manager.sh"' "$bootstrap"
|
||||
grep -Fq 'staging="$(mktemp "$(dirname "$BOOTSTRAP_COMMAND")/.ochenstarik-server-monitor-manager.XXXXXXXX")"' "$bootstrap"
|
||||
grep -Fq 'mv -fT -- "$staging" "$BOOTSTRAP_COMMAND"' "$bootstrap"
|
||||
[[ "$(grep -Fc ' install_bootstrap_command' "$bootstrap")" -eq 3 ]]
|
||||
update_role_definition="$(extract_bootstrap_function update_role)"
|
||||
validation_line="$(grep -n -m1 'validate_control_state_migration' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
environment_validation_line="$(grep -n -m1 'validate_control_environment_migration' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
control_stop_line="$(grep -n -m1 'systemctl stop \"\$unit\"' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
(( validation_line < control_stop_line && environment_validation_line < control_stop_line ))
|
||||
guard_arm_line="$(grep -n -m1 'CONTROL_UPDATE_RECOVERY_REQUIRED=1' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
guard_clear_line="$(grep -n -m1 'CONTROL_UPDATE_RECOVERY_REQUIRED=0' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
prepare_line="$(grep -n -m1 'prepare_control_state' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
rewrite_line="$(grep -n -m1 'rewrite_control_environment' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
bootstrap_line="$(grep -n -m1 'install_bootstrap_command' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
unit_line="$(grep -n -m1 'install_unit \"\$TEMP_DIR/deploy/\$CONTROL_UNIT\"' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
enable_line="$(grep -n -m1 'systemctl enable \"\$CONTROL_UNIT\"' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
binary_line="$(grep -n -m1 'install_tree_atomic \"\$TEMP_DIR/control\"' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
restart_line="$(grep -n -m1 'systemctl restart \"\$unit\"' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
active_line="$(grep -n -m1 'systemctl is-active --quiet \"\$unit\"' <<<"$update_role_definition" | cut -d: -f1)"
|
||||
for guarded_line in "$control_stop_line" "$prepare_line" "$rewrite_line" "$bootstrap_line" \
|
||||
"$unit_line" "$enable_line" "$binary_line" "$restart_line" "$active_line"; do
|
||||
(( guard_arm_line < guarded_line && guarded_line < guard_clear_line ))
|
||||
done
|
||||
grep -Fq 'install_bootstrap_command' <<<"$update_role_definition"
|
||||
grep -Fq 'install_unit "$TEMP_DIR/deploy/$CONTROL_UNIT" "$CONTROL_UNIT"' <<<"$update_role_definition"
|
||||
grep -Fq 'install_tree_atomic "$TEMP_DIR/control" "$LIB_DIR/control" "$user"' <<<"$update_role_definition"
|
||||
grep -Fq 'systemctl restart "$unit"' <<<"$update_role_definition"
|
||||
grep -Fq 'systemctl is-active --quiet "$unit"' <<<"$update_role_definition"
|
||||
grep -Fq 'install -d -m 0711 -o root -g root "$ETC_DIR" "$STATE_DIR"' "$bootstrap"
|
||||
grep -Fq 'install -d -m 0700 -o "$CONTROL_USER" -g "$CONTROL_USER" "$STATE_DIR/control"' "$bootstrap"
|
||||
grep -Fq 'Control__DatabasePath=$STATE_DIR/control/control.db' "$bootstrap"
|
||||
grep -Fq 'Control__BackupDirectory=$STATE_DIR/control/backups' "$bootstrap"
|
||||
validate_control_state_migration_definition="$(extract_bootstrap_function validate_control_state_migration)"
|
||||
prepare_control_state_definition="$(extract_bootstrap_function prepare_control_state)"
|
||||
control_state_fixture="$(mktemp -d -t smm-control-state.XXXXXXXX)"
|
||||
mkdir -p "$control_state_fixture/backups"
|
||||
printf '%s' database >"$control_state_fixture/control.db"
|
||||
printf '%s' wal >"$control_state_fixture/control.db-wal"
|
||||
printf '%s' backup >"$control_state_fixture/backups/manifest.json"
|
||||
(
|
||||
STATE_DIR="$control_state_fixture"
|
||||
CONTROL_USER=fixture
|
||||
fail() { printf '%s\n' "$*" >&2; exit 1; }
|
||||
install() {
|
||||
local arguments=()
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
-m|-o|-g) shift 2 ;;
|
||||
*) arguments+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
command install "${arguments[@]}"
|
||||
}
|
||||
chown() { :; }
|
||||
source <(printf '%s\n%s\n' "$validate_control_state_migration_definition" "$prepare_control_state_definition")
|
||||
validate_control_state_migration
|
||||
prepare_control_state
|
||||
)
|
||||
[[ ! -e "$control_state_fixture/control.db" ]]
|
||||
[[ "$(<"$control_state_fixture/control/control.db")" == database ]]
|
||||
[[ "$(<"$control_state_fixture/control/control.db-wal")" == wal ]]
|
||||
[[ "$(<"$control_state_fixture/control/backups/manifest.json")" == backup ]]
|
||||
if [[ "$(uname -s)" != MINGW* ]]; then
|
||||
[[ "$(stat -c '%a' "$control_state_fixture/control")" == 700 ]]
|
||||
[[ "$(stat -c '%a' "$control_state_fixture/control/control.db")" == 600 ]]
|
||||
fi
|
||||
printf '%s' conflict >"$control_state_fixture/control.db"
|
||||
if (
|
||||
STATE_DIR="$control_state_fixture"
|
||||
fail() { exit 1; }
|
||||
source <(printf '%s\n' "$validate_control_state_migration_definition")
|
||||
validate_control_state_migration
|
||||
); then
|
||||
printf '%s\n' 'conflicting legacy and role-isolated Control state was accepted' >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf -- "$control_state_fixture"
|
||||
|
||||
validate_control_environment_migration_definition="$(extract_bootstrap_function validate_control_environment_migration)"
|
||||
rewrite_control_environment_definition="$(extract_bootstrap_function rewrite_control_environment)"
|
||||
alpha7_fixture="$(mktemp -d -t smm-alpha7-update.XXXXXXXX)"
|
||||
mkdir -p "$alpha7_fixture/state/backups" "$alpha7_fixture/etc"
|
||||
printf '%s' alpha7-db >"$alpha7_fixture/state/control.db"
|
||||
printf '%s' alpha7-wal >"$alpha7_fixture/state/control.db-wal"
|
||||
printf '%s' alpha7-shm >"$alpha7_fixture/state/control.db-shm"
|
||||
printf '%s' alpha7-backup >"$alpha7_fixture/state/backups/manifest.json"
|
||||
cat >"$alpha7_fixture/etc/control.env" <<EOF
|
||||
# alpha.7 fixture: preserve comments and every unrelated value
|
||||
ASPNETCORE_URLS=https://0.0.0.0:7443
|
||||
Control__DatabasePath=$alpha7_fixture/state/control.db
|
||||
Control__CertificateAuthorityPath=/custom/control-ca.pfx
|
||||
Control__BackupDirectory=$alpha7_fixture/state/backups
|
||||
Control__LinkReconciliationSeconds=777
|
||||
CUSTOM_VALUE=spaces are preserved exactly
|
||||
EOF
|
||||
cp "$alpha7_fixture/etc/control.env" "$alpha7_fixture/original.env"
|
||||
(
|
||||
STATE_DIR="$alpha7_fixture/state"
|
||||
ETC_DIR="$alpha7_fixture/etc"
|
||||
CONTROL_USER=fixture
|
||||
fail() { printf '%s\n' "$*" >&2; exit 1; }
|
||||
install() {
|
||||
local arguments=()
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in -m|-o|-g) shift 2 ;; *) arguments+=("$1"); shift ;; esac
|
||||
done
|
||||
command install "${arguments[@]}"
|
||||
}
|
||||
chown() { :; }
|
||||
source <(printf '%s\n%s\n%s\n%s\n' \
|
||||
"$validate_control_state_migration_definition" \
|
||||
"$prepare_control_state_definition" \
|
||||
"$validate_control_environment_migration_definition" \
|
||||
"$rewrite_control_environment_definition")
|
||||
validate_control_state_migration
|
||||
validate_control_environment_migration
|
||||
prepare_control_state
|
||||
rewrite_control_environment
|
||||
)
|
||||
[[ "$(<"$alpha7_fixture/state/control/control.db")" == alpha7-db ]]
|
||||
[[ "$(<"$alpha7_fixture/state/control/control.db-wal")" == alpha7-wal ]]
|
||||
[[ "$(<"$alpha7_fixture/state/control/control.db-shm")" == alpha7-shm ]]
|
||||
[[ "$(<"$alpha7_fixture/state/control/backups/manifest.json")" == alpha7-backup ]]
|
||||
expected_env="$(sed \
|
||||
-e "s|^Control__DatabasePath=.*|Control__DatabasePath=$alpha7_fixture/state/control/control.db|" \
|
||||
-e "s|^Control__BackupDirectory=.*|Control__BackupDirectory=$alpha7_fixture/state/control/backups|" \
|
||||
"$alpha7_fixture/original.env")"
|
||||
[[ "$(<"$alpha7_fixture/etc/control.env")" == "$expected_env" ]]
|
||||
printf '%s\n' "Control__DatabasePath=$alpha7_fixture/state/control/control.db" \
|
||||
>>"$alpha7_fixture/etc/control.env"
|
||||
if (
|
||||
ETC_DIR="$alpha7_fixture/etc"
|
||||
fail() { exit 1; }
|
||||
source <(printf '%s\n' "$validate_control_environment_migration_definition")
|
||||
validate_control_environment_migration
|
||||
); then
|
||||
printf '%s\n' 'conflicting Control environment paths were accepted' >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf -- "$alpha7_fixture"
|
||||
|
||||
record_control_legacy_state_definition="$(extract_bootstrap_function record_control_legacy_state)"
|
||||
reverse_control_state_migration_definition="$(extract_bootstrap_function reverse_control_state_migration)"
|
||||
restore_control_update_backup_definition="$(extract_bootstrap_function restore_control_update_backup)"
|
||||
restore_control_binary_from_archive_definition="$(extract_bootstrap_function restore_control_binary_from_archive)"
|
||||
recover_control_update_definition="$(extract_bootstrap_function recover_control_update)"
|
||||
[[ -n "$record_control_legacy_state_definition" ]]
|
||||
[[ -n "$reverse_control_state_migration_definition" ]]
|
||||
[[ -n "$restore_control_update_backup_definition" ]]
|
||||
[[ -n "$restore_control_binary_from_archive_definition" ]]
|
||||
[[ -n "$recover_control_update_definition" ]]
|
||||
|
||||
recovery_fixture="$(mktemp -d -t smm-control-recovery.XXXXXXXX)"
|
||||
recovery_root="$recovery_fixture/root"
|
||||
archive_root="$recovery_fixture/archive-root"
|
||||
mkdir -p \
|
||||
"$recovery_root/var/lib/ochenstarik-server-monitor-manager/backups" \
|
||||
"$recovery_root/etc/ochenstarik-server-monitor-manager" \
|
||||
"$recovery_root/etc/systemd/system" \
|
||||
"$recovery_root/usr/local/lib/ochenstarik-server-monitor-manager/control" \
|
||||
"$archive_root/etc/ochenstarik-server-monitor-manager" \
|
||||
"$archive_root/etc/systemd/system" \
|
||||
"$archive_root/usr/local/lib/ochenstarik-server-monitor-manager/control"
|
||||
printf '%s' original-db >"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control.db"
|
||||
printf '%s' original-wal >"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control.db-wal"
|
||||
printf '%s' original-shm >"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control.db-shm"
|
||||
printf '%s' original-backup >"$recovery_root/var/lib/ochenstarik-server-monitor-manager/backups/identity"
|
||||
printf '%s' old-binary >"$archive_root/usr/local/lib/ochenstarik-server-monitor-manager/control/ochenstarik-smm-control"
|
||||
printf '%s\n' 'Control__DatabasePath=/var/lib/ochenstarik-server-monitor-manager/control.db' \
|
||||
>"$archive_root/etc/ochenstarik-server-monitor-manager/control.env"
|
||||
printf '%s' old-unit >"$archive_root/etc/systemd/system/ochenstarik-smm-control.service"
|
||||
mkdir -p "$recovery_fixture/bootstrap-backups"
|
||||
tar -C "$archive_root" -czf "$recovery_fixture/bootstrap-backups/alpha7.tar.gz" \
|
||||
usr/local/lib/ochenstarik-server-monitor-manager/control \
|
||||
etc/ochenstarik-server-monitor-manager/control.env \
|
||||
etc/systemd/system/ochenstarik-smm-control.service
|
||||
|
||||
(
|
||||
STATE_DIR="$recovery_root/var/lib/ochenstarik-server-monitor-manager"
|
||||
CONTROL_USER=fixture
|
||||
CONTROL_UPDATE_LEGACY_ITEMS=()
|
||||
fail() { printf '%s\n' "$*" >&2; exit 1; }
|
||||
install() {
|
||||
local arguments=()
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in -m|-o|-g) shift 2 ;; *) arguments+=("$1"); shift ;; esac
|
||||
done
|
||||
command install "${arguments[@]}"
|
||||
}
|
||||
chown() { :; }
|
||||
source <(printf '%s\n%s\n%s\n' \
|
||||
"$record_control_legacy_state_definition" \
|
||||
"$prepare_control_state_definition" \
|
||||
"$reverse_control_state_migration_definition")
|
||||
record_control_legacy_state
|
||||
prepare_control_state
|
||||
[[ "$(<"$STATE_DIR/control/control.db")" == original-db ]]
|
||||
reverse_control_state_migration
|
||||
[[ "$(<"$STATE_DIR/control.db")" == original-db ]]
|
||||
[[ "$(<"$STATE_DIR/control.db-wal")" == original-wal ]]
|
||||
[[ "$(<"$STATE_DIR/control.db-shm")" == original-shm ]]
|
||||
[[ "$(<"$STATE_DIR/backups/identity")" == original-backup ]]
|
||||
[[ ! -e "$STATE_DIR/control/control.db" ]]
|
||||
)
|
||||
|
||||
printf '%s' new-binary >"$recovery_root/usr/local/lib/ochenstarik-server-monitor-manager/control/ochenstarik-smm-control"
|
||||
printf '%s\n' 'Control__DatabasePath=/var/lib/ochenstarik-server-monitor-manager/control/control.db' \
|
||||
>"$recovery_root/etc/ochenstarik-server-monitor-manager/control.env"
|
||||
printf '%s' new-unit >"$recovery_root/etc/systemd/system/ochenstarik-smm-control.service"
|
||||
(
|
||||
STATE_DIR="$recovery_root/var/lib/ochenstarik-server-monitor-manager"
|
||||
BACKUP_DIR="$recovery_fixture/bootstrap-backups"
|
||||
CONTROL_UNIT=ochenstarik-smm-control.service
|
||||
CONTROL_UPDATE_LEGACY_ITEMS=()
|
||||
systemctl() { :; }
|
||||
fail() { printf '%s\n' "$*" >&2; exit 1; }
|
||||
source <(printf '%s\n%s\n%s\n%s\n' \
|
||||
"$reverse_control_state_migration_definition" \
|
||||
"$restore_control_update_backup_definition" \
|
||||
"$restore_control_binary_from_archive_definition" \
|
||||
"$recover_control_update_definition")
|
||||
restore_control_binary_from_archive "$BACKUP_DIR/alpha7.tar.gz" "$recovery_root"
|
||||
[[ "$(<"$recovery_root/usr/local/lib/ochenstarik-server-monitor-manager/control/ochenstarik-smm-control")" == old-binary ]]
|
||||
[[ "$(<"$recovery_root/etc/ochenstarik-server-monitor-manager/control.env")" == \
|
||||
'Control__DatabasePath=/var/lib/ochenstarik-server-monitor-manager/control/control.db' ]]
|
||||
)
|
||||
|
||||
rm -rf -- "$recovery_root/var/lib/ochenstarik-server-monitor-manager/control"
|
||||
mkdir -p "$recovery_root/var/lib/ochenstarik-server-monitor-manager/control/backups"
|
||||
mv "$recovery_root/var/lib/ochenstarik-server-monitor-manager/control.db" \
|
||||
"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control/control.db"
|
||||
mv "$recovery_root/var/lib/ochenstarik-server-monitor-manager/control.db-wal" \
|
||||
"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control/control.db-wal"
|
||||
mv "$recovery_root/var/lib/ochenstarik-server-monitor-manager/control.db-shm" \
|
||||
"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control/control.db-shm"
|
||||
mv "$recovery_root/var/lib/ochenstarik-server-monitor-manager/backups/identity" \
|
||||
"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control/backups/identity"
|
||||
rmdir "$recovery_root/var/lib/ochenstarik-server-monitor-manager/backups"
|
||||
printf '%s' new-binary >"$recovery_root/usr/local/lib/ochenstarik-server-monitor-manager/control/ochenstarik-smm-control"
|
||||
printf '%s\n' 'Control__DatabasePath=/var/lib/ochenstarik-server-monitor-manager/control/control.db' \
|
||||
>"$recovery_root/etc/ochenstarik-server-monitor-manager/control.env"
|
||||
printf '%s' new-unit >"$recovery_root/etc/systemd/system/ochenstarik-smm-control.service"
|
||||
(
|
||||
STATE_DIR="$recovery_root/var/lib/ochenstarik-server-monitor-manager"
|
||||
BACKUP_DIR="$recovery_fixture/bootstrap-backups"
|
||||
CONTROL_UNIT=ochenstarik-smm-control.service
|
||||
CONTROL_UPDATE_LEGACY_ITEMS=(control.db control.db-wal control.db-shm backups)
|
||||
systemctl() { :; }
|
||||
fail() { printf '%s\n' "$*" >&2; exit 1; }
|
||||
source <(printf '%s\n%s\n%s\n' \
|
||||
"$reverse_control_state_migration_definition" \
|
||||
"$restore_control_update_backup_definition" \
|
||||
"$recover_control_update_definition")
|
||||
recover_control_update alpha7 "$recovery_root"
|
||||
)
|
||||
[[ "$(<"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control.db")" == original-db ]]
|
||||
[[ "$(<"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control.db-wal")" == original-wal ]]
|
||||
[[ "$(<"$recovery_root/var/lib/ochenstarik-server-monitor-manager/control.db-shm")" == original-shm ]]
|
||||
[[ "$(<"$recovery_root/var/lib/ochenstarik-server-monitor-manager/backups/identity")" == original-backup ]]
|
||||
[[ "$(<"$recovery_root/usr/local/lib/ochenstarik-server-monitor-manager/control/ochenstarik-smm-control")" == old-binary ]]
|
||||
[[ "$(<"$recovery_root/etc/ochenstarik-server-monitor-manager/control.env")" == \
|
||||
'Control__DatabasePath=/var/lib/ochenstarik-server-monitor-manager/control.db' ]]
|
||||
[[ "$(<"$recovery_root/etc/systemd/system/ochenstarik-smm-control.service")" == old-unit ]]
|
||||
rm -rf -- "$recovery_fixture"
|
||||
|
||||
cleanup_definition="$(extract_bootstrap_function cleanup)"
|
||||
[[ -n "$cleanup_definition" ]]
|
||||
for failure_step in stop state environment bootstrap unit enable binary restart active; do
|
||||
guard_fixture="$(mktemp -d -t smm-control-guard.XXXXXXXX)"
|
||||
marker="$guard_fixture.recovered"
|
||||
set +e
|
||||
(
|
||||
set -Eeuo pipefail
|
||||
PROGRAM=test-bootstrap
|
||||
TEMP_DIR="$guard_fixture"
|
||||
ENROLLMENT_TOKEN_FILE=""
|
||||
ENROLLMENT_TOKEN_TEMP=""
|
||||
CONTROL_UPDATE_BACKUP_ID=fixture-backup
|
||||
CONTROL_UPDATE_RECOVERY_REQUIRED=1
|
||||
marker="$marker"
|
||||
failure_step="$failure_step"
|
||||
log() { :; }
|
||||
recover_control_update() { printf '%s' "$failure_step" >"$marker"; }
|
||||
source <(printf '%s\n' "$cleanup_definition")
|
||||
trap cleanup EXIT
|
||||
false
|
||||
) >/dev/null 2>&1
|
||||
guard_status=$?
|
||||
set -e
|
||||
(( guard_status != 0 )) || {
|
||||
printf 'injected Control update failure was ignored: %s\n' "$failure_step" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -f "$marker" && "$(<"$marker")" == "$failure_step" ]] || {
|
||||
printf 'Control recovery guard was not invoked for: %s\n' "$failure_step" >&2
|
||||
exit 1
|
||||
}
|
||||
rm -f -- "$marker"
|
||||
done
|
||||
|
||||
role_is_installed_definition="$(extract_bootstrap_function role_is_installed)"
|
||||
remove_shared_ca_if_unused_definition="$(extract_bootstrap_function remove_shared_ca_if_unused)"
|
||||
uninstall_agent_definition="$(extract_bootstrap_function uninstall_agent)"
|
||||
uninstall_control_definition="$(extract_bootstrap_function uninstall_control)"
|
||||
grep -Fq 'remove_shared_ca_if_unused' <<<"$uninstall_agent_definition"
|
||||
grep -Fq 'remove_shared_ca_if_unused' <<<"$uninstall_control_definition"
|
||||
if grep -Fq 'control-ca.crt' <<<"$uninstall_agent_definition$uninstall_control_definition"; then
|
||||
printf '%s\n' 'a role uninstaller deletes the shared CA directly' >&2
|
||||
exit 1
|
||||
fi
|
||||
role_fixture="$(mktemp -d -t smm-role-uninstall.XXXXXXXX)"
|
||||
mkdir -p "$role_fixture/etc" "$role_fixture/lib/control" "$role_fixture/lib/agent"
|
||||
printf '%s' ca >"$role_fixture/etc/control-ca.crt"
|
||||
(
|
||||
ETC_DIR="$role_fixture/etc"; LIB_DIR="$role_fixture/lib"
|
||||
source <(printf '%s\n%s\n' "$role_is_installed_definition" "$remove_shared_ca_if_unused_definition")
|
||||
rm -rf "$LIB_DIR/agent"
|
||||
remove_shared_ca_if_unused
|
||||
)
|
||||
[[ -f "$role_fixture/etc/control-ca.crt" ]]
|
||||
rm -rf "$role_fixture/lib/control"
|
||||
(
|
||||
ETC_DIR="$role_fixture/etc"; LIB_DIR="$role_fixture/lib"
|
||||
source <(printf '%s\n%s\n' "$role_is_installed_definition" "$remove_shared_ca_if_unused_definition")
|
||||
remove_shared_ca_if_unused
|
||||
)
|
||||
[[ ! -e "$role_fixture/etc/control-ca.crt" ]]
|
||||
mkdir -p "$role_fixture/lib/control" "$role_fixture/lib/agent"
|
||||
printf '%s' ca >"$role_fixture/etc/control-ca.crt"
|
||||
(
|
||||
ETC_DIR="$role_fixture/etc"; LIB_DIR="$role_fixture/lib"
|
||||
source <(printf '%s\n%s\n' "$role_is_installed_definition" "$remove_shared_ca_if_unused_definition")
|
||||
rm -rf "$LIB_DIR/control"
|
||||
remove_shared_ca_if_unused
|
||||
)
|
||||
[[ -f "$role_fixture/etc/control-ca.crt" ]]
|
||||
rm -rf "$role_fixture/lib/agent"
|
||||
(
|
||||
ETC_DIR="$role_fixture/etc"; LIB_DIR="$role_fixture/lib"
|
||||
source <(printf '%s\n%s\n' "$role_is_installed_definition" "$remove_shared_ca_if_unused_definition")
|
||||
remove_shared_ca_if_unused
|
||||
)
|
||||
[[ ! -e "$role_fixture/etc/control-ca.crt" ]]
|
||||
rm -rf -- "$role_fixture"
|
||||
|
||||
grep -Fq 'UMask=0077' "$root/deploy/ochenstarik-smm-control.service"
|
||||
grep -Fq 'ReadWritePaths=/var/lib/ochenstarik-server-monitor-manager/control' "$root/deploy/ochenstarik-smm-control.service"
|
||||
native_smoke="$root/tests/bootstrap/run-native-systemd-smoke.sh"
|
||||
grep -Fq 'node_code="$(sudo "$system_bootstrap" node-code smoke-node)"' "$native_smoke"
|
||||
grep -Fq 'export SMM_ENROLL_CODE="$node_code"' "$native_smoke"
|
||||
grep -Fq 'export SMM_ACCEPT_CA_FINGERPRINT=1' "$native_smoke"
|
||||
grep -Fq 'sudo test -s /var/lib/ochenstarik-server-monitor-manager/agent/agent.pfx' "$native_smoke"
|
||||
grep -Fq 'sudo systemctl is-active --quiet ochenstarik-smm-agent.service' "$native_smoke"
|
||||
grep -Fq 'device_code="$(sudo "$system_bootstrap" control-device-code smoke-device)"' "$native_smoke"
|
||||
grep -Fq '[[ "$device_code" == SMMDEV1-* ]]' "$native_smoke"
|
||||
grep -Fq 'sudo --preserve-env=SMM_ENROLL_CODE,SMM_ACCEPT_CA_FINGERPRINT' "$native_smoke"
|
||||
if grep -Fq 'sudo env SMM_ENROLL_CODE=' "$native_smoke"; then
|
||||
printf '%s\n' 'native smoke exposes the enrollment code through env argv' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -Fq 'rm -rf -- "$LIB_DIR/control" "$STATE_DIR/control"' "$bootstrap"
|
||||
if grep -Fq 'install -d -m 0750 -o root -g "$AGENT_USER" "$ETC_DIR"' "$bootstrap"; then
|
||||
printf '%s\n' 'Agent installation still takes ownership of the shared configuration parent' >&2
|
||||
exit 1
|
||||
fi
|
||||
emergency_help="$(bash "$emergency" --help)"
|
||||
grep -Fq 'mesh-disable' <<<"$emergency_help"
|
||||
grep -Fq 'firewall-restore' <<<"$emergency_help"
|
||||
|
|
|
|||
Loading…
Reference in a new issue