#!/usr/bin/env bash PATH=/usr/sbin:/usr/bin:/sbin:/bin export PATH LC_ALL=C export LC_ALL umask 077 set -Eeuo pipefail shopt -s extglob _k3slrv_initial_guard() { local candidate_euid="${1-}" shell_flags="${2-}" [[ "$candidate_euid" =~ ^[0-9]+$ ]] || return 1 (( candidate_euid != 0 )) || return 1 [[ "$shell_flags" != *x* ]] } if ! _k3slrv_initial_guard "$EUID" "$-"; then printf 'Recovery validation refused\n' >&2 if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then return 1; else exit 1; fi fi readonly K3SLRV_SECURE_PATH='/usr/sbin:/usr/bin:/sbin:/bin' readonly K3SLRV_TRUSTED_BINARIES=( /usr/bin/findmnt /usr/bin/lsblk /usr/bin/stat /usr/bin/readlink /usr/sbin/blkid /usr/bin/ntfs-3g.probe /usr/sbin/smartctl /usr/sbin/losetup /usr/sbin/cryptsetup /usr/bin/du /usr/bin/sha256sum /usr/bin/test /usr/bin/sudo /usr/bin/mawk ) readonly K3SLRV_SCRIPT_DIR="$(cd -P -- "${BASH_SOURCE[0]%/*}" && pwd -P)" readonly K3SLRV_REPOSITORY_ROOT="$(cd -P -- "${K3SLRV_SCRIPT_DIR}/../.." && pwd -P)" readonly K3SLRV_LIBRARY_PATH="${K3SLRV_REPOSITORY_ROOT}/scripts/lib/k3s-local-recovery.sh" readonly K3SLRV_CONTRACT_PATH="${K3SLRV_REPOSITORY_ROOT}/infrastructure/security/k3s/local-recovery.env" _k3slrv_usage() { printf 'Usage: bash scripts/validate/k3s-local-recovery.sh [--expect-device-ready|--expect-closed|--expect-open] [--check-latest-bundle]\n' } _k3slrv_fail() { printf 'Recovery validation failed\n' >&2 return 1 } _k3slrv_exec() { local scope="$1" shift case "$scope" in user) "$@" ;; root) /usr/bin/sudo --non-interactive -- "$@" ;; *) return 1 ;; esac } # Bash command substitution normally discards every trailing newline. Append a # non-newline sentinel inside the substitution, then remove only that sentinel # so record parsers can distinguish one record from a record plus blank lines. _k3slrv_capture() { local destination="$1" captured command_rc sentinel=$'\036' shift if captured="$( set +e "$@" command_rc=$? printf '\036' exit "$command_rc" )"; then command_rc=0 else command_rc=$? fi [[ "$captured" == *"$sentinel" ]] || return 125 captured="${captured%"$sentinel"}" [[ "$captured" != *"$sentinel"* ]] || return 125 printf -v "$destination" '%s' "$captured" return "$command_rc" } _k3slrv_normalize_one_record() { local destination="$1" value="$2" [[ -n "$value" && "$value" != *$'\r'* ]] || return 1 value="${value%$'\n'}" [[ -n "$value" && "$value" != *$'\n'* ]] || return 1 printf -v "$destination" '%s' "$value" } _k3slrv_verify_binary() { local binary="$1" metadata uid mode kind group_digit other_digit [[ "$binary" == /* ]] || return 1 _k3slrv_exec user /usr/bin/test -f "$binary" >/dev/null 2>&1 || return 1 _k3slrv_exec user /usr/bin/test ! -L "$binary" >/dev/null 2>&1 || return 1 _k3slrv_capture metadata _k3slrv_exec user /usr/bin/stat --format='%u|%a|%F' -- "$binary" 2>/dev/null || return 1 _k3slrv_normalize_one_record metadata "$metadata" || return 1 IFS='|' read -r uid mode kind <<<"$metadata" [[ "$uid" == 0 && "$mode" =~ ^[0-7]{3,4}$ && "$kind" == 'regular file' ]] || return 1 group_digit="${mode: -2:1}" other_digit="${mode: -1}" (( (10#$group_digit & 2) == 0 && (10#$other_digit & 2) == 0 )) } _k3slrv_verify_trusted_binaries() { local binary [[ "$PATH" == "$K3SLRV_SECURE_PATH" ]] || return 1 for binary in "${K3SLRV_TRUSTED_BINARIES[@]}"; do _k3slrv_verify_binary "$binary" || return 1 done } _k3slrv_require_cached_sudo() { _k3slrv_exec user /usr/bin/sudo --non-interactive --validate >/dev/null 2>&1 } _k3slrv_root() { _k3slrv_exec root "$@" } _k3slrv_trim() { local value="$1" value="${value##+([[:space:]])}" value="${value%%+([[:space:]])}" printf '%s\n' "$value" } _k3slrv_one_line() { local output _k3slrv_capture output _k3slrv_exec user "$@" 2>/dev/null || return 1 _k3slrv_normalize_one_record output "$output" || return 1 _k3slrv_trim "$output" } _k3slrv_one_line_root() { local output _k3slrv_capture output _k3slrv_root "$@" 2>/dev/null || return 1 _k3slrv_normalize_one_record output "$output" || return 1 _k3slrv_trim "$output" } _k3slrv_blkid() { _k3slrv_one_line_root /usr/sbin/blkid --output value --match-tag "$1" -- "$2" } _k3slrv_lsblk() { _k3slrv_one_line /usr/bin/lsblk --noheadings --nodeps --paths --output "$1" -- "$2" } _k3slrv_smart_evidence() { local disk="$1" output line id raw local -a fields=() lines=() local health='' reallocated='' pending='' uncorrectable='' _k3slrv_capture output _k3slrv_root /usr/sbin/smartctl -H -A -- "$disk" 2>/dev/null || return 1 mapfile -t lines < <(printf '%s' "$output") (( ${#lines[@]} > 0 )) || return 1 for line in "${lines[@]}"; do [[ "$line" != *$'\r'* ]] || return 1 if [[ "$line" =~ ^SMART[[:space:]]overall-health[[:space:]]self-assessment[[:space:]]test[[:space:]]result:[[:space:]]*PASSED[[:space:]]*$ ]]; then [[ -z "$health" ]] || return 1 health=PASSED continue fi [[ "$line" =~ ^[[:space:]]*(5|197|198)[[:space:]] ]] || continue id="${BASH_REMATCH[1]}" read -r -a fields <<<"$line" (( ${#fields[@]} >= 2 )) || return 1 raw="${fields[${#fields[@]} - 1]}" [[ "$raw" =~ ^[0-9]+$ ]] || return 1 case "$id" in 5) [[ -z "$reallocated" ]] || return 1; reallocated="$raw" ;; 197) [[ -z "$pending" ]] || return 1; pending="$raw" ;; 198) [[ -z "$uncorrectable" ]] || return 1; uncorrectable="$raw" ;; esac done [[ -n "$health" && -n "$reallocated" && -n "$pending" && -n "$uncorrectable" ]] || return 1 printf 'smart_health=%s\nsmart_reallocated=%s\nsmart_pending=%s\nsmart_uncorrectable=%s\n' \ "$health" "$reallocated" "$pending" "$uncorrectable" } _k3slrv_findmnt_line() { local output _k3slrv_capture output _k3slrv_root /usr/bin/findmnt --noheadings --raw --output SOURCE,FSTYPE,OPTIONS,ID,MAJ:MIN --mountpoint "$1" 2>/dev/null || return 1 _k3slrv_normalize_one_record output "$output" || return 1 output="$(_k3slrv_trim "$output")" [[ -n "$output" ]] || return 1 printf '%s\n' "$output" } _k3slrv_findmnt_optional() { local output rc=0 _k3slrv_capture output _k3slrv_root /usr/bin/findmnt --noheadings --raw --output SOURCE,FSTYPE,OPTIONS,ID,MAJ:MIN --mountpoint "$1" 2>/dev/null || rc=$? case "$rc" in 0) _k3slrv_normalize_one_record output "$output" || return 1 output="$(_k3slrv_trim "$output")" [[ -n "$output" ]] || return 1 printf '%s\n' "$output" ;; 1) return 0 ;; *) return 1 ;; esac } _k3slrv_findmnt_source_optional() { local output rc=0 _k3slrv_capture output _k3slrv_root /usr/bin/findmnt --noheadings --raw --output TARGET --source "$1" 2>/dev/null || rc=$? case "$rc" in 0) _k3slrv_normalize_one_record output "$output" || return 1 output="$(_k3slrv_trim "$output")" [[ -n "$output" ]] || return 1 printf '%s\n' "$output" ;; 1) return 0 ;; *) return 1 ;; esac } _k3slrv_collect_device_evidence() { local mode="$1" recovery_partition recovery_disk recovery_parent k3s_partition k3s_disk k3s_parent local recovery_model recovery_serial recovery_wwn recovery_mm k3s_model k3s_serial k3s_wwn k3s_mm local recovery_uuid recovery_partuuid recovery_type k3s_uuid k3s_partuuid k3s_type outer_mount source_mounts recovery_partition="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_RECOVERY_PARTITION_BY_ID")" || return 1 recovery_disk="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_RECOVERY_DISK_BY_ID")" || return 1 recovery_parent="$(_k3slrv_lsblk PKNAME "$recovery_partition")" || return 1 k3s_partition="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_K3S_PARTITION_BY_ID")" || return 1 k3s_disk="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_K3S_DISK_BY_ID")" || return 1 k3s_parent="$(_k3slrv_lsblk PKNAME "$k3s_partition")" || return 1 recovery_model="$(_k3slrv_lsblk MODEL "$recovery_disk")" || return 1 recovery_serial="$(_k3slrv_lsblk SERIAL "$recovery_disk")" || return 1 recovery_wwn="$(_k3slrv_lsblk WWN "$recovery_disk")" || return 1 recovery_mm="$(_k3slrv_lsblk MAJ:MIN "$recovery_disk")" || return 1 k3s_model="$(_k3slrv_lsblk MODEL "$k3s_disk")" || return 1 k3s_serial="$(_k3slrv_lsblk SERIAL "$k3s_disk")" || return 1 k3s_wwn="$(_k3slrv_lsblk WWN "$k3s_disk")" || return 1 k3s_mm="$(_k3slrv_lsblk MAJ:MIN "$k3s_disk")" || return 1 recovery_uuid="$(_k3slrv_blkid UUID "$recovery_partition")" || return 1 recovery_partuuid="$(_k3slrv_blkid PARTUUID "$recovery_partition")" || return 1 recovery_type="$(_k3slrv_blkid TYPE "$recovery_partition")" || return 1 k3s_uuid="$(_k3slrv_blkid UUID "$k3s_partition")" || return 1 k3s_partuuid="$(_k3slrv_blkid PARTUUID "$k3s_partition")" || return 1 k3s_type="$(_k3slrv_blkid TYPE "$k3s_partition")" || return 1 printf 'recovery_partition=%s\nrecovery_disk=%s\nrecovery_partition_parent=%s\n' \ "$recovery_partition" "$recovery_disk" "$recovery_parent" printf 'recovery_fs_uuid=%s\nrecovery_partuuid=%s\nrecovery_type=%s\n' \ "$recovery_uuid" "$recovery_partuuid" "$recovery_type" printf 'recovery_model=%s\nrecovery_serial=%s\nrecovery_wwn=%s\nrecovery_major_minor=%s\n' \ "$recovery_model" "$recovery_serial" "$recovery_wwn" "$recovery_mm" printf 'k3s_partition=%s\nk3s_disk=%s\nk3s_partition_parent=%s\n' \ "$k3s_partition" "$k3s_disk" "$k3s_parent" printf 'k3s_fs_uuid=%s\nk3s_partuuid=%s\nk3s_type=%s\n' \ "$k3s_uuid" "$k3s_partuuid" "$k3s_type" printf 'k3s_model=%s\nk3s_serial=%s\nk3s_wwn=%s\nk3s_major_minor=%s\n' \ "$k3s_model" "$k3s_serial" "$k3s_wwn" "$k3s_mm" _k3slrv_smart_evidence "$recovery_disk" || return 1 if [[ "$mode" == open ]]; then printf 'ntfs_probe=not_applicable\n' else outer_mount="$(_k3slrv_findmnt_optional "$K3SLR_OUTER_MOUNT")" || return 1 [[ -z "$outer_mount" ]] || return 1 source_mounts="$(_k3slrv_findmnt_source_optional "$recovery_partition")" || return 1 [[ -z "$source_mounts" ]] || return 1 if _k3slrv_root /usr/bin/ntfs-3g.probe --readwrite "$recovery_partition" >/dev/null 2>&1; then printf 'ntfs_probe=pass\n' else printf 'ntfs_probe=fail\n' fi fi } _k3slrv_mapping_evidence() { local name="$1" prefix="$2" output type='' device='' rc=0 local line local -a lines=() _k3slrv_capture output _k3slrv_root /usr/sbin/cryptsetup status -- "$name" 2>/dev/null || rc=$? if (( rc == 4 )); then printf '%s_present=0\n' "$prefix" return 0 fi (( rc == 0 )) || return 1 mapfile -t lines < <(printf '%s' "$output") (( ${#lines[@]} > 0 )) || return 1 for line in "${lines[@]}"; do [[ "$line" != *$'\r'* ]] || return 1 line="$(_k3slrv_trim "$line")" case "$line" in type:*) [[ -z "$type" ]] || return 1; type="$(_k3slrv_trim "${line#type:}")" ;; device:*) [[ -z "$device" ]] || return 1; device="$(_k3slrv_trim "${line#device:}")" ;; esac done [[ -n "$type" && -n "$device" ]] || return 1 printf '%s_present=1\n%s_type=%s\n%s_device=%s\n' "$prefix" "$prefix" "$type" "$prefix" "$device" } _k3slrv_pin_container_chain() { local container="$1" current="$K3SLR_OUTER_MOUNT" canonical metadata component index=0 snapshot='' local -a components=() IFS='/' read -r -a components <<<"$K3SLR_CONTAINER_RELATIVE" for component in '' "${components[@]}"; do if (( index > 0 )); then current="${current}/${component}"; fi _k3slrv_root /usr/bin/test ! -L "$current" >/dev/null 2>&1 || return 1 canonical="$(_k3slrv_one_line_root /usr/bin/readlink -e -- "$current")" || return 1 [[ "$canonical" == "$current" ]] || return 1 metadata="$(_k3slrv_one_line_root /usr/bin/stat --format='%d:%i|%F' -- "$current")" || return 1 if (( index < ${#components[@]} )); then [[ "$metadata" == *'|directory' ]] || return 1 else [[ "$current" == "$container" && "$metadata" == *'|regular file' ]] || return 1 fi snapshot+="${metadata%%|*}," index=$((index + 1)) done printf '%s\n' "${snapshot%,}" } _k3slrv_mapping_value() { local evidence="$1" key="$2" line found='' while IFS= read -r line; do [[ "$line" == "$key="* ]] || continue [[ -z "$found" ]] || return 1 found="${line#*=}" done <<<"$evidence" [[ -n "$found" ]] || return 1 printf '%s\n' "$found" } _k3slrv_collect_state_evidence() { local mode="$1" outer='' inner='' loops='' outer_source outer_fstype outer_options outer_mount_id outer_major_minor local inner_source inner_fstype inner_options inner_mount_id inner_major_minor container metadata kind uid gid file_mode size blocks allocated inode container_device local inner_type inner_label recovery_partition source_mounts chain_snapshot loop_device loop_back_inode loop_back_major_minor local loop_major_minor loop_offset loop_sizelimit mapping_snapshot proof_snapshot mapping_device mapping_loop_major_minor mapping_major_minor local outer_after inner_after source_after chain_after loops_after mapping_after proof_after mapping_mm_after local container_metadata_after inner_type_after inner_label_after inner_root_metadata inner_root_after local mapping_device_after mapping_loop_mm_after local -a loop_records=() outer="$(_k3slrv_findmnt_optional "$K3SLR_OUTER_MOUNT")" || return 1 inner="$(_k3slrv_findmnt_optional "$K3SLR_INNER_MOUNT")" || return 1 container="${K3SLR_OUTER_MOUNT}/${K3SLR_CONTAINER_RELATIVE}" if [[ "$mode" != open ]]; then _k3slrv_capture loops _k3slrv_root /usr/sbin/losetup --associated "$container" --noheadings --output NAME 2>/dev/null || return 1 printf 'outer_mounted=%s\ninner_mounted=%s\n' "$([[ -n "$outer" ]] && printf 1 || printf 0)" "$([[ -n "$inner" ]] && printf 1 || printf 0)" if [[ -z "$loops" ]]; then printf 'loop_count=0\n' else mapfile -t loop_records < <(printf '%s' "$loops") if (( ${#loop_records[@]} == 1 )) && [[ -n "${loop_records[0]}" && "${loop_records[0]}" != *$'\r'* ]]; then printf 'loop_count=1\n' else printf 'loop_count=2\n' fi fi _k3slrv_mapping_evidence "$K3SLR_MAPPING_NAME" mapping || return 1 _k3slrv_mapping_evidence "$K3SLR_PROOF_MAPPING_NAME" proof_mapping || return 1 return 0 fi _k3slrv_capture loops _k3slrv_root /usr/sbin/losetup --list --associated "$container" --noheadings --raw \ --output NAME,BACK-INO,BACK-MAJ:MIN,MAJ:MIN,OFFSET,SIZELIMIT 2>/dev/null || return 1 _k3slrv_normalize_one_record loops "$loops" || return 1 [[ -n "$outer" && -n "$inner" ]] || return 1 read -r outer_source outer_fstype outer_options outer_mount_id outer_major_minor <<<"$outer" read -r inner_source inner_fstype inner_options inner_mount_id inner_major_minor <<<"$inner" read -r loop_device loop_back_inode loop_back_major_minor loop_major_minor loop_offset loop_sizelimit <<<"$loops" [[ -n "$outer_mount_id" && -n "$outer_major_minor" && -n "$inner_mount_id" && -n "$inner_major_minor" && -n "$loop_device" && -n "$loop_back_inode" && -n "$loop_back_major_minor" && -n "$loop_major_minor" && -n "$loop_offset" && -n "$loop_sizelimit" ]] || return 1 outer_source="$(_k3slrv_one_line /usr/bin/readlink -f -- "$outer_source")" || return 1 recovery_partition="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_RECOVERY_PARTITION_BY_ID")" || return 1 source_mounts="$(_k3slrv_findmnt_source_optional "$recovery_partition")" || return 1 [[ "$source_mounts" == "$K3SLR_OUTER_MOUNT" ]] || return 1 chain_snapshot="$(_k3slrv_pin_container_chain "$container")" || return 1 metadata="$(_k3slrv_one_line_root /usr/bin/stat --format='%F|%u|%g|%a|%s|%b|%i|%Hd:%Ld' -- "$container")" || return 1 IFS='|' read -r kind uid gid file_mode size blocks inode container_device <<<"$metadata" [[ "$container_device" =~ ^[0-9]+:[0-9]+$ ]] || return 1 [[ "$blocks" =~ ^[0-9]+$ && "$blocks" -le 18014398509481983 ]] || return 1 allocated=$((blocks * 512)) printf 'outer_mounted=1\nouter_source=%s\nouter_fstype=%s\nouter_options=%s\n' "$outer_source" "$outer_fstype" "$outer_options" printf 'inner_mounted=1\ninner_source=%s\ninner_fstype=%s\ninner_options=%s\n' "$inner_source" "$inner_fstype" "$inner_options" printf 'container_kind=%s\ncontainer_symlink=%s\ncontainer_uid=%s\ncontainer_gid=%s\ncontainer_mode=%s\n' \ "$([[ "$kind" == 'regular file' ]] && printf regular || printf other)" \ "$(_k3slrv_root /usr/bin/test -L "$container" >/dev/null 2>&1 && printf 1 || printf 0)" "$uid" "$gid" "$file_mode" printf 'container_size=%s\ncontainer_allocated=%s\nloop_count=1\nloop_device=%s\n' "$size" "$allocated" "$loop_device" _k3slrv_capture mapping_snapshot _k3slrv_mapping_evidence "$K3SLR_MAPPING_NAME" mapping || return 1 _k3slrv_capture proof_snapshot _k3slrv_mapping_evidence "$K3SLR_PROOF_MAPPING_NAME" proof_mapping || return 1 printf '%s%s' "$mapping_snapshot" "$proof_snapshot" mapping_device="$(_k3slrv_mapping_value "$mapping_snapshot" mapping_device)" || return 1 mapping_loop_major_minor="$(_k3slrv_lsblk MAJ:MIN "$mapping_device")" || return 1 mapping_major_minor="$(_k3slrv_lsblk MAJ:MIN "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1 inner_type="$(_k3slrv_blkid TYPE "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1 inner_label="$(_k3slrv_blkid LABEL "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1 printf 'inner_type=%s\ninner_label=%s\n' "$inner_type" "$inner_label" inner_root_metadata="$(_k3slrv_one_line_root /usr/bin/stat --format='%u|%g|%a|%F' -- "$K3SLR_INNER_MOUNT")" || return 1 IFS='|' read -r uid gid file_mode kind <<<"$inner_root_metadata" printf 'inner_root_uid=%s\ninner_root_gid=%s\ninner_root_mode=%s\ninner_root_kind=%s\n' \ "$uid" "$gid" "$file_mode" "$kind" outer_after="$(_k3slrv_findmnt_optional "$K3SLR_OUTER_MOUNT")" || return 1 inner_after="$(_k3slrv_findmnt_optional "$K3SLR_INNER_MOUNT")" || return 1 source_after="$(_k3slrv_findmnt_source_optional "$recovery_partition")" || return 1 chain_after="$(_k3slrv_pin_container_chain "$container")" || return 1 _k3slrv_capture loops_after _k3slrv_root /usr/sbin/losetup --list --associated "$container" --noheadings --raw \ --output NAME,BACK-INO,BACK-MAJ:MIN,MAJ:MIN,OFFSET,SIZELIMIT 2>/dev/null || return 1 _k3slrv_normalize_one_record loops_after "$loops_after" || return 1 _k3slrv_capture mapping_after _k3slrv_mapping_evidence "$K3SLR_MAPPING_NAME" mapping || return 1 _k3slrv_capture proof_after _k3slrv_mapping_evidence "$K3SLR_PROOF_MAPPING_NAME" proof_mapping || return 1 container_metadata_after="$(_k3slrv_one_line_root /usr/bin/stat --format='%F|%u|%g|%a|%s|%b|%i|%Hd:%Ld' -- "$container")" || return 1 inner_type_after="$(_k3slrv_blkid TYPE "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1 inner_label_after="$(_k3slrv_blkid LABEL "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1 inner_root_after="$(_k3slrv_one_line_root /usr/bin/stat --format='%u|%g|%a|%F' -- "$K3SLR_INNER_MOUNT")" || return 1 mapping_device_after="$(_k3slrv_mapping_value "$mapping_after" mapping_device)" || return 1 mapping_loop_mm_after="$(_k3slrv_lsblk MAJ:MIN "$mapping_device_after")" || return 1 mapping_mm_after="$(_k3slrv_lsblk MAJ:MIN "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1 [[ "$outer_after" == "$outer" && "$inner_after" == "$inner" && "$source_after" == "$source_mounts" && "$chain_after" == "$chain_snapshot" && "$loops_after" == "$loops" && "$mapping_after" == "$mapping_snapshot" && "$proof_after" == "$proof_snapshot" && "$container_metadata_after" == "$metadata" && "$inner_type_after" == "$inner_type" && "$inner_label_after" == "$inner_label" && "$inner_root_after" == "$inner_root_metadata" && "$mapping_loop_mm_after" == "$mapping_loop_major_minor" && "$mapping_mm_after" == "$mapping_major_minor" ]] || return 1 printf '%s\n' \ 'container_chain_pinned=1' \ "container_inode=${inode}" \ "container_device=${container_device}" \ "loop_back_inode=${loop_back_inode}" \ "loop_back_major_minor=${loop_back_major_minor}" \ "outer_major_minor=${outer_major_minor}" \ "loop_major_minor=${loop_major_minor}" \ "mapping_loop_major_minor=${mapping_loop_major_minor}" \ "mapping_major_minor=${mapping_major_minor}" \ "inner_major_minor=${inner_major_minor}" \ "loop_offset=${loop_offset}" \ "loop_sizelimit=${loop_sizelimit}" \ 'snapshot_stable=1' } _k3slrv_parse_evidence() { local input="$1" destination_name="$2" shift 2 local -n destination="$destination_name" local line key value expected local -A allowed=() seen=() local -a lines=() destination=() for expected in "$@"; do allowed["$expected"]=1; done mapfile -t lines < <(printf '%s' "$input") (( ${#lines[@]} > 0 )) || return 1 for line in "${lines[@]}"; do [[ "$line" =~ ^([a-z][a-z0-9_]*)=([A-Za-z0-9._:/,+=-]*)$ ]] || return 1 key="${BASH_REMATCH[1]}"; value="${BASH_REMATCH[2]}" [[ "${allowed[$key]+yes}" == yes && "${seen[$key]+yes}" != yes ]] || return 1 seen["$key"]=1; destination["$key"]="$value" done for expected in "${!allowed[@]}"; do [[ "${seen[$expected]+yes}" == yes ]] || return 1; done } _k3slrv_validate_device() { local mode="$1" input="$2" local -A e=() local keys=(recovery_partition recovery_disk recovery_partition_parent recovery_fs_uuid recovery_partuuid recovery_type recovery_model recovery_serial recovery_wwn recovery_major_minor k3s_partition k3s_disk k3s_partition_parent k3s_fs_uuid k3s_partuuid k3s_type k3s_model k3s_serial k3s_wwn k3s_major_minor smart_health smart_reallocated smart_pending smart_uncorrectable ntfs_probe) _k3slrv_parse_evidence "$input" e "${keys[@]}" || return 1 [[ "${e[recovery_partition_parent]}" == "${e[recovery_disk]}" && "${e[k3s_partition_parent]}" == "${e[k3s_disk]}" ]] || return 1 [[ "${e[recovery_fs_uuid]}" == "$K3SLR_RECOVERY_FS_UUID" && "${e[recovery_partuuid]}" == "$K3SLR_RECOVERY_PARTUUID" && "${e[recovery_type]}" == ntfs ]] || return 1 [[ "${e[recovery_model]}" == "$K3SLR_RECOVERY_MODEL" && "${e[recovery_serial]}" == "$K3SLR_RECOVERY_SERIAL" && "${e[recovery_wwn]}" == "$K3SLR_RECOVERY_WWN" ]] || return 1 [[ "${e[k3s_fs_uuid]}" == "$K3SLR_K3S_FS_UUID" && "${e[k3s_partuuid]}" == "$K3SLR_K3S_PARTUUID" && "${e[k3s_type]}" == ext4 ]] || return 1 [[ "${e[k3s_model]}" == "$K3SLR_K3S_MODEL" && "${e[k3s_serial]}" == "$K3SLR_K3S_SERIAL" && "${e[k3s_wwn]}" == "$K3SLR_K3S_WWN" ]] || return 1 [[ "${e[recovery_major_minor]}" != "${e[k3s_major_minor]}" && "${e[smart_health]}" == PASSED ]] || return 1 [[ "${e[smart_reallocated]}" == 0 && "${e[smart_pending]}" == 0 && "${e[smart_uncorrectable]}" == 0 ]] || return 1 if [[ "$mode" == open ]]; then [[ "${e[ntfs_probe]}" == not_applicable || "${e[ntfs_probe]}" == pass ]]; else [[ "${e[ntfs_probe]}" == pass ]]; fi } _k3slrv_option_present() { [[ ",${1}," == *",${2},"* ]] } _k3slrv_outer_masks_secure() { local options="$1" option umask_count=0 dmask_count=0 fmask_count=0 invalid=false local -a option_list=() IFS=',' read -r -a option_list <<<"$options" for option in "${option_list[@]}"; do case "$option" in umask=077|umask=0077) umask_count=$((umask_count + 1)) ;; dmask=077|dmask=0077) dmask_count=$((dmask_count + 1)) ;; fmask=077|fmask=0077|fmask=177|fmask=0177) fmask_count=$((fmask_count + 1)) ;; umask=*|dmask=*|fmask=*) invalid=true ;; esac done "$invalid" && return 1 if (( umask_count == 1 && dmask_count == 0 && fmask_count == 0 )); then return 0; fi (( umask_count == 0 && dmask_count == 1 && fmask_count == 1 )) } _k3slrv_validate_open_snapshot() { local input="$1" local -A snapshot=() _k3slrv_parse_evidence "$input" snapshot \ container_chain_pinned container_inode container_device loop_back_inode loop_back_major_minor outer_major_minor \ loop_major_minor mapping_loop_major_minor mapping_major_minor inner_major_minor \ loop_offset loop_sizelimit snapshot_stable || return 1 [[ "${snapshot[container_chain_pinned]}" == 1 && "${snapshot[snapshot_stable]}" == 1 ]] || return 1 [[ "${snapshot[container_inode]}" =~ ^[0-9]+$ && "${snapshot[loop_back_inode]}" == "${snapshot[container_inode]}" ]] || return 1 [[ "${snapshot[container_device]}" =~ ^[0-9]+:[0-9]+$ && "${snapshot[loop_back_major_minor]}" == "${snapshot[container_device]}" && "${snapshot[container_device]}" == "${snapshot[outer_major_minor]}" ]] || return 1 [[ "${snapshot[mapping_loop_major_minor]}" == "${snapshot[loop_major_minor]}" ]] || return 1 [[ "${snapshot[mapping_major_minor]}" == "${snapshot[inner_major_minor]}" ]] || return 1 [[ "${snapshot[loop_offset]}" == 0 && "${snapshot[loop_sizelimit]}" == 0 ]] } _k3slrv_validate_state() { local mode="$1" input="$2" device_input="$3" approved_partition='' line snapshot_raw local -A e=() if [[ "$mode" != open ]]; then _k3slrv_parse_evidence "$input" e outer_mounted inner_mounted loop_count mapping_present proof_mapping_present || return 1 [[ "${e[outer_mounted]}" == 0 && "${e[inner_mounted]}" == 0 && "${e[loop_count]}" == 0 && "${e[mapping_present]}" == 0 && "${e[proof_mapping_present]}" == 0 ]] return fi local keys=(outer_mounted outer_source outer_fstype outer_options inner_mounted inner_source inner_fstype inner_options container_kind container_symlink container_uid container_gid container_mode container_size container_allocated loop_count loop_device mapping_present mapping_type mapping_device proof_mapping_present inner_type inner_label inner_root_uid inner_root_gid inner_root_mode inner_root_kind container_chain_pinned container_inode container_device loop_back_inode loop_back_major_minor outer_major_minor loop_major_minor mapping_loop_major_minor mapping_major_minor inner_major_minor loop_offset loop_sizelimit snapshot_stable) _k3slrv_parse_evidence "$input" e "${keys[@]}" || return 1 while IFS= read -r line; do [[ "$line" == recovery_partition=* ]] || continue [[ -z "$approved_partition" ]] || return 1 approved_partition="${line#recovery_partition=}" done <<<"$device_input" [[ -n "$approved_partition" && "${e[outer_mounted]}" == 1 && "${e[outer_source]}" == "$approved_partition" && "${e[outer_fstype]}" == ntfs3 ]] || return 1 for option in rw nodev nosuid noexec "uid=${K3SLR_OWNER_UID}" "gid=${K3SLR_OWNER_GID}"; do _k3slrv_option_present "${e[outer_options]}" "$option" || return 1 done _k3slrv_outer_masks_secure "${e[outer_options]}" || return 1 [[ "${e[inner_mounted]}" == 1 && "${e[inner_source]}" == "/dev/mapper/${K3SLR_MAPPING_NAME}" && "${e[inner_fstype]}" == ext4 ]] || return 1 for option in rw nodev nosuid noexec; do _k3slrv_option_present "${e[inner_options]}" "$option" || return 1; done [[ "${e[container_kind]}" == regular && "${e[container_symlink]}" == 0 && "${e[container_uid]}" == "$K3SLR_OWNER_UID" && "${e[container_gid]}" == "$K3SLR_OWNER_GID" ]] || return 1 [[ "${e[container_mode]}" =~ ^[0-7]{3,4}$ ]] || return 1 (( (10#${e[container_mode]: -2:1} & 7) == 0 && (10#${e[container_mode]: -1:1} & 7) == 0 )) || return 1 [[ "${e[container_size]}" == "$K3SLR_CONTAINER_SIZE_BYTES" && "${e[container_allocated]}" =~ ^[0-9]+$ ]] || return 1 (( e[container_allocated] >= K3SLR_CONTAINER_SIZE_BYTES )) || return 1 [[ "${e[loop_count]}" == 1 && "${e[mapping_present]}" == 1 && "${e[mapping_type]}" == LUKS2 && "${e[mapping_device]}" == "${e[loop_device]}" && "${e[proof_mapping_present]}" == 0 ]] || return 1 [[ "${e[inner_type]}" == ext4 && "${e[inner_label]}" == "$K3SLR_INNER_LABEL" ]] || return 1 [[ "${e[inner_root_uid]}" == 0 && "${e[inner_root_gid]}" == 0 && "${e[inner_root_mode]}" == 700 && "${e[inner_root_kind]}" == directory ]] || return 1 printf -v snapshot_raw '%s\n' \ "container_chain_pinned=${e[container_chain_pinned]}" \ "container_inode=${e[container_inode]}" \ "container_device=${e[container_device]}" \ "loop_back_inode=${e[loop_back_inode]}" \ "loop_back_major_minor=${e[loop_back_major_minor]}" \ "outer_major_minor=${e[outer_major_minor]}" \ "loop_major_minor=${e[loop_major_minor]}" \ "mapping_loop_major_minor=${e[mapping_loop_major_minor]}" \ "mapping_major_minor=${e[mapping_major_minor]}" \ "inner_major_minor=${e[inner_major_minor]}" \ "loop_offset=${e[loop_offset]}" \ "loop_sizelimit=${e[loop_sizelimit]}" \ "snapshot_stable=${e[snapshot_stable]}" _k3slrv_validate_open_snapshot "${snapshot_raw%$'\n'}" } _k3slrv_verify_manifest_targets() { local bundle="$1" manifest="$2" records record expected_hash relative target component current canonical local before after hash_line actual_hash count=0 local -a components=() manifest_records=() _k3slrv_capture records _k3slrv_root /usr/bin/mawk ' NF!=2 || length($1)!=64 || $1 !~ /^[0-9a-f]+$/ || $2 !~ /^\.\/[A-Za-z0-9._\/-]+$/ || $2 ~ /\.\./ || $2 ~ /\/\// || $2 ~ /\/\.\// || $2 ~ /\/\.$/ || $2 ~ /\/$/ { bad=1; exit } { print $1 "|" $2 } END { exit (bad || NR==0) } ' "$manifest" 2>/dev/null || return 1 mapfile -t manifest_records < <(printf '%s' "$records") (( ${#manifest_records[@]} > 0 )) || return 1 for record in "${manifest_records[@]}"; do [[ "$record" == *'|'* ]] || return 1 expected_hash="${record%%|*}" relative="${record#*|}" [[ "$expected_hash" =~ ^[0-9a-f]{64}$ && "$relative" =~ ^\./[A-Za-z0-9._/-]+$ ]] || return 1 target="${bundle}/${relative#./}" current="$bundle" IFS='/' read -r -a components <<<"${relative#./}" for component in "${components[@]}"; do [[ -n "$component" && "$component" != . && "$component" != .. ]] || return 1 current="${current}/${component}" _k3slrv_root /usr/bin/test ! -L "$current" >/dev/null 2>&1 || return 1 canonical="$(_k3slrv_one_line_root /usr/bin/readlink -e -- "$current")" || return 1 [[ "$canonical" == "$current" ]] || return 1 done _k3slrv_root /usr/bin/test -f "$target" >/dev/null 2>&1 || return 1 before="$(_k3slrv_one_line_root /usr/bin/stat --format='%d:%i|%F' -- "$target")" || return 1 [[ "$before" == *'|regular file' ]] || return 1 hash_line="$(_k3slrv_one_line_root /usr/bin/sha256sum -- "$target")" || return 1 actual_hash="${hash_line%% *}" [[ "$actual_hash" == "$expected_hash" ]] || return 1 after="$(_k3slrv_one_line_root /usr/bin/stat --format='%d:%i|%F' -- "$target")" || return 1 [[ "$after" == "$before" ]] || return 1 count=$((count + 1)) done (( count > 0 )) } _k3slrv_collect_latest_bundle_evidence() { local metadata="${K3SLR_INNER_MOUNT}/.latest-post-bundle.env" parsed relative expected_identity bundle canonical_bundle actual_identity after_identity local bundle_metadata manifest stat_value _k3slrv_root /usr/bin/test -f "$metadata" >/dev/null 2>&1 || return 1 _k3slrv_root /usr/bin/test ! -L "$metadata" >/dev/null 2>&1 || return 1 stat_value="$(_k3slrv_one_line_root /usr/bin/stat --format='%u:%g:%a:%F' -- "$metadata")" || return 1 [[ "$stat_value" == '0:0:600:regular file' ]] || return 1 _k3slrv_capture parsed _k3slrv_root /usr/bin/mawk -F= ' BEGIN { ok=1 } !/^[a-z_]+=[A-Za-z0-9._:\/-]+$/ { ok=0; next } $1=="schema" && !s++ && $2=="k3slr-latest-post-bundle-v1" { schema=$2; next } $1=="relative_path" && !p++ && $2 ~ /^k3s-secrets-encryption-[0-9]{8}T[0-9]{6}Z\/post$/ { path=$2; next } $1=="directory_identity" && !i++ && $2 ~ /^[0-9]+:[0-9]+$/ { identity=$2; next } { ok=0 } END { if (ok && NR==3 && s==1 && p==1 && i==1) print path "|" identity; else exit 1 } ' "$metadata" 2>/dev/null || return 1 _k3slrv_normalize_one_record parsed "$parsed" || return 1 [[ "$parsed" == *'|'* ]] || return 1 relative="${parsed%%|*}"; expected_identity="${parsed#*|}" [[ "$relative" =~ ^k3s-secrets-encryption-[0-9]{8}T[0-9]{6}Z/post$ && "$relative" != *..* ]] || return 1 bundle="${K3SLR_INNER_MOUNT}/${relative}" canonical_bundle="$(_k3slrv_one_line_root /usr/bin/readlink -f -- "$bundle")" || return 1 [[ "$canonical_bundle" == "$bundle" ]] || return 1 actual_identity="$(_k3slrv_one_line_root /usr/bin/stat --dereference --format='%d:%i' -- "$bundle")" || return 1 [[ "$actual_identity" == "$expected_identity" ]] || return 1 stat_value="$(_k3slrv_one_line_root /usr/bin/stat --dereference --format='%u:%g:%a:%F' -- "$bundle")" || return 1 [[ "$stat_value" == '0:0:700:directory' ]] || return 1 bundle_metadata="${bundle}/bundle.env"; manifest="${bundle}/verification.manifest" for file in "$bundle_metadata" "$manifest"; do _k3slrv_root /usr/bin/test -f "$file" >/dev/null 2>&1 || return 1 _k3slrv_root /usr/bin/test ! -L "$file" >/dev/null 2>&1 || return 1 stat_value="$(_k3slrv_one_line_root /usr/bin/stat --format='%u:%g:%a:%F' -- "$file")" || return 1 [[ "$stat_value" == '0:0:600:regular file' ]] || return 1 done _k3slrv_root /usr/bin/mawk -F= ' BEGIN { ok=1 } !/^[a-z][a-z0-9_]*=[A-Za-z0-9:+._-]+$/ { ok=0; next } $1=="schema" && !a++ && $2=="platform-k3s-bundle-v1" { next } $1=="bundle_id" && !b++ && $2 ~ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ { next } $1=="phase" && !c++ && $2=="post" { next } $1=="k3s_version" && !d++ && $2=="v1.36.2+k3s1" { next } $1=="datastore" && !e++ && ($2=="sqlite" || $2=="embedded-etcd") { next } $1=="created_at_utc" && !f++ && $2 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/ { next } $1=="secret_count" && !g++ && $2 ~ /^[0-9]+$/ { next } { ok=0 } END { exit !(ok && NR==7 && a==1 && b==1 && c==1 && d==1 && e==1 && f==1 && g==1) } ' "$bundle_metadata" >/dev/null 2>&1 || return 1 _k3slrv_verify_manifest_targets "$bundle" "$manifest" || return 1 after_identity="$(_k3slrv_one_line_root /usr/bin/stat --dereference --format='%d:%i' -- "$bundle")" || return 1 [[ "$after_identity" == "$expected_identity" ]] || return 1 printf '%s\n' \ 'metadata_schema=k3slr-latest-post-bundle-v1' \ 'relative_path_valid=1' \ 'directory_identity_match=1' \ 'directory_secure=1' \ 'bundle_schema=platform-k3s-bundle-v1' \ 'bundle_phase=post' \ 'manifest_safe=1' \ 'manifest_verified=1' \ 'directory_identity_stable=1' } _k3slrv_verify_latest_bundle() { local raw local -A evidence=() _k3slrv_capture raw _k3slrv_collect_latest_bundle_evidence || return 1 _k3slrv_parse_evidence "$raw" evidence \ metadata_schema relative_path_valid directory_identity_match directory_secure \ bundle_schema bundle_phase manifest_safe manifest_verified directory_identity_stable || return 1 [[ "${evidence[metadata_schema]}" == k3slr-latest-post-bundle-v1 && "${evidence[relative_path_valid]}" == 1 && "${evidence[directory_identity_match]}" == 1 && "${evidence[directory_secure]}" == 1 && "${evidence[bundle_schema]}" == platform-k3s-bundle-v1 && "${evidence[bundle_phase]}" == post && "${evidence[manifest_safe]}" == 1 && "${evidence[manifest_verified]}" == 1 && "${evidence[directory_identity_stable]}" == 1 ]] } k3slr_local_recovery_main() { local mode='' check_latest=false argument device_raw state_raw latest=not_checked for argument in "$@"; do case "$argument" in --expect-device-ready) [[ -z "$mode" ]] || { _k3slrv_usage >&2; return 2; }; mode=device_ready ;; --expect-closed) [[ -z "$mode" ]] || { _k3slrv_usage >&2; return 2; }; mode=closed ;; --expect-open) [[ -z "$mode" ]] || { _k3slrv_usage >&2; return 2; }; mode=open ;; --check-latest-bundle) "$check_latest" && { _k3slrv_usage >&2; return 2; }; check_latest=true ;; --help|-h) _k3slrv_usage; return 0 ;; *) _k3slrv_usage >&2; return 2 ;; esac done [[ -n "$mode" ]] || { _k3slrv_usage >&2; return 2; } if "$check_latest" && [[ "$mode" != open ]]; then _k3slrv_usage >&2; return 2; fi PATH="$K3SLRV_SECURE_PATH"; export PATH _k3slrv_verify_trusted_binaries || { _k3slrv_fail; return 1; } # Direct execution always replaces any inherited/predefined parser with the # repository library in this unprivileged process. # shellcheck source=/dev/null builtin source -- "$K3SLRV_LIBRARY_PATH" || { _k3slrv_fail; return 1; } _k3slr_load_contract "$K3SLRV_CONTRACT_PATH" || { _k3slrv_fail; return 1; } _k3slrv_require_cached_sudo || { _k3slrv_fail; return 1; } _k3slrv_capture device_raw _k3slrv_collect_device_evidence "$mode" || { _k3slrv_fail; return 1; } _k3slrv_validate_device "$mode" "$device_raw" || { _k3slrv_fail; return 1; } _k3slrv_capture state_raw _k3slrv_collect_state_evidence "$mode" || { _k3slrv_fail; return 1; } _k3slrv_validate_state "$mode" "$state_raw" "$device_raw" || { _k3slrv_fail; return 1; } if "$check_latest"; then _k3slrv_verify_latest_bundle || { _k3slrv_fail; return 1; } latest=verified fi printf 'Recovery device: match\nRecovery state: %s\nLineage: match\nLatest bundle: %s\n' "$mode" "$latest" } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then k3slr_local_recovery_main "$@" fi