refactor: 폴더 구조 변경

This commit is contained in:
donghyeon-ka
2026-08-02 00:22:19 +09:00
parent f9c463f87a
commit be2f8e4863
1869 changed files with 4565 additions and 295591 deletions
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Install (or upgrade) the MinIO Operator Helm release.
#
# MinIO Tenant CR (minio.min.io/v2) 는 이 Operator 가 CRD 를 먼저 등록해야
# apply 가능하다. bootstrap.sh 의 Phase 1 이전에 실행된다.
#
# Operator 는 자체 namespace (기본 minio-operator) 에 배포된다. 애플리케이션
# namespace (mnt) 와 분리되므로 Tenant CR 만 mnt 에 있어도 동작한다.
#
# Optional env:
# MINIO_OPERATOR_NAMESPACE — 기본 minio-operator
# MINIO_OPERATOR_RELEASE — 기본 minio-operator
# MINIO_OPERATOR_VERSION — 기본 7.0.0
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
: "${MINIO_OPERATOR_NAMESPACE:=minio-operator}"
: "${MINIO_OPERATOR_RELEASE:=minio-operator}"
: "${MINIO_OPERATOR_VERSION:=7.0.0}"
helm_install() {
log "helm repo: minio-operator 등록 (idempotent)"
helm repo add minio-operator https://operator.min.io >/dev/null 2>&1 || true
helm repo update minio-operator >/dev/null
log "helm upgrade --install ${MINIO_OPERATOR_RELEASE} (v=${MINIO_OPERATOR_VERSION} ns=${MINIO_OPERATOR_NAMESPACE})"
helm upgrade --install "$MINIO_OPERATOR_RELEASE" minio-operator/operator \
--namespace "$MINIO_OPERATOR_NAMESPACE" \
--create-namespace \
--version "$MINIO_OPERATOR_VERSION" \
--wait \
--atomic \
--timeout 5m
}
wait_ready() {
log "MinIO Operator Pod Ready 확인"
kubectl -n "$MINIO_OPERATOR_NAMESPACE" wait --for=condition=Ready \
pod -l "app.kubernetes.io/name=operator" \
--timeout=120s
log "Tenant CRD 등록 확인"
kubectl get crd tenants.minio.min.io >/dev/null \
|| die "Tenant CRD 가 여전히 없음. Operator 설치 로그 확인."
}
main() {
require_cmd helm kubectl
helm_install
wait_ready
log "minio-operator-install 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+331
View File
@@ -0,0 +1,331 @@
#!/usr/bin/env bash
# Provision the docker-registry MinIO dependency after the Tenant is ready.
#
# The registry credential is generated by MinIO as an access key with an inline,
# bucket-scoped policy. The generated secret is captured once and written to
# Vault through stdin. Root and application credentials are never passed as
# command-line arguments.
#
# Required env:
# REPO_ROOT
# ENV_NAME
#
# Optional env:
# DOCKER_REGISTRY_BUCKET default: docker-registry
# PF_LOCAL_PORT default: 9900
# MINIO_NAMESPACE default: mnt
# MINIO_SERVICE default: minio
# MINIO_SCHEME lab default: http; other envs: https
# MINIO_SERVICE_PORT lab default: 80; other envs: 443
# MINIO_TLS_INSECURE default: no
# ALLOW_INSECURE_MINIO_TLS required when non-lab explicitly uses insecure TLS
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/vault.sh
. "$SCRIPT_DIR/../lib/vault.sh"
: "${DOCKER_REGISTRY_BUCKET:=docker-registry}"
: "${PF_LOCAL_PORT:=9900}"
: "${MINIO_NAMESPACE:=mnt}"
: "${MINIO_SERVICE:=minio}"
ENV_NAME="${ENV_NAME:-}"
MC_ALIAS="minio-provision"
REGISTRY_CHECK_ALIAS="registry-check"
MC_CONFIG_DIR=""
PF_PID=""
CREATED_ACCESS_KEY=""
ACCESS_KEY_PERSISTED="no"
ROOT_USER=""
ROOT_PASSWORD=""
POLICY_FILE=""
MC_GLOBAL_ARGS=()
resolve_transport_defaults() {
case "$ENV_NAME" in
lab)
: "${MINIO_SCHEME:=http}"
: "${MINIO_SERVICE_PORT:=80}"
: "${MINIO_TLS_INSECURE:=no}"
;;
*)
: "${MINIO_SCHEME:=https}"
: "${MINIO_SERVICE_PORT:=443}"
: "${MINIO_TLS_INSECURE:=no}"
;;
esac
case "$MINIO_SCHEME" in
http | https) ;;
*) die "MINIO_SCHEME은 http 또는 https만 허용됩니다: $MINIO_SCHEME" ;;
esac
case "$MINIO_TLS_INSECURE" in
yes | no) ;;
*) die "MINIO_TLS_INSECURE는 yes 또는 no만 허용됩니다." ;;
esac
if [[ "$ENV_NAME" != "lab" && "$MINIO_TLS_INSECURE" == "yes" &&
"${ALLOW_INSECURE_MINIO_TLS:-}" != "yes" ]]; then
die "lab 외 환경에서 TLS 검증 비활성화는 ALLOW_INSECURE_MINIO_TLS=yes 가 추가로 필요합니다."
fi
}
cleanup_port_forward() {
if [[ -n "${PF_PID:-}" ]] && kill -0 "$PF_PID" 2>/dev/null; then
kill "$PF_PID" 2>/dev/null || true
wait "$PF_PID" 2>/dev/null || true
fi
}
mc_cmd() {
mc "${MC_GLOBAL_ARGS[@]}" "$@"
}
cleanup_created_access_key() {
if [[ -z "${CREATED_ACCESS_KEY:-}" || "$ACCESS_KEY_PERSISTED" == "yes" ]]; then
return 0
fi
warn "Vault 기록 전 실패: 이번 실행에서 만든 MinIO access key 정리"
mc_cmd admin accesskey rm "$MC_ALIAS" "$CREATED_ACCESS_KEY" >/dev/null 2>&1 \
|| warn "생성한 access key 자동 정리 실패 — MinIO IAM을 점검하세요."
}
wait_minio_ready() {
log "MinIO Tenant Pod Ready 대기"
retry 30 5 kubectl -n "$MINIO_NAMESPACE" wait --for=condition=Ready --timeout=10s \
pod -l "v1.min.io/tenant=$MINIO_SERVICE"
}
start_port_forward() {
log "port-forward 생성 (127.0.0.1:${PF_LOCAL_PORT} -> svc/${MINIO_SERVICE}:${MINIO_SERVICE_PORT})"
kubectl -n "$MINIO_NAMESPACE" port-forward \
"svc/${MINIO_SERVICE}" "${PF_LOCAL_PORT}:${MINIO_SERVICE_PORT}" \
>/dev/null 2>&1 &
PF_PID=$!
trap_cleanup_fn cleanup_port_forward
local curl_args=(-fsS --max-time 2)
if [[ "$MINIO_SCHEME" == "https" && "$MINIO_TLS_INSECURE" == "yes" ]]; then
curl_args+=(-k)
fi
retry 15 1 curl "${curl_args[@]}" \
"${MINIO_SCHEME}://127.0.0.1:${PF_LOCAL_PORT}/minio/health/live" \
-o /dev/null
log "MinIO health endpoint 응답 확인"
}
read_root_credentials() {
local env_blob
env_blob="$(
vault_exec kv get -format=json secret/minio/tenant-env \
| jq -r '.data.data["config.env"]'
)"
ROOT_USER="$(
printf '%s\n' "$env_blob" \
| sed -n 's/^export MINIO_ROOT_USER="\(.*\)"$/\1/p'
)"
ROOT_PASSWORD="$(
printf '%s\n' "$env_blob" \
| sed -n 's/^export MINIO_ROOT_PASSWORD="\(.*\)"$/\1/p'
)"
unset env_blob
[[ -n "$ROOT_USER" && -n "$ROOT_PASSWORD" ]] \
|| die "MinIO root 자격증명 파싱 실패 — secret/minio/tenant-env 확인"
}
init_mc_config() {
MC_CONFIG_DIR="$(mktemp -d -t project-infra-mc.XXXXXX)"
chmod 700 "$MC_CONFIG_DIR"
trap_cleanup_path "$MC_CONFIG_DIR"
MC_GLOBAL_ARGS=(--config-dir "$MC_CONFIG_DIR")
if [[ "$MINIO_SCHEME" == "https" && "$MINIO_TLS_INSECURE" == "yes" ]]; then
MC_GLOBAL_ARGS+=(--insecure)
fi
mc_cmd admin accesskey create --help >/dev/null \
|| die "현재 mc는 'admin accesskey create'를 지원하지 않습니다."
}
import_mc_alias() {
local alias_name="$1"
local endpoint="$2"
local access_key="$3"
local secret_key="$4"
# NUL-delimited stdin keeps both credentials out of jq and mc argv.
printf '%s\0%s\0%s\0' "$endpoint" "$access_key" "$secret_key" \
| jq -Rs '
split("\u0000") |
{
url: .[0],
accessKey: .[1],
secretKey: .[2],
api: "s3v4",
path: "auto"
}
' \
| mc_cmd alias import "$alias_name" >/dev/null
}
configure_root_alias() {
local endpoint="${MINIO_SCHEME}://127.0.0.1:${PF_LOCAL_PORT}"
import_mc_alias "$MC_ALIAS" "$endpoint" "$ROOT_USER" "$ROOT_PASSWORD"
unset ROOT_USER ROOT_PASSWORD
}
ensure_bucket() {
log "MinIO bucket 보장 (${DOCKER_REGISTRY_BUCKET})"
mc_cmd mb --ignore-existing "${MC_ALIAS}/${DOCKER_REGISTRY_BUCKET}" >/dev/null
}
write_inline_policy() {
POLICY_FILE="$MC_CONFIG_DIR/docker-registry-policy.json"
jq -nc --arg bucket "$DOCKER_REGISTRY_BUCKET" '{
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: [
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:GetBucketLocation"
],
Resource: ["arn:aws:s3:::\($bucket)"]
},
{
Effect: "Allow",
Action: [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload"
],
Resource: ["arn:aws:s3:::\($bucket)/*"]
}
]
}' >"$POLICY_FILE"
chmod 600 "$POLICY_FILE"
}
read_vault_registry_field() {
local field="$1"
vault_exec kv get -format=json secret/docker-registry/minio \
| jq -er --arg field "$field" '.data.data[$field]'
}
verify_registry_credentials() {
local access_key="$1"
local secret_key="$2"
local endpoint="${MINIO_SCHEME}://127.0.0.1:${PF_LOCAL_PORT}"
import_mc_alias "$REGISTRY_CHECK_ALIAS" "$endpoint" "$access_key" "$secret_key"
mc_cmd stat "${REGISTRY_CHECK_ALIAS}/${DOCKER_REGISTRY_BUCKET}" >/dev/null \
|| die "Vault의 docker-registry MinIO credential 검증 실패"
}
use_existing_access_key() {
vault_kv_exists "docker-registry/minio" || return 1
local access_key secret_key
access_key="$(read_vault_registry_field access_key)" \
|| die "secret/docker-registry/minio.access_key 읽기 실패"
secret_key="$(read_vault_registry_field secret_key)" \
|| die "secret/docker-registry/minio.secret_key 읽기 실패"
if mc_cmd admin accesskey info "$MC_ALIAS" "$access_key" >/dev/null 2>&1; then
verify_registry_credentials "$access_key" "$secret_key"
unset access_key secret_key
log "기존 docker-registry MinIO access key 검증 완료"
return 0
fi
if mc_cmd admin user info "$MC_ALIAS" "$access_key" >/dev/null 2>&1; then
unset access_key secret_key
die "기존 Vault 값은 legacy MinIO user를 가리킵니다. 승인된 credential rotation 절차로 먼저 전환하세요."
fi
unset access_key secret_key
die "Vault에는 registry credential이 있지만 대응하는 MinIO access key가 없습니다. 자동 재생성하지 않습니다."
}
persist_generated_access_key() {
local access_key="$1"
local secret_key="$2"
if ! printf '%s\0%s\0' "$access_key" "$secret_key" \
| jq -Rs '
split("\u0000") |
{
access_key: .[0],
secret_key: .[1]
}
' \
| vault_exec kv put secret/docker-registry/minio - >/dev/null; then
die "생성된 docker-registry credential의 Vault 기록 실패"
fi
ACCESS_KEY_PERSISTED="yes"
}
create_access_key() {
log "bucket-scoped docker-registry MinIO access key 생성"
local creation_json access_key secret_key
creation_json="$(
mc_cmd --json admin accesskey create "$MC_ALIAS" \
--name docker-registry \
--description "docker-registry bucket access" \
--policy "$POLICY_FILE"
)" || die "MinIO access key 생성 실패"
access_key="$(
jq -ser 'map(select(.accessKey? or .access_key?)) | last | .accessKey // .access_key' \
<<<"$creation_json"
)" || die "생성된 MinIO access key 응답 파싱 실패"
secret_key="$(
jq -ser 'map(select(.secretKey? or .secret_key?)) | last | .secretKey // .secret_key' \
<<<"$creation_json"
)" || die "생성된 MinIO secret key 응답 파싱 실패"
unset creation_json
[[ -n "$access_key" && -n "$secret_key" ]] \
|| die "MinIO가 생성한 access key 응답이 비어 있습니다."
CREATED_ACCESS_KEY="$access_key"
verify_registry_credentials "$access_key" "$secret_key"
persist_generated_access_key "$access_key" "$secret_key"
unset access_key secret_key CREATED_ACCESS_KEY
log "생성된 docker-registry credential을 Vault에 기록"
}
main() {
require_cmd kubectl jq mc curl sed
require_env REPO_ROOT ENV_NAME
resolve_transport_defaults
local keys_file="${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json}"
vault_login_root_from_keyfile "$keys_file"
wait_minio_ready
start_port_forward
read_root_credentials
init_mc_config
configure_root_alias
trap_cleanup_fn cleanup_created_access_key
ensure_bucket
write_inline_policy
use_existing_access_key || create_access_key
log "minio-provision-registry 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env bash
# Initialize Vault (Shamir 5-of-3) and bootstrap the Kubernetes auth method,
# KV v2 secrets engine, and the vault-secrets-operator policy/role.
#
# Idempotent: 이미 초기화된 Vault 에서는 init 을 skip 하지만 unseal 과
# post-init 구성은 매번 재적용해서 bootstrap.sh 에서 여러 번 호출되어도 안전.
#
# Required env:
# REPO_ROOT — repository root
#
# Optional env:
# VAULT_NAMESPACE / VAULT_POD / VAULT_KEYS_FILE — see lib/vault.sh
# ENV_NAME — dev|staging|prod (bootstrap.sh 가 주입). prod 일 때 기본 경로
# (repo working tree) 사용을 차단한다 — VAULT_KEYS_FILE 를 명시
# 해서 repo 바깥의 안전한 경로로 써야 한다.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/vault.sh
. "$SCRIPT_DIR/../lib/vault.sh"
KEY_SHARES=5
KEY_THRESHOLD=3
resolve_keys_file() {
DEFAULT_KEYS_FILE="$REPO_ROOT/vault-init-keys.json"
KEYS_FILE="${VAULT_KEYS_FILE:-$DEFAULT_KEYS_FILE}"
local keys_dir keys_name resolved_keys_dir resolved_repo_root
keys_dir="$(dirname "$KEYS_FILE")"
keys_name="$(basename "$KEYS_FILE")"
resolved_keys_dir="$(cd "$keys_dir" 2>/dev/null && pwd -P)" \
|| die "VAULT_KEYS_FILE 상위 디렉터리가 없습니다: $keys_dir"
resolved_repo_root="$(cd "$REPO_ROOT" && pwd -P)"
KEYS_FILE="$resolved_keys_dir/$keys_name"
[[ ! -L "$KEYS_FILE" ]] \
|| die "VAULT_KEYS_FILE 은 symbolic link일 수 없습니다: $KEYS_FILE"
# prod 에서 repo 내부 평문 저장 차단 — root token / unseal key 가 repo
# working tree 에 남으면 accidental commit / backup / IDE 인덱싱 경로로 유출.
if [[ "${ENV_NAME:-}" == "prod" &&
("$KEYS_FILE" == "$resolved_repo_root" || "$KEYS_FILE" == "$resolved_repo_root/"*) ]]; then
die "env=prod 에서는 VAULT_KEYS_FILE 를 repo 바깥 경로로 반드시 지정해야 합니다. (예: VAULT_KEYS_FILE=/run/secrets/vault-keys.json 또는 sops age 로 암호화)"
fi
}
initialize_if_needed() {
if vault_is_initialized; then
log "Vault 는 이미 초기화되어 있습니다. 초기화 단계를 건너뜁니다."
return 0
fi
log "Vault 초기화 중 (shares=${KEY_SHARES}, threshold=${KEY_THRESHOLD})..."
local keys_dir
keys_dir="$(dirname "$KEYS_FILE")"
[[ -d "$keys_dir" ]] || die "VAULT_KEYS_FILE 상위 디렉터리가 없습니다: $keys_dir"
local tmp_out
tmp_out="$(mktemp "${keys_dir}/.vault-init-keys.XXXXXX")"
trap_cleanup_path "$tmp_out"
vault_exec operator init \
-key-shares="$KEY_SHARES" \
-key-threshold="$KEY_THRESHOLD" \
-format=json >"$tmp_out"
chmod 600 "$tmp_out"
mv -- "$tmp_out" "$KEYS_FILE"
chmod 600 "$KEYS_FILE"
log "unseal keys + root token 저장: $KEYS_FILE (권한 0600)"
}
unseal_if_needed() {
if vault_is_sealed; then
log "Vault sealed 상태, unseal 진행"
vault_unseal_from_keyfile "$KEYS_FILE"
else
log "Vault unsealed 상태"
fi
}
enable_kv_v2() {
if vault_secrets_engine_enabled "secret"; then
log "secrets engine 'secret/' 이미 활성화됨"
return 0
fi
log "KV v2 secrets engine 을 secret/ 에 활성화"
vault_exec secrets enable -path=secret kv-v2
}
enable_k8s_auth() {
if vault_auth_method_enabled "kubernetes"; then
log "auth method 'kubernetes/' 이미 활성화됨"
else
log "Kubernetes auth method 활성화"
vault_exec auth enable kubernetes
fi
log "Kubernetes auth method 설정 (kubernetes_host + ca + token_reviewer_jwt)"
vault_exec_sh '
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc.cluster.local:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token
' >/dev/null
}
write_policies_and_roles() {
log "policy 'vso-auth-platform' 작성 (identity-postgres/*, auth-server/*, keycloak/*, oauth2-proxy/*)"
vault_exec_sh 'cat <<"EOF" | vault policy write vso-auth-platform -
path "secret/data/identity-postgres/*" {
capabilities = ["read"]
}
path "secret/data/auth-server/*" {
capabilities = ["read"]
}
path "secret/data/keycloak/*" {
capabilities = ["read"]
}
path "secret/data/oauth2-proxy/*" {
capabilities = ["read"]
}
EOF
' >/dev/null
log "policy 'vso-storage' 작성 (minio/*, docker-registry/*)"
vault_exec_sh 'cat <<"EOF" | vault policy write vso-storage -
path "secret/data/minio/*" {
capabilities = ["read"]
}
path "secret/data/docker-registry/*" {
capabilities = ["read"]
}
EOF
' >/dev/null
log "k8s auth role 'vso-auth-platform' 작성"
vault_exec write auth/kubernetes/role/vso-auth-platform \
bound_service_account_names=vault-secrets-operator \
bound_service_account_namespaces="$VAULT_NAMESPACE" \
audience=vault \
policies=vso-auth-platform \
ttl=1h >/dev/null
log "k8s auth role 'vso-storage' 작성"
vault_exec write auth/kubernetes/role/vso-storage \
bound_service_account_names=vault-secrets-operator \
bound_service_account_namespaces="$VAULT_NAMESPACE" \
audience=vault \
policies=vso-storage \
ttl=1h >/dev/null
}
cleanup_legacy() {
log "구 policy/role 정리 (존재하면 삭제)"
# 이전 단일 policy (세분화 전) 잔재
vault_exec policy delete vault-secrets-operator >/dev/null 2>&1 || true
vault_exec delete auth/kubernetes/role/vault-secrets-operator >/dev/null 2>&1 || true
# Registry auth 제거로 더 이상 사용 안 하는 policy/role
vault_exec policy delete vso-registry >/dev/null 2>&1 || true
vault_exec delete auth/kubernetes/role/vso-registry >/dev/null 2>&1 || true
}
main() {
require_cmd kubectl jq
require_env REPO_ROOT
resolve_keys_file
initialize_if_needed
unseal_if_needed
vault_login_root_from_keyfile "$KEYS_FILE"
enable_kv_v2
enable_k8s_auth
write_policies_and_roles
cleanup_legacy
log "vault-init 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+280
View File
@@ -0,0 +1,280 @@
#!/usr/bin/env bash
# Seed application secrets into Vault KV v2.
#
# 이미 Vault KV 에 있는 경로는 skip — 운영자가 회전한 값을 절대 덮어쓰지 않는다.
# 아직 없는 경로에 대해 다음 순으로 값을 결정:
# 1) 환경 변수에 값이 있으면 사용
# 2) TTY 면 read -r -s 로 프롬프트 (확인 재입력 + echo 없음, bash history 안전)
# 3) 비대화 + env 비어있고 AUTO_GENERATE=yes 면 openssl rand 로 자동 생성
# → 생성된 값은 stdout 에 절대 출력하지 않음. 필요 시 나중에:
# vault kv get -field=password secret/<path>
#
# Vault CLI 의 `vault kv put <path> -` 모드로 **JSON payload 를 stdin** 으로 전달해
# 비밀번호가 argv / 프로세스 테이블 / 쉘 히스토리 어디에도 노출되지 않는다.
#
# Required env:
# REPO_ROOT
#
# Optional env (비면 대화형 입력 or AUTO_GENERATE):
# POSTGRES_SUPERUSER_USERNAME 기본 postgres
# POSTGRES_SUPERUSER_PASSWORD
#
# KEYCLOAK_DB_PASSWORD
#
# AUTH_SERVER_DB_USERNAME 기본 auth_server
# AUTH_SERVER_DB_PASSWORD
#
# KEYCLOAK_ADMIN_USERNAME 기본 admin
# KEYCLOAK_ADMIN_PASSWORD
#
# MINIO_ROOT_USER 기본 minioadmin
# MINIO_ROOT_PASSWORD
#
# AUTH_SERVER_INGRESS_CLIENT_SECRET Keycloak realm client 'auth-server-ingress' 의 secret.
# KeycloakRealmImport 가 ${AUTH_SERVER_INGRESS_CLIENT_SECRET}
# env 치환으로 가져간다. oauth2-proxy 도 동일 값을 받음.
#
# OAUTH2_PROXY_COOKIE_SECRET oauth2-proxy session cookie signing/encryption key.
#
# DOCKER_REGISTRY_PUSH_USERNAME 외부 Ingress push 용 BasicAuth username.
# 기본값 'registry-push'.
# DOCKER_REGISTRY_PUSH_PASSWORD 외부 Ingress push 용 BasicAuth password.
#
# Options:
# AUTO_GENERATE=yes 대화형 TTY 가 아니고 env 도 비었을 때 랜덤 값 생성
#
# NOTE: 비밀번호에 큰따옴표 " 는 사용 금지 (MinIO env-file 포맷이 깨짐).
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/vault.sh
. "$SCRIPT_DIR/../lib/vault.sh"
: "${POSTGRES_SUPERUSER_USERNAME:=postgres}"
: "${AUTH_SERVER_DB_USERNAME:=auth_server}"
: "${KEYCLOAK_ADMIN_USERNAME:=admin}"
: "${MINIO_ROOT_USER:=minioadmin}"
: "${DOCKER_REGISTRY_PUSH_USERNAME:=registry-push}"
generate_random_password() {
openssl rand -base64 48 | tr -d '/+=' | head -c 24
}
resolve_password() {
local var_name="$1"
local prompt_label="$2"
local value="${!var_name:-}"
if [[ -n "$value" ]]; then
printf '%s' "$value"
return 0
fi
if [[ -t 0 ]]; then
local input confirm
while true; do
read -r -s -p "${prompt_label}: " input
echo >&2
if [[ -z "$input" ]]; then
err " 비밀번호가 비어있습니다. 다시 입력하세요."
continue
fi
if [[ "$input" == *'"'* ]]; then
err " 큰따옴표(\") 는 사용할 수 없습니다 (MinIO env 포맷)."
continue
fi
read -r -s -p "${prompt_label} 한 번 더: " confirm
echo >&2
if [[ "$input" == "$confirm" ]]; then
printf '%s' "$input"
return 0
fi
err " 일치하지 않습니다. 다시."
done
fi
if [[ "${AUTO_GENERATE:-}" == "yes" ]]; then
local generated
generated="$(generate_random_password)"
warn " ${var_name}: 자동 생성 (vault kv get 으로 조회 가능)"
printf '%s' "$generated"
return 0
fi
die "${var_name} 가 비어있고 비대화 환경이며 AUTO_GENERATE=yes 도 아닙니다."
}
seed_if_missing() {
local path="$1"
if vault_kv_exists "$path"; then
log " secret/${path}: 이미 존재 → skip (회전된 값 보호)"
return 1
fi
return 0
}
kv_put_nul_json() {
local path="$1"
local jq_expr="$2"
jq -Rs "$jq_expr" \
| kubectl exec -i -n "$VAULT_NAMESPACE" "$VAULT_POD" -- \
vault kv put "secret/${path}" - >/dev/null
}
vault_kv_get_field() {
local path="$1"
local field="$2"
vault_exec kv get -format=json "secret/${path}" \
| jq -r --arg field "$field" '.data.data[$field] // empty'
}
seed_identity_postgres_superuser() {
seed_if_missing "identity-postgres/superuser" || return 0
local pw
pw="$(resolve_password POSTGRES_SUPERUSER_PASSWORD "Postgres superuser 비밀번호")"
printf '%s\0%s\0' "$POSTGRES_SUPERUSER_USERNAME" "$pw" \
| kv_put_nul_json "identity-postgres/superuser" \
'split("\u0000") | {username: .[0], password: .[1]}'
unset pw
log " secret/identity-postgres/superuser 작성"
}
seed_keycloak_db() {
seed_if_missing "keycloak/db" || return 0
local pw
pw="$(resolve_password KEYCLOAK_DB_PASSWORD "Keycloak DB 비밀번호")"
printf '%s\0' "$pw" \
| kv_put_nul_json "keycloak/db" \
'split("\u0000") | {password: .[0]}'
unset pw
log " secret/keycloak/db 작성"
}
seed_auth_server_db() {
seed_if_missing "auth-server/db" || return 0
local pw
pw="$(resolve_password AUTH_SERVER_DB_PASSWORD "auth-server DB 비밀번호")"
printf '%s\0%s\0' "$AUTH_SERVER_DB_USERNAME" "$pw" \
| kv_put_nul_json "auth-server/db" \
'split("\u0000") | {
SPRING_DATASOURCE_USERNAME: .[0],
SPRING_DATASOURCE_PASSWORD: .[1]
}'
unset pw
log " secret/auth-server/db 작성"
}
seed_keycloak_bootstrap_admin() {
seed_if_missing "keycloak/bootstrap-admin" || return 0
local pw
pw="$(resolve_password KEYCLOAK_ADMIN_PASSWORD "Keycloak 관리자 비밀번호")"
printf '%s\0%s\0' "$KEYCLOAK_ADMIN_USERNAME" "$pw" \
| kv_put_nul_json "keycloak/bootstrap-admin" \
'split("\u0000") | {
KEYCLOAK_ADMIN: .[0],
KEYCLOAK_ADMIN_PASSWORD: .[1]
}'
unset pw
log " secret/keycloak/bootstrap-admin 작성"
}
seed_minio_tenant_env() {
seed_if_missing "minio/tenant-env" || return 0
local pw env_content
pw="$(resolve_password MINIO_ROOT_PASSWORD "MinIO 루트 비밀번호")"
env_content=$(
printf 'export MINIO_ROOT_USER="%s"\nexport MINIO_ROOT_PASSWORD="%s"\n' \
"$MINIO_ROOT_USER" "$pw"
)
printf '%s\0' "$env_content" \
| kv_put_nul_json "minio/tenant-env" \
'split("\u0000") | {"config.env": .[0]}'
unset pw env_content
log " secret/minio/tenant-env 작성"
}
# 외부에서 registry.project.com 으로 push 할 때 Traefik BasicAuth 가 검증할
# htpasswd 라인을 저장한다. Registry 자체 auth 는 켜지지 않고, 외부 Ingress
# 경계에서만 인증한다.
seed_docker_registry_basic_auth() {
seed_if_missing "docker-registry/basic-auth" || return 0
local pw hash users
pw="$(resolve_password DOCKER_REGISTRY_PUSH_PASSWORD "docker-registry 외부 push 비밀번호")"
hash="$(printf '%s' "$pw" | openssl passwd -apr1 -stdin)"
users="${DOCKER_REGISTRY_PUSH_USERNAME}:${hash}"
printf '%s\0%s\0%s\0' "$DOCKER_REGISTRY_PUSH_USERNAME" "$pw" "$users" \
| kv_put_nul_json "docker-registry/basic-auth" \
'split("\u0000") | {
username: .[0],
password: .[1],
users: .[2]
}'
unset pw hash users
log " secret/docker-registry/basic-auth 작성 (username=${DOCKER_REGISTRY_PUSH_USERNAME})"
}
# Keycloak realm import 가 client secret 을 ${AUTH_SERVER_INGRESS_CLIENT_SECRET}
# env 치환으로 요구한다. oauth2-proxy 는 동일 값을 /etc/oauth2-proxy-secrets/
# client-secret 파일로 읽는다. 두 쪽이 같은 값을 써야 OIDC client 인증이 맞는다.
seed_keycloak_client_auth_server_ingress() {
seed_if_missing "keycloak/clients/auth-server-ingress" || return 0
local pw
pw="$(resolve_password AUTH_SERVER_INGRESS_CLIENT_SECRET \
"Keycloak client 'auth-server-ingress' secret")"
AUTH_SERVER_INGRESS_CLIENT_SECRET="$pw"
printf '%s\0' "$pw" \
| kv_put_nul_json "keycloak/clients/auth-server-ingress" \
'split("\u0000") | {client_secret: .[0]}'
unset pw
log " secret/keycloak/clients/auth-server-ingress 작성"
}
seed_oauth2_proxy_forward_auth() {
seed_if_missing "oauth2-proxy/forward-auth" || return 0
local client_secret cookie_secret
if [[ -n "${AUTH_SERVER_INGRESS_CLIENT_SECRET:-}" ]]; then
client_secret="$AUTH_SERVER_INGRESS_CLIENT_SECRET"
elif vault_kv_exists "keycloak/clients/auth-server-ingress"; then
client_secret="$(vault_kv_get_field "keycloak/clients/auth-server-ingress" "client_secret")"
else
client_secret="$(resolve_password AUTH_SERVER_INGRESS_CLIENT_SECRET \
"Keycloak client 'auth-server-ingress' secret")"
fi
[[ -n "$client_secret" ]] || die "auth-server-ingress client secret 을 확인할 수 없습니다."
cookie_secret="$(resolve_password OAUTH2_PROXY_COOKIE_SECRET \
"oauth2-proxy cookie secret")"
printf '%s\0%s\0' "$client_secret" "$cookie_secret" \
| kv_put_nul_json "oauth2-proxy/forward-auth" \
'split("\u0000") | {
client_secret: .[0],
cookie_secret: .[1]
}'
unset client_secret cookie_secret
log " secret/oauth2-proxy/forward-auth 작성"
}
main() {
require_cmd kubectl jq openssl
require_env REPO_ROOT
local keys_file="${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json}"
vault_login_root_from_keyfile "$keys_file"
log "애플리케이션 Secret seed 시작 (이미 존재하는 경로는 skip)"
seed_identity_postgres_superuser
seed_keycloak_db
seed_auth_server_db
seed_keycloak_bootstrap_admin
seed_minio_tenant_env
seed_keycloak_client_auth_server_ingress
seed_oauth2_proxy_forward_auth
seed_docker_registry_basic_auth
log "vault-seed-apps 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env bash
# Configure operator authentication for Vault.
#
# Adds a `userpass` auth method + `vault-admin` policy + one or more admin users.
# Operators use these credentials instead of the root token for day-to-day work.
# The root token stays offline and is only used for break-glass / rekey.
#
# Required env:
# REPO_ROOT — repo root (for vault-init-keys.json path)
#
# Interactive inputs (대화형 TTY 에서 자동 프롬프트):
# VAULT_ADMIN_USERNAME — login name (비면 프롬프트)
# VAULT_ADMIN_PASSWORD — 초기 비밀번호 (비면 무음 입력 프롬프트)
#
# Non-interactive 시 (CI 등): 위 두 개를 env var 로 전달해야 함.
#
# Optional env:
# VAULT_ADMIN_TTL — token TTL per login (default: 8h)
# VAULT_ADMIN_MAX_TTL — maximum TTL (default: 24h)
#
# 입력 검증:
# - username: 영문/숫자/._- 만 허용, 1..64 자
# - TTL/MAX_TTL: 숫자+단위(s|m|h|d), 1..16 자
# 이 값들은 Vault API 경로/파라미터에 쓰이는데, 과거 구현은 `sh -c` 문자열에
# 직접 보간했다. allowlist 를 통과시키면 쉘 메타문자 주입 경로가 닫히고,
# 아래 `vault write` 호출은 `sh -c` 없이 kubectl exec 로 직접 실행한다.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/vault.sh
. "$SCRIPT_DIR/../lib/vault.sh"
USERNAME_RE='^[A-Za-z0-9._-]{1,64}$'
TTL_RE='^[0-9]+[smhd]?$'
validate_inputs() {
[[ "$VAULT_ADMIN_USERNAME" =~ $USERNAME_RE ]] \
|| die "VAULT_ADMIN_USERNAME 형식 불일치 (영문/숫자/._- 만 허용, 1..64자): '$VAULT_ADMIN_USERNAME'"
[[ "$ADMIN_TTL" =~ $TTL_RE ]] \
|| die "VAULT_ADMIN_TTL 형식 불일치 (예: 8h, 3600, 30m): '$ADMIN_TTL'"
[[ "$ADMIN_MAX_TTL" =~ $TTL_RE ]] \
|| die "VAULT_ADMIN_MAX_TTL 형식 불일치 (예: 24h, 86400): '$ADMIN_MAX_TTL'"
((${#ADMIN_TTL} <= 16 && ${#ADMIN_MAX_TTL} <= 16)) \
|| die "TTL 값이 너무 깁니다."
}
prompt_username_if_missing() {
[[ -n "${VAULT_ADMIN_USERNAME:-}" ]] && return 0
if [[ ! -t 0 ]]; then
die "VAULT_ADMIN_USERNAME 환경 변수가 비어있습니다 (비대화 환경)."
fi
read -r -p "운영자 username (e.g. alice): " VAULT_ADMIN_USERNAME
[[ -n "$VAULT_ADMIN_USERNAME" ]] || die "username 이 비어있습니다."
}
prompt_password_if_missing() {
[[ -n "${VAULT_ADMIN_PASSWORD:-}" ]] && return 0
if [[ ! -t 0 ]]; then
die "VAULT_ADMIN_PASSWORD 환경 변수가 비어있습니다 (비대화 환경)."
fi
read -r -s -p "${VAULT_ADMIN_USERNAME} 초기 비밀번호: " VAULT_ADMIN_PASSWORD
echo >&2
[[ -n "$VAULT_ADMIN_PASSWORD" ]] || die "비밀번호가 비어있습니다."
local confirm
read -r -s -p "비밀번호 한 번 더 입력: " confirm
echo >&2
[[ "$VAULT_ADMIN_PASSWORD" == "$confirm" ]] \
|| die "비밀번호가 일치하지 않습니다."
unset confirm
}
enable_userpass() {
if vault_auth_method_enabled "userpass"; then
log "auth method 'userpass/' 이미 활성화됨"
else
log "userpass auth method 활성화"
vault_exec auth enable userpass
fi
}
write_admin_policy() {
# sudo capability 는 sys/* 일부 엔드포인트 (seal/audit/generate-root) 때문에 필요.
# 권한이 강하므로 인프라 담당자에게만 부여.
log "policy 'vault-admin' 작성"
vault_exec_sh 'cat <<"EOF" | vault policy write vault-admin -
# KV v2 — 전 경로 관리
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/data/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/metadata/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Auth method / identity 관리
path "auth/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
path "identity/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# policy 관리 (자기 자신 포함)
path "sys/policies/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "sys/policy/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# 상태 / 감사 장치 / mount 관리
path "sys/health" { capabilities = ["read"] }
path "sys/seal-status" { capabilities = ["read"] }
path "sys/mounts" { capabilities = ["read", "list"] }
path "sys/mounts/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
path "sys/audit" { capabilities = ["read", "list"] }
path "sys/audit/*" {
capabilities = ["create", "read", "update", "delete", "sudo"]
}
EOF
' >/dev/null
}
# 사용자 생성/갱신 — `sh -c` 를 쓰지 않는다.
#
# 비밀번호는 `vault write -` 의 JSON stdin 으로 전달해 argv / 쉘 히스토리 노출이
# 전혀 없다. 나머지 파라미터 (username / TTL) 는 allowlist 를 통과한 값만
# kubectl exec argv 로 직접 전달된다 (쉘 해석 없음).
create_or_update_user() {
log "userpass user '${VAULT_ADMIN_USERNAME}' 생성/갱신 (ttl=${ADMIN_TTL} max_ttl=${ADMIN_MAX_TTL})"
printf '%s\0%s\0%s\0' "$VAULT_ADMIN_PASSWORD" "$ADMIN_TTL" "$ADMIN_MAX_TTL" \
| jq -Rs '
split("\u0000") |
{
password: .[0],
token_policies: "vault-admin",
token_ttl: .[1],
token_max_ttl: .[2]
}
' \
| kubectl exec -i -n "$VAULT_NAMESPACE" "$VAULT_POD" -- \
vault write "auth/userpass/users/${VAULT_ADMIN_USERNAME}" - >/dev/null
unset VAULT_ADMIN_PASSWORD
}
print_next_steps() {
log "vault-setup-admin 태스크 완료"
log ""
log "다음 단계:"
log " 1) 운영자는 다음 명령으로 로그인 (root token 사용 중단)"
log " vault login -method=userpass username=${VAULT_ADMIN_USERNAME}"
log " 2) 각 운영자는 첫 로그인 후 비밀번호를 변경"
log " vault write auth/userpass/users/${VAULT_ADMIN_USERNAME}/password password='<new>'"
log " 3) root token 은 오프라인 금고로 이동 후 vault-init-keys.json 에서 삭제 검토"
log " (재발급은 \"vault operator generate-root -init\" 으로 가능)"
}
main() {
require_cmd kubectl jq
require_env REPO_ROOT
prompt_username_if_missing
prompt_password_if_missing
ADMIN_TTL="${VAULT_ADMIN_TTL:-8h}"
ADMIN_MAX_TTL="${VAULT_ADMIN_MAX_TTL:-24h}"
validate_inputs
local keys_file="${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json}"
vault_login_root_from_keyfile "$keys_file"
enable_userpass
write_admin_policy
create_or_update_user
print_next_steps
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Install (or upgrade) the vault-secrets-operator Helm release.
#
# Idempotent. 이전 실행에서 failed / pending / uninstalling 상태로 남은 릴리즈
# 메타데이터가 있으면 먼저 uninstall 한 뒤 깨끗하게 재설치한다 (Helm 의
# "no deployed releases" 문제 회피).
#
# `--atomic` 은 설치 실패 시 자동 rollback 을 수행하는데, rollback 결과
# "no deployed releases" 상태가 돼서 다음 helm upgrade --install 이 실패하는
# 연쇄 문제를 낳는다. 이 스크립트는 대신 실패 상태를 명시적으로 감지해
# uninstall 로 cleanup 하므로 --atomic 없이 실행한다.
#
# VSO 는 전용 namespace 에 설치된다 (mnt 에 직접 두면 PSS Restricted 라벨과
# 차트 Pod spec 의 불일치로 Pod 생성이 막힘). Controller 는 cluster-scoped RBAC
# 를 갖고 있어 mnt 의 VaultAuth/VaultStaticSecret 도 정상 watch 한다.
#
# Optional env:
# VSO_NAMESPACE — target namespace (default: vault-secrets-operator-system)
# VSO_RELEASE — helm release name (default: vault-secrets-operator)
# VSO_VERSION — chart version pin (default: 0.9.0)
# VSO_VALUES_FILE — values file path
# (default: $REPO_ROOT/gitops/platform/secret-delivery/base/helm/values.yaml)
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
: "${VSO_NAMESPACE:=vault-secrets-operator-system}"
: "${VSO_RELEASE:=vault-secrets-operator}"
: "${VSO_VERSION:=0.9.0}"
resolve_values_file() {
: "${VSO_VALUES_FILE:=$REPO_ROOT/gitops/platform/secret-delivery/base/helm/values.yaml}"
[[ -f "$VSO_VALUES_FILE" ]] || die "values.yaml 없음: $VSO_VALUES_FILE"
}
register_repo() {
log "helm repo: hashicorp 등록 (idempotent)"
helm repo add hashicorp https://helm.releases.hashicorp.com >/dev/null 2>&1 || true
helm repo update hashicorp >/dev/null
}
# 기존 릴리즈가 dirty 상태 (failed/pending-*/uninstalling/uninstalled) 면 선제
# uninstall 해야 다음 upgrade --install 이 성공한다.
cleanup_dirty_release() {
local status=""
if helm -n "$VSO_NAMESPACE" status "$VSO_RELEASE" -o json >/dev/null 2>&1; then
status="$(helm -n "$VSO_NAMESPACE" status "$VSO_RELEASE" -o json \
| jq -r '.info.status // "unknown"')"
fi
case "$status" in
"")
log "기존 Helm 릴리즈 없음 → 신규 install"
;;
"deployed")
log "기존 릴리즈 상태=deployed → upgrade 진행"
;;
"failed" | "pending-install" | "pending-upgrade" | "pending-rollback" | "uninstalling" | "uninstalled")
warn "기존 릴리즈 상태=$status (dirty) → helm uninstall 먼저"
helm -n "$VSO_NAMESPACE" uninstall "$VSO_RELEASE" --wait --timeout 5m 2>/dev/null \
|| warn " uninstall 중 오류 무시 (이미 부분 정리됐을 수 있음)"
;;
*)
warn "기존 릴리즈 상태=$status (예상 외) — 일단 upgrade 시도"
;;
esac
}
install_or_upgrade() {
log "helm upgrade --install $VSO_RELEASE (v=$VSO_VERSION ns=$VSO_NAMESPACE)"
helm upgrade --install "$VSO_RELEASE" hashicorp/vault-secrets-operator \
--namespace "$VSO_NAMESPACE" \
--create-namespace \
--version "$VSO_VERSION" \
--values "$VSO_VALUES_FILE" \
--wait \
--timeout 5m
}
wait_ready() {
log "VSO Pod Ready 확인"
kubectl -n "$VSO_NAMESPACE" wait --for=condition=Ready \
pod -l "app.kubernetes.io/name=vault-secrets-operator" \
--timeout=120s
}
main() {
require_cmd helm kubectl jq
require_env REPO_ROOT
resolve_values_file
register_repo
cleanup_dirty_release
install_or_upgrade
wait_ready
log "vso-install 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi