scobleclaw/scoble-policy/audit-sink/provisioning/README.md
CoS 4612b5e68d feat(audit-sink): provision scrole role, per-role CNPG secrets, schema, and creds
Resolves the #63 root cause: CNPG managed.roles expects each role to reference
its own type=kubernetes.io/basic-auth Secret with literal 'username'/'password'
keys (see cloudnative-pg/internal/management/utils/secrets.go). The previous
shared 'postgres-users' Secret with role-name-suffixed keys (forgejo-password,
umami-password, scoble_audit_writer-password) was never going to reconcile, so
forgejo/umami had been silently missing from the database for the entire
lifetime of the cluster.

Changes:
- secret-{forgejo,umami,scrole}.yaml: per-role basic-auth Secrets in 'database' ns.
  forgejo/umami passwords stay at the existing <change-me> placeholder. scrole
  gets a new random 32-byte password.
- secret-scoble-policy-pg-creds.yaml: Opaque Secret in 'scoble-policy' ns with
  username/password/jdbc-url for the audit-sink Integration to mount.
- migrate.sql: drops the previous scoble_policy_audit (decision_id/actor/action
  shape) and recreates the schema.sql shape (agent_id/kamelet_ref/decision/etc.)
  with the right indexes, check constraint, and scrole GRANTs.
- apply-schema-pod.yaml: one-shot Pod that runs migrate.sql using the CNPG
  superuser Secret as credential source.
- schema.sql: now also documents the scrole GRANTs needed (idempotent).
- README.md: updated with apply+verify steps, #62/#64 known issues, and the
  full diagnosis of #63.

End-to-end verified 2026-06-07 23:55 UTC:
- All 3 managed roles (forgejo, umami, scoble_audit_writer) exist in PG.
- scrole can SELECT/INSERT on scoble_policy_audit.
- A 'smoke-1' row is in the table; a Kafka message at scoble-kafka:9092 in the
  audit-sink shape is consumable end-to-end.
- Legacy 'postgres-users' secret deleted (no longer referenced by the cluster).

Refs: marcus/engineering#43, marcus/engineering#63
2026-06-07 17:09:41 -07:00

7.7 KiB

scoble-kafka provisioning runbook

Date written: 2026-06-07 Owner: @agent-cos Related: marcus/engineering#43, marcus/engineering#63 (closed), marcus/engineering#62

Summary

Single-node Kafka (KRaft, no ZooKeeper) running in scobleclaw namespace. Topic scobleclaw.audit.decisions exists with 3 partitions, RF=1. End-to-end produce+consume verified from a pod in the scobleclaw namespace. scoble_audit_writer role exists in CNPG-managed Postgres, scoble_policy_audit table is the schema.sql shape (agent_id/kamelet_ref/decision/etc.).

State (as of 2026-06-07 23:55 UTC)

