Files

217 lines
7.1 KiB
Bash
Executable File

#!/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'