#!/usr/bin/env bash # 격리 복구 결과를 만들고 운영 host의 복구 증거를 등록·검사한다. source-safe # 함수 경계는 fixture가 live/root side effect만 치환할 수 있게 유지한다. set -Eeuo pipefail if [[ "${BASH_SOURCE[0]}" == */* ]]; then readonly KSRE_SCRIPT_DIR="${BASH_SOURCE[0]%/*}" else readonly KSRE_SCRIPT_DIR='.' fi readonly KSRE_ROOT="$(cd -- "${KSRE_SCRIPT_DIR}/../.." && pwd -P)" readonly KSRE_STATUS_VALIDATOR="${KSRE_ROOT}/scripts/validate/k3s-secret-encryption.sh" readonly KSRE_POST_BUNDLE='/etc/rancher/k3s/platform-post-bundle.env' readonly KSRE_EVIDENCE='/etc/rancher/k3s/platform-restore-evidence.env' readonly KSRE_VERSION='v1.36.2+k3s1' readonly KSRE_KUBECTL='/usr/local/bin/kubectl' readonly KSRE_LOCAL_API_SERVER='https://127.0.0.1:6443' readonly KSRE_SECURE_PATH='/usr/sbin:/usr/bin:/sbin:/bin' readonly KSRE_BROKER_RUNTIME_TIMEOUT='2s' readonly KSRE_BROKER_KILL_AFTER='0.5s' readonly KSRE_BROKER_READY_TIMEOUT='3' readonly KSRE_BROKER_ACK_TIMEOUT='1.0' readonly KSRE_BROKER_REAP_POLLS='300' readonly KSRE_BROKER_SIGNAL_POLLS='50' readonly KSRE_BROKER_POLL_INTERVAL='0.01' _ksre_fail() { printf 'ERROR: 복구 증거 검증에 실패했습니다. 수동 절차를 확인하세요.\n' >&2 return 1 } _ksre_usage() { cat <<'USAGE' 사용법: bash scripts/validate/k3s-secret-encryption-restore-evidence.sh \ --emit-result --bundle-metadata FILE --output FILE bash scripts/validate/k3s-secret-encryption-restore-evidence.sh \ --record --bundle-metadata FILE --result-file FILE bash scripts/validate/k3s-secret-encryption-restore-evidence.sh --check --emit-result는 격리 복구 host의 live 상태를 검사한 뒤 결과 파일을 만듭니다. --record는 복구 환경과 bundle 복사본 파기 확인 뒤 운영 host에 증거를 등록합니다. --check는 권위 post bundle, live 암호화 상태와 등록된 증거를 읽기 전용 검사합니다. USAGE } _ksre_allowed_key() { local kind="$1" key="$2" case "$kind:$key" in bundle:schema|bundle:bundle_id|bundle:phase|bundle:k3s_version|bundle:datastore|bundle:created_at_utc|bundle:secret_count) return 0 ;; result:schema|result:bundle_id|result:k3s_version|result:datastore|result:status_class|result:rotation_stage|result:hashes|result:local_integrity|result:api_ready|result:node_ready|result:isolation|result:secret_count_match|result:tested_at_utc) return 0 ;; evidence:schema|evidence:bundle_id|evidence:k3s_version|evidence:datastore|evidence:rotation_stage|evidence:local_integrity|evidence:node_ready|evidence:isolation|evidence:tested_at_utc|evidence:recorded_at_utc|evidence:destroyed) return 0 ;; *) return 1 ;; esac } _ksre_expected_field_count() { case "$1" in bundle) printf '7\n' ;; result) printf '13\n' ;; evidence) printf '11\n' ;; *) return 1 ;; esac } _ksre_values_valid() { local kind="$1" destination="$2" local -n values="$destination" [[ "${values[bundle_id]:-}" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ ]] || return 1 [[ "${values[k3s_version]:-}" == "$KSRE_VERSION" ]] || return 1 [[ "${values[datastore]:-}" == sqlite || "${values[datastore]:-}" == embedded-etcd ]] || return 1 case "$kind" in bundle) [[ "${values[schema]:-}" == platform-k3s-bundle-v1 && ( "${values[phase]:-}" == pre || "${values[phase]:-}" == post ) && "${values[created_at_utc]:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ && "${values[secret_count]:-}" =~ ^[0-9]+$ ]] ;; result) [[ "${values[schema]:-}" == platform-k3s-restore-result-v1 && "${values[status_class]:-}" == enabled_stable && "${values[rotation_stage]:-}" == reencrypt_finished && "${values[hashes]:-}" == match && "${values[local_integrity]:-}" == match && "${values[api_ready]:-}" == pass && "${values[node_ready]:-}" == pass && "${values[isolation]:-}" == pass && "${values[secret_count_match]:-}" == pass && "${values[tested_at_utc]:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] ;; evidence) [[ "${values[schema]:-}" == platform-k3s-restore-evidence-v1 && "${values[rotation_stage]:-}" == reencrypt_finished && "${values[local_integrity]:-}" == match && "${values[node_ready]:-}" == pass && "${values[isolation]:-}" == pass && "${values[destroyed]:-}" == confirmed && "${values[tested_at_utc]:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ && "${values[recorded_at_utc]:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] ;; *) return 1 ;; esac } _ksre_parse_stream() { local kind="$1" destination="$2" line key value count=0 expected local -n output_fields="$destination" output_fields=() expected="$(_ksre_expected_field_count "$kind")" || return 1 while IFS= read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^([a-z0-9_]+)=([^=[:cntrl:]]+)$ ]] || return 1 key="${BASH_REMATCH[1]}"; value="${BASH_REMATCH[2]}" _ksre_allowed_key "$kind" "$key" || return 1 [[ -z "${output_fields[$key]+present}" ]] || return 1 output_fields["$key"]="$value" count=$((count + 1)) done [[ "$count" == "$expected" ]] || return 1 _ksre_values_valid "$kind" "$destination" } _ksre_raw_stream_has_no_nul() { /usr/bin/od -An -v -t u1 -- "$1" 2>/dev/null | /usr/bin/awk '{ for (field = 1; field <= NF; field++) if ($field == 0) found = 1 } END { exit(found ? 1 : 0) }' } _ksre_parse_external_fd() { local kind="$1" input_fd="$2" destination="$3" handle metadata [[ "$input_fd" =~ ^[0-9]+$ ]] || return 1 handle="/proc/self/fd/${input_fd}" metadata="$(/usr/bin/stat --dereference --format='%u:%a:%F' -- "$handle" 2>/dev/null)" || return 1 [[ "$metadata" == "${EUID}:600:regular file" && -s "$handle" ]] || return 1 _ksre_raw_stream_has_no_nul "$handle" || return 1 _ksre_parse_stream "$kind" "$destination" <"$handle" } _ksre_broker_job_running() { local expected_pid="$1" active_pid active_jobs active_jobs="$(jobs -pr)" while IFS= read -r active_pid; do [[ "$active_pid" == "$expected_pid" ]] && return 0 done <<<"$active_jobs" return 1 } _ksre_reap_broker_bounded() { local broker_pid="$1" attempt for ((attempt = 0; attempt < KSRE_BROKER_REAP_POLLS; attempt++)); do if ! _ksre_broker_job_running "$broker_pid"; then if wait "$broker_pid"; then return 0; else return $?; fi fi /usr/bin/sleep "$KSRE_BROKER_POLL_INTERVAL" done /bin/kill -TERM "$broker_pid" 2>/dev/null || : for ((attempt = 0; attempt < KSRE_BROKER_SIGNAL_POLLS; attempt++)); do if ! _ksre_broker_job_running "$broker_pid"; then wait "$broker_pid" 2>/dev/null || : return 1 fi /usr/bin/sleep "$KSRE_BROKER_POLL_INTERVAL" done /bin/kill -KILL "$broker_pid" 2>/dev/null || : for ((attempt = 0; attempt < KSRE_BROKER_SIGNAL_POLLS; attempt++)); do if ! _ksre_broker_job_running "$broker_pid"; then wait "$broker_pid" 2>/dev/null || : return 1 fi /usr/bin/sleep "$KSRE_BROKER_POLL_INTERVAL" done return 1 } _ksre_open_external_nofollow() { local path="$1" destination="$2" broker_pid broker_read broker_write local ready opener_pid opener_fd opened_descriptor='' rc=0 coproc KSRE_OPEN_BROKER { exec /usr/bin/timeout --signal=TERM --kill-after="$KSRE_BROKER_KILL_AFTER" \ "$KSRE_BROKER_RUNTIME_TIMEOUT" /usr/bin/python3 -I -S -c ' import os import select import stat import sys required_flags = ("O_PATH", "O_NOFOLLOW", "O_CLOEXEC", "O_NONBLOCK") if any(not hasattr(os, flag) for flag in required_flags): sys.exit(1) try: path_descriptor = os.open( sys.argv[1], os.O_PATH | os.O_NOFOLLOW | os.O_CLOEXEC ) except OSError: sys.exit(1) try: path_status = os.fstat(path_descriptor) if not stat.S_ISREG(path_status.st_mode): sys.exit(1) try: descriptor = os.open( f"/proc/self/fd/{path_descriptor}", os.O_RDONLY | os.O_CLOEXEC | os.O_NONBLOCK, ) except OSError: sys.exit(1) try: content_status = os.fstat(descriptor) if ( not stat.S_ISREG(content_status.st_mode) or content_status.st_dev != path_status.st_dev or content_status.st_ino != path_status.st_ino ): sys.exit(1) print(f"{os.getpid()}:{descriptor}", flush=True) readable, _, _ = select.select( [sys.stdin.buffer], [], [], float(sys.argv[2]) ) if not readable or sys.stdin.buffer.read(1) != b"x": sys.exit(1) finally: os.close(descriptor) finally: os.close(path_descriptor) ' "$path" "$KSRE_BROKER_ACK_TIMEOUT" } broker_pid="$KSRE_OPEN_BROKER_PID" broker_read="${KSRE_OPEN_BROKER[0]}" broker_write="${KSRE_OPEN_BROKER[1]}" if IFS= read -r -t "$KSRE_BROKER_READY_TIMEOUT" ready <&"$broker_read"; then if [[ "$ready" =~ ^([0-9]+):([0-9]+)$ ]]; then opener_pid="${BASH_REMATCH[1]}" opener_fd="${BASH_REMATCH[2]}" exec {opened_descriptor}<"/proc/${opener_pid}/fd/${opener_fd}" || rc=1 else rc=1 fi else rc=1 fi if (( rc == 0 )); then printf 'x' >&"$broker_write" || rc=1 fi exec {broker_write}>&- exec {broker_read}<&- if (( rc != 0 )); then /bin/kill -TERM "$broker_pid" 2>/dev/null || :; fi if ! _ksre_reap_broker_bounded "$broker_pid"; then rc=1; fi if (( rc != 0 )); then [[ -z "$opened_descriptor" ]] || exec {opened_descriptor}<&- return 1 fi printf -v "$destination" '%s' "$opened_descriptor" } _ksre_parse_external_file() { local kind="$1" path="$2" destination="$3" input_fd rc=0 [[ -n "$path" ]] || return 1 _ksre_open_external_nofollow "$path" input_fd || return 1 if (( rc == 0 )); then _ksre_parse_external_fd "$kind" "$input_fd" "$destination" || rc=$?; fi exec {input_fd}<&- return "$rc" } _ksre_parse_trusted_content() { local kind="$1" content="$2" destination="$3" _ksre_parse_stream "$kind" "$destination" <<<"$content" } _ksre_current_context() { /usr/bin/timeout --signal=TERM --kill-after=1s 9s \ "$KSRE_KUBECTL" config current-context 2>/dev/null } _ksre_require_same_context() { local expected="$1" current current="$(_ksre_current_context)" || return 1 [[ -n "$current" && "$current" == "$expected" && "$current" != *$'\n'* ]] } _ksre_validate_current_connection() { [[ "$1" == "${KSRE_LOCAL_API_SERVER}|" ]] } _ksre_current_connection() { local context="$1" /usr/bin/timeout --signal=TERM --kill-after=1s 9s \ "$KSRE_KUBECTL" --context="$context" config view --minify \ -o 'jsonpath={.clusters[0].cluster.server}{"|"}{.clusters[0].cluster.proxy-url}' \ 2>/dev/null } _ksre_require_same_connection() { local context="$1" expected="$2" current _ksre_validate_current_connection "$expected" || return 1 current="$(_ksre_current_connection "$context")" || return 1 [[ "$current" == "$expected" ]] } _ksre_now_rfc3339() { /usr/bin/date -u +%Y-%m-%dT%H:%M:%SZ; } _ksre_now_epoch() { /usr/bin/date -u +%s; } _ksre_timestamp_epoch() { local timestamp="$1" normalized normalized="$(/usr/bin/date -u -d "$timestamp" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)" || return 1 [[ "$normalized" == "$timestamp" ]] || return 1 /usr/bin/date -u -d "$timestamp" +%s } _ksre_age_within() { local timestamp="$1" maximum="$2" now epoch age now="$(_ksre_now_epoch)" || return 1 epoch="$(_ksre_timestamp_epoch "$timestamp")" || return 1 [[ "$now" =~ ^[0-9]+$ && "$epoch" =~ ^[0-9]+$ && "$maximum" =~ ^[0-9]+$ ]] || return 1 age=$((now - epoch)) (( age >= 0 && age <= maximum )) } _ksre_bundle_equal() { local left_name="$1" right_name="$2" key local -n left="$left_name" right="$right_name" for key in schema bundle_id phase k3s_version datastore created_at_utc secret_count; do [[ "${left[$key]}" == "${right[$key]}" ]] || return 1 done } _ksre_read_root_file() { local path="$1" kind="$2" destination="$3" metadata temporary rc=0 /usr/bin/sudo --non-interactive -- /usr/bin/test ! -L "$path" || return 1 /usr/bin/sudo --non-interactive -- /usr/bin/test -f "$path" || return 1 /usr/bin/sudo --non-interactive -- /usr/bin/test -s "$path" || return 1 metadata="$(/usr/bin/sudo --non-interactive -- /usr/bin/stat \ --format='%u:%g:%a:%F' -- "$path" 2>/dev/null)" || return 1 [[ "$metadata" == '0:0:600:regular file' ]] || return 1 temporary="$(/usr/bin/mktemp /tmp/platform-k3s-restore-read.XXXXXX)" || return 1 /usr/bin/chmod 0600 "$temporary" || { /bin/rm -f -- "$temporary"; return 1; } /usr/bin/sudo --non-interactive -- /bin/cat -- "$path" >"$temporary" || rc=$? if (( rc == 0 )); then _ksre_parse_external_file "$kind" "$temporary" "$destination" || rc=$?; fi /bin/rm -f -- "$temporary" return "$rc" } _ksre_read_authoritative_bundle() { _ksre_read_root_file "$KSRE_POST_BUNDLE" bundle "$1"; } _ksre_read_installed_evidence() { _ksre_read_root_file "$KSRE_EVIDENCE" evidence "$1"; } _ksre_inventory_value() { local file="$1" key="$2" line found='' while IFS= read -r line; do [[ "$line" =~ ^([a-z_]+)=([A-Za-z0-9_.+-]+)$ ]] || return 1 if [[ "${BASH_REMATCH[1]}" == "$key" ]]; then [[ -z "$found" ]] || return 1 found="${BASH_REMATCH[2]}" fi done <"$file" [[ -n "$found" ]] || return 1 printf '%s\n' "$found" } _ksre_secret_count_from_stream() { /usr/bin/jq -er '.items | length' 2>/dev/null } _ksre_require_same_ready_node() { local local_node="$1" local_node_uid="$2" nodes_json="$3" [[ "$local_node" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] || return 1 [[ "$local_node_uid" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ ]] || return 1 /usr/bin/jq -e --arg local_node "$local_node" --arg local_node_uid "$local_node_uid" ' [.items[] | select(any(.status.conditions[]?; .type == "Ready" and .status == "True"))] | length == 1 and .[0].metadata.name == $local_node and .[0].metadata.uid == $local_node_uid ' >/dev/null 2>&1 <<<"$nodes_json" } _ksre_run_restore_status_validator() { /usr/bin/bash "$KSRE_STATUS_VALIDATOR" --expect-reencrypted-restore \ --verified-output-dir "$1" >/dev/null } _ksre_current_user_api_ready() { local context="$1" server="$2" /usr/bin/timeout --signal=TERM --kill-after=1s 9s \ /usr/bin/env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u NO_PROXY \ -u http_proxy -u https_proxy -u all_proxy -u no_proxy \ "$KSRE_KUBECTL" --context="$context" --server="$server" \ get --raw=/readyz >/dev/null 2>&1 } _ksre_current_user_nodes_json() { local context="$1" server="$2" /usr/bin/timeout --signal=TERM --kill-after=1s 15s \ /usr/bin/env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u NO_PROXY \ -u http_proxy -u https_proxy -u all_proxy -u no_proxy \ "$KSRE_KUBECTL" --context="$context" --server="$server" \ get nodes -o json 2>/dev/null } _ksre_current_user_secret_count() { local context="$1" server="$2" /usr/bin/timeout --signal=TERM --kill-after=1s 30s \ /usr/bin/env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u NO_PROXY \ -u http_proxy -u https_proxy -u all_proxy -u no_proxy \ "$KSRE_KUBECTL" --context="$context" --server="$server" \ get secrets --all-namespaces -o json 2>/dev/null | _ksre_secret_count_from_stream } _ksre_live_restore_inventory() { local context="$1" connection="$2" server local handoff inventory nodes version datastore encryption rotation hashes integrity local local_node local_node_uid local_node_ready local api_ready node_ready secret_count rc=0 _ksre_validate_current_connection "$connection" || return 1 server="${connection%|}" handoff="$(/usr/bin/mktemp -d /tmp/platform-k3s-encryption.XXXXXX)" || return 1 /usr/bin/chmod 0700 "$handoff" || { /bin/rm -rf -- "$handoff"; return 1; } if ! _ksre_run_restore_status_validator "$handoff"; then /bin/rm -rf -- "$handoff"; return 1 fi inventory="$handoff/inventory.env" version="$(_ksre_inventory_value "$inventory" version)" || rc=1 datastore="$(_ksre_inventory_value "$inventory" datastore)" || rc=1 encryption="$(_ksre_inventory_value "$inventory" encryption)" || rc=1 rotation="$(_ksre_inventory_value "$inventory" rotation)" || rc=1 hashes="$(_ksre_inventory_value "$inventory" server_hashes)" || rc=1 integrity="$(_ksre_inventory_value "$inventory" integrity)" || rc=1 local_node="$(_ksre_inventory_value "$inventory" node_name)" || rc=1 local_node_uid="$(_ksre_inventory_value "$inventory" node_uid)" || rc=1 local_node_ready="$(_ksre_inventory_value "$inventory" node_ready)" || rc=1 /bin/rm -rf -- "$handoff" (( rc == 0 )) || return 1 [[ "$encryption" == Enabled && "$local_node_ready" == Ready ]] || return 1 if _ksre_current_user_api_ready "$context" "$server"; then api_ready=pass; else return 1; fi nodes="$(_ksre_current_user_nodes_json "$context" "$server")" || return 1 _ksre_require_same_ready_node "$local_node" "$local_node_uid" "$nodes" || return 1 node_ready=pass secret_count="$(_ksre_current_user_secret_count "$context" "$server")" || return 1 [[ "$secret_count" =~ ^[0-9]+$ ]] || return 1 _ksre_require_same_connection "$context" "$connection" || return 1 printf '%s|%s|enabled_stable|%s|%s|%s|%s|%s|%s\n' \ "$version" "$datastore" "$rotation" "$hashes" "$integrity" \ "$api_ready" "$node_ready" "$secret_count" } _ksre_isolation_attested() { local context="$1" answer [[ -t 0 ]] || return 1 printf '격리·egress 차단 시험 완료 후 Type ISOLATED %s: ' "$context" >&2 IFS= read -r answer [[ "$answer" == "ISOLATED $context" ]] } _ksre_output_target_absent() { local output="$1" parent base [[ -n "$output" && ! -e "$output" && ! -L "$output" ]] || return 1 if [[ "$output" == */* ]]; then parent="${output%/*}"; base="${output##*/}" else parent='.'; base="$output"; fi [[ -n "$parent" && -n "$base" && "$base" != . && "$base" != .. ]] || return 1 [[ -d "$parent" && ! -L "$parent" ]] || return 1 } _ksre_write_output() { local output="$1" content="$2" kind="$3" parent base temporary _ksre_output_target_absent "$output" || return 1 if [[ "$output" == */* ]]; then parent="${output%/*}"; base="${output##*/}" else parent='.'; base="$output"; fi temporary="$(/usr/bin/mktemp "${parent}/.${base}.tmp.XXXXXX")" || return 1 /usr/bin/chmod 0600 "$temporary" || { /bin/rm -f -- "$temporary"; return 1; } printf '%s\n' "$content" >"$temporary" || { /bin/rm -f -- "$temporary"; return 1; } declare -A verification=() _ksre_parse_external_file "$kind" "$temporary" verification || { /bin/rm -f -- "$temporary"; return 1; } /bin/ln -- "$temporary" "$output" 2>/dev/null || { /bin/rm -f -- "$temporary"; return 1; } /bin/rm -f -- "$temporary" } _ksre_evidence_target_absent() { /usr/bin/sudo --non-interactive -- /usr/bin/test ! -e "$KSRE_EVIDENCE" && /usr/bin/sudo --non-interactive -- /usr/bin/test ! -L "$KSRE_EVIDENCE" } _ksre_prompt_destroyed() { local answer [[ -t 0 ]] || return 1 printf '일회용 환경과 bundle 복사본 파기 후 Type DESTROYED default: ' >&2 IFS= read -r answer [[ "$answer" == 'DESTROYED default' ]] } _ksre_install_evidence() { local content="$1" local_file root_temporary local_file="$(/usr/bin/mktemp /tmp/platform-k3s-restore-evidence.XXXXXX)" || return 1 /usr/bin/chmod 0600 "$local_file" || { /bin/rm -f -- "$local_file"; return 1; } printf '%s\n' "$content" >"$local_file" || { /bin/rm -f -- "$local_file"; return 1; } declare -A verification=() _ksre_parse_external_file evidence "$local_file" verification || { /bin/rm -f -- "$local_file"; return 1; } root_temporary="${KSRE_EVIDENCE}.new.$$" /usr/bin/sudo --non-interactive -- /usr/bin/test ! -e "$KSRE_EVIDENCE" || { /bin/rm -f -- "$local_file"; return 1; } /usr/bin/sudo --non-interactive -- /usr/bin/install -o root -g root -m 0600 -- \ "$local_file" "$root_temporary" || { /bin/rm -f -- "$local_file"; return 1; } /usr/bin/sudo --non-interactive -- /usr/bin/cmp --silent -- "$local_file" "$root_temporary" || { /usr/bin/sudo --non-interactive -- /bin/rm -f -- "$root_temporary" >/dev/null 2>&1 || : /bin/rm -f -- "$local_file"; return 1 } if ! /usr/bin/sudo --non-interactive -- /bin/ln -- "$root_temporary" "$KSRE_EVIDENCE" 2>/dev/null; then /usr/bin/sudo --non-interactive -- /bin/rm -f -- "$root_temporary" >/dev/null 2>&1 || : /bin/rm -f -- "$local_file"; return 1 fi /usr/bin/sudo --non-interactive -- /bin/rm -f -- "$root_temporary" || return 1 /bin/rm -f -- "$local_file" } _ksre_render_result() { local -n bundle_fields="$1" local tested_at="$2" printf 'schema=platform-k3s-restore-result-v1\nbundle_id=%s\nk3s_version=%s\ndatastore=%s\nstatus_class=enabled_stable\nrotation_stage=reencrypt_finished\nhashes=match\nlocal_integrity=match\napi_ready=pass\nnode_ready=pass\nisolation=pass\nsecret_count_match=pass\ntested_at_utc=%s\n' \ "${bundle_fields[bundle_id]}" "${bundle_fields[k3s_version]}" "${bundle_fields[datastore]}" "$tested_at" } _ksre_render_evidence() { local -n bundle_fields="$1" result_fields="$2" local recorded_at="$3" printf 'schema=platform-k3s-restore-evidence-v1\nbundle_id=%s\nk3s_version=%s\ndatastore=%s\nrotation_stage=reencrypt_finished\nlocal_integrity=match\nnode_ready=pass\nisolation=pass\ntested_at_utc=%s\nrecorded_at_utc=%s\ndestroyed=confirmed\n' \ "${bundle_fields[bundle_id]}" "${bundle_fields[k3s_version]}" "${bundle_fields[datastore]}" \ "${result_fields[tested_at_utc]}" "$recorded_at" } _ksre_emit_result() { local metadata="$1" output="$2" context connection live tested_at content local version datastore status rotation hashes integrity api node count declare -A bundle=() context="$(_ksre_current_context)" || return 1 [[ -n "$context" && "$context" != *$'\n'* ]] || return 1 connection="$(_ksre_current_connection "$context")" || return 1 _ksre_validate_current_connection "$connection" || return 1 _ksre_parse_external_file bundle "$metadata" bundle || return 1 [[ "${bundle[phase]}" == post ]] || return 1 _ksre_output_target_absent "$output" || return 1 live="$(_ksre_live_restore_inventory "$context" "$connection")" || return 1 IFS='|' read -r version datastore status rotation hashes integrity api node count <<<"$live" [[ "$version" == "${bundle[k3s_version]}" && "$datastore" == "${bundle[datastore]}" && "$status" == enabled_stable && "$rotation" == reencrypt_finished && "$hashes" == match && "$integrity" == match && "$api" == pass && "$node" == pass && "$count" == "${bundle[secret_count]}" ]] || return 1 _ksre_isolation_attested "$context" || return 1 _ksre_require_same_connection "$context" "$connection" || return 1 _ksre_require_same_context "$context" || return 1 tested_at="$(_ksre_now_rfc3339)" || return 1 content="$(_ksre_render_result bundle "$tested_at")" || return 1 _ksre_write_output "$output" "$content" result } _ksre_record() { local metadata="$1" result_file="$2" context recorded_at content declare -A bundle=() result=() authority=() verification=() context="$(_ksre_current_context)" || return 1 [[ -n "$context" && "$context" != *$'\n'* ]] || return 1 _ksre_parse_external_file bundle "$metadata" bundle || return 1 _ksre_parse_external_file result "$result_file" result || return 1 [[ "${bundle[phase]}" == post && "${result[bundle_id]}" == "${bundle[bundle_id]}" && "${result[k3s_version]}" == "${bundle[k3s_version]}" && "${result[datastore]}" == "${bundle[datastore]}" ]] || return 1 _ksre_age_within "${result[tested_at_utc]}" 86400 || return 1 _ksre_read_authoritative_bundle authority || return 1 _ksre_bundle_equal bundle authority || return 1 _ksre_evidence_target_absent || return 1 _ksre_prompt_destroyed || return 1 _ksre_require_same_context "$context" || return 1 recorded_at="$(_ksre_now_rfc3339)" || return 1 content="$(_ksre_render_evidence bundle result "$recorded_at")" || return 1 _ksre_parse_trusted_content evidence "$content" verification || return 1 _ksre_install_evidence "$content" } _ksre_check() { local context connection live version datastore status rotation hashes integrity api node count declare -A authority=() evidence=() context="$(_ksre_current_context)" || return 1 [[ -n "$context" && "$context" != *$'\n'* ]] || return 1 connection="$(_ksre_current_connection "$context")" || return 1 _ksre_validate_current_connection "$connection" || return 1 _ksre_read_authoritative_bundle authority || return 1 _ksre_read_installed_evidence evidence || return 1 [[ "${authority[phase]}" == post && "${evidence[bundle_id]}" == "${authority[bundle_id]}" && "${evidence[k3s_version]}" == "${authority[k3s_version]}" && "${evidence[datastore]}" == "${authority[datastore]}" ]] || return 1 _ksre_age_within "${evidence[tested_at_utc]}" 2592000 || return 1 live="$(_ksre_live_restore_inventory "$context" "$connection")" || return 1 IFS='|' read -r version datastore status rotation hashes integrity api node count <<<"$live" [[ "$version" == "${evidence[k3s_version]}" && "$datastore" == "${evidence[datastore]}" && "$status" == enabled_stable && "$rotation" == reencrypt_finished && "$hashes" == match && "$integrity" == match && "$api" == pass && "$node" == pass ]] || return 1 _ksre_require_same_context "$context" } k3s_secret_encryption_restore_evidence_main() { local mode='' metadata='' result_file='' output='' while (( $# > 0 )); do case "$1" in --emit-result|--record|--check) [[ -z "$mode" ]] || { _ksre_usage >&2; return 2; } mode="$1"; shift ;; --bundle-metadata) [[ $# -ge 2 && -z "$metadata" ]] || { _ksre_usage >&2; return 2; } metadata="$2"; shift 2 ;; --result-file) [[ $# -ge 2 && -z "$result_file" ]] || { _ksre_usage >&2; return 2; } result_file="$2"; shift 2 ;; --output) [[ $# -ge 2 && -z "$output" ]] || { _ksre_usage >&2; return 2; } output="$2"; shift 2 ;; -h|--help) _ksre_usage; return 0 ;; *) _ksre_usage >&2; return 2 ;; esac done case "$mode" in --emit-result) [[ -n "$metadata" && -n "$output" && -z "$result_file" ]] || { _ksre_usage >&2; return 2; } _ksre_emit_result "$metadata" "$output" || _ksre_fail ;; --record) [[ -n "$metadata" && -n "$result_file" && -z "$output" ]] || { _ksre_usage >&2; return 2; } _ksre_record "$metadata" "$result_file" || _ksre_fail ;; --check) [[ -z "$metadata" && -z "$result_file" && -z "$output" ]] || { _ksre_usage >&2; return 2; } _ksre_check || _ksre_fail ;; *) _ksre_usage >&2; return 2 ;; esac } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then PATH="$KSRE_SECURE_PATH" export PATH k3s_secret_encryption_restore_evidence_main "$@" fi