706 lines
30 KiB
Bash
Executable File
706 lines
30 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# k3s Secret 복구 증거의 parser, 세 mode와 권한 경계를 검증한다.
|
|
# 이 파일은 source-safe 함수만 호출하며 live host나 Kubernetes API를 건드리지 않는다.
|
|
set -Eeuo pipefail
|
|
|
|
readonly TEST_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)"
|
|
readonly BOOTSTRAP_PATH="${TEST_ROOT}/scripts/bootstrap/apply-k3s-secret-encryption.sh"
|
|
readonly RESTORE_VALIDATOR_PATH="${TEST_ROOT}/scripts/validate/k3s-secret-encryption-restore-evidence.sh"
|
|
|
|
fail() {
|
|
printf 'RESTORE EVIDENCE TEST FAILURE: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
assert_eq() {
|
|
local expected="$1" actual="$2" description="$3"
|
|
[[ "$actual" == "$expected" ]] || fail "$description"
|
|
}
|
|
|
|
# 잘못된 legacy field를 쓰는 producer 변경을 잡는다. 기대값은 Task 5 계약에서
|
|
# 손으로 적었으며 production parser나 formatter로 만들지 않는다.
|
|
probe_canonical_bundle_metadata_producer() (
|
|
# shellcheck source=/dev/null
|
|
source "$BOOTSTRAP_PATH"
|
|
declare -F _kseb_render_bundle_metadata >/dev/null || return 1
|
|
local actual
|
|
actual="$(_kseb_render_bundle_metadata \
|
|
'01234567-89ab-4cde-8fab-0123456789ab' post embedded-etcd \
|
|
'2026-08-01T00:00:00Z' 7 'v1.36.2+k3s1')" || return 1
|
|
assert_eq $'schema=platform-k3s-bundle-v1\nbundle_id=01234567-89ab-4cde-8fab-0123456789ab\nphase=post\nk3s_version=v1.36.2+k3s1\ndatastore=embedded-etcd\ncreated_at_utc=2026-08-01T00:00:00Z\nsecret_count=7' \
|
|
"$actual" 'canonical bundle metadata fields/order'
|
|
)
|
|
|
|
probe_canonical_bundle_metadata_producer || fail 'canonical bundle metadata producer'
|
|
|
|
# 아래 production 변경이 깨질 때 잡는 결함은 allowlist parser 우회, mode 간
|
|
# 검증 누락, sudo 전 입력 검증 누락과 기존 evidence overwrite다.
|
|
[[ -f "$RESTORE_VALIDATOR_PATH" ]] || fail 'restore evidence validator is absent'
|
|
# shellcheck source=/dev/null
|
|
source "$RESTORE_VALIDATOR_PATH"
|
|
|
|
fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/k3s-restore-evidence-test.XXXXXX")"
|
|
cleanup() {
|
|
case "$fixture_root" in
|
|
/tmp/k3s-restore-evidence-test.*|"${TMPDIR:-/tmp}"/k3s-restore-evidence-test.*)
|
|
rm -rf -- "$fixture_root"
|
|
;;
|
|
*) fail 'unexpected fixture root' ;;
|
|
esac
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
readonly FIXTURE_BUNDLE_ID='01234567-89ab-4cde-8fab-0123456789ab'
|
|
readonly FIXTURE_OTHER_ID='11234567-89ab-4cde-8fab-0123456789ab'
|
|
readonly FIXTURE_LOCAL_NODE_UID='aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'
|
|
readonly FIXTURE_OTHER_NODE_UID='11111111-2222-4333-8444-555555555555'
|
|
readonly FIXTURE_TESTED_AT='2026-08-01T00:00:00Z'
|
|
readonly FIXTURE_RECORDED_AT='2026-08-01T01:00:00Z'
|
|
|
|
# Fix round 1 / finding 4: invalid compact timestamp는 production transaction의
|
|
# directory/backup/marker mutation 전에 거부해야 한다.
|
|
probe_invalid_timestamp_precedes_transaction_mutation() (
|
|
local parent="${fixture_root}/invalid-stamp" root data fake_k3s command_log rc=0
|
|
parent="$(mktemp -d "${fixture_root}/invalid-stamp.XXXXXX")"
|
|
root="$parent/recovery"; data="$parent/data"; fake_k3s="$parent/k3s"
|
|
command_log="$parent/commands.log"
|
|
mkdir -p "$root" "$data/server/cred"
|
|
chmod 0700 "$root"
|
|
printf 'fixture\n' >"$data/server/token"
|
|
printf '{}\n' >"$data/server/cred/encryption-config.json"
|
|
: >"$command_log"
|
|
printf '%s\n' '#!/usr/bin/env bash' \
|
|
'printf "called\n" >>"${KSEB_INVALID_STAMP_LOG:?}"' \
|
|
'exit 1' >"$fake_k3s"
|
|
chmod 0700 "$fake_k3s"
|
|
# shellcheck source=/dev/null
|
|
source "$BOOTSTRAP_PATH"
|
|
KSEB_BACKUP_ROOT_IDENTITY="$(stat --format='%d:%i' -- "$root")"
|
|
_kseb_run_privileged_shell() { /usr/bin/bash "$@"; }
|
|
_kseb_transaction_data_dir() { printf '%s\n' "$data"; }
|
|
_kseb_transaction_config_dir() { printf '%s\n' "$parent/no-config"; }
|
|
_kseb_transaction_systemd_paths() { :; }
|
|
_kseb_transaction_k3s_binary() { printf '%s\n' "$fake_k3s"; }
|
|
_kseb_transaction_post_marker() { printf '%s\n' "$parent/post-marker"; }
|
|
_kseb_transaction_restore_evidence() { printf '%s\n' "$parent/restore-evidence"; }
|
|
export KSEB_INVALID_STAMP_LOG="$command_log"
|
|
_kseb_pinned_phase_transaction "$root" bundle/pre embedded-etcd \
|
|
"$FIXTURE_BUNDLE_ID" pre 20261301T000000Z 7 v1.36.2+k3s1 \
|
|
>/dev/null 2>&1 || rc=$?
|
|
[[ "$rc" -ne 0 && ! -e "$root/bundle" && ! -s "$command_log" ]]
|
|
)
|
|
|
|
write_metadata() {
|
|
local path="$1" phase="${2:-post}" bundle_id="${3:-$FIXTURE_BUNDLE_ID}"
|
|
local version="${4:-v1.36.2+k3s1}" datastore="${5:-embedded-etcd}"
|
|
local created="${6:-2026-07-31T23:00:00Z}" count="${7:-7}"
|
|
printf 'schema=platform-k3s-bundle-v1\nbundle_id=%s\nphase=%s\nk3s_version=%s\ndatastore=%s\ncreated_at_utc=%s\nsecret_count=%s\n' \
|
|
"$bundle_id" "$phase" "$version" "$datastore" "$created" "$count" >"$path"
|
|
chmod 0600 "$path"
|
|
}
|
|
|
|
write_result() {
|
|
local path="$1" bundle_id="${2:-$FIXTURE_BUNDLE_ID}" version="${3:-v1.36.2+k3s1}"
|
|
local datastore="${4:-embedded-etcd}" node="${5:-pass}" isolation="${6:-pass}"
|
|
local integrity="${7:-match}" count="${8:-pass}" tested="${9:-$FIXTURE_TESTED_AT}"
|
|
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=%s\napi_ready=pass\nnode_ready=%s\nisolation=%s\nsecret_count_match=%s\ntested_at_utc=%s\n' \
|
|
"$bundle_id" "$version" "$datastore" "$integrity" "$node" "$isolation" "$count" "$tested" >"$path"
|
|
chmod 0600 "$path"
|
|
}
|
|
|
|
write_evidence() {
|
|
local path="$1" bundle_id="${2:-$FIXTURE_BUNDLE_ID}" version="${3:-v1.36.2+k3s1}"
|
|
local datastore="${4:-embedded-etcd}" integrity="${5:-match}" destroyed="${6:-confirmed}"
|
|
local tested="${7:-$FIXTURE_TESTED_AT}" recorded="${8:-$FIXTURE_RECORDED_AT}"
|
|
printf 'schema=platform-k3s-restore-evidence-v1\nbundle_id=%s\nk3s_version=%s\ndatastore=%s\nrotation_stage=reencrypt_finished\nlocal_integrity=%s\nnode_ready=pass\nisolation=pass\ntested_at_utc=%s\nrecorded_at_utc=%s\ndestroyed=%s\n' \
|
|
"$bundle_id" "$version" "$datastore" "$integrity" "$tested" "$recorded" "$destroyed" >"$path"
|
|
chmod 0600 "$path"
|
|
}
|
|
|
|
assert_fails() {
|
|
if "$@" >/dev/null 2>&1; then fail "expected failure: $*"; fi
|
|
}
|
|
|
|
assert_succeeds() {
|
|
"$@" || fail "expected success: $*"
|
|
}
|
|
|
|
assert_succeeds probe_invalid_timestamp_precedes_transaction_mutation
|
|
|
|
metadata="${fixture_root}/bundle.env"
|
|
result="${fixture_root}/result.env"
|
|
evidence_fixture="${fixture_root}/evidence.env"
|
|
write_metadata "$metadata"
|
|
write_result "$result"
|
|
write_evidence "$evidence_fixture"
|
|
|
|
probe_parse() (
|
|
local kind="$1" path="$2"
|
|
declare -A parsed=()
|
|
_ksre_parse_external_file "$kind" "$path" parsed
|
|
)
|
|
|
|
assert_succeeds probe_parse bundle "$metadata"
|
|
assert_succeeds probe_parse result "$result"
|
|
assert_succeeds probe_parse evidence "$evidence_fixture"
|
|
|
|
# Fix round 1 / finding 1: local restore validator와 current-user API가
|
|
# 서로 다른 cluster를 보면 production identity gate가 실패해야 한다.
|
|
matching_nodes='{"items":[{"metadata":{"name":"isolated-ready","uid":"aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}'
|
|
mismatched_nodes='{"items":[{"metadata":{"name":"other-ready","uid":"aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}'
|
|
multiple_ready_nodes='{"items":[{"metadata":{"name":"isolated-ready","uid":"aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"},"status":{"conditions":[{"type":"Ready","status":"True"}]}},{"metadata":{"name":"other-ready","uid":"11111111-2222-4333-8444-555555555555"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}'
|
|
assert_succeeds _ksre_require_same_ready_node isolated-ready "$FIXTURE_LOCAL_NODE_UID" "$matching_nodes"
|
|
assert_fails _ksre_require_same_ready_node isolated-ready "$FIXTURE_LOCAL_NODE_UID" "$mismatched_nodes"
|
|
assert_fails _ksre_require_same_ready_node isolated-ready "$FIXTURE_LOCAL_NODE_UID" "$multiple_ready_nodes"
|
|
|
|
# Fix round 2 / finding 1: 같은 UID가 복제된 다른 API도 허용하지 않도록
|
|
# current-user context의 effective endpoint는 exact loopback이며 proxy가 없어야 한다.
|
|
assert_succeeds _ksre_validate_current_connection 'https://127.0.0.1:6443|'
|
|
assert_fails _ksre_validate_current_connection 'https://192.0.2.10:6443|'
|
|
assert_fails _ksre_validate_current_connection 'https://localhost:6443|'
|
|
assert_fails _ksre_validate_current_connection 'https://127.0.0.1:6443/|'
|
|
assert_fails _ksre_validate_current_connection 'https://127.0.0.1:6443|http://127.0.0.1:8080'
|
|
|
|
probe_live_inventory_cluster_binding() (
|
|
local api_node="$1" api_node_uid="$2" output
|
|
declare -F _ksre_current_connection >/dev/null || return 1
|
|
declare -F _ksre_require_same_connection >/dev/null || return 1
|
|
declare -F _ksre_run_restore_status_validator >/dev/null || return 1
|
|
declare -F _ksre_current_user_api_ready >/dev/null || return 1
|
|
declare -F _ksre_current_user_nodes_json >/dev/null || return 1
|
|
declare -F _ksre_current_user_secret_count >/dev/null || return 1
|
|
_ksre_run_restore_status_validator() {
|
|
local handoff="$1"
|
|
printf 'version=v1.36.2+k3s1\nserver_count=1\nnode_name=isolated-ready\nnode_uid=%s\nnode_ready=Ready\ndatastore=embedded-etcd\nencryption=Enabled\nrotation=reencrypt_finished\nserver_hashes=match\nintegrity=match\napi_readyz=pass\n' \
|
|
"$FIXTURE_LOCAL_NODE_UID" \
|
|
>"$handoff/inventory.env"
|
|
chmod 0600 "$handoff/inventory.env"
|
|
}
|
|
_ksre_current_connection() {
|
|
[[ "${1:-}" == fixture-context ]] || return 1
|
|
printf 'https://127.0.0.1:6443|\n'
|
|
}
|
|
_ksre_current_user_api_ready() {
|
|
[[ "${1:-}" == fixture-context && "${2:-}" == https://127.0.0.1:6443 ]]
|
|
}
|
|
_ksre_current_user_nodes_json() {
|
|
[[ "${1:-}" == fixture-context && "${2:-}" == https://127.0.0.1:6443 ]] || return 1
|
|
printf '{"items":[{"metadata":{"name":"%s","uid":"%s"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}\n' \
|
|
"$api_node" "$api_node_uid"
|
|
}
|
|
_ksre_current_user_secret_count() {
|
|
[[ "${1:-}" == fixture-context && "${2:-}" == https://127.0.0.1:6443 ]] || return 1
|
|
printf '7\n'
|
|
}
|
|
output="$(_ksre_live_restore_inventory fixture-context 'https://127.0.0.1:6443|')" || return 1
|
|
[[ "$output" == 'v1.36.2+k3s1|embedded-etcd|enabled_stable|reencrypt_finished|match|match|pass|pass|7' ]]
|
|
)
|
|
|
|
assert_succeeds probe_live_inventory_cluster_binding isolated-ready "$FIXTURE_LOCAL_NODE_UID"
|
|
assert_fails probe_live_inventory_cluster_binding isolated-ready "$FIXTURE_OTHER_NODE_UID"
|
|
assert_fails probe_live_inventory_cluster_binding other-ready "$FIXTURE_LOCAL_NODE_UID"
|
|
|
|
# Fix round 1 / finding 2: Bash read가 버리는 NUL도 raw byte 단계에서 거부한다.
|
|
nul_metadata="${fixture_root}/metadata-nul.env"
|
|
{
|
|
printf 'schema=platform-k3s-bundle-v1\n'
|
|
printf 'bundle_id=%s\n' "$FIXTURE_BUNDLE_ID"
|
|
printf 'phase=po\0st\n'
|
|
printf 'k3s_version=v1.36.2+k3s1\n'
|
|
printf 'datastore=embedded-etcd\n'
|
|
printf 'created_at_utc=2026-07-31T23:00:00Z\n'
|
|
printf 'secret_count=7\n'
|
|
} >"$nul_metadata"
|
|
chmod 0600 "$nul_metadata"
|
|
assert_fails probe_parse bundle "$nul_metadata"
|
|
|
|
# Fix round 1 / finding 3: pathname이 아니라 실제로 열린 FD identity를 검사한다.
|
|
probe_pinned_external_fd_survives_path_swap() (
|
|
local input="${fixture_root}/toctou-valid.env" moved="${fixture_root}/toctou-valid-opened.env"
|
|
local input_fd
|
|
write_metadata "$input"
|
|
exec {input_fd}<"$input"
|
|
/bin/mv -- "$input" "$moved"
|
|
printf 'unsafe replacement\n' >"$input"
|
|
chmod 0644 "$input"
|
|
declare -A parsed=()
|
|
_ksre_parse_external_fd bundle "$input_fd" parsed || return 1
|
|
exec {input_fd}<&-
|
|
[[ "${parsed[bundle_id]}" == "$FIXTURE_BUNDLE_ID" ]]
|
|
)
|
|
|
|
probe_unsafe_opened_fd_cannot_borrow_safe_path() (
|
|
local input="${fixture_root}/toctou-unsafe.env" moved="${fixture_root}/toctou-unsafe-opened.env"
|
|
local input_fd
|
|
write_metadata "$input"
|
|
chmod 0644 "$input"
|
|
exec {input_fd}<"$input"
|
|
/bin/mv -- "$input" "$moved"
|
|
write_metadata "$input"
|
|
declare -A parsed=()
|
|
if _ksre_parse_external_fd bundle "$input_fd" parsed; then return 1; fi
|
|
exec {input_fd}<&-
|
|
)
|
|
|
|
assert_succeeds probe_pinned_external_fd_survives_path_swap
|
|
assert_succeeds probe_unsafe_opened_fd_cannot_borrow_safe_path
|
|
|
|
# Fix round 2 / finding 2: production final-component open의 정확한 순간을
|
|
# test-only interposer로 멈춘다. precheck 뒤 symlink로 바꾸고 open 뒤 같은
|
|
# target inode의 hard link로 되돌려도 parser가 반드시 실패해야 한다.
|
|
race_interposer_source="${fixture_root}/nofollow-race.c"
|
|
race_interposer="${fixture_root}/nofollow-race.so"
|
|
cat >"$race_interposer_source" <<'RACE_INTERPOSER'
|
|
#define _GNU_SOURCE
|
|
#include <dlfcn.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
static int triggered;
|
|
static int (*real_open_fn)(const char *, int, ...);
|
|
static int (*real_open64_fn)(const char *, int, ...);
|
|
static int (*real_openat_fn)(int, const char *, int, ...);
|
|
static int (*real_openat64_fn)(int, const char *, int, ...);
|
|
|
|
static void resolve_symbols(void) {
|
|
if (!real_open_fn) real_open_fn = dlsym(RTLD_NEXT, "open");
|
|
if (!real_open64_fn) real_open64_fn = dlsym(RTLD_NEXT, "open64");
|
|
if (!real_openat_fn) real_openat_fn = dlsym(RTLD_NEXT, "openat");
|
|
if (!real_openat64_fn) real_openat64_fn = dlsym(RTLD_NEXT, "openat64");
|
|
}
|
|
|
|
static void marker_path(char *buffer, size_t size, const char *name) {
|
|
const char *directory = getenv("KSRE_RACE_SYNC");
|
|
if (!directory || snprintf(buffer, size, "%s/%s", directory, name) >= (int)size) _exit(125);
|
|
}
|
|
|
|
static void touch_marker(const char *name) {
|
|
char path[PATH_MAX];
|
|
int fd;
|
|
marker_path(path, sizeof(path), name);
|
|
resolve_symbols();
|
|
fd = real_open_fn(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
|
if (fd < 0) _exit(125);
|
|
dprintf(fd, "%ld\n", (long)getpid());
|
|
close(fd);
|
|
}
|
|
|
|
static void wait_for_marker(const char *name) {
|
|
char path[PATH_MAX];
|
|
marker_path(path, sizeof(path), name);
|
|
while (access(path, F_OK) != 0) usleep(1000);
|
|
}
|
|
|
|
static int should_race(const char *path) {
|
|
const char *target = getenv("KSRE_RACE_PATH");
|
|
if (!triggered && target && path && strcmp(path, target) == 0) {
|
|
triggered = 1;
|
|
touch_marker("entered-open");
|
|
wait_for_marker("continue-open");
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void after_open(void) {
|
|
touch_marker("opened");
|
|
wait_for_marker("continue-postcheck");
|
|
}
|
|
|
|
int open(const char *path, int flags, ...) {
|
|
mode_t mode = 0;
|
|
int raced, result;
|
|
va_list arguments;
|
|
resolve_symbols();
|
|
if ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) {
|
|
va_start(arguments, flags); mode = va_arg(arguments, mode_t); va_end(arguments);
|
|
}
|
|
raced = should_race(path);
|
|
result = ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE))
|
|
? real_open_fn(path, flags, mode) : real_open_fn(path, flags);
|
|
if (raced) after_open();
|
|
return result;
|
|
}
|
|
|
|
int open64(const char *path, int flags, ...) {
|
|
mode_t mode = 0;
|
|
int raced, result;
|
|
va_list arguments;
|
|
resolve_symbols();
|
|
if ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) {
|
|
va_start(arguments, flags); mode = va_arg(arguments, mode_t); va_end(arguments);
|
|
}
|
|
raced = should_race(path);
|
|
if (!real_open64_fn) real_open64_fn = real_open_fn;
|
|
result = ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE))
|
|
? real_open64_fn(path, flags, mode) : real_open64_fn(path, flags);
|
|
if (raced) after_open();
|
|
return result;
|
|
}
|
|
|
|
int openat(int directory_fd, const char *path, int flags, ...) {
|
|
mode_t mode = 0;
|
|
int raced, result;
|
|
va_list arguments;
|
|
resolve_symbols();
|
|
if ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) {
|
|
va_start(arguments, flags); mode = va_arg(arguments, mode_t); va_end(arguments);
|
|
}
|
|
raced = should_race(path);
|
|
result = ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE))
|
|
? real_openat_fn(directory_fd, path, flags, mode) : real_openat_fn(directory_fd, path, flags);
|
|
if (raced) after_open();
|
|
return result;
|
|
}
|
|
|
|
int openat64(int directory_fd, const char *path, int flags, ...) {
|
|
mode_t mode = 0;
|
|
int raced, result;
|
|
va_list arguments;
|
|
resolve_symbols();
|
|
if ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE)) {
|
|
va_start(arguments, flags); mode = va_arg(arguments, mode_t); va_end(arguments);
|
|
}
|
|
raced = should_race(path);
|
|
if (!real_openat64_fn) real_openat64_fn = real_openat_fn;
|
|
result = ((flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE))
|
|
? real_openat64_fn(directory_fd, path, flags, mode) : real_openat64_fn(directory_fd, path, flags);
|
|
if (raced) after_open();
|
|
return result;
|
|
}
|
|
RACE_INTERPOSER
|
|
/usr/bin/cc -shared -fPIC -O2 -o "$race_interposer" "$race_interposer_source" -ldl ||
|
|
fail 'compile no-follow race interposer'
|
|
|
|
wait_for_race_marker() {
|
|
local marker="$1" process_id="$2" attempt
|
|
for ((attempt = 0; attempt < 5000; attempt++)); do
|
|
[[ -e "$marker" ]] && return 0
|
|
/bin/kill -0 "$process_id" 2>/dev/null || return 1
|
|
/usr/bin/sleep 0.001
|
|
done
|
|
return 1
|
|
}
|
|
|
|
probe_open_instant_symlink_replacement_rejected() (
|
|
local target="${fixture_root}/race-target.env" input="${fixture_root}/race-input.env"
|
|
local original="${fixture_root}/race-input-original.env" sync="${fixture_root}/race-sync"
|
|
local process_id parse_rc=0
|
|
write_metadata "$target"
|
|
write_metadata "$input"
|
|
mkdir "$sync"
|
|
/usr/bin/env KSRE_RACE_PATH="$input" KSRE_RACE_SYNC="$sync" LD_PRELOAD="$race_interposer" \
|
|
/usr/bin/timeout --signal=TERM --kill-after=1s 10s \
|
|
/usr/bin/bash -c '
|
|
set -Eeuo pipefail
|
|
source "$1"
|
|
declare -A parsed=()
|
|
_ksre_parse_external_file bundle "$2" parsed
|
|
' bash "$RESTORE_VALIDATOR_PATH" "$input" &
|
|
process_id=$!
|
|
if ! wait_for_race_marker "$sync/entered-open" "$process_id"; then
|
|
: >"$sync/continue-open"; : >"$sync/continue-postcheck"
|
|
wait "$process_id" 2>/dev/null || :
|
|
return 1
|
|
fi
|
|
/bin/mv -- "$input" "$original"
|
|
/bin/ln -s -- "$target" "$input"
|
|
: >"$sync/continue-open"
|
|
if ! wait_for_race_marker "$sync/opened" "$process_id"; then
|
|
: >"$sync/continue-postcheck"
|
|
wait "$process_id" 2>/dev/null || :
|
|
return 1
|
|
fi
|
|
/bin/rm -- "$input"
|
|
/bin/ln -- "$target" "$input"
|
|
: >"$sync/continue-postcheck"
|
|
if wait "$process_id"; then parse_rc=0; else parse_rc=$?; fi
|
|
[[ "$parse_rc" -ne 0 ]]
|
|
)
|
|
|
|
assert_succeeds probe_open_instant_symlink_replacement_rejected
|
|
|
|
# Fix round 3: production broker가 readiness 전에 멈춰도 parent 자체 상한 안에
|
|
# nonzero로 끝나고 partial success output과 살아 있는 broker를 남기면 안 된다.
|
|
probe_broker_readiness_delay_is_bounded() (
|
|
local input="${fixture_root}/broker-delay.env" sync="${fixture_root}/broker-delay-sync"
|
|
local partial_output="${fixture_root}/broker-delay.out" broker_process='' parser_rc=0 attempt
|
|
write_metadata "$input"
|
|
mkdir "$sync"
|
|
/usr/bin/env KSRE_RACE_PATH="$input" KSRE_RACE_SYNC="$sync" LD_PRELOAD="$race_interposer" \
|
|
/usr/bin/timeout --signal=TERM --kill-after=0.5s 4s \
|
|
/usr/bin/bash -c '
|
|
set -Eeuo pipefail
|
|
source "$1"
|
|
declare -A parsed=()
|
|
if _ksre_parse_external_file bundle "$2" parsed; then
|
|
printf "unexpected-success\n"
|
|
exit 0
|
|
fi
|
|
exit 23
|
|
' bash "$RESTORE_VALIDATOR_PATH" "$input" >"$partial_output" 2>/dev/null || parser_rc=$?
|
|
[[ -s "$sync/entered-open" ]] || return 1
|
|
broker_process="$(<"$sync/entered-open")"
|
|
[[ "$broker_process" =~ ^[0-9]+$ ]] || return 1
|
|
: >"$sync/continue-open"
|
|
: >"$sync/continue-postcheck"
|
|
for ((attempt = 0; attempt < 1000; attempt++)); do
|
|
/bin/kill -0 "$broker_process" 2>/dev/null || break
|
|
/usr/bin/sleep 0.001
|
|
done
|
|
if [[ "$parser_rc" == 124 || "$parser_rc" == 137 ]]; then
|
|
printf 'broker readiness required the outer watchdog\n' >&2
|
|
return 1
|
|
fi
|
|
[[ "$parser_rc" -ne 0 && ! -s "$partial_output" ]]
|
|
! /bin/kill -0 "$broker_process" 2>/dev/null
|
|
)
|
|
|
|
assert_succeeds probe_broker_readiness_delay_is_bounded
|
|
|
|
# Fix round 2 pre-review concern: writer가 없는 FIFO는 regular-file 검사 전에
|
|
# block하지 않고 즉시 거부되어야 한다. timeout은 실패가 아니라 RED 증거다.
|
|
probe_fifo_rejected_without_blocking() (
|
|
local fifo="${fixture_root}/external-input.fifo" parser_rc=0
|
|
/usr/bin/mkfifo -- "$fifo"
|
|
/usr/bin/chmod 0600 "$fifo"
|
|
/usr/bin/timeout --signal=TERM --kill-after=0.25s 0.25s \
|
|
/usr/bin/bash -c '
|
|
set -Eeuo pipefail
|
|
source "$1"
|
|
declare -A parsed=()
|
|
_ksre_parse_external_file bundle "$2" parsed
|
|
' bash "$RESTORE_VALIDATOR_PATH" "$fifo" >/dev/null 2>&1 || parser_rc=$?
|
|
if [[ "$parser_rc" == 124 || "$parser_rc" == 137 ]]; then
|
|
/usr/bin/timeout --signal=TERM --kill-after=0.25s 1s \
|
|
/usr/bin/bash -c 'printf x >"$1"' bash "$fifo" >/dev/null 2>&1 || :
|
|
printf 'FIFO parser blocked before regular-file rejection\n' >&2
|
|
return 1
|
|
fi
|
|
[[ "$parser_rc" -ne 0 ]]
|
|
)
|
|
|
|
assert_succeeds probe_fifo_rejected_without_blocking
|
|
|
|
secret_stream="${fixture_root}/secret-stream.json"
|
|
secret_trace="${fixture_root}/secret-stream.trace"
|
|
printf '{"items":[{"data":{"value":"fixture-sensitive-payload"}}]}\n' >"$secret_stream"
|
|
probe_stream_secret_count() (
|
|
exec 9>"$secret_trace"
|
|
BASH_XTRACEFD=9
|
|
set -x
|
|
local count
|
|
count="$(_ksre_secret_count_from_stream <"$secret_stream")"
|
|
set +x
|
|
[[ "$count" == 1 ]]
|
|
)
|
|
assert_succeeds probe_stream_secret_count
|
|
[[ "$(<"$secret_trace")" != *fixture-sensitive-payload* ]] || fail 'Secret payload reached xtrace'
|
|
|
|
# Parser/security matrix. 각 fixture는 parser의 실제 branch를 지나며 privileged
|
|
# helper가 한 번이라도 호출되면 별도 marker를 남기도록 mode fixture에서 확인한다.
|
|
for attack in command_substitution backtick duplicate unknown empty control malformed trailing; do
|
|
candidate="${fixture_root}/metadata-${attack}.env"
|
|
write_metadata "$candidate"
|
|
case "$attack" in
|
|
command_substitution) /usr/bin/sed -i '$c\secret_count=$(id)' "$candidate" ;;
|
|
backtick) /usr/bin/sed -i '$c\secret_count=`id`' "$candidate" ;;
|
|
duplicate) printf 'phase=post\n' >>"$candidate" ;;
|
|
unknown) printf 'credential=forbidden\n' >>"$candidate" ;;
|
|
empty) /usr/bin/sed -i 's/^phase=.*/phase=/' "$candidate" ;;
|
|
control) /usr/bin/sed -i 's/^phase=.*/phase=post\r/' "$candidate" ;;
|
|
malformed) printf 'not-an-assignment\n' >>"$candidate" ;;
|
|
trailing) printf 'trailing-data\n' >>"$candidate" ;;
|
|
esac
|
|
assert_fails probe_parse bundle "$candidate"
|
|
done
|
|
|
|
empty_file="${fixture_root}/empty.env"; : >"$empty_file"; chmod 0600 "$empty_file"
|
|
assert_fails probe_parse bundle "$empty_file"
|
|
mode_file="${fixture_root}/mode.env"; write_metadata "$mode_file"; chmod 0644 "$mode_file"
|
|
assert_fails probe_parse bundle "$mode_file"
|
|
symlink_file="${fixture_root}/link.env"; ln -s -- "$metadata" "$symlink_file"
|
|
assert_fails probe_parse bundle "$symlink_file"
|
|
|
|
probe_mode_rejects_before_privilege() (
|
|
local path="$1" marker="${fixture_root}/unexpected-privilege"
|
|
_ksre_current_context() { printf 'default\n'; }
|
|
_ksre_read_authoritative_bundle() { : >"$marker"; return 1; }
|
|
_ksre_install_evidence() { : >"$marker"; return 1; }
|
|
_ksre_record "$path" "$result" <<<"DESTROYED default" >/dev/null 2>&1 && return 1
|
|
[[ ! -e "$marker" ]]
|
|
)
|
|
for attack in command_substitution backtick duplicate unknown control malformed trailing; do
|
|
assert_succeeds probe_mode_rejects_before_privilege "${fixture_root}/metadata-${attack}.env"
|
|
done
|
|
assert_succeeds probe_mode_rejects_before_privilege "$mode_file"
|
|
assert_succeeds probe_mode_rejects_before_privilege "$symlink_file"
|
|
|
|
mode_events="${fixture_root}/mode-events.log"
|
|
installed_evidence="${fixture_root}/installed-evidence.env"
|
|
|
|
install_mode_fakes() {
|
|
: >"$mode_events"
|
|
rm -f -- "$installed_evidence"
|
|
active_evidence_file="$evidence_fixture"
|
|
_ksre_current_context() { printf 'context\n' >>"$mode_events"; printf 'default\n'; }
|
|
_ksre_current_connection() { printf 'connection\n' >>"$mode_events"; printf 'https://127.0.0.1:6443|\n'; }
|
|
_ksre_require_same_context() { printf 'context-recheck\n' >>"$mode_events"; }
|
|
_ksre_now_rfc3339() { printf '%s\n' "$FIXTURE_RECORDED_AT"; }
|
|
_ksre_now_epoch() { printf '1785546000\n'; }
|
|
_ksre_live_restore_inventory() {
|
|
printf 'live\n' >>"$mode_events"
|
|
printf 'v1.36.2+k3s1|embedded-etcd|enabled_stable|reencrypt_finished|match|match|pass|pass|7\n'
|
|
}
|
|
_ksre_isolation_attested() { printf 'isolation\n' >>"$mode_events"; }
|
|
_ksre_read_authoritative_bundle() {
|
|
local destination="$1"
|
|
printf 'authority\n' >>"$mode_events"
|
|
_ksre_parse_trusted_content bundle "$(<"$metadata")" "$destination"
|
|
}
|
|
_ksre_read_installed_evidence() {
|
|
local destination="$1"
|
|
printf 'evidence\n' >>"$mode_events"
|
|
_ksre_parse_trusted_content evidence "$(<"$active_evidence_file")" "$destination"
|
|
}
|
|
_ksre_evidence_target_absent() { printf 'target-check\n' >>"$mode_events"; }
|
|
_ksre_prompt_destroyed() { printf 'destroyed\n' >>"$mode_events"; }
|
|
_ksre_install_evidence() {
|
|
local content="$1"
|
|
printf 'install\n' >>"$mode_events"
|
|
printf '%s\n' "$content" >"$installed_evidence"
|
|
chmod 0600 "$installed_evidence"
|
|
}
|
|
}
|
|
|
|
probe_emit_success() (
|
|
install_mode_fakes
|
|
local output="${fixture_root}/emitted.env"
|
|
rm -f -- "$output"
|
|
_ksre_emit_result "$metadata" "$output" >/dev/null
|
|
declare -A parsed=()
|
|
_ksre_parse_external_file result "$output" parsed
|
|
[[ "${parsed[bundle_id]}" == "$FIXTURE_BUNDLE_ID" &&
|
|
"${parsed[secret_count_match]}" == pass && -z "${parsed[destroyed]+present}" &&
|
|
"$(<"$mode_events")" == $'context\nconnection\nlive\nisolation\nconnection\ncontext-recheck' ]]
|
|
)
|
|
assert_succeeds probe_emit_success
|
|
|
|
probe_emit_relative_output() (
|
|
install_mode_fakes
|
|
cd -- "$fixture_root"
|
|
rm -f -- relative-result.env
|
|
_ksre_emit_result "$metadata" relative-result.env >/dev/null
|
|
[[ -f relative-result.env ]]
|
|
)
|
|
assert_succeeds probe_emit_relative_output
|
|
|
|
probe_emit_existing_output() (
|
|
install_mode_fakes
|
|
local output="${fixture_root}/existing-output.env"
|
|
printf 'keep\n' >"$output"; chmod 0600 "$output"
|
|
_ksre_emit_result "$metadata" "$output" >/dev/null 2>&1 && return 1
|
|
[[ "$(<"$output")" == keep ]]
|
|
)
|
|
assert_succeeds probe_emit_existing_output
|
|
|
|
probe_emit_live_failure() (
|
|
local field="$1"
|
|
install_mode_fakes
|
|
local output="${fixture_root}/emit-failure-${field}.env"
|
|
_ksre_live_restore_inventory() {
|
|
case "$field" in
|
|
count) printf 'v1.36.2+k3s1|embedded-etcd|enabled_stable|reencrypt_finished|match|match|pass|pass|6\n' ;;
|
|
node) printf 'v1.36.2+k3s1|embedded-etcd|enabled_stable|reencrypt_finished|match|match|pass|fail|7\n' ;;
|
|
integrity) printf 'v1.36.2+k3s1|embedded-etcd|enabled_stable|reencrypt_finished|match|mismatch|pass|pass|7\n' ;;
|
|
esac
|
|
}
|
|
if [[ "$field" == isolation ]]; then _ksre_isolation_attested() { return 1; }; fi
|
|
_ksre_emit_result "$metadata" "$output" >/dev/null 2>&1 && return 1
|
|
[[ ! -e "$output" ]]
|
|
)
|
|
for field in count node isolation integrity; do assert_succeeds probe_emit_live_failure "$field"; done
|
|
|
|
probe_record_success() (
|
|
install_mode_fakes
|
|
_ksre_record "$metadata" "$result" <<<"DESTROYED default" >/dev/null
|
|
declare -A parsed=()
|
|
_ksre_parse_external_file evidence "$installed_evidence" parsed
|
|
[[ "${parsed[destroyed]}" == confirmed && "$(head -n 1 "$mode_events")" == context ]]
|
|
)
|
|
assert_succeeds probe_record_success
|
|
|
|
probe_record_failure() (
|
|
local scenario="$1" local_metadata="${fixture_root}/record-${scenario}-metadata.env"
|
|
local local_result="${fixture_root}/record-${scenario}-result.env"
|
|
install_mode_fakes
|
|
write_metadata "$local_metadata"; write_result "$local_result"
|
|
case "$scenario" in
|
|
metadata_id) write_metadata "$local_metadata" post "$FIXTURE_OTHER_ID" ;;
|
|
result_id) write_result "$local_result" "$FIXTURE_OTHER_ID" ;;
|
|
result_version) write_result "$local_result" "$FIXTURE_BUNDLE_ID" v1.36.1+k3s1 ;;
|
|
result_backend) write_result "$local_result" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 sqlite ;;
|
|
pre) write_metadata "$local_metadata" pre ;;
|
|
stale) write_result "$local_result" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 embedded-etcd pass pass match pass 2026-07-30T00:00:00Z ;;
|
|
secret_count) write_result "$local_result" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 embedded-etcd pass pass match fail ;;
|
|
node) write_result "$local_result" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 embedded-etcd fail ;;
|
|
isolation) write_result "$local_result" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 embedded-etcd pass fail ;;
|
|
integrity) write_result "$local_result" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 embedded-etcd pass pass mismatch ;;
|
|
authority_id)
|
|
_ksre_read_authoritative_bundle() {
|
|
local destination="$1" alternate="${fixture_root}/alternate-authority.env"
|
|
write_metadata "$alternate" post "$FIXTURE_OTHER_ID"
|
|
_ksre_parse_trusted_content bundle "$(<"$alternate")" "$destination"
|
|
}
|
|
;;
|
|
destroyed) _ksre_prompt_destroyed() { return 1; } ;;
|
|
target_exists) _ksre_evidence_target_absent() { return 1; } ;;
|
|
esac
|
|
_ksre_record "$local_metadata" "$local_result" <<<"DESTROYED default" >/dev/null 2>&1 && return 1
|
|
[[ ! -e "$installed_evidence" ]]
|
|
)
|
|
for scenario in metadata_id result_id result_version result_backend pre stale secret_count node isolation integrity authority_id destroyed target_exists; do
|
|
assert_succeeds probe_record_failure "$scenario"
|
|
done
|
|
|
|
probe_check_success() (
|
|
install_mode_fakes
|
|
_ksre_check >/dev/null
|
|
[[ "$(head -n 1 "$mode_events")" == context ]]
|
|
)
|
|
assert_succeeds probe_check_success
|
|
|
|
probe_check_failure() (
|
|
local scenario="$1" altered="${fixture_root}/check-${scenario}.env"
|
|
install_mode_fakes
|
|
case "$scenario" in
|
|
authority_id)
|
|
_ksre_read_authoritative_bundle() {
|
|
local destination="$1" alternate="${fixture_root}/check-authority.env"
|
|
write_metadata "$alternate" post "$FIXTURE_OTHER_ID"
|
|
_ksre_parse_trusted_content bundle "$(<"$alternate")" "$destination"
|
|
}
|
|
;;
|
|
version) write_evidence "$altered" "$FIXTURE_BUNDLE_ID" v1.36.1+k3s1; active_evidence_file="$altered" ;;
|
|
backend) write_evidence "$altered" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 sqlite; active_evidence_file="$altered" ;;
|
|
integrity) write_evidence "$altered" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 embedded-etcd mismatch; active_evidence_file="$altered" ;;
|
|
destroyed) write_evidence "$altered" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 embedded-etcd match pending; active_evidence_file="$altered" ;;
|
|
stale) write_evidence "$altered" "$FIXTURE_BUNDLE_ID" v1.36.2+k3s1 embedded-etcd match confirmed 2026-06-01T00:00:00Z 2026-06-01T01:00:00Z; active_evidence_file="$altered" ;;
|
|
live_stage) _ksre_live_restore_inventory() { printf 'v1.36.2+k3s1|embedded-etcd|enabled_stable|start|match|match|pass|pass|7\n'; } ;;
|
|
live_integrity) _ksre_live_restore_inventory() { printf 'v1.36.2+k3s1|embedded-etcd|enabled_stable|reencrypt_finished|match|mismatch|pass|pass|7\n'; } ;;
|
|
esac
|
|
_ksre_check >/dev/null 2>&1 && return 1
|
|
return 0
|
|
)
|
|
for scenario in authority_id version backend integrity destroyed stale live_stage live_integrity; do
|
|
assert_succeeds probe_check_failure "$scenario"
|
|
done
|
|
|
|
printf 'K3S SECRET ENCRYPTION RESTORE EVIDENCE TEST PASS\n'
|