Add deployment manifests, custody-class guard and request counters
AUDIT-WP-0005-T03 (progress). Manifests validated --dry-run=server
--validate=strict against railiance01; not applied, since deployment is gated
on RAPP-POSTGRES-WP-0002 and T02 credentials. Nothing here mutates the cluster.
Conventions read off the deployed user-engine workload rather than invented:
digest-pinned image from forgejo.coulomb.social, runAsNonRoot with
RuntimeDefault seccomp, no privilege escalation, all capabilities dropped,
readOnlyRootFilesystem, probes on a named http port, same resource envelope.
The namespace carries railiance.io/postgres-client: platform-pg, which is what
platform-pg-consumer-ingress in rapp-postgres admits; without that label the
pod cannot reach the database at all.
NetworkPolicies default-deny both directions, then permit ingress from the
user-engine namespace only, a separately labelled operator read path, and
egress to PostgreSQL in databases plus DNS.
Three decisions worth naming. Liveness is /healthz while readiness is /readyz,
so a database outage drops the pod from the Service rather than restarting it
in a loop. readOnlyRootFilesystem enforces the empty-filesystem property rather
than trusting it, so the SQLite fallback physically cannot accumulate audit
records on ephemeral storage. AUDIT_CORE_REQUIRE_CUSTODY_CLASS=archive makes a
missing database URL a startup failure instead of a silent downgrade to the
development store.
Counters deferred from WP-0004-T06 are exposed as JSON at /v1/stats behind the
read privilege, not as Prometheus exposition format: the cluster runs no
Prometheus, no ServiceMonitor CRD and no other scrape target, so an exposition
endpoint would target a scrape path that does not exist. Usable with curl now
and a small step from /metrics later.
Tests 77 -> 80.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:42:43 +02:00
|
|
|
# Default-deny plus the narrowest set of exceptions (AUDIT-WP-0005-T03).
|
|
|
|
|
#
|
|
|
|
|
# The receiver holds the audit trail, so reachability is part of its threat
|
|
|
|
|
# model: only the declared sender may write, and only the declared operator
|
|
|
|
|
# path may read.
|
|
|
|
|
---
|
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
|
|
|
kind: NetworkPolicy
|
|
|
|
|
metadata:
|
|
|
|
|
name: audit-core-default-deny
|
|
|
|
|
namespace: audit-core
|
|
|
|
|
spec:
|
|
|
|
|
podSelector: {}
|
|
|
|
|
policyTypes: [Ingress, Egress]
|
|
|
|
|
# No rules: everything not permitted below is denied.
|
|
|
|
|
---
|
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
|
|
|
kind: NetworkPolicy
|
|
|
|
|
metadata:
|
|
|
|
|
name: audit-core-sender-ingress
|
|
|
|
|
namespace: audit-core
|
|
|
|
|
spec:
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: audit-core
|
|
|
|
|
policyTypes: [Ingress]
|
|
|
|
|
ingress:
|
|
|
|
|
# user-engine is the only sender. A second sender is a deliberate change
|
|
|
|
|
# here and a matching entry in AUDIT_CORE_SENDERS — the network rule and
|
|
|
|
|
# the credential binding must move together.
|
|
|
|
|
- from:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
kubernetes.io/metadata.name: user-engine
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: TCP, port: 8080}
|
|
|
|
|
---
|
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
|
|
|
kind: NetworkPolicy
|
AUDIT-WP-0010 T01/T03/T04 — admit tenant-engine, and the envelope does not match
Registration: attributive, with the declared completeness_trade recorded on the
receiver side rather than only in the emitter, per §9.6's requirement that the
trade travel with the trail. tenants ["*"] is justified rather than inherited —
tenant-engine's events carry the affected tenant, the set is every tenant
including ones created later, and an explicit list would fail closed at exactly
the moment a tenant is provisioned, dead-lettering the creation evidence of the
tenant whose creation it is. source stays pinned exactly. Ingress ANDs namespace
and pod label; a weaker evidence class is not a reason for a wider network rule.
All inert until the token exists.
Then the finding T04 existed to find: tenant-engine cannot deliver a single
event today. envelope_for sends five required fields under other names —
event_id, action, resource, observed_at, details — and omits correlation_id
entirely, so normalize() raises invalid_event. Verified by running the real
envelope through the real function, not by reading.
Worse than an ordinary integration bug. The drain treats 400 as terminal, so the
outbox row is marked handled while audit-core holds only a dead letter, which is
not chained and is not custody. Lost on both sides, and since the drain is
non-blocking and attributive, nothing fails loudly — a silent total loss of the
stream presenting as a working integration.
Taking the correction the intake invited rather than accepting a lossy record.
normalize() is NOT relaxed to accept the alternate spellings: a receiver that
guesses which sender key means which stored field has made the mapping its own,
and the record stops being the sender's assertion. correlation_id cannot be
synthesized at all — an invented one ties an event to an operation audit-core
never observed.
Root cause is ours. The accepted envelope was published nowhere a sender could
read it; audit-backend-contract.md describes the stored record, and a sender
reading it would reasonably infer exactly the names tenant-engine used.
schema_version audit-core.event.v1alpha1 selects nothing here and gave a false
impression of a negotiated contract. Published docs/event-envelope.md as the
wire contract, including the point that a 400 means the event is not in the
archive and must be treated as a defect to fix rather than a delivery outcome.
T05 moved to wait: nothing to prove end to end until an event can be accepted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
2026-09-10 16:34:45 +02:00
|
|
|
metadata:
|
|
|
|
|
name: audit-core-tenant-engine-ingress
|
|
|
|
|
namespace: audit-core
|
|
|
|
|
spec:
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: audit-core
|
|
|
|
|
policyTypes: [Ingress]
|
|
|
|
|
ingress:
|
|
|
|
|
# AUDIT-WP-0010-T03 / AUDIT-IN-0002. Attributive mutation evidence.
|
|
|
|
|
# Both selectors belong to one peer and are therefore ANDed. Attributive
|
|
|
|
|
# rather than load-bearing changes what may be claimed of the stream, not
|
|
|
|
|
# how narrow its reachability should be — a weaker evidence class is not a
|
|
|
|
|
# reason for a wider network rule.
|
|
|
|
|
- from:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
kubernetes.io/metadata.name: tenant-engine
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: tenant-engine
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: TCP, port: 8080}
|
|
|
|
|
---
|
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
|
|
|
kind: NetworkPolicy
|
2026-08-22 09:37:30 +02:00
|
|
|
metadata:
|
|
|
|
|
name: audit-core-whitehat-ingress
|
|
|
|
|
namespace: audit-core
|
|
|
|
|
spec:
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: audit-core
|
|
|
|
|
policyTypes: [Ingress]
|
|
|
|
|
ingress:
|
|
|
|
|
# Governed E2 evidence plane. Both selectors belong to one peer and are
|
|
|
|
|
# therefore ANDed: only the registered audit-core probe in the dedicated
|
|
|
|
|
# whitehat namespace reaches this port. Application sender authentication
|
|
|
|
|
# and tenant scope remain the inner boundary.
|
|
|
|
|
- from:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
kubernetes.io/metadata.name: whitehat
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
whitehat.security/plane: "true"
|
|
|
|
|
whitehat.security/target: audit-core
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: TCP, port: 8080}
|
|
|
|
|
---
|
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
|
|
|
kind: NetworkPolicy
|
AUDIT-WP-0009-T03/T09 — evidence_kind, and approval-engine's registration inputs
T03. §9.6 gives load-bearing and attributive sources different obligations, so
the archive must record which one a source declared rather than infer it from
traffic. evidence_kind and completeness_trade now sit on SenderIdentity, the
AUDIT_CORE_SENDERS schema, and the non-secret scope overlay.
Two asymmetries are deliberate. The default is attributive, because the other
default would have audit-core imply a completeness obligation no source ever
accepted. And the overlay may raise the kind but never lower it — the same
principle that stops an ExternalSecret refresh shrinking user-engine's
tenants: a ConfigMap refresh must not drop a source's atomicity and detection
obligations without anyone deciding to. A load-bearing source may not carry a
completeness trade at all, since §9.6 requires atomicity of it, and
evidence_declaration() reports completeness_claimed: false for both kinds.
T09 (in progress). approval-engine's registration inputs are prepared and
recorded in docs/approval-engine-source-registration.md: scope entry declared
load-bearing, and audit-core-approval-engine-ingress with namespace and pod
label ANDed in one `from` peer — narrower than user-engine's namespace-only
rule, which is left unchanged. The scope entry lands ahead of the credential
because the overlay only applies to senders the Secret already carries, so it
admits nothing until the token exists; a test asserts that rather than
trusting the reading.
Two inputs remain approval-engine's: a confirmed tenant scope, since senders.py
requires a missing tenant restriction be justified per sender and audit-core
cannot justify it on another repo's behalf, and an explicit secret_policy
choice. Applying the manifests is an operator action.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185wifnLzCxjEY2MT1XbK7L
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713962@bnt-lap001
Assistant-Session: 2718d99d-d3ff-478f-83a2-3a30f01a02fc
2026-09-06 22:31:37 +02:00
|
|
|
metadata:
|
|
|
|
|
name: audit-core-approval-engine-ingress
|
|
|
|
|
namespace: audit-core
|
|
|
|
|
spec:
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: audit-core
|
|
|
|
|
policyTypes: [Ingress]
|
|
|
|
|
ingress:
|
|
|
|
|
# AUDIT-WP-0009-T09 / AUDIT-IN-0001. Load-bearing approval evidence
|
|
|
|
|
# (§9.4). Both selectors belong to one peer and are therefore ANDed:
|
|
|
|
|
# only the approval-engine workload in its own namespace reaches this
|
|
|
|
|
# port. Splitting them into two list items would turn AND into OR and
|
|
|
|
|
# admit every pod in either set.
|
|
|
|
|
#
|
|
|
|
|
# Narrower than user-engine's namespace-only rule on purpose: this is a
|
|
|
|
|
# new sender, and a new rule should not inherit an older rule's breadth.
|
|
|
|
|
# user-engine's policy is deliberately left unchanged.
|
|
|
|
|
- from:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
kubernetes.io/metadata.name: approval-engine
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: approval-engine
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: TCP, port: 8080}
|
|
|
|
|
---
|
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
|
|
|
kind: NetworkPolicy
|
AUDIT-WP-0009-T11 — register informed-decision, and answer GH-DEC-2026-014
informed-decision is the browser-facing approver surface; GH-DEC-2026-012
limit 3 makes its evidence copy the one that must reach audit-core
independently of the emitter, because there the actor being audited and the
evidence source are the same component.
Registration accepted on every proposed field — exact source,
["tenant:platform"], write true, read false, load-bearing, secret_policy
redact. Prepared and inert: the scope overlay applies only to a sender the
Secret already carries, asserted by test rather than by reading. Ingress ANDs
namespace and pod label in one peer, following approval-engine rather than
user-engine's older breadth.
Gate House asked whether the record shape can carry a source-held-content
declaration with a retrieval expectation, and asked for a straight answer
rather than a rule the storage cannot meet. Both halves, which must travel
together:
It CAN carry the declaration. data is stored verbatim into details.data and
hash-chained, so content_exists and custody need no schema change and become
as tamper-evident as the commitment they accompany.
It CANNOT detect non-production. audit-core performs no retrieval and its
egress permits Postgres and DNS only. Detection happens at retrieval, by the
reviewer; the stored declaration is what turns a blank into a failure
attributable to the named custodian.
Residual stated rather than left to be found: a custodian that never held the
content can emit a false content_exists. audit-core validates the declaration's
shape, never its truth — the same class as omission at source, and not closed
by the chain, by attestation, or by T04/T06. A test asserts no egress to the
emitter exists, because that claim silently stops being true if one appears.
Cadence: reconciliation plus heartbeat is right for a mixed-volume source, with
both scoped per class rather than per source — a per-source heartbeat is
satisfied by the high-volume presentation stream and says nothing about a quiet
month of dispositions. Bound: a compromised emitter suppresses the event and
its own count together.
Also recorded: commitment-only satisfies non-alteration and never
reconstructability, in this repo's documents as in theirs; and tenant
provenance under GH-DEC-2026-013 lands in the registration record, not the
envelope, since audit-core checks a value the credential may write rather than
resolving an identity claim.
No secret was created and no production manifest applied.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
2026-09-10 15:15:35 +02:00
|
|
|
metadata:
|
|
|
|
|
name: audit-core-informed-decision-ingress
|
|
|
|
|
namespace: audit-core
|
|
|
|
|
spec:
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: audit-core
|
|
|
|
|
policyTypes: [Ingress]
|
|
|
|
|
ingress:
|
|
|
|
|
# AUDIT-WP-0009-T11 / AUDIT-IN-0003. Load-bearing presentation evidence
|
|
|
|
|
# under GH-DEC-2026-012 limit 3 and GH-DEC-2026-014. Both selectors belong
|
|
|
|
|
# to one peer and are therefore ANDed, following the approval-engine rule
|
|
|
|
|
# rather than user-engine's older namespace-only breadth.
|
|
|
|
|
#
|
|
|
|
|
# Note what this rule does NOT create: no egress from audit-core to
|
|
|
|
|
# informed-decision. The commitment-only record's custody declaration is
|
|
|
|
|
# carried, never dereferenced from here — audit-core makes no retrieval
|
|
|
|
|
# call, and the egress policy below is the proof.
|
|
|
|
|
- from:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
kubernetes.io/metadata.name: informed-decision
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: informed-decision
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: TCP, port: 8080}
|
|
|
|
|
---
|
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
|
|
|
kind: NetworkPolicy
|
Add deployment manifests, custody-class guard and request counters
AUDIT-WP-0005-T03 (progress). Manifests validated --dry-run=server
--validate=strict against railiance01; not applied, since deployment is gated
on RAPP-POSTGRES-WP-0002 and T02 credentials. Nothing here mutates the cluster.
Conventions read off the deployed user-engine workload rather than invented:
digest-pinned image from forgejo.coulomb.social, runAsNonRoot with
RuntimeDefault seccomp, no privilege escalation, all capabilities dropped,
readOnlyRootFilesystem, probes on a named http port, same resource envelope.
The namespace carries railiance.io/postgres-client: platform-pg, which is what
platform-pg-consumer-ingress in rapp-postgres admits; without that label the
pod cannot reach the database at all.
NetworkPolicies default-deny both directions, then permit ingress from the
user-engine namespace only, a separately labelled operator read path, and
egress to PostgreSQL in databases plus DNS.
Three decisions worth naming. Liveness is /healthz while readiness is /readyz,
so a database outage drops the pod from the Service rather than restarting it
in a loop. readOnlyRootFilesystem enforces the empty-filesystem property rather
than trusting it, so the SQLite fallback physically cannot accumulate audit
records on ephemeral storage. AUDIT_CORE_REQUIRE_CUSTODY_CLASS=archive makes a
missing database URL a startup failure instead of a silent downgrade to the
development store.
Counters deferred from WP-0004-T06 are exposed as JSON at /v1/stats behind the
read privilege, not as Prometheus exposition format: the cluster runs no
Prometheus, no ServiceMonitor CRD and no other scrape target, so an exposition
endpoint would target a scrape path that does not exist. Usable with curl now
and a small step from /metrics later.
Tests 77 -> 80.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:42:43 +02:00
|
|
|
metadata:
|
|
|
|
|
name: audit-core-operator-ingress
|
|
|
|
|
namespace: audit-core
|
|
|
|
|
spec:
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: audit-core
|
|
|
|
|
policyTypes: [Ingress]
|
|
|
|
|
ingress:
|
|
|
|
|
# Operator read path: lookup, dead letters, secret findings, stats.
|
|
|
|
|
# Namespace-scoped rather than open, and still gated on a credential
|
|
|
|
|
# carrying may_read — the network rule is the outer of two checks, not the
|
|
|
|
|
# only one.
|
|
|
|
|
- from:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
railiance.io/audit-core-reader: "true"
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: TCP, port: 8080}
|
|
|
|
|
---
|
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
|
|
|
kind: NetworkPolicy
|
AUDIT-WP-0009 T02/T10 — schedule attestation, and make the §5 check total
T02. deploy/attest-cronjob.yaml: daily at 03:17 UTC against the 168h window,
its own ServiceAccount, and a Role reaching exactly one named ConfigMap —
get/update/patch, no create, no list. audit_core/attest_publish.py does the
publish in stdlib; the image carries no kubectl, and adding one to an audit
receiver's image to write a single file is the worse trade.
Three refusals, all deliberate:
The producer is not the receiver. A receiver that could rewrite its own
attestation could forge it. audit-core-egress is now scoped to
component: receiver and a separate audit-core-attest-egress carries the 6443
rule, so the receiver never gains API-server reach. Asserted by test.
It refuses to publish over a broken chain. A fresh head written over a break
replaces an honest chain_break with a fresh-looking attestation. Stale
degrades the claim visibly; false does not.
Mounted as a directory, not subPath. Found while writing the manifest: a
subPath ConfigMap mount is resolved once at pod start and never updates, so
the daily attestation would land in the ConfigMap and never reach the running
receiver — tamper_evidence would age out to false while the job reported
success every night, silent in both directions.
The offsite copy stays an operator step. audit-core holds no Nextcloud
credential and should not acquire one to publish a hash, so docs/integrity.md
states the bound plainly: until that copy exists the delivered control defends
against a database owner, not a cluster owner, and no stronger claim may be
made from it.
T10. layer.yaml lists four infrastructure contacts — platform-pg, state-hub,
kube-apiserver, the container registry — each with its role and whether another
layer reads it. tooling_contacts stays [], which is true under §5 as written;
the companion's totality request is met by the uncatalogued list rather than by
inventing a Tooling row. tests/test_layer_conformance.py derives the egress
destinations from the manifests and the registry from the pinned digests, so a
new contact appearing in deploy/ without a row fails the test rather than
waiting for a reviewer to notice.
Applying the manifests remains an operator action; nothing here was applied.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
2026-09-10 16:39:20 +02:00
|
|
|
metadata:
|
|
|
|
|
name: audit-core-attest-egress
|
|
|
|
|
namespace: audit-core
|
|
|
|
|
spec:
|
|
|
|
|
# Scoped to the attestation job by component label, so the receiver itself
|
|
|
|
|
# gains nothing from this rule. The receiver must not be able to reach the
|
|
|
|
|
# API server: a compromised receiver that could rewrite the chain-head
|
|
|
|
|
# ConfigMap could forge its own attestation, which is the one thing the
|
|
|
|
|
# separation of these two workloads exists to prevent.
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: audit-core
|
|
|
|
|
app.kubernetes.io/component: attest
|
|
|
|
|
policyTypes: [Egress]
|
|
|
|
|
egress:
|
|
|
|
|
- to:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
kubernetes.io/metadata.name: databases
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: TCP, port: 5432}
|
|
|
|
|
- to:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
kubernetes.io/metadata.name: kube-system
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: UDP, port: 53}
|
|
|
|
|
- {protocol: TCP, port: 53}
|
|
|
|
|
# kube-apiserver. On this single-node k3s cluster the API server is the
|
|
|
|
|
# host itself, so this is a host-network destination rather than a pod
|
|
|
|
|
# selector; narrow it to the API port.
|
|
|
|
|
- ports:
|
|
|
|
|
- {protocol: TCP, port: 6443}
|
|
|
|
|
---
|
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
|
|
|
kind: NetworkPolicy
|
Add deployment manifests, custody-class guard and request counters
AUDIT-WP-0005-T03 (progress). Manifests validated --dry-run=server
--validate=strict against railiance01; not applied, since deployment is gated
on RAPP-POSTGRES-WP-0002 and T02 credentials. Nothing here mutates the cluster.
Conventions read off the deployed user-engine workload rather than invented:
digest-pinned image from forgejo.coulomb.social, runAsNonRoot with
RuntimeDefault seccomp, no privilege escalation, all capabilities dropped,
readOnlyRootFilesystem, probes on a named http port, same resource envelope.
The namespace carries railiance.io/postgres-client: platform-pg, which is what
platform-pg-consumer-ingress in rapp-postgres admits; without that label the
pod cannot reach the database at all.
NetworkPolicies default-deny both directions, then permit ingress from the
user-engine namespace only, a separately labelled operator read path, and
egress to PostgreSQL in databases plus DNS.
Three decisions worth naming. Liveness is /healthz while readiness is /readyz,
so a database outage drops the pod from the Service rather than restarting it
in a loop. readOnlyRootFilesystem enforces the empty-filesystem property rather
than trusting it, so the SQLite fallback physically cannot accumulate audit
records on ephemeral storage. AUDIT_CORE_REQUIRE_CUSTODY_CLASS=archive makes a
missing database URL a startup failure instead of a silent downgrade to the
development store.
Counters deferred from WP-0004-T06 are exposed as JSON at /v1/stats behind the
read privilege, not as Prometheus exposition format: the cluster runs no
Prometheus, no ServiceMonitor CRD and no other scrape target, so an exposition
endpoint would target a scrape path that does not exist. Usable with curl now
and a small step from /metrics later.
Tests 77 -> 80.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:42:43 +02:00
|
|
|
metadata:
|
|
|
|
|
name: audit-core-egress
|
|
|
|
|
namespace: audit-core
|
|
|
|
|
spec:
|
|
|
|
|
podSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
app.kubernetes.io/name: audit-core
|
AUDIT-WP-0009 T02/T10 — schedule attestation, and make the §5 check total
T02. deploy/attest-cronjob.yaml: daily at 03:17 UTC against the 168h window,
its own ServiceAccount, and a Role reaching exactly one named ConfigMap —
get/update/patch, no create, no list. audit_core/attest_publish.py does the
publish in stdlib; the image carries no kubectl, and adding one to an audit
receiver's image to write a single file is the worse trade.
Three refusals, all deliberate:
The producer is not the receiver. A receiver that could rewrite its own
attestation could forge it. audit-core-egress is now scoped to
component: receiver and a separate audit-core-attest-egress carries the 6443
rule, so the receiver never gains API-server reach. Asserted by test.
It refuses to publish over a broken chain. A fresh head written over a break
replaces an honest chain_break with a fresh-looking attestation. Stale
degrades the claim visibly; false does not.
Mounted as a directory, not subPath. Found while writing the manifest: a
subPath ConfigMap mount is resolved once at pod start and never updates, so
the daily attestation would land in the ConfigMap and never reach the running
receiver — tamper_evidence would age out to false while the job reported
success every night, silent in both directions.
The offsite copy stays an operator step. audit-core holds no Nextcloud
credential and should not acquire one to publish a hash, so docs/integrity.md
states the bound plainly: until that copy exists the delivered control defends
against a database owner, not a cluster owner, and no stronger claim may be
made from it.
T10. layer.yaml lists four infrastructure contacts — platform-pg, state-hub,
kube-apiserver, the container registry — each with its role and whether another
layer reads it. tooling_contacts stays [], which is true under §5 as written;
the companion's totality request is met by the uncatalogued list rather than by
inventing a Tooling row. tests/test_layer_conformance.py derives the egress
destinations from the manifests and the registry from the pinned digests, so a
new contact appearing in deploy/ without a row fails the test rather than
waiting for a reviewer to notice.
Applying the manifests remains an operator action; nothing here was applied.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
2026-09-10 16:39:20 +02:00
|
|
|
app.kubernetes.io/component: receiver
|
Add deployment manifests, custody-class guard and request counters
AUDIT-WP-0005-T03 (progress). Manifests validated --dry-run=server
--validate=strict against railiance01; not applied, since deployment is gated
on RAPP-POSTGRES-WP-0002 and T02 credentials. Nothing here mutates the cluster.
Conventions read off the deployed user-engine workload rather than invented:
digest-pinned image from forgejo.coulomb.social, runAsNonRoot with
RuntimeDefault seccomp, no privilege escalation, all capabilities dropped,
readOnlyRootFilesystem, probes on a named http port, same resource envelope.
The namespace carries railiance.io/postgres-client: platform-pg, which is what
platform-pg-consumer-ingress in rapp-postgres admits; without that label the
pod cannot reach the database at all.
NetworkPolicies default-deny both directions, then permit ingress from the
user-engine namespace only, a separately labelled operator read path, and
egress to PostgreSQL in databases plus DNS.
Three decisions worth naming. Liveness is /healthz while readiness is /readyz,
so a database outage drops the pod from the Service rather than restarting it
in a loop. readOnlyRootFilesystem enforces the empty-filesystem property rather
than trusting it, so the SQLite fallback physically cannot accumulate audit
records on ephemeral storage. AUDIT_CORE_REQUIRE_CUSTODY_CLASS=archive makes a
missing database URL a startup failure instead of a silent downgrade to the
development store.
Counters deferred from WP-0004-T06 are exposed as JSON at /v1/stats behind the
read privilege, not as Prometheus exposition format: the cluster runs no
Prometheus, no ServiceMonitor CRD and no other scrape target, so an exposition
endpoint would target a scrape path that does not exist. Usable with curl now
and a small step from /metrics later.
Tests 77 -> 80.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:42:43 +02:00
|
|
|
policyTypes: [Egress]
|
|
|
|
|
egress:
|
|
|
|
|
# PostgreSQL custody store. This is the only destination the receiver needs;
|
|
|
|
|
# it calls no other service.
|
|
|
|
|
- to:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
kubernetes.io/metadata.name: databases
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: TCP, port: 5432}
|
|
|
|
|
- to:
|
|
|
|
|
- namespaceSelector:
|
|
|
|
|
matchLabels:
|
|
|
|
|
kubernetes.io/metadata.name: kube-system
|
|
|
|
|
ports:
|
|
|
|
|
- {protocol: UDP, port: 53}
|
|
|
|
|
- {protocol: TCP, port: 53}
|