Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
163 lines
6 KiB
YAML
163 lines
6 KiB
YAML
# 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:c82e0442de0fd181342916ae9cd5d6de41d859e1efda637bd93936c67873afa5
|
|
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: "{}"
|