120 lines
5.7 KiB
YAML
120 lines
5.7 KiB
YAML
# 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"]
|