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