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