325 lines
10 KiB
Bash
Executable File
325 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -Eeuo pipefail
|
|
# 비밀값이 명령 추적, argv 또는 표준 출력에 노출되지 않도록 한다.
|
|
set +x
|
|
|
|
readonly REQUIRED_CONFIRMATION="APPLY AISTOR SECRETS"
|
|
readonly -a SECRET_CONTRACTS=(
|
|
"aistor/minio-license"
|
|
"object-storage/aistor-root-configuration"
|
|
)
|
|
|
|
license_file=""
|
|
root_config_file=""
|
|
generate_root_config=false
|
|
execute_requested=false
|
|
license_created=false
|
|
root_config_created=false
|
|
|
|
fail() {
|
|
printf 'ERROR: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage:
|
|
bash scripts/bootstrap/create-aistor-secrets.sh \
|
|
--license-file /home/donghyeon/.secrets/aistor/minio.license \
|
|
--root-config-file /home/donghyeon/.secrets/aistor/root.env \
|
|
--generate-root-config \
|
|
--execute
|
|
|
|
Creates exactly these two Secrets only when both are absent:
|
|
aistor/minio-license
|
|
object-storage/aistor-root-configuration
|
|
|
|
When both already exist, validates and reuses them unchanged. A partial state
|
|
is refused. --generate-root-config creates the local 0600 credential file only
|
|
when it is absent; it never overwrites or rotates an existing credential.
|
|
USAGE
|
|
}
|
|
|
|
while (( $# > 0 )); do
|
|
case "$1" in
|
|
--license-file)
|
|
(( $# >= 2 )) || fail "--license-file requires a path"
|
|
license_file="$2"
|
|
shift 2
|
|
;;
|
|
--root-config-file)
|
|
(( $# >= 2 )) || fail "--root-config-file requires a path"
|
|
root_config_file="$2"
|
|
shift 2
|
|
;;
|
|
--generate-root-config)
|
|
generate_root_config=true
|
|
shift
|
|
;;
|
|
--execute)
|
|
execute_requested=true
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
usage >&2
|
|
fail "unsupported argument: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ "$execute_requested" == true ]] || {
|
|
usage >&2
|
|
exit 2
|
|
}
|
|
[[ "$license_file" == /* ]] || fail "--license-file must be an absolute path"
|
|
[[ "$root_config_file" == /* ]] || \
|
|
fail "--root-config-file must be an absolute path"
|
|
|
|
for command_name in awk base64 chmod cmp find install jq kubectl mktemp \
|
|
openssl sort stat wc; do
|
|
command -v "$command_name" >/dev/null 2>&1 || \
|
|
fail "${command_name} is required"
|
|
done
|
|
|
|
validate_private_file() {
|
|
local file="$1"
|
|
local description="$2"
|
|
|
|
[[ -f "$file" && ! -L "$file" && -O "$file" && -r "$file" && -s "$file" ]] || \
|
|
fail "${description} must be a readable, non-empty, current-user-owned regular file"
|
|
[[ "$(stat --format='%a' -- "$file")" == "600" ]] || \
|
|
fail "${description} must have mode 0600: ${file}"
|
|
}
|
|
|
|
validate_license_file() {
|
|
local payload
|
|
|
|
validate_private_file "$license_file" "license file"
|
|
payload="$(<"$license_file")"
|
|
payload="${payload%$'\r'}"
|
|
[[ "$payload" =~ ^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$ ]] || \
|
|
fail "license file must contain one JWT value beginning with eyJ"
|
|
unset payload
|
|
}
|
|
|
|
validate_root_config_file() {
|
|
local file="$1"
|
|
|
|
validate_private_file "$file" "AIStor root configuration file"
|
|
awk '
|
|
BEGIN {
|
|
user_prefix = "export MINIO_ROOT_USER="
|
|
password_prefix = "export MINIO_ROOT_PASSWORD="
|
|
}
|
|
NR == 1 && index($0, user_prefix) == 1 {
|
|
value = substr($0, length(user_prefix) + 1)
|
|
if (length(value) >= 3 && substr(value, 1, 1) == "\042" && substr(value, length(value), 1) == "\042") {
|
|
value = substr(value, 2, length(value) - 2)
|
|
users++
|
|
user = value
|
|
if (length(value) < 8 || value == "minioadmin") bad = 1
|
|
next
|
|
}
|
|
}
|
|
NR == 2 && index($0, password_prefix) == 1 {
|
|
value = substr($0, length(password_prefix) + 1)
|
|
if (length(value) >= 3 && substr(value, 1, 1) == "\042" && substr(value, length(value), 1) == "\042") {
|
|
value = substr(value, 2, length(value) - 2)
|
|
passwords++
|
|
password = value
|
|
if (length(value) < 16 || value == "minioadmin") bad = 1
|
|
next
|
|
}
|
|
}
|
|
{ bad = 1 }
|
|
END {
|
|
if (NR != 2 || users != 1 || passwords != 1 ||
|
|
user == password || bad) exit 1
|
|
}
|
|
' "$file" || \
|
|
fail "root configuration must contain exactly valid MINIO_ROOT_USER and MINIO_ROOT_PASSWORD exports"
|
|
}
|
|
|
|
generate_root_configuration() {
|
|
local parent_dir="${root_config_file%/*}"
|
|
local root_user
|
|
local root_password
|
|
|
|
[[ "$parent_dir" != "$root_config_file" ]] || \
|
|
fail "root configuration path has no parent directory"
|
|
[[ ! -e "$root_config_file" && ! -L "$root_config_file" ]] || \
|
|
fail "refusing to overwrite existing root configuration: ${root_config_file}"
|
|
|
|
install -d -m 0700 -- "$parent_dir"
|
|
[[ -d "$parent_dir" && ! -L "$parent_dir" && -O "$parent_dir" ]] || \
|
|
fail "root configuration parent must be a current-user-owned directory"
|
|
[[ "$(stat --format='%a' -- "$parent_dir")" == "700" ]] || \
|
|
fail "root configuration parent must have mode 0700: ${parent_dir}"
|
|
|
|
root_user="hyeonworks-aistor-$(openssl rand -hex 4)"
|
|
root_password="$(openssl rand -hex 24)"
|
|
umask 077
|
|
{
|
|
printf 'export MINIO_ROOT_USER="%s"\n' "$root_user"
|
|
printf 'export MINIO_ROOT_PASSWORD="%s"\n' "$root_password"
|
|
} >"$root_config_file"
|
|
chmod 0600 -- "$root_config_file"
|
|
unset root_user root_password
|
|
validate_root_config_file "$root_config_file"
|
|
printf 'Generated a local AIStor root configuration with mode 0600: %s\n' \
|
|
"$root_config_file"
|
|
}
|
|
|
|
validate_license_file
|
|
|
|
for namespace in aistor object-storage; do
|
|
kubectl get namespace "$namespace" >/dev/null 2>&1 || \
|
|
fail "namespace ${namespace} does not exist; apply AIStor namespaces first"
|
|
done
|
|
|
|
existing_secret_count=0
|
|
for contract in "${SECRET_CONTRACTS[@]}"; do
|
|
namespace="${contract%%/*}"
|
|
name="${contract#*/}"
|
|
if kubectl --namespace "$namespace" get secret "$name" >/dev/null 2>&1; then
|
|
(( existing_secret_count += 1 ))
|
|
fi
|
|
done
|
|
|
|
if (( existing_secret_count > 0 && existing_secret_count < ${#SECRET_CONTRACTS[@]} )); then
|
|
fail "partial AIStor Secret state detected; no Secret was created or rotated"
|
|
fi
|
|
|
|
umask 077
|
|
secret_temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-aistor-secrets.XXXXXX")"
|
|
existing_license_file="${secret_temp_dir}/existing-minio-license"
|
|
existing_root_config_file="${secret_temp_dir}/existing-root-config"
|
|
|
|
cleanup() {
|
|
case "$secret_temp_dir" in
|
|
/tmp/platform-aistor-secrets.*|"${TMPDIR:-/tmp}"/platform-aistor-secrets.*)
|
|
rm -rf -- "$secret_temp_dir"
|
|
;;
|
|
*)
|
|
printf 'WARNING: refusing to remove unexpected temporary directory: %s\n' \
|
|
"$secret_temp_dir" >&2
|
|
;;
|
|
esac
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
validate_secret_contract() {
|
|
local namespace="$1"
|
|
local name="$2"
|
|
local expected_key="$3"
|
|
local actual_type
|
|
local actual_keys
|
|
|
|
actual_type="$(
|
|
kubectl --namespace "$namespace" get secret "$name" \
|
|
--output=jsonpath='{.type}'
|
|
)"
|
|
[[ "$actual_type" == "Opaque" ]] || \
|
|
fail "${namespace}/${name} must have type Opaque"
|
|
actual_keys="$(
|
|
kubectl --namespace "$namespace" get secret "$name" --output=json |
|
|
jq -r '.data | keys[]' | sort
|
|
)"
|
|
[[ "$actual_keys" == "$expected_key" ]] || \
|
|
fail "${namespace}/${name} must contain only the ${expected_key} key"
|
|
}
|
|
|
|
if (( existing_secret_count == ${#SECRET_CONTRACTS[@]} )); then
|
|
validate_secret_contract aistor minio-license minio.license
|
|
validate_secret_contract \
|
|
object-storage aistor-root-configuration config.env
|
|
|
|
kubectl --namespace aistor get secret minio-license \
|
|
--output=jsonpath='{.data.minio\.license}' |
|
|
base64 --decode >"$existing_license_file"
|
|
cmp --silent -- "$license_file" "$existing_license_file" || \
|
|
fail "the supplied license differs from the existing Secret; rotation was not performed"
|
|
|
|
kubectl --namespace object-storage get secret aistor-root-configuration \
|
|
--output=jsonpath='{.data.config\.env}' |
|
|
base64 --decode >"$existing_root_config_file"
|
|
chmod 0600 -- "$existing_root_config_file"
|
|
validate_root_config_file "$existing_root_config_file"
|
|
|
|
if [[ -e "$root_config_file" || -L "$root_config_file" ]]; then
|
|
validate_root_config_file "$root_config_file"
|
|
cmp --silent -- "$root_config_file" "$existing_root_config_file" || \
|
|
fail "local root configuration differs from the existing Secret; rotation was not performed"
|
|
else
|
|
fail "existing Secret is valid, but the local root configuration file is missing"
|
|
fi
|
|
|
|
printf 'Existing AIStor Secret contracts are valid and were reused unchanged.\n'
|
|
printf 'No credential or license rotation was performed.\n'
|
|
exit 0
|
|
fi
|
|
|
|
if [[ ! -e "$root_config_file" && ! -L "$root_config_file" ]]; then
|
|
[[ "$generate_root_config" == true ]] || \
|
|
fail "root configuration is absent; pass --generate-root-config to create it"
|
|
generate_root_configuration
|
|
else
|
|
validate_root_config_file "$root_config_file"
|
|
fi
|
|
|
|
[[ -t 0 ]] || fail "an interactive terminal is required for initial Secret creation"
|
|
printf '\nThis will create exactly two AIStor Secrets. Type %s to continue: ' \
|
|
"$REQUIRED_CONFIRMATION"
|
|
read -r confirmation
|
|
[[ "$confirmation" == "$REQUIRED_CONFIRMATION" ]] || fail "cancelled"
|
|
|
|
rollback_new_secrets() {
|
|
set +e
|
|
if [[ "$root_config_created" == true ]]; then
|
|
kubectl --namespace object-storage delete secret \
|
|
aistor-root-configuration --ignore-not-found >/dev/null
|
|
fi
|
|
if [[ "$license_created" == true ]]; then
|
|
kubectl --namespace aistor delete secret \
|
|
minio-license --ignore-not-found >/dev/null
|
|
fi
|
|
if [[ "$root_config_created" == true || "$license_created" == true ]]; then
|
|
printf 'ROLLBACK: removed only Secrets created by this failed invocation.\n' >&2
|
|
fi
|
|
}
|
|
|
|
on_error() {
|
|
local status="$1"
|
|
local line="$2"
|
|
|
|
trap - ERR
|
|
rollback_new_secrets
|
|
printf 'ERROR: Secret creation failed at line %s (exit %s).\n' \
|
|
"$line" "$status" >&2
|
|
exit "$status"
|
|
}
|
|
trap 'on_error "$?" "$LINENO"' ERR
|
|
|
|
kubectl --namespace aistor create secret generic minio-license \
|
|
--type=Opaque \
|
|
--from-file="minio.license=${license_file}"
|
|
license_created=true
|
|
|
|
kubectl --namespace object-storage create secret generic \
|
|
aistor-root-configuration \
|
|
--type=Opaque \
|
|
--from-file="config.env=${root_config_file}"
|
|
root_config_created=true
|
|
|
|
validate_secret_contract aistor minio-license minio.license
|
|
validate_secret_contract object-storage aistor-root-configuration config.env
|
|
|
|
printf 'Created both AIStor Secret contracts without printing payloads.\n'
|
|
printf 'Local root credentials remain only in: %s\n' "$root_config_file"
|
|
printf 'This script does not perform credential or license rotation.\n'
|