Add platform infrastructure configuration
This commit is contained in:
Executable
+298
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
# 호출자가 bash -x로 실행해도 비밀번호가 추적 출력에 노출되지 않도록 한다.
|
||||
set +x
|
||||
|
||||
readonly REQUIRED_CONFIRMATION="APPLY KEYCLOAK SECRETS"
|
||||
readonly DB_USERNAME="keycloak"
|
||||
readonly TOTAL_SECRET_CONTRACTS=2
|
||||
readonly -a SECRET_CONTRACTS=(
|
||||
"platform-data/keycloak-db-credentials"
|
||||
"keycloak/keycloak-db-credentials"
|
||||
)
|
||||
|
||||
mutation_started=false
|
||||
secret_temp_dir=""
|
||||
|
||||
report_retained_state() {
|
||||
if [[ "$mutation_started" == true ]]; then
|
||||
printf '%s\n' \
|
||||
'SAFE STOP: no Secret or database data was deleted or rolled back.' \
|
||||
'Any Secret created before the failure remains in the cluster.' \
|
||||
'Inspect Secret names and events without printing Secret data, then rerun after resolving the cause.' >&2
|
||||
fi
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf 'ERROR: %s\n' "$*" >&2
|
||||
report_retained_state
|
||||
exit 1
|
||||
}
|
||||
|
||||
on_error() {
|
||||
local status="$1"
|
||||
local line="$2"
|
||||
|
||||
trap - ERR
|
||||
set +e
|
||||
printf 'ERROR: command failed at line %s (exit %s).\n' "$line" "$status" >&2
|
||||
report_retained_state
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
on_signal() {
|
||||
local status="$1"
|
||||
|
||||
trap - INT TERM
|
||||
set +e
|
||||
printf 'INTERRUPTED: stopping without deleting cluster state.\n' >&2
|
||||
report_retained_state
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
bash scripts/bootstrap/create-keycloak-secrets.sh --execute
|
||||
bash scripts/bootstrap/create-keycloak-secrets.sh --generate --execute
|
||||
|
||||
Creates exactly these two Secrets only when both are absent:
|
||||
platform-data/keycloak-db-credentials
|
||||
keycloak/keycloak-db-credentials
|
||||
|
||||
When both already exist, validates and reuses their data unchanged. A partial
|
||||
state is refused. This script does not require or modify an AIStor license,
|
||||
does not rotate credentials, and never prints Secret payloads.
|
||||
|
||||
--execute prompts twice for a database password and requires the exact
|
||||
confirmation text. --generate --execute is an explicit non-interactive mode:
|
||||
it generates 32 random bytes with OpenSSL in a private 0600 temporary file.
|
||||
USAGE
|
||||
}
|
||||
|
||||
execute_requested=false
|
||||
generate_requested=false
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--execute)
|
||||
execute_requested=true
|
||||
shift
|
||||
;;
|
||||
--generate)
|
||||
generate_requested=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
fail "unsupported argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ "$execute_requested" == true ]] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
for command_name in kubectl base64 cmp mktemp sort stat tr wc; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || \
|
||||
fail "${command_name} is required"
|
||||
done
|
||||
if [[ "$generate_requested" == true ]]; then
|
||||
command -v openssl >/dev/null 2>&1 || fail "openssl is required for --generate"
|
||||
fi
|
||||
|
||||
for namespace in platform-data keycloak; do
|
||||
kubectl get namespace "$namespace" >/dev/null 2>&1 || \
|
||||
fail "namespace ${namespace} does not exist"
|
||||
done
|
||||
|
||||
umask 077
|
||||
secret_temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-keycloak-secrets.XXXXXX")"
|
||||
db_user_file="${secret_temp_dir}/expected-username"
|
||||
db_password_file="${secret_temp_dir}/new-password"
|
||||
platform_username_file="${secret_temp_dir}/platform-username"
|
||||
keycloak_username_file="${secret_temp_dir}/keycloak-username"
|
||||
platform_password_file="${secret_temp_dir}/platform-password"
|
||||
keycloak_password_file="${secret_temp_dir}/keycloak-password"
|
||||
|
||||
cleanup() {
|
||||
unset db_password
|
||||
if [[ -n "$secret_temp_dir" ]]; then
|
||||
rm -f -- \
|
||||
"$db_user_file" \
|
||||
"$db_password_file" \
|
||||
"$platform_username_file" \
|
||||
"$keycloak_username_file" \
|
||||
"$platform_password_file" \
|
||||
"$keycloak_password_file"
|
||||
rmdir -- "$secret_temp_dir" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'on_error "$?" "$LINENO"' ERR
|
||||
trap 'on_signal 130' INT
|
||||
trap 'on_signal 143' TERM
|
||||
|
||||
printf '%s' "$DB_USERNAME" >"$db_user_file"
|
||||
|
||||
secret_data_b64() {
|
||||
local namespace="$1"
|
||||
local name="$2"
|
||||
local key="$3"
|
||||
|
||||
kubectl --namespace "$namespace" get secret "$name" \
|
||||
--output="go-template={{ index .data \"${key}\" }}"
|
||||
}
|
||||
|
||||
validate_secret_type_and_keys() {
|
||||
local namespace="$1"
|
||||
local name="$2"
|
||||
local actual_type
|
||||
local actual_keys
|
||||
local actual_keys_sorted
|
||||
|
||||
actual_type="$(
|
||||
kubectl --namespace "$namespace" get secret "$name" \
|
||||
--output='jsonpath={.type}'
|
||||
)"
|
||||
[[ "$actual_type" == "kubernetes.io/basic-auth" ]] || \
|
||||
fail "${namespace}/${name} must have type kubernetes.io/basic-auth"
|
||||
|
||||
actual_keys="$(
|
||||
kubectl --namespace "$namespace" get secret "$name" \
|
||||
--output='go-template={{range $key, $_ := .data}}{{$key}}{{"\n"}}{{end}}'
|
||||
)"
|
||||
actual_keys_sorted="$(printf '%s\n' "$actual_keys" | LC_ALL=C sort)"
|
||||
[[ "$actual_keys_sorted" == $'password\nusername' ]] || \
|
||||
fail "${namespace}/${name} has an unexpected data key set"
|
||||
}
|
||||
|
||||
validate_existing_contracts() {
|
||||
validate_secret_type_and_keys platform-data keycloak-db-credentials
|
||||
validate_secret_type_and_keys keycloak keycloak-db-credentials
|
||||
|
||||
secret_data_b64 platform-data keycloak-db-credentials username \
|
||||
| base64 --decode >"$platform_username_file"
|
||||
secret_data_b64 keycloak keycloak-db-credentials username \
|
||||
| base64 --decode >"$keycloak_username_file"
|
||||
cmp --silent -- "$db_user_file" "$platform_username_file" || \
|
||||
fail "platform-data/keycloak-db-credentials username must be keycloak"
|
||||
cmp --silent -- "$db_user_file" "$keycloak_username_file" || \
|
||||
fail "keycloak/keycloak-db-credentials username must be keycloak"
|
||||
|
||||
secret_data_b64 platform-data keycloak-db-credentials password \
|
||||
| base64 --decode >"$platform_password_file"
|
||||
secret_data_b64 keycloak keycloak-db-credentials password \
|
||||
| base64 --decode >"$keycloak_password_file"
|
||||
(( $(wc -c <"$platform_password_file") >= 16 )) || \
|
||||
fail "platform-data/keycloak-db-credentials password must contain at least 16 bytes"
|
||||
(( $(wc -c <"$keycloak_password_file") >= 16 )) || \
|
||||
fail "keycloak/keycloak-db-credentials password must contain at least 16 bytes"
|
||||
cmp --silent -- "$platform_password_file" "$keycloak_password_file" || \
|
||||
fail "the two Keycloak DB Secret passwords do not match"
|
||||
}
|
||||
|
||||
existing_secret_count=0
|
||||
for contract in "${SECRET_CONTRACTS[@]}"; do
|
||||
namespace="${contract%%/*}"
|
||||
name="${contract#*/}"
|
||||
existing_resource="$(
|
||||
kubectl --namespace "$namespace" get secret "$name" \
|
||||
--ignore-not-found --output=name
|
||||
)"
|
||||
if [[ -n "$existing_resource" ]]; then
|
||||
((existing_secret_count += 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if (( existing_secret_count > 0 && existing_secret_count < TOTAL_SECRET_CONTRACTS )); then
|
||||
fail "partial Keycloak Secret state detected (${existing_secret_count}/${TOTAL_SECRET_CONTRACTS}); refusing creation or rotation"
|
||||
fi
|
||||
|
||||
if (( existing_secret_count == TOTAL_SECRET_CONTRACTS )); then
|
||||
validate_existing_contracts
|
||||
|
||||
reload_label="$(
|
||||
kubectl --namespace platform-data get secret keycloak-db-credentials \
|
||||
--output='jsonpath={.metadata.labels.cnpg\.io/reload}'
|
||||
)"
|
||||
if [[ "$reload_label" != "true" ]]; then
|
||||
mutation_started=true
|
||||
kubectl --namespace platform-data label secret keycloak-db-credentials \
|
||||
cnpg.io/reload=true --overwrite
|
||||
mutation_started=false
|
||||
printf 'Repaired cnpg.io/reload=true without changing Secret data.\n'
|
||||
fi
|
||||
|
||||
printf 'Existing Keycloak Secret contracts are valid and were reused unchanged.\n'
|
||||
printf 'No credential rotation was performed.\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
read_secret_twice() {
|
||||
local prompt="$1"
|
||||
local first
|
||||
local second
|
||||
|
||||
read -r -s -p "${prompt}: " first
|
||||
printf '\n' >&2
|
||||
read -r -s -p "Confirm ${prompt}: " second
|
||||
printf '\n' >&2
|
||||
|
||||
[[ "$first" == "$second" ]] || fail "the two values do not match"
|
||||
(( ${#first} >= 16 )) || fail "${prompt} must contain at least 16 characters"
|
||||
[[ "$first" != *$'\n'* && "$first" != *$'\r'* ]] || \
|
||||
fail "${prompt} contains an unsupported line break"
|
||||
printf '%s' "$first"
|
||||
}
|
||||
|
||||
if [[ "$generate_requested" == true ]]; then
|
||||
openssl rand -hex 32 | tr -d '\n' >"$db_password_file"
|
||||
[[ "$(stat --format='%a' -- "$db_password_file")" == "600" ]] || \
|
||||
fail "generated password file must have mode 0600"
|
||||
[[ "$(wc -c <"$db_password_file" | tr -d '[:space:]')" == "64" ]] || \
|
||||
fail "OpenSSL did not generate the expected 32-byte password"
|
||||
printf '%s\n' \
|
||||
'Authorized by explicit --generate --execute flags.' \
|
||||
'A 32-byte random database password was generated without printing it.'
|
||||
else
|
||||
[[ -t 0 ]] || fail "an interactive terminal is required for initial Secret creation"
|
||||
db_password="$(read_secret_twice 'Keycloak database password')"
|
||||
printf '%s' "$db_password" >"$db_password_file"
|
||||
unset db_password
|
||||
|
||||
printf '\nThe script will create exactly two Keycloak DB Secrets.\n'
|
||||
printf 'It will not read or modify any AIStor Secret.\n'
|
||||
printf 'Type %s to continue: ' "$REQUIRED_CONFIRMATION"
|
||||
read -r confirmation
|
||||
[[ "$confirmation" == "$REQUIRED_CONFIRMATION" ]] || fail "cancelled"
|
||||
fi
|
||||
|
||||
create_basic_auth_secret() {
|
||||
local namespace="$1"
|
||||
|
||||
kubectl --namespace "$namespace" create secret generic keycloak-db-credentials \
|
||||
--type=kubernetes.io/basic-auth \
|
||||
--from-file="username=${db_user_file}" \
|
||||
--from-file="password=${db_password_file}" \
|
||||
--dry-run=client \
|
||||
--output=yaml \
|
||||
| kubectl create --filename=-
|
||||
}
|
||||
|
||||
mutation_started=true
|
||||
create_basic_auth_secret platform-data
|
||||
create_basic_auth_secret keycloak
|
||||
kubectl --namespace platform-data label secret keycloak-db-credentials \
|
||||
cnpg.io/reload=true --overwrite
|
||||
|
||||
validate_existing_contracts
|
||||
mutation_started=false
|
||||
|
||||
printf 'Initial Keycloak Secret contracts were created without printing payloads.\n'
|
||||
printf 'This script does not perform credential rotation.\n'
|
||||
Reference in New Issue
Block a user