# Chain-head attestation on a schedule (AUDIT-WP-0009-T02). # # T01 made `tamper_evidence` conditional on a fresh attestation. Until this # runs, the honest answer in production is `false` — the precondition is simply # not met. This is the job that meets it. # # WHERE THE ATTESTATION GOES, AND WHY IT MATTERS MORE THAN THE SCHEDULE. # An attestation is worth exactly as much as its independence from the thing it # attests. Two copies, with different properties, and neither is optional: # # 1. ConfigMap `audit-core-chain-head`, in-cluster, written here and mounted # read-only by the Deployment. Outside platform-pg, so a database owner # who rewrites a suffix cannot also rewrite the attestation without # separate cluster access. This is the copy the receiver reads and the one # that makes /readyz truthful. # # 2. The logical-offsite copy (rapp-postgres / Nextcloud + age, the path # RESOURCE-WP-0002-T06 already uses). Survives loss of the cluster. This # job does NOT write it — audit-core holds no Nextcloud credential and # should not — so it remains an operator step, recorded in # docs/integrity.md. # # NOT the Barman prefix. A copy restored alongside the table proves nothing: # whoever rewrote the table restores the attestation that matches it. # # So copy 1 alone is a real but bounded control: it defends against a database # owner, not against a cluster owner. docs/integrity.md states that bound; do # not let this file be read as delivering more. # # Image digest must match deploy/audit-core.yaml. --- apiVersion: v1 kind: ServiceAccount metadata: name: audit-core-attest namespace: audit-core labels: app.kubernetes.io/name: audit-core app.kubernetes.io/component: attest --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: audit-core-attest namespace: audit-core rules: # One named ConfigMap, in this namespace, and no `create` on the collection. # The ConfigMap is created once by the operator; this job may only replace # its contents. Deliberately not `list` — a job that can enumerate the # namespace's ConfigMaps has more reach than writing one head needs. - apiGroups: [""] resources: ["configmaps"] resourceNames: ["audit-core-chain-head"] verbs: ["get", "update", "patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: audit-core-attest namespace: audit-core roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: audit-core-attest subjects: - kind: ServiceAccount name: audit-core-attest namespace: audit-core --- apiVersion: batch/v1 kind: CronJob metadata: name: audit-core-attest-chain namespace: audit-core labels: app.kubernetes.io/name: audit-core app.kubernetes.io/component: attest spec: # Daily, against the 168h freshness window declared in docs/integrity.md. # Seven cadences of headroom on purpose: a missed run degrades the claim # gradually rather than flapping tamper_evidence false on one bad night. schedule: "17 3 * * *" timeZone: Etc/UTC concurrencyPolicy: Forbid startingDeadlineSeconds: 3600 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 7 jobTemplate: spec: backoffLimit: 2 ttlSecondsAfterFinished: 172800 template: metadata: labels: app.kubernetes.io/name: audit-core app.kubernetes.io/component: attest spec: restartPolicy: OnFailure serviceAccountName: audit-core-attest securityContext: runAsNonRoot: true runAsUser: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault containers: - name: attest image: forgejo.coulomb.social/coulomb/audit-core@sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6 imagePullPolicy: IfNotPresent command: - python - -m - audit_core.attest_publish # attest_publish runs `attest-chain`, refuses to publish over a # broken chain, and PATCHes the ConfigMap through the API with # the projected ServiceAccount token. Written in stdlib rather # than shelling out because this image carries no kubectl, and # adding one to an audit receiver's image to write one file is a # worse trade than twenty lines of urllib. env: - name: AUDIT_CORE_CREDENTIAL_DIR value: /etc/audit-core/db - name: AUDIT_CORE_DATABASE_SCHEMA value: audit_core # Read-only work: never migrate from the attestation job. - name: AUDIT_CORE_AUTO_MIGRATE value: "0" resources: requests: {cpu: 50m, memory: 64Mi} limits: {cpu: 500m, memory: 256Mi} securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] readOnlyRootFilesystem: true volumeMounts: - name: tmp mountPath: /tmp - name: database-credential mountPath: /etc/audit-core/db readOnly: true volumes: - name: tmp emptyDir: {} - name: database-credential secret: secretName: audit-core-database defaultMode: 0440 --- # Created empty by the operator so the CronJob's Role needs no `create`, and # so the Deployment can mount it before the first run. An absent or undated # attestation degrades the claim rather than breaking the receiver — that is # T01's `no_attestation` path, and it is the correct behaviour on day one. apiVersion: v1 kind: ConfigMap metadata: name: audit-core-chain-head namespace: audit-core labels: app.kubernetes.io/name: audit-core app.kubernetes.io/component: attest data: chain-head.json: "{}"