Files
platform-core/scripts/validate/k3s-secret-encryption-restore-evidence.sh

637 lines
26 KiB
Bash
Executable File

#!/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