Resource Status Notes
Strimzi operator 1/1 Running Healthy, leader-elected. Watches kafka,scobleclaw,scoble-policy.
Kafka CR scoble-kafka NotReady Strimzi operator's admin client can't talk to the broker on port 9091 (see #62). Broker IS responding on 9092 from in-cluster clients. Non-fatal.
KafkaNodePool dual-role Created 1 replica, dual-role (controller+broker), 5Gi PVC on nfs-csi.
KafkaTopic scobleclaw.audit.decisions Created manually 3 partitions, RF=1. TopicId Bn0QeTBWQQeaqVYDyusBTw. Entity operator not deployed (#62 blocks).
scoble-kafka Service (ExternalName) Created Aliases the bootstrap service so the audit-sink's scoble-kafka:9092 reference resolves.
scrole Secret (basic-auth) Created database/postgres-scobleclaw-scrole, type kubernetes.io/basic-auth.
forgejo/umami Secrets (basic-auth) Created database/postgres-scobleclaw-{forgejo,umami}.
scoble_audit_writer Postgres role CREATED Via CNPG managed.roles. Password from postgres-scobleclaw-scrole.
forgejo/umami Postgres roles CREATED Were missing from DB before this session — silent breakage in CNPG managed.roles controller due to wrong secret format.
scoble_policy_audit table Created Shape: agent_id/kamelet_ref/decision/reasons/rule_id/request_id (per schema.sql). Old shape (decision_id/actor/action/resource/verdict/metadata) was dropped.
scoble-policy-pg-creds Secret Created scoble-policy/scoble-policy-pg-creds, type Opaque, keys username/password/jdbc-url.
scrole GRANTs on scoble_policy_audit Applied SELECT, INSERT; USAGE+SELECT on the id sequence.

Files in this directory

File Purpose
scoble-kafka.yaml Kafka CR (KRaft single-node) + KafkaNodePool CR (dual-role, 5Gi nfs-csi PVC). Apply as a single file (multi-document YAML).
scoble-kafka-topic.yaml KafkaTopic CR for scobleclaw.audit.decisions (3 partitions, RF=1, 7-day retention).
scoble-kafka-service.yaml scoble-kafka Service (ExternalName) aliasing the bootstrap service.
secret-forgejo.yaml Per-role basic-auth secret for forgejo (used by CNPG managed.roles).
secret-umami.yaml Per-role basic-auth secret for umami.
secret-scrole.yaml Per-role basic-auth secret for scoble_audit_writer.
secret-scoble-policy-pg-creds.yaml Opaque secret in scoble-policy namespace with scrole credentials, for the audit-sink Integration to mount.
migrate.sql DROP+CREATE+GRANT for the scoble_policy_audit table. Idempotent (DROPs first).
apply-schema-pod.yaml One-shot pod that runs migrate.sql against the in-cluster CNPG using the superuser secret.
README.md This file.
strimzi-0.45.2.tar.gz and strimzi-0.45.2/ Upstream Strimzi 0.45.2 install bundle (gitignored). Re-download from the upstream release.

Apply

export KUBECONFIG=~/.kube/config

# 1. Kafka cluster
kubectl apply -f audit-sink/provisioning/scoble-kafka.yaml

# 2. Topic (currently applied manually because entity operator is blocked by #62)
kubectl apply -f audit-sink/provisioning/scoble-kafka-topic.yaml

# 3. Service alias
kubectl apply -f audit-sink/provisioning/scoble-kafka-service.yaml

# 4. CNPG per-role basic-auth secrets
kubectl apply -f audit-sink/provisioning/secret-forgejo.yaml
kubectl apply -f audit-sink/provisioning/secret-umami.yaml
kubectl apply -f audit-sink/provisioning/secret-scrole.yaml

# 5. Cluster spec (patch existing postgres-scobleclaw Cluster to point each
# managed role at its own basic-auth secret). Already applied; included for
# re-deploy reference.
kubectl patch cluster -n database postgres-scobleclaw --type=json -p '[
  {"op":"replace","path":"/spec/managed/roles/0/passwordSecret/name","value":"postgres-scobleclaw-forgejo"},
  {"op":"replace","path":"/spec/managed/roles/1/passwordSecret/name","value":"postgres-scobleclaw-umami"},
  {"op":"replace","path":"/spec/managed/roles/2/passwordSecret/name","value":"postgres-scobleclaw-scrole"}
]'

# 6. Postgres schema (drops old shape, creates new shape, applies grants)
kubectl create configmap -n scobleclaw scrole-schema --from-file=migrate.sql=audit-sink/provisioning/migrate.sql
kubectl apply -f audit-sink/provisioning/apply-schema-pod.yaml
kubectl logs -n scobleclaw apply-schema
kubectl delete pod -n scobleclaw apply-schema --force --grace-period=0

# 7. Audit-sink creds in scoble-policy namespace
kubectl apply -f audit-sink/provisioning/secret-scoble-policy-pg-creds.yaml

Verify

# Topic exists
kubectl run -n scobleclaw verify-topic --rm --restart=Never --image=quay.io/strimzi/kafka:0.45.2-kafka-3.9.2 \
  -- /opt/kafka/bin/kafka-topics.sh --list \
  --bootstrap-server scoble-kafka:9092

# Role + table + INSERT roundtrip
kubectl run -n scobleclaw verify-pg --rm --restart=Never --image=postgres:17-alpine \
  -- sh -c "apk add --no-cache postgresql-client >/dev/null 2>&1; \
    PGPASSWORD='<the-scrole-pw>' psql \
    -h postgres-scobleclaw-rw.database.svc.cluster.local -U scoble_audit_writer -d app \
    -c \"INSERT INTO scoble_policy_audit (timestamp, agent_id, kamelet_ref, decision, reasons, request_id) VALUES (now(), 'verify', 'audit-sink', 'allow', '[]'::jsonb, 'verify-1') RETURNING id, agent_id, request_id;\""

Why the previous scoble-kafka Kafka CR was GC'd

Strimzi 0.45.2 still defaults to ZooKeeper mode. A Kafka CR with no spec.zookeeper block is invalid; the operator reconciles, marks it NotReady with InvalidResourceException: The .spec.zookeeper section of the Kafka custom resource is missing, and the next reconcile GC's the assembly (the Assembly scoble-kafka or some parts of it will be deleted by garbage collection log line). The current manifests use the proper KRaft pattern.

Why CNPG managed.roles were broken (root cause from #63)

spec.managed.roles[] references a passwordSecret per role. CNPG's GetUserPasswordFromSecret (in internal/management/utils/secrets.go) expects each secret to be type: kubernetes.io/basic-auth with username and password keys (literal). The cluster was using a single shared postgres-users Secret with role-name-suffixed keys (forgejo-password, etc.) — that doesn't match the function's expectations, and the operator kept logging username key doesn't exist inside the secret. As a result, forgejo/umami/scoble_audit_writer were silently missing from the database even though they were defined in spec.

Fix: create per-role basic-auth secrets and patch the cluster spec to point each role at its own secret.

Known issues (separate tickets)

  • #62: Strimzi operator's admin client can't reach the bootstrap service on port 9091. The operator keeps rolling the broker every few minutes. Workaround: create topics manually via kafka-topics.sh from inside the broker pod. Recommended fix: move the operator into the same namespace as the Kafka cluster.
  • #64: cross-namespace pod-to-pod IP routing broken (Host unreachable from kafka ns to broker pod IP 10.42.0.x). Service-virtual-IP routing works. Doesn't block the audit-sink (same namespace, goes through the service).