commit
64be99308b
53 files changed
+1983
No files matched your search
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Запустить пример Go-сервиса: mise run example:up [сервис] [static|dynamic]"
|
||||
set -euo pipefail
|
||||
|
||||
roles=$(terraform -chdir=vault output -json approle_role_ids)
|
||||
service=${1:-$(jq -r 'keys[0]' <<<"$roles")}
|
||||
creds=${2:-static}
|
||||
compose=(docker compose -f examples/go-service/compose.yml)
|
||||
url=http://127.0.0.1:${EXAMPLE_PORT:-8090}/
|
||||
|
||||
role_id=$(jq -er --arg s "$service" '.[$s]' <<<"$roles") ||
|
||||
{ echo "нет роли AppRole для сервиса $service" >&2; exit 1; }
|
||||
secret_id=$(curl -sS -X POST -H "X-Vault-Token: $VAULT_TOKEN" -d '{"ttl": "5m", "num_uses": 1}' \
|
||||
"$VAULT_ADDR/v1/auth/approle/role/$service/secret-id" | jq -er .data.secret_id)
|
||||
|
||||
export SERVICE_NAME=$service VAULT_ROLE_ID=$role_id VAULT_SECRET_ID=$secret_id POSTGRES_CREDS=$creds
|
||||
INFRA_NETWORK=$(docker compose config --format json | jq -er .networks.infra.name)
|
||||
export INFRA_NETWORK
|
||||
"${compose[@]}" up -d --build --force-recreate --quiet-pull 2>&1 | tail -3
|
||||
|
||||
for _ in $(seq 1 30); do
|
||||
if response=$(curl -fsS "$url" 2>/dev/null); then
|
||||
"${compose[@]}" logs --no-log-prefix example
|
||||
printf '\nGET %s\n' "$url"
|
||||
jq . <<<"$response"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
"${compose[@]}" logs --no-log-prefix example
|
||||
echo "сервис не ответил на $url" >&2
|
||||
exit 1
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Сгенерировать .env.config и backend.hcl из config.yaml"
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
INFRA=$(echo 'jsonencode(yamldecode(file("config.yaml")).infra)' | terraform console | jq -r .)
|
||||
get() { jq -er "$1" <<<"$INFRA" || { echo "в config.yaml нет infra$1" >&2; exit 1; }; }
|
||||
|
||||
cat >.env.config.tmp <<ENV
|
||||
# Сгенерировано задачей mise run render из config.yaml, не редактировать
|
||||
POSTGRES_IMAGE=$(get .postgres.image)
|
||||
POSTGRES_USER=$(get .postgres.admin_user)
|
||||
POSTGRES_PORT=$(get .postgres.port)
|
||||
PGADMIN_IMAGE=$(get .pgadmin.image)
|
||||
PGADMIN_PORT=$(get .pgadmin.port)
|
||||
PGADMIN_EMAIL=$(get .pgadmin.email)
|
||||
SILO_IMAGE=$(get .silo.image)
|
||||
SILO_USER=$(get .silo.admin_user)
|
||||
SILO_STORAGE_PORT=$(get .silo.port)
|
||||
SILO_WEB_PORT=$(get .silo.console_port)
|
||||
VAULT_IMAGE=$(get .vault.image)
|
||||
VAULT_PORT=$(get .vault.port)
|
||||
VAULT_ADDR=http://$(get .vault.host):$(get .vault.port)
|
||||
VAULT_MOUNT=$(get .vault.mount)
|
||||
VAULT_DATABASE_MOUNT=$(get .vault.database_mount)
|
||||
BACKUP_BUCKET=$(get .silo.backup_bucket)
|
||||
ENV
|
||||
|
||||
cat >backend.hcl.tmp <<HCL
|
||||
# Сгенерировано задачей mise run render из config.yaml, не редактировать
|
||||
bucket = "$(get .silo.state_bucket)"
|
||||
region = "us-east-1"
|
||||
endpoints = {
|
||||
s3 = "http://$(get .silo.host):$(get .silo.port)"
|
||||
}
|
||||
use_path_style = true
|
||||
skip_credentials_validation = true
|
||||
skip_region_validation = true
|
||||
skip_requesting_account_id = true
|
||||
skip_metadata_api_check = true
|
||||
skip_s3_checksum = true
|
||||
HCL
|
||||
|
||||
mv .env.config.tmp .env.config
|
||||
mv backend.hcl.tmp backend.hcl
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Terraform с окружением из .env и config.yaml: mise run tf storage plan"
|
||||
set -euo pipefail
|
||||
|
||||
[[ $# -ge 2 ]] || {
|
||||
echo "использование: mise run tf <vault|storage|database> <команда terraform> [аргументы]" >&2
|
||||
exit 1
|
||||
}
|
||||
dir=$1
|
||||
shift
|
||||
|
||||
if [[ $1 == init ]]; then
|
||||
shift
|
||||
exec terraform -chdir="$dir" init -backend-config=../backend.hcl "$@"
|
||||
fi
|
||||
exec terraform -chdir="$dir" "$@"
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Проверка глазами сервиса: вход по AppRole, секреты из Vault, доступ к S3 и Postgres"
|
||||
set -euo pipefail
|
||||
|
||||
KV=$VAULT_MOUNT
|
||||
DB=$VAULT_DATABASE_MOUNT
|
||||
|
||||
step() { printf '\n== %s\n' "$*"; }
|
||||
ok() { printf ' ok: %s\n' "$*"; }
|
||||
fail() {
|
||||
printf ' ОШИБКА: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
vault_api() {
|
||||
local method=$1 path=$2 token=${3:-} body=${4:-}
|
||||
curl -sS -X "$method" -w '\n%{http_code}' \
|
||||
${token:+-H "X-Vault-Token: $token"} \
|
||||
${body:+-d "$body"} \
|
||||
"$VAULT_ADDR/v1/$path"
|
||||
}
|
||||
code() { tail -n1 <<<"$1"; }
|
||||
body() { sed '$d' <<<"$1"; }
|
||||
|
||||
psql_as() {
|
||||
docker compose exec -T -e PGPASSWORD="$2" postgres \
|
||||
psql -h postgres -U "$1" -d "$3" -Atc "select session_user || ' работает как ' || current_user" 2>&1 | tail -1 || true
|
||||
}
|
||||
|
||||
token=""
|
||||
revoke_token() {
|
||||
[[ -z "$token" ]] || vault_api POST auth/token/revoke-self "$token" >/dev/null 2>&1 || true
|
||||
token=""
|
||||
}
|
||||
trap revoke_token EXIT
|
||||
|
||||
admin_token=$VAULT_TOKEN
|
||||
ROLES=$(terraform -chdir=vault output -json approle_role_ids)
|
||||
mapfile -t ALL < <(jq -r 'keys[]' <<<"$ROLES")
|
||||
if [[ $# -gt 0 ]]; then SERVICES=("$@"); else SERVICES=("${ALL[@]}"); fi
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
printf '\n################ %s\n' "$svc"
|
||||
|
||||
role_id=$(jq -r --arg s "$svc" '.[$s] // empty' <<<"$ROLES")
|
||||
[[ -n "$role_id" ]] || fail "нет роли AppRole для $svc"
|
||||
secret_id=$(vault_api POST "auth/approle/role/$svc/secret-id" "$admin_token" '{"ttl": "5m", "num_uses": 1}' |
|
||||
sed '$d' | jq -r .data.secret_id)
|
||||
|
||||
step "1. Вход через AppRole"
|
||||
r=$(vault_api POST auth/approle/login "" "$(jq -n --arg r "$role_id" --arg s "$secret_id" '{role_id: $r, secret_id: $s}')")
|
||||
[[ $(code "$r") == 200 ]] || fail "вход не удался: $(body "$r")"
|
||||
token=$(body "$r" | jq -r .auth.client_token)
|
||||
ok "токен получен, политики: $(body "$r" | jq -r '.auth.policies | join(", ")'), ttl: $(body "$r" | jq -r .auth.lease_duration)s"
|
||||
|
||||
step "2. Ключи S3 из $KV/$svc/s3"
|
||||
r=$(vault_api GET "$KV/data/$svc/s3" "$token")
|
||||
if [[ $(code "$r") == 200 ]]; then
|
||||
s3=$(body "$r" | jq .data.data)
|
||||
ok "поля: $(jq -r 'keys | join(", ")' <<<"$s3")"
|
||||
cfg=/tmp/app-check-$$
|
||||
docker compose exec -T object-storage mc --config-dir "$cfg" alias set app http://localhost:9000 \
|
||||
"$(jq -r .access_key <<<"$s3")" "$(jq -r .secret_key <<<"$s3")" >/dev/null
|
||||
IFS=, read -ra own <<<"$(jq -r .buckets <<<"$s3")"
|
||||
for b in "${own[@]}"; do
|
||||
docker compose exec -T object-storage mc --config-dir "$cfg" ls "app/$b" >/dev/null 2>&1 || fail "свой бакет $b недоступен"
|
||||
ok "бакет $b доступен"
|
||||
done
|
||||
docker compose exec -T object-storage rm -rf "$cfg"
|
||||
else
|
||||
ok "секрета нет (у сервиса нет бакетов), HTTP $(code "$r")"
|
||||
fi
|
||||
|
||||
step "3. Постоянные учётные данные Postgres из $KV/$svc/postgres"
|
||||
r=$(vault_api GET "$KV/data/$svc/postgres" "$token")
|
||||
own_db=""
|
||||
if [[ $(code "$r") == 200 ]]; then
|
||||
pg=$(body "$r" | jq .data.data)
|
||||
own_db=$(jq -r .database <<<"$pg")
|
||||
ok "поля: $(jq -r 'keys | join(", ")' <<<"$pg")"
|
||||
out=$(psql_as "$(jq -r .username <<<"$pg")" "$(jq -r .password <<<"$pg")" "$own_db")
|
||||
[[ "$out" == *"работает как"* ]] || fail "не подключился к $own_db: $out"
|
||||
ok "$own_db: $out"
|
||||
else
|
||||
ok "секрета нет (у сервиса нет базы), HTTP $(code "$r")"
|
||||
fi
|
||||
|
||||
step "4. Временные учётные данные Postgres из $DB/creds/$svc"
|
||||
if [[ -n "$own_db" ]]; then
|
||||
r=$(vault_api GET "$DB/creds/$svc" "$token")
|
||||
[[ $(code "$r") == 200 ]] || fail "Vault не выдал временную роль: $(body "$r")"
|
||||
dyn_user=$(body "$r" | jq -r .data.username)
|
||||
dyn_pass=$(body "$r" | jq -r .data.password)
|
||||
ok "выдана роль $dyn_user на $(body "$r" | jq -r .lease_duration)s"
|
||||
out=$(psql_as "$dyn_user" "$dyn_pass" "$own_db")
|
||||
[[ "$out" == *"работает как"* ]] || fail "временная роль не подключилась к $own_db: $out"
|
||||
ok "$own_db: $out"
|
||||
for other in $(docker compose exec -T postgres psql -U "$POSTGRES_USER" -d postgres -Atc \
|
||||
"select datname from pg_database where datdba <> 10 and datname <> '$own_db'"); do
|
||||
out=$(psql_as "$dyn_user" "$dyn_pass" "$other")
|
||||
[[ "$out" == *"CONNECT privilege"* ]] || fail "временная роль попала в чужую базу $other: $out"
|
||||
ok "$other: отказ, нет права CONNECT"
|
||||
done
|
||||
else
|
||||
ok "пропущено, у сервиса нет базы"
|
||||
fi
|
||||
|
||||
step "5. Чужие секреты"
|
||||
for path in "$KV/data/infra/postgres" "$KV/data/infra/silo"; do
|
||||
[[ $(code "$(vault_api GET "$path" "$token")") == 403 ]] || fail "сервис прочитал $path"
|
||||
ok "$path: 403"
|
||||
done
|
||||
for other in "${ALL[@]}"; do
|
||||
[[ "$other" == "$svc" ]] && continue
|
||||
for path in "$KV/data/$other/s3" "$KV/data/$other/postgres" "$DB/creds/$other"; do
|
||||
[[ $(code "$(vault_api GET "$path" "$token")") == 403 ]] || fail "сервис прочитал $path"
|
||||
ok "$path: 403"
|
||||
done
|
||||
done
|
||||
|
||||
step "6. Отзыв токена"
|
||||
old_token=$token
|
||||
revoke_token
|
||||
[[ $(code "$(vault_api GET "$KV/data/$svc/s3" "$old_token")") == 403 ]] || fail "токен всё ещё работает"
|
||||
ok "токен больше не принимается"
|
||||
if [[ -n "${dyn_user:-}" ]]; then
|
||||
sleep 2
|
||||
left=$(docker compose exec -T postgres psql -U "$POSTGRES_USER" -d postgres -Atc \
|
||||
"select count(*) from pg_roles where rolname = '$dyn_user'")
|
||||
[[ "$left" == 0 ]] || fail "временная роль $dyn_user осталась в Postgres"
|
||||
ok "временная роль $dyn_user удалена вместе с токеном"
|
||||
dyn_user=""
|
||||
fi
|
||||
done
|
||||
|
||||
printf '\nВсе проверки пройдены.\n'
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Снапшот Vault в бакет бэкапов"
|
||||
set -euo pipefail
|
||||
|
||||
BUCKET=$BACKUP_BUCKET
|
||||
|
||||
NAME="vault/$(date -u +%Y-%m-%dT%H-%M-%SZ).snap"
|
||||
TMP=/tmp/vault-backup.snap
|
||||
|
||||
docker compose exec -T -e BAO_TOKEN="$VAULT_TOKEN" vault \
|
||||
bao operator raft snapshot save "$TMP" >/dev/null
|
||||
|
||||
docker compose exec -T vault cat "$TMP" |
|
||||
docker compose exec -T -e MC_HOST_local="http://${SILO_USER}:${SILO_PASSWORD}@localhost:9000" \
|
||||
object-storage mc pipe --quiet "local/$BUCKET/$NAME" >/dev/null
|
||||
docker compose exec -T vault rm -f "$TMP"
|
||||
|
||||
docker compose exec -T -e MC_HOST_local="http://${SILO_USER}:${SILO_PASSWORD}@localhost:9000" \
|
||||
object-storage mc stat "local/$BUCKET/$NAME" | grep -E '^(Name|Size)'
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Токен Terraform для Vault, проверка политик, отзыв root-токена (нужен ROOT_TOKEN)"
|
||||
set -euo pipefail
|
||||
|
||||
MOUNT=$VAULT_MOUNT
|
||||
ENV_FILE=.env
|
||||
KEYS_FILE=vault_keys.txt
|
||||
REVOKE_ROOT=false
|
||||
[[ "${1:-}" == "--revoke-root" ]] && REVOKE_ROOT=true
|
||||
|
||||
bao_as() {
|
||||
local token=$1
|
||||
shift
|
||||
docker compose exec -T -e BAO_TOKEN="$token" vault bao "$@"
|
||||
}
|
||||
|
||||
step() { printf '\n== %s\n' "$*"; }
|
||||
ok() { printf ' ok: %s\n' "$*"; }
|
||||
fail() {
|
||||
printf ' ОШИБКА: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ROOT_TOKEN=${ROOT_TOKEN:-$(sed -n 's/^Initial Root Token: *//p' "$KEYS_FILE" 2>/dev/null || true)}
|
||||
[[ -n "$ROOT_TOKEN" ]] || fail "нет root-токена: задайте ROOT_TOKEN или положите $KEYS_FILE"
|
||||
bao_as "$ROOT_TOKEN" token lookup >/dev/null 2>&1 || fail "root-токен не принят (отозван или Vault запечатан)"
|
||||
|
||||
step "Шаг 1. Токен для Terraform"
|
||||
TF_TOKEN=${VAULT_TOKEN:-}
|
||||
if [[ -n "$TF_TOKEN" ]] && bao_as "$TF_TOKEN" token lookup -format=json 2>/dev/null |
|
||||
jq -e '.data.policies | index("terraform")' >/dev/null; then
|
||||
ok "в $ENV_FILE уже есть рабочий токен, новый не создаю"
|
||||
else
|
||||
TF_TOKEN=$(bao_as "$ROOT_TOKEN" token create -orphan -policy=terraform -period=720h \
|
||||
-display-name=terraform -format=json | jq -r .auth.client_token)
|
||||
[[ -n "$TF_TOKEN" && "$TF_TOKEN" != null ]] || fail "токен не создан"
|
||||
sed -i '/^VAULT_TOKEN=/d' "$ENV_FILE"
|
||||
printf 'VAULT_TOKEN=%s\n' "$TF_TOKEN" >>"$ENV_FILE"
|
||||
ok "токен создан и записан в $ENV_FILE как VAULT_TOKEN"
|
||||
fi
|
||||
bao_as "$TF_TOKEN" token lookup -format=json |
|
||||
jq -r '.data | " политики: \(.policies | join(", ")); orphan: \(.orphan); period: \(.period)s"'
|
||||
|
||||
step "Шаг 2. terraform plan в vault/ с новым токеном"
|
||||
set +e
|
||||
TF_VAR_vault_token=$TF_TOKEN terraform -chdir=vault plan -input=false -no-color -detailed-exitcode >/tmp/vault-plan.$$ 2>&1
|
||||
PLAN_EXIT=$?
|
||||
set -e
|
||||
case $PLAN_EXIT in
|
||||
0) ok "No changes: политики terraform хватает" ;;
|
||||
2)
|
||||
grep -E '^ # |^Plan:' /tmp/vault-plan.$$ || true
|
||||
rm -f /tmp/vault-plan.$$
|
||||
fail "plan хочет что-то изменить, прочитайте его: terraform -chdir=vault plan"
|
||||
;;
|
||||
*)
|
||||
grep -vE 'Refreshing state' /tmp/vault-plan.$$ | tail -30
|
||||
rm -f /tmp/vault-plan.$$
|
||||
fail "plan упал, скорее всего в политике terraform не хватает пути из ошибки выше"
|
||||
;;
|
||||
esac
|
||||
rm -f /tmp/vault-plan.$$
|
||||
|
||||
step "Шаг 3. Границы сервисов через AppRole"
|
||||
ROLES_JSON=$(terraform -chdir=vault output -json approle_role_ids)
|
||||
mapfile -t SERVICES < <(jq -r 'keys[]' <<<"$ROLES_JSON")
|
||||
|
||||
cleanup() {
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
bao_as "$ROOT_TOKEN" kv metadata delete -mount="$MOUNT" "$svc/bootstrap-check" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
bao_as "$ROOT_TOKEN" kv put -mount="$MOUNT" "$svc/bootstrap-check" ok=1 >/dev/null
|
||||
done
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
role_id=$(jq -r --arg s "$svc" '.[$s]' <<<"$ROLES_JSON")
|
||||
secret_id=$(bao_as "$ROOT_TOKEN" write -f -field=secret_id "auth/approle/role/$svc/secret-id")
|
||||
svc_token=$(bao_as "$ROOT_TOKEN" write -field=token auth/approle/login role_id="$role_id" secret_id="$secret_id")
|
||||
|
||||
for target in "${SERVICES[@]}"; do
|
||||
set +e
|
||||
out=$(bao_as "$svc_token" kv get -mount="$MOUNT" "$target/bootstrap-check" 2>&1)
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$target" == "$svc" ]]; then
|
||||
[[ $rc -eq 0 ]] || fail "$svc не читает свой путь $MOUNT/$target: $out"
|
||||
ok "$svc читает $MOUNT/$target/*"
|
||||
else
|
||||
[[ $rc -ne 0 && "$out" == *"permission denied"* ]] || fail "$svc прочитал чужой путь $MOUNT/$target"
|
||||
ok "$svc получает permission denied на $MOUNT/$target/*"
|
||||
fi
|
||||
done
|
||||
bao_as "$svc_token" token revoke -self >/dev/null
|
||||
done
|
||||
|
||||
step "Шаг 4. Root-токен"
|
||||
if $REVOKE_ROOT; then
|
||||
cleanup
|
||||
trap - EXIT
|
||||
bao_as "$ROOT_TOKEN" token revoke -self >/dev/null
|
||||
ok "root-токен отозван; новый выпускается через bao operator generate-root с unseal-ключом"
|
||||
else
|
||||
ok "не отозван (запустите с --revoke-root, когда будете готовы)"
|
||||
fi
|
||||
Reference in new issue
Block a user