Tablet 45: the courtyard gate checks the key, not the shape of the header

/courtyard/* and /chatzer/* admitted any `Authorization: Bearer <x>`.
Every test sent `Bearer test-token`; none sent a wrong one. Every
admitted request spends the day's manna.

VavPipeline.BigdeiKodesh.Gate: keys are a file (Construction §4.6 —
never env, never config, never git), one `tier key` per line, tiers
courtyard < chatzer (the inner ring opens the outer). Constant-time
compare against every line; no file / empty / unreadable → nobody
passes, GATE_NO_KEYS in the log, the caller told only "missing or
invalid". 403 FORBIDDEN for an outer key at an inner gate. Read on
every request so a rotated key is live in ~1 min without a rollout.

Deploy: Secret shorashim-api-keys projected at
/etc/shorashim/secrets/api_keys (vav-pipeline only, 0440),
API_KEYS_FILE in the configmap, template + issue/rotate recipe in
secrets.yaml and KOHEN.md "Issuing a key". Spec: tamid §1.1 (gate,
403), vayakhel §4.6. Doorpost 91d09d8d… → 42b38626….
Tests: tablet45_gate_test (parse, tiers, wrong/near/empty/no key, no
file) — Elixir 527/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Fable 5.1 committed 2026-10-06 15:16:33 +00:00
1 parent 996a8efe68
commit 79eb34840f
17 files changed
+372 -34

No files matched your search

+5
View File
@@ -20,6 +20,11 @@ data:
# shared tmp-shorashim volume. Pod-internal; never on the network.
KORBAN_SOCKET_PATH: "/tmp/shorashim/korban.sock"
# Courtyard / chatzer keys — a FILE (Construction §4.6), projected from
# the shorashim-api-keys Secret (secrets.yaml). The path is config; the
# keys never are.
API_KEYS_FILE: "/etc/shorashim/secrets/api_keys"
# Shoresh Engine binary — staged onto the shared shoresh-bin volume
# by the shoresh-engine-install init container (see deployment.yaml).
# vav_pipeline spawns it as an Erlang Port, i.e. as a child process of
+10
View File
@@ -76,6 +76,13 @@ spec:
# vav-pipeline, which spawns it as an Erlang Port.
- name: shoresh-bin
emptyDir: {}
# The courtyard keys, as a file (Construction §4.6). Only
# vav-pipeline mounts it. Absent → the pod waits at
# ContainerCreating, loudly; there is no "open gate" fallback.
- name: api-keys
secret:
secretName: shorashim-api-keys
defaultMode: 0440
# --- POD SECURITY CONTEXT ---
# Every image runs as the non-root user `shorashim` (uid/gid
@@ -221,6 +228,9 @@ spec:
# directory both live under it.
- name: tmp-shorashim
mountPath: /tmp
- name: api-keys
mountPath: /etc/shorashim/secrets
readOnly: true
- name: shoresh-bin
mountPath: /opt/shoresh
readOnly: true
+35
View File
@@ -88,3 +88,38 @@ type: Opaque
stringData:
# ecto://avnadmin:PASSWORD@host:port/database?sslmode=require
DATABASE_URL: "REPLACE_WITH_AVNADMIN_DATABASE_URL"
---
# ============================================================
# shorashim-api-keys — who may enter the courtyard
# ============================================================
#
# Mounted as the file /etc/shorashim/secrets/api_keys in vav-pipeline
# (Construction §4.6: a secret is a file). One key per line:
#
# courtyard <key> offerings and bedikot (/courtyard/*)
# chatzer <key> also the Sanhedrin (/chatzer/*); opens the courtyard too
#
# Issue a key (KOHEN.md "Issuing a key"):
#
# umask 077; TMP=$(mktemp)
# printf 'courtyard %s\n' "$(openssl rand -hex 32)" >> "$TMP"
# kubectl create secret generic shorashim-api-keys -n shorashim \
# --from-file=api_keys="$TMP"
# # rotate: kubectl create ... --dry-run=client -o yaml | kubectl apply -f -
# rm -f "$TMP"
#
# The kubelet refreshes a projected Secret within about a minute; the
# gate reads the file on every request, so a rotated key takes effect
# without a rollout. No file, no keys → nobody passes (GATE_NO_KEYS in
# the log).
apiVersion: v1
kind: Secret
metadata:
name: shorashim-api-keys
namespace: shorashim
type: Opaque
stringData:
api_keys: |
# tier key
courtyard REPLACE_WITH_openssl_rand_hex_32