audit-core/deploy/networkpolicies.yaml
tegwick c4016a70d5
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
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

171 lines
5.4 KiB
YAML

# 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
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
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
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
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
metadata:
name: audit-core-egress
namespace: audit-core
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: audit-core
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}