Sync the self-hosting stack

This commit is contained in:
2026-08-27 17:52:23 +02:00
parent af6710d849
commit 02ab18aff1
12 changed files with 1407 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
# Mint the generated secrets ONCE, from inside the cluster.
#
# ─── WHAT THIS REPLACES ───────────────────────────────────────────────────────────────────────
#
# On Compose, `init-secrets.cjs` runs as a one-shot writing into a volume every container shares,
# and its guarantee that a key is generated exactly once is `openSync(path, "wx")` — two writers
# race, the kernel picks one. There is no shared filesystem across pods, so the exclusion moves to
# the one thing every pod does share: the API server. Creating a Secret that already exists is a
# 409, decided by one serialized writer. Same guarantee, same strength.
#
# This is the alternative to minting by hand, NOT a replacement for understanding what it mints. If
# your estate already has a secrets manager — SealedSecrets, ExternalSecrets, Vault — use it and
# skip this file; write `jarvis-generated` yourself with the four keys listed in the Role below.
#
# ─── THE ONE WAY TO GET HURT ──────────────────────────────────────────────────────────────────
#
# Deleting `jarvis-generated` and re-running this mints a NEW vault key against a database whose
# credentials are sealed under the old one. Nothing fails at boot. Every stored credential becomes
# permanently unreadable, and you find out the first time somebody opens one. There is no undo, and
# the key is not in the database backup by design.
#
# kubectl apply -f 01-secret.example.yaml # the half you write: DATABASE_URL, OPENAI_API_KEY…
# kubectl apply -f 01-secret-job.yaml # the half that is random bytes
# kubectl wait --for=condition=complete job/jarvis-mint-secrets -n jarvis --timeout=2m
# # Then back the vault key up, out of this cluster:
# kubectl get secret jarvis-generated -n jarvis -o jsonpath='{.data.VAULT_MASTER_KEY}' | base64 -d
#
# ──────────────────────────────────────────────────────────────────────────────────────────────
apiVersion: v1
kind: ServiceAccount
metadata:
name: jarvis-secret-minter
namespace: jarvis
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: jarvis-secret-minter
namespace: jarvis
rules:
# `create` AND NOTHING ELSE. No `get`, no `list`, no `update`.
#
# Deliberate, and worth the paragraph: this identity cannot read any Secret in the namespace,
# including the one it just wrote. That is why the script does not look before it writes — the
# 409 is the check, and it is a stronger one than a read-then-write could be, because a read and
# a write are two operations and something else can happen between them.
#
# `create` cannot be narrowed by resourceName — Kubernetes evaluates authorization before the
# object's name is known — so this permits creating any Secret in this namespace. That is the
# floor, and it is why the ServiceAccount belongs to this Job and to nothing else.
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: jarvis-secret-minter
namespace: jarvis
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: jarvis-secret-minter
subjects:
- kind: ServiceAccount
name: jarvis-secret-minter
namespace: jarvis
---
apiVersion: batch/v1
kind: Job
metadata:
name: jarvis-mint-secrets
namespace: jarvis
spec:
# Re-running is safe (409 → left untouched), so a retry costs nothing. A high limit would only
# mean a broken RBAC took longer to become visible.
backoffLimit: 2
# The Job object is tidied up an hour later. The Secret it created is a separate object and is
# not touched by this.
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: jarvis-mint-secrets
spec:
restartPolicy: Never
serviceAccountName: jarvis-secret-minter
securityContext:
runAsNonRoot: true
runAsUser: 10001
# Required by the `restricted` Pod Security Standard that 00-namespace.yaml enforces.
# Omitted, the Job is created and its pod is refused — which reads as a Job that never runs.
seccompProfile:
type: RuntimeDefault
containers:
- name: mint
# The api image, because the script ships in it and a first deployment should pull one
# image rather than three. By digest like every other image here — the same one you put
# in 03-migrate-job.yaml and 04-api.yaml.
image: git.luxit.be/luxit/jarvis-api@sha256:REPLACE_ME
command: ["node", "/app/apps/api/mint-k8s-secret.cjs"]
env:
# Node verifies the API server's certificate against the cluster CA. Without this the
# request would fail — which is the correct failure. Do not reach for
# NODE_TLS_REJECT_UNAUTHORIZED: a bearer token good enough to write Secrets is a bearer
# token worth stealing, and an unverified TLS session is where that happens.
- name: NODE_EXTRA_CA_CERTS
value: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]