fix(k8s): harden control-plane backup safety
This commit is contained in:
@@ -41,6 +41,7 @@ CURL_BIN="${CURL_BIN:-curl}"
|
||||
FLOCK_BIN="${FLOCK_BIN:-flock}"
|
||||
MKTEMP_BIN="${MKTEMP_BIN:-mktemp}"
|
||||
DATE_BIN="${DATE_BIN:-date}"
|
||||
REALPATH_BIN="${REALPATH_BIN:-realpath}"
|
||||
REMOTE_BASH_BIN="${REMOTE_BASH_BIN:-bash}"
|
||||
|
||||
# Notifications follow the JSON formats used by automated-nfs-backup.sh.
|
||||
@@ -55,6 +56,7 @@ declare -a ARCHIVE_ENTRIES=()
|
||||
declare -a SERVICE_FILE_LIST=()
|
||||
declare -a SSH_ARGS=()
|
||||
declare -a SCP_ARGS=()
|
||||
declare -a PROTECTED_PATHS=()
|
||||
readonly -a K3S_SERVER_ENTRIES=(cred etc manifests static tls token agent-token node-token)
|
||||
|
||||
RUN_TMP_DIR=""
|
||||
@@ -175,16 +177,83 @@ require_absolute_path() {
|
||||
fi
|
||||
}
|
||||
|
||||
path_is_ancestor_or_same() {
|
||||
local ancestor="${1:?ancestor path is required}"
|
||||
local path="${2:?path is required}"
|
||||
|
||||
[[ "$ancestor" == "/" || "$path" == "$ancestor" || "$path" == "$ancestor"/* ]]
|
||||
}
|
||||
|
||||
paths_overlap() {
|
||||
local first="${1:?first path is required}"
|
||||
local second="${2:?second path is required}"
|
||||
|
||||
path_is_ancestor_or_same "$first" "$second" || \
|
||||
path_is_ancestor_or_same "$second" "$first"
|
||||
}
|
||||
|
||||
canonicalize_protected_path() {
|
||||
local path="${1:?protected path is required}"
|
||||
local canonical_path
|
||||
local canonical_parent
|
||||
local basename
|
||||
|
||||
if [[ -e "$path" || -L "$path" ]]; then
|
||||
canonical_path="$("$REALPATH_BIN" -e -- "$path" 2>/dev/null)" || return 1
|
||||
printf '%s' "$canonical_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
basename="${path##*/}"
|
||||
canonical_parent="${path%/*}"
|
||||
if ! canonical_parent="$("$REALPATH_BIN" -e -- "$canonical_parent" 2>/dev/null)"; then
|
||||
canonical_parent="$("$REALPATH_BIN" -m -- "$canonical_parent" 2>/dev/null)" || return 1
|
||||
fi
|
||||
printf '%s/%s' "${canonical_parent%/}" "$basename"
|
||||
}
|
||||
|
||||
determine_protected_paths() {
|
||||
local canonical_server_dir
|
||||
local protected_path
|
||||
|
||||
if ! canonical_server_dir="$("$REALPATH_BIN" -e -- "$K3S_SERVER_DIR" 2>/dev/null)"; then
|
||||
canonical_server_dir="$("$REALPATH_BIN" -m -- "$K3S_SERVER_DIR" 2>/dev/null)" || \
|
||||
die "Configured source path is not safe to archive"
|
||||
fi
|
||||
|
||||
PROTECTED_PATHS=()
|
||||
if ! protected_path="$(canonicalize_protected_path "${canonical_server_dir%/}/db")"; then
|
||||
die "Configured source path is not safe to archive"
|
||||
fi
|
||||
PROTECTED_PATHS+=("$protected_path")
|
||||
if ! protected_path="$(canonicalize_protected_path "${canonical_server_dir%/}/kine.sock")"; then
|
||||
die "Configured source path is not safe to archive"
|
||||
fi
|
||||
PROTECTED_PATHS+=("$protected_path")
|
||||
}
|
||||
|
||||
add_archive_entry_if_present() {
|
||||
local absolute_path="${1:?source path is required}"
|
||||
local canonical_path
|
||||
local relative_path
|
||||
local protected_path
|
||||
|
||||
if [[ ! -e "$absolute_path" && ! -L "$absolute_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
relative_path="${absolute_path#/}"
|
||||
if [[ -z "$relative_path" || "$relative_path" == *"/../"* || "$relative_path" == ../* ]]; then
|
||||
if ! canonical_path="$("$REALPATH_BIN" -e -- "$absolute_path" 2>/dev/null)"; then
|
||||
die "Configured source path is not safe to archive"
|
||||
fi
|
||||
|
||||
for protected_path in "${PROTECTED_PATHS[@]}"; do
|
||||
if paths_overlap "$canonical_path" "$protected_path"; then
|
||||
die "Configured source path is not safe to archive"
|
||||
fi
|
||||
done
|
||||
|
||||
relative_path="${canonical_path#/}"
|
||||
if [[ -z "$relative_path" ]]; then
|
||||
die "Configured source path is not safe to archive"
|
||||
fi
|
||||
ARCHIVE_ENTRIES+=("$relative_path")
|
||||
@@ -211,6 +280,7 @@ validate_inputs() {
|
||||
require_cmd "$FLOCK_BIN"
|
||||
require_cmd "$MKTEMP_BIN"
|
||||
require_cmd "$DATE_BIN"
|
||||
require_cmd "$REALPATH_BIN"
|
||||
if [[ -n "$NOTIFY_SUCCESS_URL" || -n "$NOTIFY_FAILURE_URL" ]]; then
|
||||
require_cmd "$CURL_BIN"
|
||||
fi
|
||||
@@ -224,6 +294,7 @@ validate_inputs() {
|
||||
if [[ ! -d "$K3S_CONFIG_DIR" ]]; then
|
||||
die "K3S_CONFIG_DIR is not an available directory"
|
||||
fi
|
||||
determine_protected_paths
|
||||
if [[ ! "$ARCHIVE_PREFIX" =~ ^[[:alnum:]][[:alnum:]._-]*$ ]]; then
|
||||
die "ARCHIVE_PREFIX may contain only letters, numbers, dots, underscores, and hyphens"
|
||||
fi
|
||||
@@ -300,7 +371,7 @@ create_and_validate_archive() {
|
||||
log_info "Creating k3s control-plane configuration archive"
|
||||
(
|
||||
cd /
|
||||
"$ZIP_BIN" -r "$LOCAL_ARCHIVE_PATH" "${ARCHIVE_ENTRIES[@]}" >/dev/null
|
||||
"$ZIP_BIN" -r "$LOCAL_ARCHIVE_PATH" -- "${ARCHIVE_ENTRIES[@]}" >/dev/null
|
||||
)
|
||||
"$UNZIP_BIN" -t "$LOCAL_ARCHIVE_PATH" >/dev/null
|
||||
|
||||
@@ -363,22 +434,20 @@ remote_dir="$1"
|
||||
archive_prefix="$2"
|
||||
keep_count="$3"
|
||||
|
||||
shopt -s nullglob
|
||||
candidates=("${remote_dir%/}/${archive_prefix}_"*.zip)
|
||||
shopt -u nullglob
|
||||
mapfile -d '' -t sorted_archives < <(
|
||||
find -P "$remote_dir" -maxdepth 1 -type f \
|
||||
-name "${archive_prefix}_*.zip" -printf '%T@\t%p\0' |
|
||||
sort -z -t $'\t' -k1,1nr
|
||||
)
|
||||
|
||||
matching_archives=()
|
||||
for candidate in "${candidates[@]}"; do
|
||||
[[ -f "$candidate" ]] && matching_archives+=("$candidate")
|
||||
done
|
||||
|
||||
if (( ${#matching_archives[@]} <= keep_count )); then
|
||||
if (( ${#sorted_archives[@]} <= keep_count )); then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mapfile -t sorted_archives < <(ls -1t -- "${matching_archives[@]}")
|
||||
for ((index = keep_count; index < ${#sorted_archives[@]}; index++)); do
|
||||
rm -f -- "${sorted_archives[$index]}"
|
||||
archive_record="${sorted_archives[$index]}"
|
||||
archive_path="${archive_record#*$'\t'}"
|
||||
rm -f -- "$archive_path"
|
||||
done
|
||||
REMOTE_CLEANUP
|
||||
|
||||
|
||||
Reference in New Issue
Block a user