init: 폴더구조 설계 및 인프라 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:31:53 +09:00
parent 34ad612281
commit f9c463f87a
1839 changed files with 323096 additions and 1 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
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
# MinIO Tenant 기동 후 docker-registry 용 bucket / 서비스 user / policy 를
# 프로비저닝.
#
# 전제:
# - MinIO Tenant Pod 가 Ready (VSO 가 minio-tenant-env Secret 을 이미 동기화)
# - Vault 가 init + unseal 완료
# - vault-seed 가 다음 경로를 seed 완료:
# secret/minio/tenant-env (root 계정, config.env 포맷)
# secret/docker-registry/minio (access_key / secret_key)
#
# 수행 순서:
# 1. Vault 에서 MinIO root 자격증명 + registry 전용 AK/SK 읽기
# 2. kubectl port-forward 로 127.0.0.1 → MinIO Service 터널 생성
# 3. mc alias 등록 → bucket 생성 → policy 작성 → user 생성 → policy 부착
#
# idempotent: 이미 있는 bucket/user/policy 는 건너뜀 (회전은 별도 절차).
#
# Required env:
# REPO_ROOT
#
# Optional env:
# DOCKER_REGISTRY_BUCKET 기본 docker-registry
# DOCKER_REGISTRY_POLICY 기본 docker-registry-rw
# PF_LOCAL_PORT 기본 9900 (local port-forward)
# MINIO_SERVICE_PORT 기본 443 (Service 쪽 포트 — autoCert HTTPS)
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}"
: "${DOCKER_REGISTRY_POLICY:=docker-registry-rw}"
: "${PF_LOCAL_PORT:=9900}"
: "${MINIO_SERVICE_PORT:=443}"
: "${MINIO_NAMESPACE:=mnt}"
: "${MINIO_SERVICE:=minio}"
MC_ALIAS="minio-prov"
PF_PID=""
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
}
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
retry 15 1 curl -ks --max-time 2 \
"https://127.0.0.1:${PF_LOCAL_PORT}/minio/health/live" -o /dev/null
log " 터널 응답 OK"
}
read_root_credentials() {
# secret/minio/tenant-env 는 "config.env" 필드 하나에 쉘 export 문 2 줄이
# 들어 있다. 파싱해서 ROOT_USER / ROOT_PASSWORD 전역에 적재.
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')"
[[ -n "$ROOT_USER" && -n "$ROOT_PASSWORD" ]] \
|| die "MinIO root 자격증명 파싱 실패 — secret/minio/tenant-env 확인"
}
read_registry_credentials() {
local json
json="$(vault_exec kv get -format=json secret/docker-registry/minio)"
REG_AK="$(printf '%s' "$json" | jq -r '.data.data.access_key')"
REG_SK="$(printf '%s' "$json" | jq -r '.data.data.secret_key')"
[[ -n "$REG_AK" && -n "$REG_SK" && "$REG_AK" != "null" && "$REG_SK" != "null" ]] \
|| die "docker-registry 자격증명 읽기 실패 — secret/docker-registry/minio 확인"
}
configure_mc_alias() {
log "mc alias 등록 (alias=${MC_ALIAS})"
mc --insecure alias set "$MC_ALIAS" \
"https://127.0.0.1:${PF_LOCAL_PORT}" \
"$ROOT_USER" "$ROOT_PASSWORD" >/dev/null
}
ensure_bucket() {
log "bucket 보장 (${DOCKER_REGISTRY_BUCKET})"
mc --insecure mb --ignore-existing "${MC_ALIAS}/${DOCKER_REGISTRY_BUCKET}" >/dev/null
}
ensure_policy() {
log "policy 작성 (${DOCKER_REGISTRY_POLICY}) — bucket-scoped read/write"
local policy_file policy_json
policy_file="$(mktemp)"
trap_cleanup_path "$policy_file"
policy_json="$(jq -n --arg b "$DOCKER_REGISTRY_BUCKET" '{
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: [
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:GetBucketLocation"
],
Resource: ["arn:aws:s3:::\($b)"]
},
{
Effect: "Allow",
Action: [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload"
],
Resource: ["arn:aws:s3:::\($b)/*"]
}
]
}')"
printf '%s' "$policy_json" > "$policy_file"
chmod 600 "$policy_file"
# `policy create` 는 idempotent: 이미 있으면 덮어쓴다.
mc --insecure admin policy create "$MC_ALIAS" \
"$DOCKER_REGISTRY_POLICY" "$policy_file" >/dev/null
}
ensure_user() {
if mc --insecure admin user info "$MC_ALIAS" "$REG_AK" >/dev/null 2>&1; then
log "user '${REG_AK}' 이미 존재 — 재생성 skip (secret key 회전은 별도 절차)"
return 0
fi
log "user '${REG_AK}' 생성"
mc --insecure admin user add "$MC_ALIAS" "$REG_AK" "$REG_SK" >/dev/null
}
attach_policy() {
log "policy '${DOCKER_REGISTRY_POLICY}' → user '${REG_AK}' 부착"
# 이미 부착돼 있으면 mc 가 exit 1 을 반환. 우리는 idempotent 를 의도하므로
# stderr 를 소비해서 noise 만 판별한 뒤 무시한다.
local out rc=0
out="$(mc --insecure admin policy attach "$MC_ALIAS" \
"$DOCKER_REGISTRY_POLICY" --user "$REG_AK" 2>&1)" || rc=$?
if (( rc != 0 )) && ! grep -qi "already" <<<"$out"; then
err " policy attach 실패: $out"
return "$rc"
fi
}
main() {
require_cmd kubectl jq mc curl
require_env REPO_ROOT
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
read_registry_credentials
configure_mc_alias
ensure_bucket
ensure_policy
ensure_user
attach_policy
log "minio-provision-registry 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+166
View File
@@ -0,0 +1,166 @@
#!/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}"
# prod 에서 repo 내부 평문 저장 차단 — root token / unseal key 가 repo
# working tree 에 남으면 accidental commit / backup / IDE 인덱싱 경로로 유출.
if [[ "${ENV_NAME:-}" == "prod" && "$KEYS_FILE" == "$DEFAULT_KEYS_FILE" ]]; 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 tmp_out
tmp_out="$(mktemp)"
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
+286
View File
@@ -0,0 +1,286 @@
#!/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_MINIO_ACCESSKEY docker-registry 가 MinIO 에 접근할 때 쓸 AK.
# 기본값 'docker-registry'. MinIO user 이름이 됨.
# DOCKER_REGISTRY_MINIO_SECRETKEY docker-registry MinIO SK (대소문자+숫자 8자 이상).
#
# 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_MINIO_ACCESSKEY:=docker-registry}"
: "${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_json() {
local path="$1"; shift
local jq_expr="$1"; shift
jq -n "$@" "$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 비밀번호")"
kv_put_json "identity-postgres/superuser" \
'{username: $u, password: $p}' \
--arg u "$POSTGRES_SUPERUSER_USERNAME" \
--arg p "$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 비밀번호")"
kv_put_json "keycloak/db" \
'{password: $p}' \
--arg p "$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 비밀번호")"
kv_put_json "auth-server/db" \
'{SPRING_DATASOURCE_USERNAME: $u, SPRING_DATASOURCE_PASSWORD: $p}' \
--arg u "$AUTH_SERVER_DB_USERNAME" \
--arg p "$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 관리자 비밀번호")"
kv_put_json "keycloak/bootstrap-admin" \
'{KEYCLOAK_ADMIN: $u, KEYCLOAK_ADMIN_PASSWORD: $p}' \
--arg u "$KEYCLOAK_ADMIN_USERNAME" \
--arg p "$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"
)
kv_put_json "minio/tenant-env" \
'{"config.env": $cfg}' \
--arg cfg "$env_content"
log " secret/minio/tenant-env 작성"
}
# docker-registry 가 MinIO 에 접근할 때 쓸 AK/SK. 이 값으로 bootstrap 후속
# phase 에서 MinIO admin user 를 만들고 bucket-scoped policy 를 부착한다.
# registry Deployment 는 VSO 가 동기화한 Secret `docker-registry-minio` 에서
# 동일 값을 REGISTRY_STORAGE_S3_ACCESSKEY / SECRETKEY 로 읽는다.
seed_docker_registry_minio() {
seed_if_missing "docker-registry/minio" || return 0
local sk
sk="$(resolve_password DOCKER_REGISTRY_MINIO_SECRETKEY "docker-registry MinIO secret key")"
kv_put_json "docker-registry/minio" \
'{access_key: $ak, secret_key: $sk}' \
--arg ak "$DOCKER_REGISTRY_MINIO_ACCESSKEY" \
--arg sk "$sk"
log " secret/docker-registry/minio 작성 (access_key=${DOCKER_REGISTRY_MINIO_ACCESSKEY})"
}
# 외부에서 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="$(openssl passwd -apr1 "$pw")"
users="${DOCKER_REGISTRY_PUSH_USERNAME}:${hash}"
kv_put_json "docker-registry/basic-auth" \
'{username: $username, password: $password, users: $users}' \
--arg username "$DOCKER_REGISTRY_PUSH_USERNAME" \
--arg password "$pw" \
--arg users "$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"
kv_put_json "keycloak/clients/auth-server-ingress" \
'{client_secret: $p}' \
--arg p "$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")"
kv_put_json "oauth2-proxy/forward-auth" \
'{client_secret: $client_secret, cookie_secret: $cookie_secret}' \
--arg client_secret "$client_secret" \
--arg cookie_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 "앱 시크릿 5 개 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_minio
seed_docker_registry_basic_auth
log "vault-seed-apps 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+180
View File
@@ -0,0 +1,180 @@
#!/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})"
jq -n --arg p "$VAULT_ADMIN_PASSWORD" \
--arg ttl "$ADMIN_TTL" \
--arg max "$ADMIN_MAX_TTL" \
'{password:$p, token_policies:"vault-admin", token_ttl:$ttl, token_max_ttl:$max}' \
| kubectl exec -i -n "$VAULT_NAMESPACE" "$VAULT_POD" -- \
vault write "auth/userpass/users/${VAULT_ADMIN_USERNAME}" - >/dev/null
}
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
+102
View File
@@ -0,0 +1,102 @@
#!/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/k8s/base/plugins/vso/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/k8s/base/plugins/vso/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