vault: provision Hermes LAN model gateway credentials

This commit is contained in:
jenkins 2026-09-28 17:06:07 -05:00
parent fc321d2fcc
commit 0274335dfe
5 changed files with 278 additions and 1 deletions

View File

@ -3,7 +3,7 @@
apiVersion: batch/v1
kind: Job
metadata:
name: vault-k8s-auth-hermes-11
name: vault-k8s-auth-hermes-12
namespace: vault
spec:
backoffLimit: 2

View File

@ -0,0 +1,79 @@
# services/vault/hermes-model-gate-lan-token-seed-job.yaml
# Purpose: create-only seed of the model-gate LAN API bearer token at
# kv/atlas/hermes/model-gate-lan-api (field: token). KV-v2 CAS create
# semantics -- an existing token is never overwritten. The script waits for
# the vault-k8s-auth-config CronJob (every 15 minutes) to write the seeder
# auth role, so a fresh merge does not fail the Job on the role race.
apiVersion: v1
kind: ServiceAccount
metadata:
name: hermes-model-gate-lan-token-seed
namespace: vault
---
apiVersion: batch/v1
kind: Job
metadata:
name: vault-hermes-model-gate-lan-token-seed-1
namespace: vault
spec:
backoffLimit: 2
template:
spec:
serviceAccountName: hermes-model-gate-lan-token-seed
enableServiceLinks: false
restartPolicy: Never
nodeSelector:
hardware: rpi5
kubernetes.io/arch: arm64
node-role.kubernetes.io/worker: "true"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values: [titan-04, titan-14, titan-18, titan-19, titan-24]
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: seed
image: docker.io/hashicorp/vault@sha256:4e33b126a59c0c333b76fb4e894722462659a6bec7c48c9ee8cea56fccfd2569
imagePullPolicy: IfNotPresent
command: [sh, /scripts/vault_hermes_model_gate_lan_token_ensure.sh]
env:
- name: HOME
value: /tmp
- name: VAULT_ADDR
value: http://vault.vault.svc.cluster.local:8200
- name: VAULT_K8S_ROLE
value: hermes-model-gate-lan-token-seed
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 100
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: scripts
mountPath: /scripts
readOnly: true
- name: tmp
mountPath: /tmp
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 250m, memory: 128Mi}
volumes:
- name: scripts
configMap:
name: vault-hermes-model-gate-lan-token-seed-script
defaultMode: 0555
- name: tmp
emptyDir: {}

View File

@ -12,6 +12,7 @@ resources:
- statefulset.yaml
- k8s-auth-config-cronjob.yaml
- hermes-auth-role-bootstrap-job.yaml
- hermes-model-gate-lan-token-seed-job.yaml
- oidc-config-cronjob.yaml
- service.yaml
- certificate.yaml
@ -27,6 +28,9 @@ configMapGenerator:
- name: vault-k8s-auth-config-script
files:
- vault_k8s_auth_configure.sh=scripts/vault_k8s_auth_configure.sh
- name: vault-hermes-model-gate-lan-token-seed-script
files:
- vault_hermes_model_gate_lan_token_ensure.sh=scripts/vault_hermes_model_gate_lan_token_ensure.sh
- name: vault-entrypoint
files:
- vault-entrypoint.sh=scripts/vault-entrypoint.sh

View File

