- PostgreSQL 3-replica HA with pgvector - Vault S3 storage backend (MinIO) - Authentik federated OIDC provider - Vault auto-unseal via postStart hook
528 lines
23 KiB
Bash
Executable File
528 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# k8s/talos-iam/setup_vault.sh
|
|
# Deploys HashiCorp Vault into the iam namespace, initialises it, and wires
|
|
# the JWT auth backend to Authentik so the talos-cli secrets subcommand works.
|
|
#
|
|
# Prerequisites:
|
|
# - Authentik already running in the iam namespace (run setup_talos_iam.sh first)
|
|
# - kubectl configured (KUBECONFIG → cluster-config/kubeconfig)
|
|
# - helm >= 3.x
|
|
# - vault CLI installed locally (https://developer.hashicorp.com/vault/downloads)
|
|
# - talos-iam/.env containing (see .env.example):
|
|
# MINIO_ROOT_USER=...
|
|
# MINIO_ROOT_PASSWORD=...
|
|
# The script seeds initial secrets and needs read access to MinIO credentials.
|
|
#
|
|
# What this script does (in order):
|
|
# 1. Create vault-minio-creds K8s Secret (MinIO creds for the S3 backend)
|
|
# 2. Helm install hashicorp/vault
|
|
# 3. Wait for vault pod to be Running
|
|
# 4. vault operator init → capture unseal keys + root token
|
|
# 5. vault operator unseal (3 of 5 key shares)
|
|
# 6. Login with root token
|
|
# 7. Enable KV v2 secret engine at secret/
|
|
# 8. Enable JWT auth backend, configure with Authentik JWKS
|
|
# 9. Write policies (shell-secrets, mcp-readonly, cluster-admin)
|
|
# 10. Create JWT roles (shell, mcp, cluster)
|
|
# 11. Seed initial secrets from .env
|
|
# 12. Register talos-cli-shell OIDC app in Authentik via API
|
|
# 13. Print next steps
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
KUBECONFIG="${KUBECONFIG:-${REPO_ROOT}/cluster-config/kubeconfig}"
|
|
export KUBECONFIG
|
|
|
|
NAMESPACE=storage
|
|
RELEASE=vault
|
|
AUTHENTIK_URL="http://authentik-server.iam.svc.cluster.local"
|
|
VAULT_ADDR_CLUSTER="http://vault.storage.svc.cluster.local:8200"
|
|
# Local access via port-forward during setup
|
|
VAULT_PORT_FWD_ADDR="http://127.0.0.1:8200"
|
|
UNSEAL_KEYS_FILE="$HOME/.vault-data/.vault-init.json" # KEEP OFFLINE after setup
|
|
|
|
# ── Failure surfacing ─────────────────────────────────────────────────────────
|
|
CURRENT_STEP="init"
|
|
on_err() {
|
|
local rc=$?
|
|
echo "[ERROR] step '${CURRENT_STEP}' failed (exit ${rc} at line ${BASH_LINENO[0]})" >&2
|
|
exit "${rc}"
|
|
}
|
|
trap on_err ERR
|
|
|
|
step() { CURRENT_STEP="$1"; echo ""; echo "==> $2"; }
|
|
|
|
# ── Load credentials ──────────────────────────────────────────────────────────
|
|
step load_env "Loading credentials from talos-iam/.env..."
|
|
if [[ -f "$HOME/.authentik/.env" ]]; then
|
|
# shellcheck source=/dev/null
|
|
source "$HOME/.authentik/.env"
|
|
fi
|
|
|
|
for var in MINIO_ROOT_USER MINIO_ROOT_PASSWORD AUTHENTIK_BOOTSTRAP_TOKEN; do
|
|
if [[ -z "${!var:-}" ]]; then
|
|
echo "ERROR: ${var} is not set — export it or add it to talos-iam/.env" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# ── 1. MinIO credentials secret for Vault S3 backend ─────────────────────────
|
|
step minio_secret "Creating vault-minio-creds secret..."
|
|
kubectl create secret generic vault-minio-creds \
|
|
--namespace="${NAMESPACE}" \
|
|
--from-literal=access_key="${MINIO_ROOT_USER}" \
|
|
--from-literal=secret_key="${MINIO_ROOT_PASSWORD}" \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
# ── 1b. Placeholder vault-unseal-keys secret (required before Helm install) ───
|
|
# extraSecretEnvironmentVars references vault-unseal-keys at pod start time,
|
|
# but the real keys only exist after vault operator init. Create empty placeholders
|
|
# so the pod starts; step vault_unseal_secret overwrites them with real values.
|
|
step vault_unseal_placeholder "Creating placeholder vault-unseal-keys secret..."
|
|
kubectl create secret generic vault-unseal-keys \
|
|
--namespace="${NAMESPACE}" \
|
|
--from-literal=key1="" \
|
|
--from-literal=key2="" \
|
|
--from-literal=key3="" \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
# ── 1c. Register talos-cli-shell OIDC app in Authentik ───────────────────────
|
|
# Must exist before Helm install so Vault can validate the JWKS URL at jwt/config time.
|
|
step authentik_app "Registering talos-cli-shell OIDC provider in Authentik..."
|
|
AUTHENTIK_API="http://127.0.0.1:7000/api/v3"
|
|
|
|
kubectl port-forward -n "${NAMESPACE}" svc/authentik-server 7000:80 &
|
|
AK_PF_PID=$!
|
|
trap 'kill ${AK_PF_PID} 2>/dev/null' EXIT
|
|
sleep 3
|
|
|
|
# Fetch required flow PKs from Authentik
|
|
_auth_flow_pk=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API}/flows/instances/?slug=default-provider-authorization-implicit-consent" \
|
|
| jq -r '.results[0].pk // empty')
|
|
_inval_flow_pk=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API}/flows/instances/?slug=default-provider-invalidation-flow" \
|
|
| jq -r '.results[0].pk // empty')
|
|
|
|
if [[ -z "${_auth_flow_pk}" || -z "${_inval_flow_pk}" ]]; then
|
|
echo "ERROR: required Authentik flows not found — is provision_oidc.py already run?" >&2
|
|
exit 1
|
|
fi
|
|
|
|
_signing_key_pk=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API}/crypto/certificatekeypairs/?has_key=true&page_size=1" \
|
|
| jq -r '.results[0].pk // empty')
|
|
|
|
if [[ -z "${_signing_key_pk}" ]]; then
|
|
echo "ERROR: no signing key found in Authentik — create one under System → Certificates" >&2
|
|
exit 1
|
|
fi
|
|
|
|
_provider_payload=$(jq -n \
|
|
--arg auth_flow "${_auth_flow_pk}" \
|
|
--arg inval_flow "${_inval_flow_pk}" \
|
|
--arg signing_key "${_signing_key_pk}" \
|
|
'{
|
|
name: "talos-cli-shell",
|
|
client_type: "confidential",
|
|
grant_types: ["client_credentials"],
|
|
token_validity: "minutes=5",
|
|
sub_mode: "hashed_user_id",
|
|
include_claims_in_id_token: true,
|
|
audience: "vault",
|
|
authorization_flow: $auth_flow,
|
|
invalidation_flow: $inval_flow,
|
|
signing_key: $signing_key,
|
|
redirect_uris: []
|
|
}')
|
|
|
|
_existing_provider=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API}/providers/oauth2/?name=talos-cli-shell" | jq -r '.results[0].pk // empty')
|
|
|
|
if [[ -n "${_existing_provider}" ]]; then
|
|
echo "Provider talos-cli-shell exists (pk=${_existing_provider}) — patching config..."
|
|
PROVIDER_PK="${_existing_provider}"
|
|
curl -sf -X PATCH "${AUTHENTIK_API}/providers/oauth2/${PROVIDER_PK}/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "${_provider_payload}" > /dev/null
|
|
CLIENT_SECRET="(unchanged — retrieve from Authentik admin UI if needed)"
|
|
else
|
|
PROVIDER_PK=$(curl -sf -X POST "${AUTHENTIK_API}/providers/oauth2/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "${_provider_payload}" | jq -r '.pk')
|
|
|
|
CLIENT_SECRET=$(curl -sf -X POST \
|
|
"${AUTHENTIK_API}/providers/oauth2/${PROVIDER_PK}/set_secret/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{}' | jq -r '.client_secret')
|
|
|
|
echo "Created provider pk=${PROVIDER_PK}"
|
|
echo ""
|
|
echo "┌─────────────────────────────────────────────────────────────────┐"
|
|
echo "│ Authentik OIDC app registered. │"
|
|
echo "│ Client ID: talos-cli-shell │"
|
|
echo "│ Client Secret: ${CLIENT_SECRET} │"
|
|
echo "│ Store this secret — it will not be shown again. │"
|
|
echo "└─────────────────────────────────────────────────────────────────┘"
|
|
fi
|
|
|
|
# Idempotent: ensure the application exists and is bound to the provider.
|
|
# Runs on both create and patch paths so re-runs always produce a consistent state.
|
|
_existing_app=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API}/core/applications/?slug=talos-cli-shell" | jq -r '.results[0].pk // empty')
|
|
|
|
if [[ -n "${_existing_app}" ]]; then
|
|
echo "Application talos-cli-shell exists (pk=${_existing_app}) — ensuring provider binding..."
|
|
curl -sf -X PATCH "${AUTHENTIK_API}/core/applications/${_existing_app}/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"provider\": ${PROVIDER_PK}}" > /dev/null
|
|
else
|
|
curl -sf -X POST "${AUTHENTIK_API}/core/applications/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$(jq -n --argjson pk "${PROVIDER_PK}" \
|
|
'{"name":"talos-cli-shell","slug":"talos-cli-shell","provider":$pk}')" > /dev/null
|
|
echo "Application talos-cli-shell created and bound to provider pk=${PROVIDER_PK}"
|
|
fi
|
|
|
|
kill "${AK_PF_PID}" 2>/dev/null || true
|
|
unset AK_PF_PID
|
|
|
|
# ── 1d. Register vault-browser OIDC app in Authentik (browser / UI login) ────
|
|
# Separate from talos-cli-shell: this uses authorization_code grant so humans
|
|
# can log in via the Vault UI or `vault login -method=oidc`.
|
|
step authentik_vault_browser "Registering vault-browser OIDC provider in Authentik..."
|
|
AUTHENTIK_API_BROWSER="http://127.0.0.1:7000/api/v3"
|
|
|
|
kubectl port-forward -n iam svc/authentik-server 7000:80 &
|
|
AK_PF2_PID=$!
|
|
trap 'kill ${AK_PF2_PID} 2>/dev/null' EXIT
|
|
sleep 3
|
|
|
|
_auth_flow_pk2=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API_BROWSER}/flows/instances/?slug=default-provider-authorization-implicit-consent" \
|
|
| jq -r '.results[0].pk // empty')
|
|
_inval_flow_pk2=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API_BROWSER}/flows/instances/?slug=default-provider-invalidation-flow" \
|
|
| jq -r '.results[0].pk // empty')
|
|
_signing_key_pk2=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API_BROWSER}/crypto/certificatekeypairs/?has_key=true&page_size=1" \
|
|
| jq -r '.results[0].pk // empty')
|
|
|
|
_vault_browser_payload=$(jq -n \
|
|
--arg auth_flow "${_auth_flow_pk2}" \
|
|
--arg inval_flow "${_inval_flow_pk2}" \
|
|
--arg signing_key "${_signing_key_pk2}" \
|
|
'{
|
|
name: "vault-browser",
|
|
client_type: "confidential",
|
|
client_id: "vault-browser",
|
|
grant_types: ["authorization_code", "refresh"],
|
|
token_validity: "hours=8",
|
|
sub_mode: "hashed_user_id",
|
|
include_claims_in_id_token: true,
|
|
authorization_flow: $auth_flow,
|
|
invalidation_flow: $inval_flow,
|
|
signing_key: $signing_key,
|
|
redirect_uris: [
|
|
{"matching_mode": "strict", "url": "http://vault.riotpiao.homelab.com/ui/vault/auth/oidc/oidc/callback"},
|
|
{"matching_mode": "strict", "url": "http://localhost:8250/oidc/callback"}
|
|
]
|
|
}')
|
|
|
|
_existing_vault_browser=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API_BROWSER}/providers/oauth2/?name=vault-browser" | jq -r '.results[0].pk // empty')
|
|
|
|
if [[ -n "${_existing_vault_browser}" ]]; then
|
|
echo "Provider vault-browser exists (pk=${_existing_vault_browser}) — patching..."
|
|
VAULT_BROWSER_PK="${_existing_vault_browser}"
|
|
curl -sf -X PATCH "${AUTHENTIK_API_BROWSER}/providers/oauth2/${VAULT_BROWSER_PK}/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "${_vault_browser_payload}" > /dev/null
|
|
# Re-fetch the client secret from the detail endpoint (list responses mask it)
|
|
VAULT_BROWSER_CLIENT_SECRET=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API_BROWSER}/providers/oauth2/${VAULT_BROWSER_PK}/" \
|
|
| jq -r '.client_secret')
|
|
else
|
|
VAULT_BROWSER_PK=$(curl -sf -X POST "${AUTHENTIK_API_BROWSER}/providers/oauth2/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "${_vault_browser_payload}" | jq -r '.pk')
|
|
|
|
VAULT_BROWSER_CLIENT_SECRET=$(curl -sf -X POST \
|
|
"${AUTHENTIK_API_BROWSER}/providers/oauth2/${VAULT_BROWSER_PK}/set_secret/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{}' | jq -r '.client_secret')
|
|
|
|
echo "Created vault-browser provider pk=${VAULT_BROWSER_PK}"
|
|
fi
|
|
|
|
# Bind application
|
|
_existing_vault_app=$(curl -sf \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
"${AUTHENTIK_API_BROWSER}/core/applications/?slug=vault-browser" | jq -r '.results[0].pk // empty')
|
|
|
|
if [[ -n "${_existing_vault_app}" ]]; then
|
|
curl -sf -X PATCH "${AUTHENTIK_API_BROWSER}/core/applications/${_existing_vault_app}/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"provider\": ${VAULT_BROWSER_PK}}" > /dev/null
|
|
echo "Application vault-browser updated."
|
|
else
|
|
curl -sf -X POST "${AUTHENTIK_API_BROWSER}/core/applications/" \
|
|
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$(jq -n --argjson pk "${VAULT_BROWSER_PK}" \
|
|
'{"name":"vault-browser","slug":"vault-browser","provider":$pk}')" > /dev/null
|
|
echo "Application vault-browser created."
|
|
fi
|
|
|
|
kill "${AK_PF2_PID}" 2>/dev/null || true
|
|
unset AK_PF2_PID
|
|
|
|
# ── 2. Ensure vault bucket exists in MinIO ────────────────────────────────────
|
|
step minio_bucket "Ensuring 'vault' bucket exists in MinIO..."
|
|
kubectl run vault-bucket-init --rm -i --restart=Never \
|
|
--namespace=storage \
|
|
--image=minio/mc:latest \
|
|
--env="MC_HOST_local=http://${MINIO_ROOT_USER}:${MINIO_ROOT_PASSWORD}@minio.storage.svc.cluster.local:9000" \
|
|
--command -- mc mb --ignore-existing local/vault
|
|
|
|
# ── 3. Helm install Vault ─────────────────────────────────────────────────────
|
|
step helm_install "Installing HashiCorp Vault via Helm..."
|
|
helm repo add hashicorp https://helm.releases.hashicorp.com
|
|
helm repo update hashicorp
|
|
|
|
helm upgrade --install "${RELEASE}" hashicorp/vault \
|
|
--namespace "${NAMESPACE}" \
|
|
--values "${SCRIPT_DIR}/vault-values.yaml" \
|
|
--wait --timeout=120s
|
|
|
|
# ── 4. Wait for vault pod ─────────────────────────────────────────────────────
|
|
step wait_pod "Waiting for vault-0 pod to be Running..."
|
|
kubectl wait pod/vault-0 \
|
|
--namespace="${NAMESPACE}" \
|
|
--for=condition=Ready=false \
|
|
--timeout=60s 2>/dev/null || true # pod starts uninitialized (not Ready) — that's expected
|
|
|
|
# Give it a moment to bind the port
|
|
sleep 5
|
|
|
|
# ── 5. Port-forward for init/unseal ──────────────────────────────────────────
|
|
step port_forward "Starting port-forward to vault-0 on 127.0.0.1:8200..."
|
|
kubectl port-forward -n "${NAMESPACE}" pod/vault-0 8200:8200 &
|
|
PF_PID=$!
|
|
trap 'kill ${PF_PID} 2>/dev/null; on_err' ERR
|
|
trap 'kill ${PF_PID} 2>/dev/null' EXIT
|
|
sleep 3
|
|
|
|
export VAULT_ADDR="${VAULT_PORT_FWD_ADDR}"
|
|
|
|
# ── 6. Init ───────────────────────────────────────────────────────────────────
|
|
step vault_init "Initialising Vault (5 key shares, threshold 3)..."
|
|
if vault status 2>/dev/null | grep -q "Initialized.*true"; then
|
|
echo "Vault is already initialised — skipping init."
|
|
else
|
|
vault operator init \
|
|
-key-shares=5 \
|
|
-key-threshold=3 \
|
|
-format=json > "${UNSEAL_KEYS_FILE}"
|
|
|
|
chmod 600 "${UNSEAL_KEYS_FILE}"
|
|
echo ""
|
|
echo "┌─────────────────────────────────────────────────────────────────┐"
|
|
echo "│ IMPORTANT: unseal keys + root token saved to: │"
|
|
echo "│ ${UNSEAL_KEYS_FILE}"
|
|
echo "│ Move this file OFFLINE (USB / password manager) immediately. │"
|
|
echo "└─────────────────────────────────────────────────────────────────┘"
|
|
fi
|
|
|
|
# ── 6b. Store unseal keys in K8s Secret for auto-unseal on restart ───────────
|
|
step vault_unseal_secret "Storing 3 unseal keys in vault-unseal-keys Secret..."
|
|
kubectl create secret generic vault-unseal-keys \
|
|
--namespace="${NAMESPACE}" \
|
|
--from-literal=key1="$(jq -r '.unseal_keys_b64[0]' "${UNSEAL_KEYS_FILE}")" \
|
|
--from-literal=key2="$(jq -r '.unseal_keys_b64[1]' "${UNSEAL_KEYS_FILE}")" \
|
|
--from-literal=key3="$(jq -r '.unseal_keys_b64[2]' "${UNSEAL_KEYS_FILE}")" \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
# ── 7. Unseal ─────────────────────────────────────────────────────────────────
|
|
step vault_unseal "Unsealing Vault (3 of 5 shares)..."
|
|
if vault status 2>/dev/null | grep -q "Sealed.*false"; then
|
|
echo "Vault is already unsealed — skipping."
|
|
else
|
|
for i in 0 1 2; do
|
|
KEY=$(jq -r ".unseal_keys_b64[${i}]" "${UNSEAL_KEYS_FILE}")
|
|
vault operator unseal "${KEY}"
|
|
done
|
|
fi
|
|
|
|
# ── 8. Login with root token ──────────────────────────────────────────────────
|
|
step vault_login "Logging in with root token..."
|
|
ROOT_TOKEN=$(jq -r ".root_token" "${UNSEAL_KEYS_FILE}")
|
|
vault login "${ROOT_TOKEN}"
|
|
|
|
# ── 9. Enable KV v2 ──────────────────────────────────────────────────────────
|
|
step kv_enable "Enabling KV v2 at secret/..."
|
|
vault secrets enable -path=secret kv-v2 2>/dev/null || echo "KV v2 already enabled."
|
|
|
|
# ── 10. Enable JWT auth backend ───────────────────────────────────────────────
|
|
step jwt_enable "Enabling JWT auth backend..."
|
|
vault auth enable jwt 2>/dev/null || echo "JWT auth already enabled."
|
|
|
|
JWKS_URL="${AUTHENTIK_URL}/application/o/talos-cli-shell/jwks/"
|
|
vault write auth/jwt/config \
|
|
jwks_url="${JWKS_URL}" \
|
|
default_role="shell"
|
|
|
|
echo "JWT auth configured with JWKS at: ${JWKS_URL}"
|
|
|
|
# ── 11. Write policies ────────────────────────────────────────────────────────
|
|
step policies "Writing Vault policies..."
|
|
|
|
vault policy write shell-secrets - <<'EOF'
|
|
# shell-secrets: read MCP keys and cloud tokens for interactive shell sessions
|
|
path "secret/data/mcp/*" {
|
|
capabilities = ["read"]
|
|
}
|
|
path "secret/data/cloud/*" {
|
|
capabilities = ["read"]
|
|
}
|
|
EOF
|
|
|
|
vault policy write mcp-readonly - <<'EOF'
|
|
# mcp-readonly: each MCP server reads only its own path
|
|
path "secret/data/mcp/{{identity.entity.aliases.*.metadata.client_id}}" {
|
|
capabilities = ["read"]
|
|
}
|
|
EOF
|
|
|
|
vault policy write cluster-admin - <<'EOF'
|
|
# cluster-admin: read cluster service credentials (MinIO, Grafana, etc.)
|
|
path "secret/data/cluster/*" {
|
|
capabilities = ["read", "update"]
|
|
}
|
|
path "secret/data/mcp/*" {
|
|
capabilities = ["read", "create", "update", "delete"]
|
|
}
|
|
path "secret/data/cloud/*" {
|
|
capabilities = ["read", "create", "update", "delete"]
|
|
}
|
|
EOF
|
|
|
|
# ── 12. Create JWT roles ──────────────────────────────────────────────────────
|
|
step jwt_roles "Creating JWT roles..."
|
|
|
|
# shell role: interactive shell sessions via talos-cli
|
|
vault write auth/jwt/role/shell \
|
|
role_type=jwt \
|
|
bound_audiences="vault" \
|
|
user_claim="sub" \
|
|
policies="shell-secrets" \
|
|
ttl=4h \
|
|
max_ttl=8h
|
|
|
|
# mcp role: MCP server processes (short TTL, non-renewable)
|
|
vault write auth/jwt/role/mcp \
|
|
role_type=jwt \
|
|
bound_audiences="vault" \
|
|
user_claim="sub" \
|
|
policies="mcp-readonly" \
|
|
ttl=1h \
|
|
max_ttl=1h
|
|
|
|
# cluster role: admin operations (bootstrap scripts, rotation jobs)
|
|
vault write auth/jwt/role/cluster - <<'EOF'
|
|
{
|
|
"role_type": "jwt",
|
|
"bound_audiences": ["vault"],
|
|
"user_claim": "sub",
|
|
"bound_claims": {"groups": ["homelab-admins"]},
|
|
"token_policies": ["cluster-admin"],
|
|
"token_ttl": "1h"
|
|
}
|
|
EOF
|
|
|
|
# ── 13. Enable OIDC auth (browser / UI login via Authentik) ──────────────────
|
|
step oidc_enable "Enabling OIDC auth method for browser login..."
|
|
vault auth enable oidc 2>/dev/null || echo "OIDC auth already enabled."
|
|
|
|
vault write auth/oidc/config \
|
|
oidc_discovery_url="http://authentik.riotpiao.homelab.com/application/o/vault-browser/" \
|
|
oidc_client_id="vault-browser" \
|
|
oidc_client_secret="${VAULT_BROWSER_CLIENT_SECRET}" \
|
|
default_role="homelab"
|
|
|
|
# homelab role: all authenticated Authentik users get shell-secrets + cluster-admin.
|
|
# Restrict further by adding bound_claims once group-based mapping is configured.
|
|
vault write auth/oidc/role/homelab \
|
|
role_type=oidc \
|
|
bound_audiences="vault-browser" \
|
|
allowed_redirect_uris="http://vault.riotpiao.homelab.com/ui/vault/auth/oidc/oidc/callback,http://localhost:8250/oidc/callback" \
|
|
user_claim="sub" \
|
|
oidc_scopes="openid,profile,email" \
|
|
token_policies="shell-secrets,cluster-admin" \
|
|
token_ttl=8h \
|
|
token_max_ttl=12h
|
|
|
|
echo "OIDC auth configured."
|
|
echo " Browser login: http://vault.riotpiao.homelab.com → sign in with Authentik"
|
|
echo " CLI login: VAULT_ADDR=http://vault.riotpiao.homelab.com vault login -method=oidc"
|
|
|
|
# ── 14. Seed initial secrets ──────────────────────────────────────────────────
|
|
step seed_secrets "Seeding initial secrets from environment (accessible after OIDC login)..."
|
|
|
|
# Cluster service credentials
|
|
vault kv put secret/cluster/minio \
|
|
user="${MINIO_ROOT_USER}" \
|
|
password="${MINIO_ROOT_PASSWORD}"
|
|
|
|
if [[ -n "${GRAFANA_ADMIN_PASSWORD:-}" ]]; then
|
|
vault kv put secret/cluster/grafana \
|
|
password="${GRAFANA_ADMIN_PASSWORD}"
|
|
fi
|
|
|
|
if [[ -n "${DUCKDNS_TOKEN:-}" ]]; then
|
|
vault kv put secret/cloud/duckdns \
|
|
token="${DUCKDNS_TOKEN}"
|
|
fi
|
|
|
|
# MCP / AI tooling keys (optional — add when available)
|
|
if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then
|
|
vault kv put secret/mcp/anthropic api_key="${ANTHROPIC_API_KEY}"
|
|
fi
|
|
|
|
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
|
vault kv put secret/mcp/github token="${GITHUB_TOKEN}"
|
|
fi
|
|
|
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo "==> Vault bootstrap complete."
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Move ${UNSEAL_KEYS_FILE} OFFLINE (USB / password manager)."
|
|
echo " 2. Browser login: open http://vault.riotpiao.homelab.com → choose OIDC → sign in with Authentik"
|
|
echo " 3. CLI login: export VAULT_ADDR=http://vault.riotpiao.homelab.com"
|
|
echo " vault login -method=oidc"
|
|
echo " 4. Read MinIO credentials from Vault after login:"
|
|
echo " vault kv get secret/cluster/minio"
|
|
echo " 5. talos-cli (JWT): talos secrets login && talos secrets status"
|