Add platform infrastructure configuration
This commit is contained in:
+636
@@ -0,0 +1,636 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user