@ -0,0 +1,177 @@
#!/usr/bin/env sh
set -eu
secret_path="kv/atlas/hermes/model-gate-lan-api"
secret_api_path="kv/data/atlas/hermes/model-gate-lan-api"
jwt_file="${VAULT_K8S_JWT_FILE:-/var/run/secrets/kubernetes.io/serviceaccount/token}"
vault_role="${VAULT_K8S_ROLE:-hermes-model-gate-lan-token-seed}"
payload_file="${TMPDIR:-/tmp}/hermes-model-gate-lan-token.json"
log() { printf '[model-gate-lan-token] %s\n' "$*" >&2; }
cleanup() { rm -f "${payload_file}"; }
trap cleanup EXIT HUP INT TERM
retry_command() {
attempt=1
while [ "${attempt}" -le 5 ]; do
set +e
command_output="$("$@" 2>&1)"
command_status=$?
set -e
if [ "${command_status}" -eq 0 ]; then
printf '%s' "${command_output}"
return 0
fi
if [ "${attempt}" -lt 5 ]; then
sleep "${attempt}"
fi
attempt=$((attempt + 1))
done
return "${command_status}"
}
ensure_token() {
if [ -n "${VAULT_TOKEN:-}" ]; then
return
fi
# The seeder auth role is written by the vault-k8s-auth-config CronJob
# (every 15 minutes), so a fresh merge can reach this Job before the role
# exists. Wait out that window instead of failing the Job.
attempt=1
while [ "${attempt}" -le 40 ]; do
jwt="$(cat "${jwt_file}")"
set +e
VAULT_TOKEN="$(vault write -field=token \
auth/kubernetes/login role="${vault_role}" jwt="${jwt}" 2>&1)"
login_status=$?
set -e
unset jwt
if [ "${login_status}" -eq 0 ] && [ -n "${VAULT_TOKEN}" ]; then
export VAULT_TOKEN
return
fi
log "Vault Kubernetes login not ready (attempt ${attempt}/40); retrying"
sleep 30
attempt=$((attempt + 1))
done
log "Vault Kubernetes login failed after waiting for the auth role"
exit 1
}
# Return 0 for the complete field, 10 when absent, 11 when present but
# incomplete, and 12 for a persistent authorization/transport failure.
read_lan_token() {
attempt=1
while [ "${attempt}" -le 5 ]; do
set +e
secret_json="$(vault read -format=json "${secret_api_path}" 2>&1)"
secret_status=$?
set -e
if [ "${secret_status}" -eq 0 ]; then
unset secret_json
set +e
lan_token="$(vault kv get -field=token "${secret_path}" 2>&1)"
field_status=$?
set -e
if [ "${field_status}" -eq 0 ]; then
case "${lan_token}" in
''|*[!0-9a-f]*) unset lan_token; return 11 ;;
esac
if [ "${#lan_token}" -eq 64 ]; then
unset lan_token
return 0
fi
unset lan_token
return 11
fi
if printf '%s' "${lan_token}" | grep -q 'No value found'; then
unset lan_token
return 11
fi
unset lan_token
elif printf '%s' "${secret_json}" | grep -Eq 'Code: 404|No value found at'; then
unset secret_json
return 10
fi
if [ "${attempt}" -lt 5 ]; then
sleep "${attempt}"
fi
attempt=$((attempt + 1))
done
unset secret_json
return 12
}
ensure_token
if read_lan_token; then
read_status=0
else
read_status=$?
fi
case "${read_status}" in
0)
log "LAN API token already present; no write performed"
exit 0
;;
11)
log "secret exists without a valid token; refusing to overwrite existing fields"
exit 1
;;
12)
log "secret read failed after retries; refusing to seed"
exit 1
;;
10) ;;
*)
log "unexpected secret read state ${read_status}"
exit 1
;;
esac
new_token="$(retry_command vault write -field=random_bytes \
sys/tools/random/32 format=hex)" || {
log "Vault random token generation failed after retries"
exit 1
}
case "${new_token}" in
''|*[!0-9a-f]*)
unset new_token
log "Vault returned an invalid random token"
exit 1
;;
esac
if [ "${#new_token}" -ne 64 ]; then
unset new_token
log "Vault returned an invalid random token"
exit 1
fi
umask 077
printf '{"options":{"cas":0},"data":{"token":"%s"}}\n' \
"${new_token}" > "${payload_file}"
unset new_token
set +e
create_error="$(vault write "${secret_api_path}" @"${payload_file}" 2>&1 >/dev/null)"
create_status=$?
set -e
cleanup
if [ "${create_status}" -eq 0 ]; then
log "LAN API token created with KV-v2 CAS create-only semantics"
exit 0
fi
if printf '%s' "${create_error}" | grep -qi 'check-and-set'; then
unset create_error
if read_lan_token; then
log "another seeder won CAS; existing token preserved"
exit 0
fi
log "CAS race did not produce a readable token; refusing to continue"
exit 1
fi
unset create_error
log "KV-v2 CAS create failed; refusing to retry a write"
exit 1

View File

@ -278,6 +278,23 @@ write_policy_and_role "hermes-agent" "hermes" "hermes-agent" \
"hermes/agent-oidc hermes/agent-tokens hermes/chat-telegram hermes/developer-keycloak hermes/developer-harbor hermes/developer-jenkins hermes/developer-ssh" ""
write_policy_and_role "hermes-switchyard" "hermes" "hermes-switchyard" \
"hermes/chat-telegram" ""
write_policy_and_role "hermes-model-gate" "hermes" "hermes-model-gate" \
"hermes/model-gate-lan-api" ""
hermes_model_gate_lan_token_seed_policy='
path "kv/data/atlas/hermes/model-gate-lan-api" {
capabilities = ["create", "read"]
}
path "sys/tools/random/32" {
capabilities = ["update"]
}
'
write_raw_policy "hermes-model-gate-lan-token-seed" "${hermes_model_gate_lan_token_seed_policy}"
log "writing role hermes-model-gate-lan-token-seed"
vault_cmd write "auth/kubernetes/role/hermes-model-gate-lan-token-seed" \
bound_service_account_names="hermes-model-gate-lan-token-seed" \
bound_service_account_namespaces="vault" \
policies="hermes-model-gate-lan-token-seed" \
ttl="${role_ttl}"
hermes_jenkins_token_seed_policy='
path "kv/data/atlas/hermes/developer-jenkins" {
capabilities = ["create", "read"]