Files
buun-stack/vault/justfile

480 lines
16 KiB
Makefile

set fallback := true
export K8S_VAULT_NAMESPACE := env("K8S_VAULT_NAMESPACE", "vault")
export VAULT_CHART_VERSION := env("VAULT_CHART_VERSION", "0.31.0")
export VAULT_HOST := env("VAULT_HOST", "")
export VAULT_ADDR := "https://" + VAULT_HOST
export VAULT_DEBUG := env("VAULT_DEBUG", "false")
SECRET_PATH := "secret"
# Vault environment setup scripts
[private]
_vault_root_env_setup := '''
if [ -z "${VAULT_TOKEN:-}" ]; then
if [ "${VAULT_DEBUG}" = "true" ]; then
echo "" >&2
echo "💡 To avoid entering Vault admin or root token repeatedly:" >&2
echo " • Set environment variable: export VAULT_TOKEN=your_root_token" >&2
echo " • or write it in .env.local file: VAULT_TOKEN=your_root_token" >&2
echo " • Use 1Password reference: VAULT_TOKEN=op://vault/root/token" >&2
echo "" >&2
fi
VAULT_TOKEN=$(gum input --prompt="Vault admin or root token: " --password --width=100)
elif [[ "${VAULT_TOKEN}" == op://* ]]; then
if ! command -v op &>/dev/null; then
echo "Error: 1Password CLI (op) is not installed." >&2
echo "" >&2
echo "To use 1Password secret references (op://...), please install the 1Password CLI:" >&2
echo " https://developer.1password.com/docs/cli/get-started/" >&2
exit 1
fi
VAULT_TOKEN=$(op read "${VAULT_TOKEN}")
fi
export VAULT_TOKEN
'''
[private]
_vault_oidc_env_setup := '''
if [ -z "${VAULT_TOKEN:-}" ]; then
if [ "${VAULT_DEBUG}" = "true" ]; then
echo "" >&2
echo "💡 Authenticating with OIDC..." >&2
echo " • Browser will open for authentication" >&2
echo " • After login, token will be automatically set" >&2
echo "" >&2
fi
vault login -method=oidc &>/dev/null
VAULT_TOKEN=$(vault print token)
fi
export VAULT_TOKEN
'''
[private]
default:
@just --list --unsorted --list-submodules
# Add Helm repository
add-helm-repo:
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
# Remove Helm repository
remove-helm-repo:
helm repo remove hashicorp
# Create Keycloak namespace
create-namespace:
@kubectl get namespace ${K8S_VAULT_NAMESPACE} &>/dev/null || \
kubectl create namespace ${K8S_VAULT_NAMESPACE}
# Delete Keycloak namespace
delete-namespace:
@kubectl delete namespace ${K8S_VAULT_NAMESPACE} --ignore-not-found
# Install Vault
install: check-env
#!/bin/bash
set -eu
just create-namespace
just add-helm-repo
kubectl label namespace ${K8S_VAULT_NAMESPACE} \
pod-security.kubernetes.io/enforce=restricted --overwrite
gomplate -f vault-values.gomplate.yaml -o vault-values.yaml
helm upgrade --cleanup-on-fail --install vault hashicorp/vault \
--version ${VAULT_CHART_VERSION} -n ${K8S_VAULT_NAMESPACE} --wait -f vault-values.yaml
# Wait for the primary vault pod to complete init containers and be ready to accept commands
kubectl wait pod --for=condition=PodReadyToStartContainers \
-n ${K8S_VAULT_NAMESPACE} vault-0 --timeout=5m
# Wait for Vault service to be ready to accept connections
echo "Waiting for Vault service to be ready..."
for i in {1..30}; do
if kubectl exec -n ${K8S_VAULT_NAMESPACE} vault-0 -- \
vault status 2>&1 | grep -qE "(Initialized|Sealed)"; then
echo "✓ Vault service is ready"
break
fi
if [ $i -eq 30 ]; then
echo "Error: Timeout waiting for Vault service to be ready"
exit 1
fi
sleep 3
done
init_output=$(kubectl exec -n ${K8S_VAULT_NAMESPACE} vault-0 -- \
vault operator init -key-shares=1 -key-threshold=1 -format=json || true)
root_token=""
if echo "${init_output}" | grep -q "Vault is already initialized"; then
echo "Vault is already initialized"
while [ -z "${root_token}" ]; do
root_token=$(gum input --prompt="Vault root token: " --password --width=100)
done
else
unseal_key=$(echo "${init_output}" | jq -r '.unseal_keys_b64[0]')
root_token=$(echo "${init_output}" | jq -r '.root_token')
kubectl exec -n ${K8S_VAULT_NAMESPACE} vault-0 -- \
vault operator unseal "${unseal_key}"
echo "Vault initialized and unsealed successfully"
echo "Root Token: ${root_token}"
echo "Unseal Key: ${unseal_key}"
echo "Please save these credentials securely!"
fi
# Wait for all vault instances to pass readiness checks and be ready to serve requests
kubectl wait pod --for=condition=ready -n ${K8S_VAULT_NAMESPACE} \
-l app.kubernetes.io/name=vault --timeout=5m
just setup-kubernetes-auth "${root_token}"
just create-secrets-engine {{ SECRET_PATH }} "${root_token}"
just create-admin-policy "${root_token}"
echo "Installing External Secrets Operator is recommended to manage secrets in Kubernetes."
echo "It can fetch secrets from Vault and sync them to Kubernetes Secret resources."
if gum confirm "Install External Secrets Operator?"; then
just external-secrets::install
fi
# Uninstall Vault
uninstall delete-ns='false':
#!/bin/bash
set -euo pipefail
helm uninstall vault -n ${K8S_VAULT_NAMESPACE} --ignore-not-found --wait
just delete-namespace
# Create admin token
create-admin-token root_token='': check-env
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
# Create admin policy first
just create-admin-policy "${VAULT_TOKEN}"
# Create token with admin policy
vault token create -policy=admin
# Create token with specified policy and store in Vault
create-token-and-store policy path ttl="24h" max_ttl="" root_token='': check-env
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
echo "Creating token with policy '{{ policy }}'..."
# Create token with specified policy
max_ttl_arg=""
if [ -n "{{ max_ttl }}" ]; then
max_ttl_arg="-explicit-max-ttl={{ max_ttl }}"
fi
token_output=$(vault token create -policy={{ policy }} -ttl={{ ttl }} ${max_ttl_arg} -format=json)
service_token=$(echo "${token_output}" | jq -r '.auth.client_token')
echo "Storing token in Vault at path '{{ path }}'..."
# Store the token in Vault itself for later retrieval
vault kv put -mount=secret {{ path }} token="${service_token}"
echo "✓ Token created and stored in Vault"
echo "Policy: {{ policy }}"
echo "Path: secret/{{ path }}"
echo "Token (first 20 chars): ${service_token:0:20}..."
echo ""
echo "To retrieve the token later:"
echo " just vault::get {{ path }} token"
# Create admin policy for Vault
create-admin-policy root_token='':
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
vault policy write admin - <<EOF
path "sys/auth" {
capabilities = ["read", "list", "sudo"]
}
path "sys/auth/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "auth/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "sys/policy/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "sys/policies/acl/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "auth/token/create" {
capabilities = ["create", "update"]
}
path "auth/token/create/*" {
capabilities = ["create", "update"]
}
EOF
echo "Admin policy created successfully"
# Create secrets engine
create-secrets-engine path root_token='':
#!/bin/bash
set -euo pipefail
export VAULT_TOKEN="{{ root_token }}"
while [ -z "${VAULT_TOKEN}" ]; do
VAULT_TOKEN=$(gum input --prompt="Vault root token: " --password --width=100)
done
vault secrets enable -path="{{ path }}" kv-v2
# Setup Kubernetes authentication
setup-kubernetes-auth root_token='':
#!/bin/bash
set -euo pipefail
export VAULT_TOKEN="{{ root_token }}"
while [ -z "${VAULT_TOKEN}" ]; do
VAULT_TOKEN=$(gum input --prompt="Vault root token: " --password --width=100)
done
gomplate -f ./serviceaccount.gomplate.yaml | kubectl apply -n "${K8S_VAULT_NAMESPACE}" -f -
gomplate -f ./rolebinding.gomplate.yaml | kubectl apply -n "${K8S_VAULT_NAMESPACE}" -f -
kubectl apply -n "${K8S_VAULT_NAMESPACE}" -f ./auth-token-secret.yaml
SA_SECRET="vault-auth-token"
SA_JWT=$(kubectl get secret -n ${K8S_VAULT_NAMESPACE} ${SA_SECRET} -o jsonpath='{.data.token}' | \
base64 --decode)
SA_CA=$(kubectl get secret -n ${K8S_VAULT_NAMESPACE} ${SA_SECRET} -o jsonpath='{.data.ca\.crt}' | \
base64 --decode)
vault auth list -format=json | jq -e '.["kubernetes/"]' >/dev/null 2>&1 || \
vault auth enable kubernetes
vault write auth/kubernetes/config \
token_reviewer_jwt="${SA_JWT}" \
kubernetes_host="https://kubernetes.default.svc" \
kubernetes_ca_cert="${SA_CA}"
# Setup OIDC authentication with Keycloak
setup-oidc-auth:
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
echo "Creating Keycloak client for Vault..."
just keycloak::delete-client "${KEYCLOAK_REALM}" "vault" || true
oidc_client_secret=$(just utils::random-password)
redirect_urls="https://${VAULT_HOST}/ui/vault/auth/oidc/oidc/callback,http://localhost:8250/oidc/callback,http://localhost:8200/ui/vault/auth/oidc/oidc/callback"
just keycloak::create-client realm="${KEYCLOAK_REALM}" client_id="vault" redirect_url="${redirect_urls}" client_secret="${oidc_client_secret}"
echo "Using client secret: ${oidc_client_secret}"
just keycloak::add-audience-mapper "vault" "vault"
just keycloak::add-groups-mapper "vault"
echo "✓ Keycloak client 'vault' created"
echo "Configuring Vault OIDC authentication..."
# Enable OIDC auth method
vault auth list -format=json | jq -e '.["oidc/"]' >/dev/null 2>&1 || \
vault auth enable oidc
# Configure OIDC with Keycloak
vault write auth/oidc/config \
oidc_discovery_url="https://${KEYCLOAK_HOST}/realms/${KEYCLOAK_REALM}" \
oidc_client_id="vault" \
oidc_client_secret="${oidc_client_secret}" \
default_role="default"
# Create default policy for secret access
vault policy write default - <<EOF
path "secret/data/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/metadata/*" {
capabilities = ["list"]
}
EOF
# Create default role for all authenticated users
vault write auth/oidc/role/default \
bound_audiences="vault" \
allowed_redirect_uris="https://${VAULT_HOST}/ui/vault/auth/oidc/oidc/callback" \
allowed_redirect_uris="http://localhost:8250/oidc/callback" \
allowed_redirect_uris="http://localhost:8200/ui/vault/auth/oidc/oidc/callback" \
user_claim="preferred_username" \
groups_claim="groups" \
token_policies="default"
# Create admin role for vault-admins group
vault write auth/oidc/role/admin \
bound_audiences="vault" \
allowed_redirect_uris="https://${VAULT_HOST}/ui/vault/auth/oidc/oidc/callback" \
allowed_redirect_uris="http://localhost:8250/oidc/callback" \
allowed_redirect_uris="http://localhost:8200/ui/vault/auth/oidc/oidc/callback" \
bound_claims/groups="vault-admins" \
user_claim="preferred_username" \
groups_claim="groups" \
token_policies="admin"
echo "✓ Vault OIDC authentication configured"
echo ""
echo "=== OIDC Setup Complete ==="
echo "You can now login to Vault using:"
echo " VAULT_ADDR=${VAULT_ADDR} vault login -method=oidc"
# Disable OIDC authentication
disable-oidc-auth:
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
vault auth disable oidc
# Setup JWT authentication for Keycloak tokens (not used currently)
setup-jwt-auth audience role policy='default':
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
echo "Setting up JWT authentication for audience: {{ audience }}"
# Enable JWT auth if not already enabled
vault auth list -format=json | jq -e '.["jwt/"]' >/dev/null 2>&1 || \
vault auth enable -path=jwt jwt
# Configure JWT to validate Keycloak tokens
vault write auth/jwt/config \
jwks_url="https://${KEYCLOAK_HOST}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs" \
bound_issuer="https://${KEYCLOAK_HOST}/realms/${KEYCLOAK_REALM}"
# Delete existing role if it exists
vault delete auth/jwt/role/{{ role }} || true
# Create role for the specified audience
vault write auth/jwt/role/{{ role }} \
role_type="jwt" \
bound_audiences="{{ audience }},account" \
user_claim="preferred_username" \
token_policies="{{ policy }}" \
ttl="1h" \
max_ttl="48h"
echo "✓ JWT authentication configured"
echo " Audience: {{ audience }}"
echo " Role: {{ role }}"
echo " Policy: {{ policy }}"
echo ""
echo "Usage: client.auth.jwt.jwt_login(role='{{ role }}', jwt=token, path='jwt')"
# Get key value
get path field:
#!/bin/bash
set -euo pipefail
{{ _vault_oidc_env_setup }}
vault kv get -mount=secret -field={{ field }} {{ path }}
# Get key value with root token
get-root path field:
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
vault kv get -mount=secret -field={{ field }} {{ path }}
# Put key value
put path *args:
#!/bin/bash
set -euo pipefail
{{ _vault_oidc_env_setup }}
vault kv put -mount=secret {{ path }} {{ args }}
# Put key value with root token
put-root path *args:
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
vault kv put -mount=secret {{ path }} {{ args }}
# Delete key value
delete path:
#!/bin/bash
set -euo pipefail
{{ _vault_oidc_env_setup }}
vault kv delete -mount=secret {{ path }}
# Delete key value with root token
delete-root path:
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
vault kv delete -mount=secret {{ path }}
# Check if key exists
[no-exit-message]
exist path:
#!/bin/bash
set -euo pipefail
{{ _vault_oidc_env_setup }}
vault kv get -mount=secret {{ path }} &>/dev/null
# Check if key exists with root token
[no-exit-message]
exist-root path:
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
vault kv get -mount=secret {{ path }} &>/dev/null
# Check the environment
[private]
check-env:
#!/bin/bash
set -euo pipefail
if [ -z "${VAULT_HOST}" ]; then
while [ -z "${VAULT_HOST}" ]; do
VAULT_HOST=$(
gum input --prompt="Vault host: " --width=100 --placeholder="vault.example.com"
)
done
just env::set VAULT_HOST="${VAULT_HOST}"
fi
# Setup vault environment with root token (for initial configuration)
setup-root-token:
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
# Setup vault environment with OIDC token (for regular usage)
setup-token:
#!/bin/bash
set -euo pipefail
{{ _vault_oidc_env_setup }}
# Print vault URL address
vault-addr:
#!/bin/bash
set -euo pipefail
if [ -z "${VAULT_HOST}" ]; then
echo "Error: VAULT_HOST is not set." >&2
exit 1
fi
echo "https://${VAULT_HOST}"
# Write data to Vault at the given path
write *args:
#!/bin/bash
set -euo pipefail
{{ _vault_oidc_env_setup }}
vault write {{ args }}
# Write data to Vault at the given path with root token
root-write *args:
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
vault write {{ args }}
# Upload a policy to Vault
write-policy name file:
#!/bin/bash
set -euo pipefail
{{ _vault_root_env_setup }}
vault policy write {{ name }} {{ file }}
# Login to Vault using OIDC
login:
@vault login -method=oidc
# NOTE: Vault monitoring is not supported
# Reason: Prometheus ServiceMonitor does not support custom HTTP headers (X-Vault-Token)
# Alternative: Use Vault Exporter or manual Prometheus scrape_configs