diff --git a/.github/workflows/linux-control-agent.yml b/.github/workflows/linux-control-agent.yml index 41765b5..6df62d2 100644 --- a/.github/workflows/linux-control-agent.yml +++ b/.github/workflows/linux-control-agent.yml @@ -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' >"$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 diff --git a/deploy/ochenstarik-server-monitor-manager.sh b/deploy/ochenstarik-server-monitor-manager.sh index 0d87103..db3e7b1 100755 --- a/deploy/ochenstarik-server-monitor-manager.sh +++ b/deploy/ochenstarik-server-monitor-manager.sh @@ -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" </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 diff --git a/deploy/ochenstarik-smm-control.service b/deploy/ochenstarik-smm-control.service index cd18b5a..82937fb 100644 --- a/deploy/ochenstarik-smm-control.service +++ b/deploy/ochenstarik-smm-control.service @@ -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 diff --git a/src/ServerMonitorManager.Agent/AgentConfiguration.cs b/src/ServerMonitorManager.Agent/AgentConfiguration.cs new file mode 100644 index 0000000..0566fa9 --- /dev/null +++ b/src/ServerMonitorManager.Agent/AgentConfiguration.cs @@ -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() ?? 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; + } +} diff --git a/src/ServerMonitorManager.Agent/AgentOptions.cs b/src/ServerMonitorManager.Agent/AgentOptions.cs index 509e848..4d215c7 100644 --- a/src/ServerMonitorManager.Agent/AgentOptions.cs +++ b/src/ServerMonitorManager.Agent/AgentOptions.cs @@ -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; } diff --git a/src/ServerMonitorManager.Agent/Program.cs b/src/ServerMonitorManager.Agent/Program.cs index 7bc09c6..46416f9 100644 --- a/src/ServerMonitorManager.Agent/Program.cs +++ b/src/ServerMonitorManager.Agent/Program.cs @@ -6,7 +6,18 @@ var configuration = new ConfigurationBuilder() .AddEnvironmentVariables("SMM_") .AddCommandLine(args) .Build(); -var options = configuration.Get() ?? 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 '-')) { diff --git a/src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj b/src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj index a769654..9d32ca8 100644 --- a/src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj +++ b/src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj @@ -5,6 +5,7 @@ enable enable true + true 0.1.0 ochenstarik-smm-agent diff --git a/tests/ServerMonitorManager.Control.Tests/AgentConfigurationTests.cs b/tests/ServerMonitorManager.Control.Tests/AgentConfigurationTests.cs new file mode 100644 index 0000000..fe43857 --- /dev/null +++ b/tests/ServerMonitorManager.Control.Tests/AgentConfigurationTests.cs @@ -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 + { + ["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 + { + ["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 + { + ["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 + { + ["ControlUrl"] = controlUrl + }) + .Build(); + + var success = AgentConfiguration.TryBind( + configuration, + environmentControlUrl: controlUrl, + environmentNodeId: null, + out _, + out var error); + + Assert.True(success, error); + } +} diff --git a/tests/bootstrap/run-native-systemd-smoke.sh b/tests/bootstrap/run-native-systemd-smoke.sh index 8789577..605a254 100755 --- a/tests/bootstrap/run-native-systemd-smoke.sh +++ b/tests/bootstrap/run-native-systemd-smoke.sh @@ -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" diff --git a/tests/bootstrap/test-bootstrap-contract.sh b/tests/bootstrap/test-bootstrap-contract.sh index 6d0c8fc..bc50333 100755 --- a/tests/bootstrap/test-bootstrap-contract.sh +++ b/tests/bootstrap/test-bootstrap-contract.sh @@ -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" <&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"