#!/usr/bin/env bash set -Eeuo pipefail # 비밀값이 명령 추적 출력에 노출되지 않도록 호출자가 활성화한 xtrace도 끈다. set +x readonly REQUIRED_CONFIRMATION="APPLY" readonly DB_USERNAME="gitea" readonly TOTAL_SECRET_CONTRACTS=3 readonly -a SECRET_CONTRACTS=( "platform-data/gitea-db-credentials" "gitea/gitea-db-credentials" "gitea/gitea-admin" ) fail() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } usage() { cat <<'USAGE' Usage: bash scripts/bootstrap/create-phase1-secrets.sh --execute Creates these Secrets only when all three are absent: platform-data/gitea-db-credentials gitea/gitea-db-credentials gitea/gitea-admin When all three already exist, validates and reuses their data unchanged. The script refuses a partial state and does not perform credential rotation. It may repair only the cnpg.io/reload=true label on the platform-data DB Secret. USAGE } [[ "${1:-}" == "--execute" && "$#" -eq 1 ]] || { usage exit 2 } command -v kubectl >/dev/null 2>&1 || fail "kubectl is required" command -v base64 >/dev/null 2>&1 || fail "base64 is required" for namespace in platform-data gitea; do kubectl get namespace "$namespace" >/dev/null 2>&1 || \ fail "namespace ${namespace} does not exist; apply namespaces first" done 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 Phase 1 Secret state detected (${existing_secret_count}/${TOTAL_SECRET_CONTRACTS}); refusing creation or rotation" fi secret_data_b64() { local namespace="$1" local name="$2" local key="$3" kubectl --namespace "$namespace" get secret "$name" \ --output="jsonpath={.data.${key}}" } validate_basic_auth_secret() { local namespace="$1" local name="$2" local secret_type local username_b64 local password_b64 secret_type="$( kubectl --namespace "$namespace" get secret "$name" \ --output='jsonpath={.type}' )" [[ "$secret_type" == "kubernetes.io/basic-auth" ]] || \ fail "${namespace}/${name} must have type kubernetes.io/basic-auth" username_b64="$(secret_data_b64 "$namespace" "$name" username)" password_b64="$(secret_data_b64 "$namespace" "$name" password)" [[ -n "$username_b64" ]] || fail "${namespace}/${name} is missing non-empty data.username" [[ -n "$password_b64" ]] || fail "${namespace}/${name} is missing non-empty data.password" } if (( existing_secret_count == TOTAL_SECRET_CONTRACTS )); then validate_basic_auth_secret platform-data gitea-db-credentials validate_basic_auth_secret gitea gitea-db-credentials validate_basic_auth_secret gitea gitea-admin platform_db_username_b64="$( secret_data_b64 platform-data gitea-db-credentials username )" platform_db_password_b64="$( secret_data_b64 platform-data gitea-db-credentials password )" gitea_db_username_b64="$( secret_data_b64 gitea gitea-db-credentials username )" gitea_db_password_b64="$( secret_data_b64 gitea gitea-db-credentials password )" expected_db_username_b64="$(printf '%s' "$DB_USERNAME" | base64)" [[ "$platform_db_username_b64" == "$expected_db_username_b64" ]] || \ fail "platform-data/gitea-db-credentials username must be gitea" [[ "$gitea_db_username_b64" == "$expected_db_username_b64" ]] || \ fail "gitea/gitea-db-credentials username must be gitea" [[ "$platform_db_username_b64" == "$gitea_db_username_b64" ]] || \ fail "the two DB Secret usernames do not match" [[ "$platform_db_password_b64" == "$gitea_db_password_b64" ]] || \ fail "the two DB Secret passwords do not match" reload_label="$( kubectl --namespace platform-data get secret gitea-db-credentials \ --output='jsonpath={.metadata.labels.cnpg\.io/reload}' )" if [[ "$reload_label" != "true" ]]; then kubectl --namespace platform-data label secret gitea-db-credentials \ cnpg.io/reload=true --overwrite printf 'Repaired cnpg.io/reload=true without changing Secret data.\n' fi unset platform_db_username_b64 platform_db_password_b64 unset gitea_db_username_b64 gitea_db_password_b64 expected_db_username_b64 printf 'Existing Phase 1 Secret contracts are valid and were reused unchanged.\n' printf 'No credential rotation was performed.\n' exit 0 fi [[ -t 0 ]] || fail "an interactive terminal is required for initial Secret creation" 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 "use at least 16 characters" printf '%s' "$first" } read -r -p 'Gitea administrator username: ' admin_username [[ "$admin_username" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || \ fail "administrator username contains unsupported characters" db_password="$(read_secret_twice 'Gitea database password')" admin_password="$(read_secret_twice 'Gitea administrator password')" secret_temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-phase1-secrets.XXXXXX")" db_user_file="${secret_temp_dir}/db-username" db_password_file="${secret_temp_dir}/db-password" admin_user_file="${secret_temp_dir}/admin-username" admin_password_file="${secret_temp_dir}/admin-password" cleanup() { unset db_password admin_password rm -f -- "$db_user_file" "$db_password_file" "$admin_user_file" "$admin_password_file" rmdir -- "$secret_temp_dir" 2>/dev/null || true } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM umask 077 printf '%s' "$DB_USERNAME" >"$db_user_file" printf '%s' "$db_password" >"$db_password_file" printf '%s' "$admin_username" >"$admin_user_file" printf '%s' "$admin_password" >"$admin_password_file" unset db_password admin_password printf '\nThe script will create exactly three Secrets. Type %s to continue: ' "$REQUIRED_CONFIRMATION" read -r confirmation [[ "$confirmation" == "$REQUIRED_CONFIRMATION" ]] || fail "cancelled" apply_basic_auth_secret() { local namespace="$1" local name="$2" local username_file="$3" local password_file="$4" kubectl --namespace "$namespace" create secret generic "$name" \ --type=kubernetes.io/basic-auth \ --from-file="username=${username_file}" \ --from-file="password=${password_file}" \ --dry-run=client \ --output=yaml \ | kubectl apply --filename=- } apply_basic_auth_secret \ platform-data gitea-db-credentials "$db_user_file" "$db_password_file" kubectl --namespace platform-data label secret gitea-db-credentials \ cnpg.io/reload=true --overwrite apply_basic_auth_secret \ gitea gitea-db-credentials "$db_user_file" "$db_password_file" apply_basic_auth_secret \ gitea gitea-admin "$admin_user_file" "$admin_password_file" printf 'Initial Phase 1 Secret contracts were created. No values were written to the repository.\n' printf 'This script does not perform credential rotation.\n'