Files
bikoret/deploy/k8s/secrets.yaml
T
Scott KarpandClaude Fable 5.1 d6cf9355fd the ledger: a caller's own bedikot, read back and signed
The examination is the product; the record of it is what a caller can
carry away. Every bedikah was already engraved in L3 — output hash and
length, source digests, coupling trace, claims, violations, verdict,
never the text — and no caller could get theirs back.

- bedikot.key_id (migration): a SHA-256 digest of the courtyard key that
  brought the offering, never the key; threaded as :key_id through
  Bedikah.examine from every site that goes through the gate (courtyard
  bedikah, pair, the gate's pass and stream). Unique index on
  bedikah_ref.
- VavPipeline.Ledger: entry (the examination's fields, none of the
  database's, not the owner's), canonical JSON (keys sorted at every
  level, no whitespace, UTF-8), Ed25519 sign/verify, public key, render.
  Seed from BIKORET_LEDGER_SIGNING_KEY (base64 of 32 bytes) in
  shorashim-secrets; absent → unsigned and says so.
- GET /courtyard/ledger?limit&before — this key's bedikot, newest first,
  each rendered and signed; GET /courtyard/bedikah/<ref> — one, 404 for
  another key's or none alike; GET /peah/ledger_key — the public key.
- Tamid §1.1 "The ledger" (Doorpost pin moved → 02ddf313…), KOHEN
  "Issuing a key" (the seed, once), secrets.yaml template.

Tests: ledger_test (digest, entry, canonical, sign/verify, unsigned
rendering, signed rendering); ledger_l3_test (own records listed newest
first and only own; 404 alike for another's and none; an examination
engraved under the key reads back signed and verifies against
/peah/ledger_key). Elixir 642/0; L3 13/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-21 18:45:47 +00:00

130 lines
5.2 KiB
YAML

# Bikoret Secrets — template only
# NEVER commit actual secret values to git.
# Apply with: kubectl apply -f - after filling in values.
#
# The DATABASE_URL connects to Exoscale PostgreSQL (temple-council-db)
# in ch-gva-2 zone. Retrieve credentials from:
# exo dbaas show temple-council-db --zone ch-gva-2
apiVersion: v1
kind: Secret
metadata:
name: shorashim-secrets
namespace: shorashim
type: Opaque
stringData:
# PostgreSQL connection for L3 Oral Tradition — the APPLICATION role.
# Format: ecto://user:password@host:port/database?sslmode=require
# This is shorashim_app: INSERT and SELECT on the L3 tables, no DDL,
# no CREATE on schema public. Append-only is enforced by the grants,
# not by the application's good behaviour.
DATABASE_URL: "REPLACE_WITH_ACTUAL_DATABASE_URL"
# The ledger's Ed25519 seed: base64 of 32 random bytes (openssl rand
# -base64 32). Optional — absent, the ledger is served unsigned and
# says so. KOHEN.md "Issuing a key".
BIKORET_LEDGER_SIGNING_KEY: "REPLACE_WITH_BASE64_32_BYTES_OR_REMOVE"
---
# ============================================================
# shorashim-migrate-secrets — the DDL role
# ============================================================
#
# KOHEN.md:184 — "the migration runs as avnadmin; the application runs
# as shorashim_app." TWO ROLES, TWO SECRETS. Only the `migrate` init
# container reads this one (deployment.yaml overrides DATABASE_URL for
# that container alone); every app container keeps shorashim-secrets
# above.
#
# WHY IT IS SEPARATE AND NOT JUST A GRANT: Ecto.Migrator.run/3 calls
# ensure_schema_migrations_table! BEFORE checking what is pending, so
# it issues CREATE TABLE IF NOT EXISTS schema_migrations on EVERY pod
# start, even with migrations current. As shorashim_app that is
# ERROR 42501 permission denied for schema public, and the pod never
# reaches Running. The alternative — granting shorashim_app CREATE on
# schema public — collapses the app/migration boundary the runbook
# draws deliberately. Do not take it.
#
# THIS FILE IS A TEMPLATE. Never commit a real value. Create the secret
# from the live database credential instead:
#
# export KUBECONFIG="$HOME/.kube/<your-cluster>.yaml"
# umask 077
# TMP=$(mktemp)
#
# exo dbaas user reveal-secrets temple-council-db avnadmin \
# --zone ch-gva-2 -O json \
# | python3 -c '
# import base64, json, subprocess, sys, urllib.parse
# avn = json.load(sys.stdin)["password"]
# app = subprocess.run(
# ["kubectl", "get", "secret", "shorashim-secrets", "-n", "bikoret",
# "-o", "jsonpath={.data.DATABASE_URL}"],
# capture_output=True, text=True, check=True).stdout
# u = urllib.parse.urlsplit(base64.b64decode(app).decode())
# pw = urllib.parse.quote(avn, safe="")
# netloc = "avnadmin:%s@%s:%d" % (pw, u.hostname, u.port)
# sys.stdout.write(
# urllib.parse.urlunsplit((u.scheme, netloc, u.path, u.query, u.fragment)))
# ' > "$TMP"
#
# kubectl create secret generic shorashim-migrate-secrets \
# --namespace shorashim --from-file=DATABASE_URL="$TMP"
#
# rm -f "$TMP"
#
# The rewrite is done in Python, not with `sed s|...|...|`, on purpose.
# A generated password containing & or | or \ corrupts a sed
# replacement silently — & re-inserts the matched text and | ends the
# expression early — and the result is a DATABASE_URL that looks
# plausible and does not authenticate. quote(safe="") also percent-
# encodes any character that is not URL-safe in the userinfo field,
# which sed would have passed through raw. The value goes via a
# umask-077 temp file rather than --from-literal so the password is
# never an argv entry visible to `ps`.
#
# Absence is meant to be loud: without this secret the pod stops at
# CreateContainerConfigError. There is no fallback to the app role.
apiVersion: v1
kind: Secret
metadata:
name: shorashim-migrate-secrets
namespace: shorashim
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/bikoret/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