Skip to content

IEC 62443 Compliance Traceability

Standard: IEC 62443 -- Security for Industrial Automation and Control Systems Applicability: All components of the Cloud-Native DCS platform Overall Status: Target capability vector SL-C(control system) = { 2 2 2 2 2 2 2 } (62443-3-3 Annex A.3.3 notation, ordered IAC UC SI DC RDF TRE RA), self-assessed clause-by-clause against the purchased ANSI/ISA-62443-3-3 (2013) text on 2026-07-18 (#968). The named conditionals are all closed (#971–#974, and #977, the decommission purge, closed by the Decommissioning and Data Purge runbook). Clause-verified SL 3 elements: non-repudiation (SR 2.12), protection of audit information (SR 3.9), programmatic audit-log access (SR 6.1 RE(1)), and cryptographic communication integrity on IP conduits (SR 3.1 RE(1), with the documented legacy-field-protocol exception), and session integrity incl. session-ID invalidation on termination (SR 3.8, #971). The hardening narrative adds image signing, OPC UA SignAndEncrypt with X.509 client-certificate authentication (#317), encryption-at-rest operator guidance, and Talos for the cluster control plane. The reference cluster completed its Talos cutover on 2026-05-12 and runs Talos v1.10.9 / Kubernetes v1.33.6 (#363, closed). The remaining SL-3 narrative item (intrusion detection) is documented as an operator responsibility. See Self-Assessed SL-C Vector.

Status Legend

Indicator Meaning
Implemented Requirement is fully implemented with tests and evidence
Partial Core functionality exists but gaps remain
Planned Architecture is designed but implementation has not started
Delegated Requirement is met by a documented platform/deployment capability external to the product (the 62443-4-2 CCSC 2 compensating-countermeasure shape). Every delegation is recorded in the CCSC 2 delegation register in the component assessment (#969)
N/A Requirement does not apply; the rationale is stated in the row

Each row also states the claimed capability level for that SR as (SL-C n). The Normative selection column is the standard's own per-clause "Security levels" mapping: which REs a level selects, cumulative (+RE(1) @ SL 3 means SL 3 selects the base plus RE(1)). — @ SL 1 means the SR is Not Selected at that level. Every one of the 51 SRs of ANSI/ISA-62443-3-3 has a row. A requirement we do not meet says so explicitly, and there are no silent omissions.

Self-Assessed SL-C Vector

IEC 62443-3-3 defines four security levels per foundational requirement (FR). A system's capability is the seven-element vector (Annex A.3.3), and no single scalar can carry it. Self-assessed:

SL-C(control system) = { 2 2 2 2 2 2 2 } (target), with these conditionals:

FR SL-C Binding items
FR 1 -- IAC 2 SR 1.11 (LDAP-path lockout with timed and administrator unlock) and SR 1.12 (use notification) closed by #974
FR 2 -- UC 2 SR 2.8 categories, SR 2.9 capacity behavior, SR 2.10 failure policy closed by #974; SR 2.5/2.6 by #971. SL 3 element: SR 2.12
FR 3 -- SI 2 (conditional) SR 3.6 Partial: the fail state is configurable per output (#972), and an output driven below the tag model through the raw device write path has no predetermined state the product provides (#1683); SR 3.3 Partial (procedure-based verification). SL 3 elements: SR 3.1 RE(1) on IP conduits, SR 3.8 (#971), SR 3.9
FR 4 -- DC 2 SR 4.2 decommission purge procedure closed by #977 (Decommissioning and Data Purge); at-rest confidentiality delegated (SR 4.1 RE(1), register D7)
FR 5 -- RDF 2 SR 5.1 RE(1) (physical segmentation) met by documented deployment delegation
FR 6 -- TRE 2 SR 6.2 breach detection above operational telemetry delegated to the operator IDS/SIEM layer. SL 3 element: SR 6.1 RE(1)
FR 7 -- RA 2 SR 7.3 closed by #973 (verify command + barman delegation + recorded drill)

A conditional 2 means SL 2 is the target and the named items are the only open selections at or below that level for the FR. Until they land, the honest claim for that FR is "SL 2 with the named exceptions". SL-C is a supplier capability claim: reaching it in a deployment additionally requires the operator-scope items in the SL-3 Claim -- Operator Checklist and the delegations in the CCSC 2 register to be configured as documented.

IEC 62443-3-3 -- System Security Requirements

FR 1 -- Identification and Authentication Control (IAC)

Self-assessed SL-C(IAC): 2. SR 1.11 (LDAP-path lockout) and SR 1.12 (use notification) closed by #974. Multifactor authentication selects at SL 3 (SR 1.1 RE(2)) and SL 4 (RE(3)) only, so its absence in-product is compatible with the SL 2 claim (verified against 5.3.4).

SR 1.1 and SR 1.12: the unauthenticated request bounced to the login page, the system-use notification shown before any credential, the PKCE hop to the plant's own identity provider, and the CLI device-code flow against the same provider.
Requirement Normative selection Disposition Implementation
SR 1.1 -- Human user identification and authentication base @ SL 1; +RE(1) @ SL 2; +RE(2) @ SL 3; +RE(3) @ SL 4 Implemented (SL-C 2) OIDC authentication on the gateway REST API and Web UI; Bearer-token JWTs verified against the configured identity provider; LDAP bind mode (pkg/auth/ldap.go) authenticates against the customer directory and issues gateway session JWTs (pkg/auth/session.go); per-user identity carried end-to-end into AuditRecord actor fields and ElectronicSignature. RE(1) (unique identification and authentication): identities are individual IdP/directory subjects; the product defines no shared-account mechanism. RE(2)/RE(3) (multifactor for untrusted networks / all networks) select at SL 3 / SL 4: not provided in-product; MFA applies when the customer IdP enforces it during the OIDC flow, which is a documented delegation and no part of an SL 3 claim
SR 1.2 -- Software process and device identification and authentication — @ SL 1; base @ SL 2; +RE(1) @ SL 3 Implemented (SL-C 2) Kubernetes ServiceAccounts authenticate operator pods; device nodes authenticate to the cluster via the deployment layer's join mechanism (e.g. Talos machine identity — ADR 0004); MQTT clients authenticated via per-role credentials (dcs-gateway, dcs-operator, dcs-runtime, dcs-historian); OPC UA sessions (Unit serviceBinding orchestrate mode and IOModule mirror mode) authenticate to remote servers via X.509 client certificates with Sign or SignAndEncrypt security policies (#317); inter-component HTTP requires mTLS client certificates. Corrected label: previously listed at "SL 1", where SR 1.2 is Not Selected — the base first selects at SL 2 (5.4.4). RE(1) (hardware security for credentials) @ SL 3 not claimed
SR 1.3 -- Account management base @ SL 1–2; +RE(1) @ SL 3 Delegated (SL-C 2) Account lifecycle (create/disable/remove) is deliberately delegated to the customer's external IdP (OIDC/LDAP) — the product stores no human accounts; delegation recorded in the CCSC 2 delegation register (D1). Role/permission definition is customer-controlled, change-managed configuration: role→permission mappings load from a declarative roles file (--authz-roles-file, Helm gateway.auth.roles), validated fail-fast at startup against the fixed seven-permission vocabulary (ADR 0005, extended by ADR 0010 with the least-privilege interlock:bypass capability); shipped five-role default applies when unconfigured. RE(1) (unified account management) @ SL 3: the external IdP is itself the unified account system. See Custom roles
SR 1.4 -- Identifier management base @ SL 1–4 Delegated (SL-C 2) Human identifiers are managed in the customer IdP/directory (OIDC sub, directory account names) — per user, group and role; machine identifiers by Kubernetes (ServiceAccount names), cert-manager certificate identities, and per-role MQTT client credentials. Delegation recorded in the delegation register (D1)
SR 1.5 -- Authenticator management base @ SL 1–2; +RE(1) @ SL 3 Partial (SL-C 2) Kubernetes manages ServiceAccount tokens; node join credentials are managed by the deployment layer (ADR 0004); OPC UA client certificates for the runtime serviceBinding path are operator-supplied through the credentials Secret's clientCert/clientKey keys (PEM-encoded RSA; the connector validates the cert/key pairing and the NotBefore/NotAfter window at every connect — see pkg/opcuaclient/keys.go). The gateway discovery/wizard surface additionally provisions a single gateway-owned client identity on first boot (self-signed RSA-2048, persisted to the dcs-opcua-client Secret, stable ApplicationURI urn:cndcs:gateway:opcua-discovery) used as the default channel-layer cert when a secured discovery connect supplies no explicit credentials (#919, internal/gateway/opcua_identity.go); its public cert is served for server-side trust-listing at GET /api/v1/discovery/opcua/client-identity, and rotation is manual (delete the Secret to re-provision). This is scoped to the transient discovery path — it does not auto-issue per-Unit runtime certs (cf. the #511 history entry below). Note (post-#348): the standalone trust-list inbox UX shipped in #301 was removed when the Asset CRD dissolved; server-side trust is currently established by pinning the server cert SHA-256 explicitly on each Unit.spec.serviceBinding.security.serverCertSha256Pin. A reworked bulk-approval inbox keyed on Unit is on the rebuild backlog (migration guide) and is the path back to the prior SL-2 evidence shape. See § Compliance claim history
SR 1.6 -- Wireless access management base @ SL 1; +RE(1) @ SL 2–4 N/A The product ships no wireless interface: all product communication is over the cluster network and wired field buses. Wireless segments in a deployment (e.g. plant Wi-Fi reaching the ingress) are asset-owner network architecture outside the system-under-consideration boundary (62443-3-3 0.2 scoping)
SR 1.7 -- Strength of password-based authentication base @ SL 1–2; +RE(1) @ SL 3; +RE(1)(2) @ SL 4 Delegated (SL-C 2) Corrected rationale — the previous row asserted "no password-based authentication implemented", which is false: LDAP bind mode (pkg/auth/ldap.go) is password-based, and OIDC logins may be password-backed at the IdP. The product never stores or verifies password material itself — the bind is performed by the customer directory — so password-strength policy (minimum length, character variety) is enforced by the directory/IdP password policy. This is the conditional-plus-delegation posture: where password authentication is used, strength enforcement is a documented IdP/directory delegation (delegation register, D2). RE(1) lifetime restrictions @ SL 3 are likewise directory policy
SR 1.8 -- Public key infrastructure (PKI) certificates — @ SL 1; base @ SL 2–4 Implemented (SL-C 2) Where PKI is utilized, cert-manager operates the in-cluster CA per commonly accepted practice: short-lived certificates, automated renewal, narrow trust domains (inter-component mTLS, MQTT TLS). Deployments may substitute an organizational PKI via cert-manager issuer configuration. OPC UA server trust is established by explicit per-Unit certificate pinning (see SR 1.5)
SR 1.9 -- Strength of public key authentication — @ SL 1; base @ SL 2; +RE(1) @ SL 3 Partial (SL-C 2) OPC UA path: 2048-bit RSA client certificates with SHA-256 signatures meet OPC UA Basic256Sha256 security profile; cert/key pairing and NotBefore/NotAfter validity window enforced at every connect (pkg/opcuaclient/keys.go); server-cert trust pinned via Unit.spec.serviceBinding.security.serverCertSha256Pin (see SR 1.5). mTLS certificates validate signature and chain to the cert-manager CA. Revocation checking is not implemented — compensated by short-lived cert-manager certificates (rotation covers what revocation would), hence Partial. Corrected label: previously "SL 3"; the SL 3 selection is RE(1) (hardware security for private keys), which is not claimed (5.11.4)
SR 1.10 -- Authenticator feedback base @ SL 1–4 Implemented (SL-C 2) The login form obscures password entry (internal/gateway/static/login.html, type="password"); dcs login reads the password without echo via term.ReadPassword (cmd/dcs/internal/cmd/login_term.go); in OIDC mode credential feedback is the IdP's login surface
SR 1.11 -- Unsuccessful login attempts base @ SL 1–4 Implemented (SL-C 2) LDAP-path login lockout (#974): after a configurable number of consecutive credential failures within a window (gateway.auth.lockout, default 5 in 15 m) the account locks for the configured duration, with earlier administrator unlock via dcs auth unlock (admin, audited; DELETE /api/v1/auth/lockouts/{username}) and lockout state readable with dcs auth lockouts (GET /api/v1/auth/lockouts) — the login response itself stays a generic 401 so lockout state leaks nothing to a caller. Only credential failures consume lockout slots — a directory outage does not lock the plant's accounts (#993). Clause 4.2 essential-function carve-out: lockout.exemptUsers accounts are never locked out, compensated by physical and network access control. Failed logins are audited (internal/gateway/auth_login.go, 21 CFR Part 11 §11.10(e)) and the gateway additionally rate-limits per IP. OIDC mode delegates attempt limiting to the IdP (documented)
SR 1.12 -- System use notification base @ SL 1–4 Implemented (SL-C 2) Configurable pre-authentication system-use notification (#974, gateway.auth.systemUseNotification): the login page renders the configured notice before any credential is entered (login-init.js), and dcs login prints the same notice before prompting. An empty value shows none. On camera in the FR 1 figure above
SR 1.13 -- Access via untrusted networks base @ SL 1; +RE(1) @ SL 2–4 Delegated (SL-C 2) The product does not itself terminate untrusted-network access: the gateway is exposed through the deployment's TLS ingress, and remote-access architecture (VPN, zero-trust proxy, DMZ placement per SR 5.2 guidance) is deployment scope. RE(1) (explicit access request approval) is delegated to the deployment's access-management/IdP workflow. Recorded in the delegation register (D11)
SR 1.11: five consecutive credential failures lock the account while every response stays the same generic 401, so lockout state leaks nothing to a caller. dcs auth lockouts and the audit trail carry the truth, and the administrator's audited unlock clears it early.

FR 2 -- Use Control (UC)

Self-assessed SL-C(UC): 2. SR 2.8/2.9/2.10 closed by #974, SR 2.5/2.6 by #971. Genuine SL 3 element: SR 2.12 (non-repudiation).

SR 2.1: authorization enforced as behavior. Four directory personas work the same screens. Each is stopped at the next rung, one by an affordance the product never renders and the others by a server-side refusal naming the action they may not perform.
Requirement Normative selection Disposition Implementation
SR 2.1 -- Authorization enforcement base @ SL 1; +RE(1)(2) @ SL 2; +RE(3) @ SL 3; +RE(4) @ SL 4 Implemented (SL-C 2) Humans: permission-gated gateway routes resolved from the declarative role table; per-binary operator ClusterRoles with least-privilege verbs; AuditRecords restricted to create-only; human Kubernetes access limited to the opt-in read-only dcs:k8s:viewer / dcs:k8s:platform roles, with writes gated by GitOps admission policies (see Security Hardening § Personas). RE(1) (authorization enforcement for all users, incl. software processes and devices): Kubernetes RBAC per binary, MQTT topic ACLs bound to per-role credentials, unit-runtime HTTP mTLS + fail-closed bearer auth. The shipped table also carries a read-only role (dcs-viewer, read alone, #1551), so an identity that only observes — a SIEM collector polling the audit API, a reporting job, a SCADA frontend commissioned before it is trusted with writes — is granted least privilege by group assignment and needs no roles file of its own; the read tier is confined to reads for that grant to hold, which is why OPC UA discovery (an outbound session to a caller-named endpoint) is engineer-tiered. RE(2) (an authorized user can define/modify permission-to-role mapping): the declarative roles file (--authz-roles-file, ADR 0005/0010) is exactly this capability, exercised as change-managed deployment configuration. ADR 0024 extends it with per-role action-level allow/deny refinements over the product action catalog (exact names, wildcard-free, fail-fast validation), and an audit-facing resolution surface (GET /api/v1/auth/policy, dcs auth policy) so least-privilege evidence is the shipped tiers plus the reviewed delta document, verified against the running system. Enforcement is evidenced as well as configured: a refused mutating action leaves a Rejected AuditRecord naming the action and the deciding layer, and an MES integration identity caught outside /api/v1/mes/ leaves one under its own zone-violation category — the application-layer counterpart to the SR 5.x network zoning rows, since that fence is what keeps an integration credential off plant endpoints (#1296). The live transports (/api/v1/ws, /api/v1/sites/{site}/events/sse) are exempt from the middleware chain because they authenticate from a query parameter, so they re-apply it in the handler: the zone fence at connect, the caller's site scope on every message, and a per-channel action check on each subscription (#1440). Until then those routes enforced authentication and nothing else, and a subscription filter is a client-supplied convenience and no authorization boundary. Corrected label: previously "SL 1" — with RE(1)+RE(2) evidenced, the row meets the SL 2 selection (6.3.4). RE(3) (supervisor override) not implemented. Note: change-control approvals with fail-closed segregation-of-duties (#769) provide an RE(4)-shaped dual-approval capability for procedure/recipe changes; not claimed as SL 4
SR 2.2 -- Wireless use control base @ SL 1–2; +RE(1) @ SL 3 N/A No wireless protocols; all communication over Kubernetes network and wired field buses (see SR 1.6)
SR 2.3 -- Use control for portable and mobile devices base @ SL 1–2; +RE(1) @ SL 3 N/A The product exposes no portable/mobile-device interface: distroless pods have no removable-media path and the product defines no device-attachment workflow. Engineering workstations and their removable-media hygiene are asset-owner scope (62443-3-3 0.2)
SR 2.4 -- Mobile code base @ SL 1–2; +RE(1) @ SL 3 Implemented (SL-C 2) The control plane executes no mobile code: all control logic is compiled into static Go binaries, no dynamic loading, distroless images. The web UI serves exclusively first-party, same-origin static JavaScript — no CDN/remote includes, no eval/new Function/dynamic import() (verified across internal/gateway/static/) — delivered over TLS from cosign-signed images
SR 2.5 -- Session lock base @ SL 1–4 Implemented (SL-C 2) Configurable browser inactivity lock plus a manual Lock control on every app header (default 15 min, gateway.auth.uiIdleLockSeconds); the lock revokes the session ID server-side and re-authentication resumes at the saved location (#971; internal/gateway/static/js/idle-lock.js, js/auth.js). Clause 4.2 essential-function carve-out documented: control execution never depends on a browser session, and dedicated operator stations set uiIdleLockSeconds: 0 with physical access control (Security Hardening § Session Lifecycle)
SR 2.6 -- Remote session termination — @ SL 1; base @ SL 2–4 Implemented (SL-C 2) Server-side session registry (#971, pkg/auth/registry.go): sessions terminate automatically after a configurable inactivity window (gateway.auth.sessionInactivityTimeout, default 30 min), manually by the initiating user (POST /api/v1/auth/logout — UI, HMI and dcs logout), and manually by a local authority (DELETE /api/v1/auth/sessions/{id}, admin, audited). Enforced on the REST middleware and the WS/SSE/terminal connect paths. HA note: registry is per-replica (like the nonce store); shared backing store tracked
SR 2.7 -- Concurrent session control — @ SL 1–2; base @ SL 3–4 Planned (not selected at SL 2) No concurrent-session limit exists (stateless JWTs are unlimited within validity). First selects at SL 3 (6.9.4); recorded for completeness — becomes in-scope work only for an SL 3 target
SR 2.8 -- Auditable events base @ SL 1–2; +RE(1) @ SL 3 Implemented (SL-C 2) AuditRecord carries the record fields SR 2.8 names: server-generated timestamp, source/actor (component, controller, user ID, session ID, source IP — api/audit/v1alpha1/auditrecord_types.go), category (dcs.io/audit-category label), type (16 action values), and result (Success/Failure/Rejected). Category coverage completed by #974 — access control (login/lockout/unlock, access-control), request errors (Failure/Rejected results), control-system events, backup-and-restore events (backup-restore on bulk exports; restores as per-resource applies), configuration changes, audit-log events (audit-log on archive-integrity runs) — mapping table in Security Operations § Audit Event Categories. Authorization refusals by the gateway's route gate are auditable events too, with a bounded scope stated on the record (#1296): a refused mutating action writes a Rejected record (authorization-denied) carrying the action name and the ADR 0024 deciding layer (deny vs no-grant), while a refused read-shaped action stays in the gateway log, because recording those would mint a record per poll tick per user for as long as a role stayed misconfigured. Read-shaped means any hyphen-separated segment of the action's verb is read/list/browse/poll — 130 of 286 routes, several polled by the UI, including controlmodule:read-tag, which the HMI dashboard fetches once per equipment card on a three-second poll and which an enumerated whole-verb rule misclassified as a mutation. The split is derived from the verb's segments in one place (pkg/auth.Action.IsReadShaped), matches each segment exactly, and fails closed, so an unclassified verb is recorded. Every such record precedes the 403 that answers the caller (#1251). Scope stated in Security Operations § Authorization Denials. Operating-system events and reconnaissance detection are platform/IDS scope (delegations D12/D9). RE(1) (centrally managed, system-wide trail) @ SL 3: the two-tier etcd + archived-PostgreSQL trail is centrally queryable through the gateway
SR 2.9 -- Audit storage capacity base @ SL 1–2; +RE(1) @ SL 3 Implemented (SL-C 2) Two-tier retention: AuditRecords kept in etcd for an active window (default 90 days) and archived to historian PostgreSQL/TimescaleDB (default 3-year retention) by the audit-archiver CronJob; Gateway queries both active and archived transparently. Capacity behavior defined (#974, Security Operations § Audit Storage Capacity): bounded rolling windows prevent exhaustion, components stay up at capacity (fail-open policy), DCSAuditProcessingFailure alerts on write failures, dcs_audit_records_created_total exposes creation rate. RE(1) (warn at capacity threshold) @ SL 3: storage-threshold alerting is deployment monitoring configuration
SR 2.10 -- Response to audit processing failures base @ SL 1–4 Implemented (SL-C 2) Defined per action class (#974, Security Operations § Audit Processing Failures): essential/control actions fail open — bounded retry, then the action proceeds with the failure logged and counted (dcs_audit_record_failures_total) so audit can never take down essential functions (Clause 4.2); 21 CFR Part 11 change-control transitions fail closed via RecordResult (#642 — the record is the compliance artifact). Personnel alerting via the shipped DCSAuditProcessingFailure PrometheusRule (critical, any failure in 10 min)
SR 2.11 -- Timestamps — @ SL 1; base @ SL 2; +RE(1) @ SL 3; +RE(1)(2) @ SL 4 Implemented (SL-C 2) Audit records carry server-generated timestamps (pkg/audit/builder.go); historian samples preserve source timestamps with server-clock fallback (pkg/historian/ingester.go), stored UTC. Deployment time-synchronization requirements are documented (stratum ≤ 2 NTP, < 50 ms offset — docs/network-requirements.md § Time Synchronisation). RE(1) (internal time synchronization) @ SL 3 is node/platform scope — delegated, documented in the delegation register (D5)
SR 2.12 -- Non-repudiation — @ SL 1–2; base @ SL 3; +RE(1) @ SL 4 Implemented (SL-C 3) A genuine SL 3 capability (6.14.4 selects the base at SL 3): 21 CFR Part 11 electronic signatures bind signer identity to the signed record with HMAC-SHA256 record integrity; segregation-of-duties enforcement fails closed (#769); AuditRecords are immutable (SR 3.9). Together these establish non-repudiation for signature-controlled actions. RE(1) (non-repudiation for all users) @ SL 4 not claimed. Clause 4.2 note: signature ceremonies gate change-management actions alone, so they add no delay to essential-function response

Three of the FR 2 rows are on camera.

SR 2.5, SR 2.6, and SR 3.8 RE(1): the manual lock demanding re-authentication and resuming at the saved view, the idle window locking the session with no input on camera, and logout leaving a session the browser's Back press cannot resurrect. The administrator-termination prong of SR 2.6 (DELETE /api/v1/auth/sessions/{id}) is not in this take.
SR 2.1 RE(2): the running gateway prints back its own role-to-action policy, the shipped tiers plus this plant's deny refinements, and a signed-in non-admin account resolves exactly the actions it is entitled to (ADR 0024).
SR 2.8 and SR 2.12: an operator action landing in the trail attributed to the named actor with the typed reason, and a QA approval carrying its signer, signature meaning, and HMAC digest. The closing beat is the SR 3.9 / SR 3.3 evidence: Archive Integrity reading Clean, re-proved on demand with dcs audit verify --archived.

FR 3 -- System Integrity (SI)

Self-assessed SL-C(SI): 2, conditional. #972 made the fail state configurable per output, which closed the delta SR 3.6 surfaced for program-driven outputs. SR 3.6 is Partial nonetheless. An output driven through the raw device write path has no predetermined state the product provides, and #1683 is the open work. SR 3.3 is Partial as well (procedure-based verification). SL 3 elements: SR 3.1 RE(1) on IP conduits (with the documented legacy-field-protocol exception), SR 3.8 (restored by #971) and SR 3.9.

Requirement Normative selection Disposition Implementation
SR 3.1 -- Communication integrity base @ SL 1–2; +RE(1) @ SL 3 Implemented (SL-C 2; RE(1) on IP conduits) All inter-component HTTP uses mTLS (cert-manager CA, short-lived certs, client cert verification); MQTT supports TLS; metrics endpoint supports TLS; OPC UA sessions default to Sign and may be configured to SignAndEncrypt per Unit serviceBinding (or per IOModule), signing every message with the client cert's private key against the server's public key (#317) — defends against tampering on the OT segment between the runtime and the OPC UA server. RE(1) (cryptographic integrity protection) @ SL 3 is therefore met on every IP conduit — but not on the legacy plaintext field protocols: Modbus TCP and EtherNet/IP are plaintext by design, with compensating controls (dedicated field-bus zone, networkPolicies.fieldBusCIDRs, physical conduit security) documented in the threat model. The SL 3 enhancement is claimed only with that conduit exception on the record
SR 3.2 -- Malicious code protection base @ SL 1; +RE(1) @ SL 2; +RE(1)(2) @ SL 3 Delegated (SL-C 2) Prevention and mitigation are architectural: immutable distroless images (no shell, no package manager), read-only root filesystem, static Go binaries, no dynamic code loading, cosign-signed images. In-product detection/reporting of malicious code does not exist, and RE(1) (protection at entry and exit points) @ SL 2 is met by documented platform delegation: admission-time image-signature verification (Kyverno/OPA, deployment-configured) at the cluster's software entry point, plus operator endpoint tooling per the Intrusion Detection Narrative. Register entry D6 in the delegation register
SR 3.3 -- Security functionality verification base @ SL 1–2; +RE(1) @ SL 3; +RE(1)(2) @ SL 4 Partial Verification support exists as operator-run procedures — the FAT/SAT/scheduled-maintenance shape the base requirement describes: dcs qualify oq / dcs qualify dr qualification drivers (--fail-on-deviation exits non-zero on failed checks), audit signature/archive verification endpoints (/api/v1/audit/verify, /api/v1/audit/verify-archived, /api/v1/change-control/verify), and the documented verification procedures in this document (etcd encryption, PVC encryption) plus the annual cert-expiry and archive-corruption drills. Gap: no consolidated procedure verifies the authentication/authorization/TLS enforcement functions themselves, hence Partial. RE(1) (automated mechanisms) @ SL 3 not present
SR 3.4 -- Software and information integrity — @ SL 1; base @ SL 2; +RE(1) @ SL 3 Implemented (SL-C 2) CI verifies generated code matches source; SHA256 checksums for release artifacts; runtime content integrity via spec hash comparison on each ControlProgram reconcile; phantom network GC removes orphaned control logic after pod restart; audit trail for all ControlProgram deploy/replace/delete operations. Corrected label: previously "SL 1", where SR 3.4 is Not Selected — the base first selects at SL 2 (7.6.4). RE(1) (automated notification of integrity violations) @ SL 3: webhook rejections and Result=Failure audit records notify on specific violation classes; not claimed as the full enhancement
SR 3.5 -- Input validation base @ SL 1–4 Implemented (SL-C 2) AuditRecord webhook validates required fields and enforces immutability; CRD schema validation via OpenAPI; Gateway REST API validates all state-changing endpoints via go-playground/validator with typed request DTOs, custom Kubernetes name and ISA-88 command/mode validators, unknown-field rejection (DisallowUnknownFields), and structured 400 error responses with per-field details
SR 3.6 -- Deterministic output base @ SL 1–4 Partial Predetermined-state capability with user-configurable fail state (#972): every output block carries a failState parameter — safeValue (default; drives the configured safeValue, shipped defaults de-energized-equivalent) or holdLast (deliberately holds the last commanded value) — driven exactly once when its program halts (ADR 0009 + amendment, pkg/fbruntime WriteSafeOutputs; AO/DO blocks; invalid values rejected at Init). The edge additionally runs an armed local-hold program after control-plane heartbeat loss (ADR 0008, --hold-grace-seconds) and driver loss during an active batch triggers watchdog Hold. A graceful stop of the whole runtime is covered as a fourth halt path (#1283): a stop declared terminal drives every output through the same predetermined-state write before the drivers disconnect, and the runtime queues a safe-stop record the operator materializes into an AuditRecord, so the write-out is evidenced in the trail. An undeclared stop is a redeploy and preserves output continuity by decision, which is the same reasoning the hot-swap path uses. Truly unpowered states are the I/O hardware's own comm-loss fail-safe, which the IOModule now declares through spec.failSafe (ADR 0068, #1683): hold or clear with a timeout and a recovery posture, applied to the device by the single io-probe that serves it, read back into status.failSafe and reported through the FailSafeDeclared and FailSafeApplied conditions. What a device can express is per model. A driver that cannot apply a declaration refuses it, and the refusal reaches the condition. The timeout carries a floor of three io-probe cadences, 45 s, refused at admission by a CEL rule, and a FailSafeTimeoutMargin condition reports the measured gaps each device actually sees between telegrams (ADR 0071, #1701): any traffic feeds the watchdog, so a deadline under our own read cadence fires on a routine gap and drives the node to its predetermined state when nothing has been lost. Relationship to #962: standalone — outputs behave per this configuration during any failover gap. Gap — outputs driven below the tag model. The io-probe does write outputs: POST /api/v1/sites/{site}/iomodules/{name}/write forwards every raw channel write for a non-simulation IOModule to POST /api/v1/write on the prober, which reaches the device driver (internal/gateway/tags.go, internal/ioprobe/http.go). A raw channel sits below the tag model, so a channel no ControlModule declares has no block behind it and no failState declared for it. Neither WriteSafeOutputs nor the ADR 0008 armed hold reaches it either, the second because make lint-edge-armable confines an armed chart to the unit's own control-module tag space. Nothing shipped returns such an output to a predetermined state on any loss. What remains is the device's own comm-loss watchdog. The product can declare and apply that since #1683, so the gap is now narrower and differently shaped. A declared spec.failSafe reaches the whole node, including channels no ControlModule declares, because the watchdog acts on every output the coupler has. Two limits keep this Partial. The action is node-wide on the reference hardware, so it cannot be chosen per channel the way PROFINET substitute values or CIP Fault Actions can. And an IOModule that declares nothing still inherits its coupler's default, which on the reference WAGO 750-352 read never-armed at the bench baseline and means hold-last-value. That default is deliberately not overwritten, because doing so would change a running plant's outputs on upgrade. The FailSafeDeclared condition reports it, so the state is never silent. The raw-write guard (#1580, ADR 0060) narrows the exposure by refusing a raw write while a Unit executing through the module is running. It does not close it. An undeclared channel never acquires a block, and a channel whose program has been removed is re-driven by a raw write with nothing left to halt. The honest scope of this row is therefore program-driven outputs, and #1683 is the path back to Implemented (#1691).
SR 3.7 -- Error handling — @ SL 1; base @ SL 2–4 Implemented (SL-C 2) gRPC panic recovery interceptor; controllers use structured logging; health probes detect failures; API error responses return structured, field-level validation detail without internal state. Corrected label: previously "SL 1", where SR 3.7 is Not Selected — it first selects at SL 2 (7.9.4)
SR 3.8 -- Session integrity — @ SL 1; base @ SL 2; +RE(1)(2) @ SL 3; +RE(1)(2)(3) @ SL 4 Implemented (SL-C 3) Base: TLS integrity on every session, plus anti-replay middleware requiring a unique X-DCS-Nonce header on all mutating (POST/PUT/DELETE) API requests; in-memory nonce store with configurable TTL window (default 10 min) and bounded capacity; CLI and browser UI generate nonces automatically; duplicate nonces rejected with HTTP 409. RE(1) (invalidation of session IDs after termination) @ SL 3: met by the server-side session registry (#971) — logout, inactivity, and administrator termination all invalidate the session ID before token expiry, enforced on REST and on the live-stream transports. On the streams, invalidation reaches a connection that is already open (#1445): the gateway indexes every WebSocket, SSE and terminal connection by session ID and closes the matching ones when the session is revoked, with a distinguishable reason (WebSocket close code 4001, a session-ended event on SSE) so a client stops reconnecting cleanly, with no 401 loop. Each connection additionally re-checks the identity behind it on its 50-second keepalive tick, which is what ends a stream whose bearer token has passed its own exp or whose session has crossed the inactivity window with no request to notice it; the check deliberately does not record activity, so an open stream never keeps an idle session alive. Before #1445 the check ran only at connect, which is accurate for a new connection and silent about the one an auditor asks about (see Compliance claim history). RE(2) (unique system-generated session IDs, unexpected IDs invalid) @ SL 3: every session carries a unique UUID jti; tokens are HMAC-signed so a fabricated session ID fails verification, and revoked IDs are rejected. RE(3) (commonly accepted randomness) @ SL 4: the UUID source is crypto/rand — noted, SL 4 not claimed. This row returned to SL-C 3 when #971 landed (it was downgraded from the unsupported \"SL 3\" claim in the 2026-07-18 audit; see Compliance claim history). HA note: nonce store and session registry are per-replica; shared backing store tracked for multi-replica deployments
SR 3.9 -- Protection of audit information — @ SL 1; base @ SL 2–3; +RE(1) @ SL 4 Implemented (SL-C 3) Audit information is protected from unauthorized access, modification and deletion: create-only admission webhook (update/delete rejected regardless of RBAC), RBAC restricting audit reads, HMAC-SHA256 record signatures on e-signed entries, and the optional S3 Object-Lock Compliance-mode WORM mirror for archive bundles (pkg/audit/archive/mirror, ≥ 365-day retention floor). The base selects unchanged at SL 3 (7.11.4), so this is a clause-verified SL 3 capability. RE(1) (hardware-enforced write-once media) @ SL 4: Object Lock is provider-enforced retention, an element in that direction. The enhancement itself is not claimed. Archive-integrity verification is on camera in the FR 2 figure above

Two of the FR 3 rows are on camera.

SR 3.3: the operator-run qualification drivers executed against the live cluster, each report landing in the audit trail as a signed record. The row stays Partial: no consolidated procedure verifies the authentication, authorization, and TLS enforcement functions themselves.
SR 3.4: a running control program edited out from under the declared model and driven back to the declared value on the next reconcile, leaving a DriftCorrected audit record. The enforcement layer on camera is the ControlModule compiler's spec-hash comparison.

FR 4 -- Data Confidentiality (DC)

Self-assessed SL-C(DC): 2. The SR 4.2 decommission purge procedure (#977) is documented in Decommissioning and Data Purge. At-rest confidentiality is an SL 2 obligation met by documented platform delegation (SR 4.1 RE(1) selects at SL 2, corrected from the former SL 3 positioning).

Requirement Normative selection Disposition Implementation
SR 4.1 -- Information confidentiality base @ SL 1; +RE(1) @ SL 2–3; +RE(1)(2) @ SL 4 Implemented (SL-C 2; at-rest delegated) In transit: mTLS encrypts all inter-component HTTP traffic; MQTT broker uses TLS (port 8883) by default in production profile; cert-manager issues certificates for both mTLS and MQTT; OPC UA sessions configured with SignAndEncrypt (Basic256Sha256 profile) encrypt the field-side payload end to end between the unit runtime and the remote server (#317), preventing recipe parameters and process values from being read by network observers on the OT segment. RE(1) (protection of confidentiality at rest or in transit via untrusted networks) selects at SL 2 (8.3.4): the at-rest leg is met by documented platform delegation — etcd EncryptionConfiguration, encrypted StorageClasses, node-OS disk encryption per Encryption at Restre-filed from the former SL 3 positioning and recorded in the delegation register (D7)
SR 4.2 -- Information persistence — @ SL 1; base @ SL 2; +RE(1) @ SL 3 Implemented (SL-C 2; erasure execution delegated) Decommissioning and Data Purge (#977) documents the purge procedure for every data-bearing tier when a component is released from service: edge hostPath (full node wipe / LUKS keyslot destruction / NIST SP 800-88 sanitization), historian & MQTT PVCs (crypto-erase via volume deletion on encrypted StorageClasses + KMS key destruction), etcd-resident CRs/Secrets (site cascade delete + compaction, hardware purge at node release), and archive buckets (retention-respecting — Object-Lock Compliance mode forbids early deletion by design). Retention-first ordering protects 21 CFR Part 11 records. Platform-executed erasure (volume-key destruction, node wipe) recorded in the delegation register (D14). RE(1) (purge shared memory resources) @ SL 3 not claimed
SR 4.3 -- Use of cryptography base @ SL 1–4 Implemented (SL-C 2, operator-verified at rest) OPC UA sessions use FIPS-aligned Basic256Sha256 (RSA-2048 + SHA-256 + AES-256 GCM/CBC); MQTT and inter-component HTTP use TLS 1.2+ with cert-manager-issued certs; e-signature record-integrity uses HMAC-SHA256. Encryption-at-rest depends on platform configuration — see Encryption at Rest for the per-tier guidance and operator checklist. Corrected label: previously "SL 3" — SR 4.3 selects the base at every level (8.5.4); there is no SL 3 enhancement to claim

FR 5 -- Restricted Data Flow (RDF)

Self-assessed SL-C(RDF): 2. SR 5.1's SL 2 enhancement (physical segmentation) is met by documented deployment delegation. SL 3 is not claimed: island mode and fail close (SR 5.2 RE(2)/RE(3)) do not exist (corrected from the former "SL 3 Implemented").

Requirement Normative selection Disposition Implementation
SR 5.1 -- Network segmentation base @ SL 1; +RE(1) @ SL 2; +RE(1)(2) @ SL 3 Implemented (SL-C 2; RE(1) delegated) Logical segmentation implemented: Helm-managed NetworkPolicies enforce default-deny ingress with per-component allow rules; runtime egress restricted to field bus + MQTT + DNS. RE(1) at SL 2 requires physical network segmentation capability (9.3.4) — for a Kubernetes-native system this is deployment architecture: dedicated OT NICs/VLANs, networkPolicies.fieldBusCIDRs, hostNetwork unit runtimes bound to dedicated field interfaces. Documented deployment delegation (delegation register, D8); newly surfaced by the 2026-07-18 clause audit
SR 5.2 -- Zone boundary protection base @ SL 1; +RE(1) @ SL 2; +RE(1)(2)(3) @ SL 3 Implemented (SL-C 2) — downgraded from "SL 3" IEC 62443 zone/conduit model: Control Zone (operators, gateway, MQTT), Field Zone (site namespaces with runtimes), External Zone (ingress), Field Bus Zone (device CIDRs); cross-zone traffic restricted by namespace selectors and pod selectors. RE(1) (deny by default, allow by exception) @ SL 2: implemented — the NetworkPolicy model is default-deny with explicit conduit allows. The prior "SL 3 Implemented" claim overstated: SL 3 additionally selects RE(2) (island mode — commandable prevention of all boundary communication) and RE(3) (fail close on boundary-protection failure), neither of which exists. Partition autonomy (ADR 0006) keeps the edge operating through a control-plane partition, but it is an availability behavior with no commandable boundary isolation in it; boundary enforcement delegates to Kubernetes NetworkPolicy, which has no fail-close semantics of its own. One further conduit is declared and absent by default: the power provider (ADR 0034 section 4, #1237) reaches a host-layer management channel from the physical operator so a chassis can be rebooted from the product, and it exists only where powerProvider.kind is set. See 4-2 CR 5.1 for the component-level statement, including the operator policies' ingress-only scope
SR 5.3 -- General purpose person-to-person communication restrictions base @ SL 1–2; +RE(1) @ SL 3 N/A The product contains no general-purpose person-to-person messaging surface — no email or chat reception exists to restrict. ISA-88 operator PROMPT/MESSAGE interactions are control-recipe I/O with defined semantics
SR 5.4 -- Application partitioning base @ SL 1–4 Implemented (SL-C 2) Criticality-based partitioning: per-site namespaces (site-{name}), Control vs Field vs External vs Field Bus zones, historian and MQTT as separate services with separate data stores, audit archive separated from live records, per-binary operators with scoped RBAC. Partitioning extends to the host layer: device nodes carry the dcs.io/role=device-node:NoSchedule taint, tolerated only by unit-runtime/io-probe, so L3 operations services (historian, MQTT, gateway) cannot share controller compute (ADR 0031). The taint is applied by the deployment, so evidence for this row is the Node object, because a declarative node-taint field can be silently refused and the taint is lost on node re-enrolment (ADR 0031 Consequences)

FR 6 -- Timely Response to Events (TRE)

Self-assessed SL-C(TRE): 2. SR 6.2's breach-detection expectation above operational telemetry is a documented operator delegation. SL 3 element: SR 6.1 RE(1) (programmatic audit access) is met today.

Requirement Normative selection Disposition Implementation
SR 6.1 -- Audit log accessibility base @ SL 1–2; +RE(1) @ SL 3 Implemented (SL-C 3) Read-only audit access for authorized humans and tools: gateway REST query endpoints and CLI dcs audit trace/export/verify — the CLI surface is read-only (its only write is a local export file). RE(1) (programmatic access to audit records via an API) @ SL 3 is precisely the gateway REST API, met today (10.3.4) — previously under-claimed at "SL 1"
SR 6.2 -- Continuous monitoring — @ SL 1; base @ SL 2–4 Partial (SL-C 2; detection delegated) Prometheus metrics exported by all operators (dcs_audit_records_created_total, controller-runtime metrics); per-CM FB-network health (dcs_cm_fb_network_running) and raw-fallback publish counts (dcs_cm_raw_fallback_publishes_total) expose ISA-88 continuous-observability state; Kubernetes audit logging and authentication-failure telemetry are operator-configured. Application does not ship an IDS — see Intrusion Detection Narrative for the defense-in-depth posture and what an operator adds for SL-3+ detection; delegation recorded in the delegation register (D9)

FR 7 -- Resource Availability (RA)

Self-assessed SL-C(RA): 2. SR 7.3 closed by #973 (pre-restore verification + recorded drill).

Requirement Normative selection Disposition Implementation
SR 7.1 -- Denial of service protection base @ SL 1; +RE(1) @ SL 2; +RE(1)(2) @ SL 3 Implemented (SL-C 2) Degraded-mode operation backed by resource limits (CPU/memory) on all pods via Helm values. RE(1) (manage communication loads, "such as using rate limiting" — 11.3.3) @ SL 2: per-IP token-bucket rate limiting on the Gateway API with separate read/write/static-asset budgets (internal/gateway/ratelimit.go, defaults annotated to this SR). The static tier (#986) keeps the cacheable UI bundle off the API read budget, so a normal page load cannot exhaust it — the availability failure it prevents (an operator's console half-booting under its own navigation) is itself a self-inflicted DoS. The live-stream transports are bounded by connection count, since a subscriber makes one request and then holds it: a per-source-address ceiling on concurrent WebSocket + SSE connections (gateway.streamLimits.maxConnectionsPerIP, default 10) refuses a connect over it with 429 + Retry-After and counts it in dcs_gateway_stream_connections_refused_total. That ceiling was a compile-time constant no deployment could reach until #1447; it is now a flag and a chart value, and deliberately has no setting that removes it. Previously under-claimed at "SL 1". RE(2) (limit DoS effects on other systems) @ SL 3: runtime egress NetworkPolicies restrict what a compromised component can reach; not claimed
SR 7.2 -- Resource management base @ SL 1–4 Implemented (SL-C 2) Kubernetes resource requests/limits; startup probes (150s budget) + liveness/readiness probes on all components; graceful shutdown; grace-period health check on unit runtime (NOT_SERVING after 60s all-driver loss); watchdog Hold on driver loss during active batch; HA overlay with 2 replicas + auto-enabled PDBs + anti-affinity; optional MQTT HA mode (StatefulSet); PrometheusRule alerts; failure mode analysis
SR 7.3 -- Control system backup base @ SL 1; +RE(1) @ SL 2; +RE(1)(2) @ SL 3 Implemented (SL-C 2) Backups run without affecting plant operations: CNPG ScheduledBackup for historian PostgreSQL (online, cron-scheduled, retention-managed, S3 target — Helm historian.backup), dcs backup crds / dcs backup audit for CR/system state (each export writes a sidecar SHA-256 integrity manifest), DR runbook + dcs qualify dr restore qualification, and the audit-archive WORM mirror as an immutable restore source. RE(1) (backup verification before restore, selects at SL 2): dcs backup verify cryptographically validates exports and refuses tampered/truncated/manifest-less files (#973), and additionally refuses an intact-but-incomplete export: every export carries a completeness footer naming any kind the gateway could not read, so an RBAC gap can no longer produce a backup that verifies clean while missing a kind (#1299); historian-tier artifact integrity is the barman catalogue's checksums, validated at recovery (register D13). Secrets/signing keys are excluded from backup sets by design. RE(2) (automated backups on configurable frequency) @ SL 3 met for the historian tier. Drill recorded: Backup/Restore Drill 2026-07-18 — which caught and fixed a restore-path defect (labeled resources 400-rejected on apply)
SR 7.4 -- Control system recovery and reconstitution base @ SL 1–4 Implemented (SL-C 2) Edge-runtime redundancy (ADR 0006): hold-then-resume failover re-binds a unit's runtime to a deployment-designated standby node. Per-unit availability.modeAutonomy (default: partition autonomy + manual fenced failover) or Failover (control lease + self-fence + automatic re-bind after a safety margin). One-writer protection via lease/epoch fencing prevents dual writers on field devices during recovery. Running phase/SFC state survives in Phase.Status (on the control plane, off the edge node); recovery is the ISA-88 Clause 7.4 Hold→Restart exception path. Lifts the unit-runtime availability target from 99.5% (Autonomy) to 99.9% (Failover); see HA and Failure Modes
SR 7.5 -- Emergency power base @ SL 1–4 Delegated (SL-C 2) Emergency power (UPS on nodes, controllers, network gear) is facility/asset-owner scope. The product's part — switching to and from emergency power without affecting the security state — holds by construction: all security state is persisted (etcd, Secrets, PVCs), pods restart into the same posture, edge outputs drive configured safe values on halt (ADR 0009), and ISA-88 procedural state survives in Phase.Status. Delegation recorded in the delegation register (D10)
SR 7.6 -- Network and security configuration settings base @ SL 1–2; +RE(1) @ SL 3 Implemented (SL-C 2) Helm values.yaml for deployment configuration; RBAC templated per component; security hardening guide with production baseline. RE(1) (machine-readable report of current security settings) @ SL 3: helm get values and the Kubernetes API expose the deployed configuration machine-readably. Noted as an element. The enhancement itself is unclaimed
SR 7.7 -- Least functionality base @ SL 1–4 Implemented (SL-C 2) Label note: SR 7.7 has no REs — every level selects the base (11.9.4); the former "SL 3" label conflated this row with the node-OS narrative. Application layer: distroless images with only the binary on every component; capabilities dropped, non-root enforcement, seccomp RuntimeDefault, and read-only root filesystem on the chart-deployed control-plane workloads and the operator-created unit-runtime pod (#722; the runtime pod keeps hostNetwork and a hostPath state volume by design — see Threat Model UR-E-01 and Security Hardening §6). Cluster-control-plane layer: supported on Talos Linux — no shell, no package manager, signed kernel/boot chain, minimal kernel, immutable root, talosctl API as the only management surface. Reference cluster cutover completed 2026-05-12 (#363, closed); the reference cluster runs Talos v1.10.9 / Kubernetes v1.33.6.
SR 7.8 -- Control system component inventory — @ SL 1; base @ SL 2–4 Implemented (SL-C 2) Helm chart tracks components (helm get manifest reports the deployed set); SPDX SBOM generated per image in release pipeline. Corrected label: previously "SL 1", where SR 7.8 is Not Selected — it first selects at SL 2 (11.10.4)
SR 7.3 RE(1): one byte of a copied export flipped on camera, and dcs backup verify refuses it with both digests, "do not restore from this file", and a non-zero exit a pipeline can gate on. The pristine export re-verifies green as the only file a restore may proceed from.

IEC 62443-4-1 -- Secure Development Lifecycle

Assessed practice-by-practice in the dedicated IEC 62443-4-1 SDL Assessment (all 47 requirements of the 8 practices SM/SR/SD/SI/SVV/DM/SUM/SG, maturity self-assessment, V&V + tester-independence plan, and the backlog to ML 2/ML 3). The SDL process documents themselves live under docs/sdl/. Note: the table formerly in this section used an invented "SD 1–SD 7" numbering that does not exist in the standard. It was replaced by the clause-accurate assessment on 2026-07-18 (#970, and see Compliance claim history).

IEC 62443-4-2 -- Component Security

Container Hardening

Scope: "chart-deployed" below means the workloads the Helm chart templates directly (gateway, operators, historian, MQTT, audit components). The unit-runtime pod is created by the physical operator, outside the chart's templating. Since #722 it carries the same enforced securityContext (runAsNonRoot, no privilege escalation, all capabilities dropped, seccomp RuntimeDefault, read-only root filesystem) but keeps hostNetwork: true and a hostPath FB-state volume by design. See Threat Model UR-E-01 and Security Hardening §6.

Control Status Implementation
Distroless base image Implemented gcr.io/distroless/static:nonroot for all components
Non-root execution Implemented UID 65532:65532 (distroless nonroot); enforced via runAsNonRoot on chart-deployed pods and the unit-runtime app container (#722). The runtime pod's short-lived init-data-perms init container runs as root to chown the kubelet-created hostPath data dir to the runtime UID, bounded to CHOWN/DAC_OVERRIDE/FOWNER with all other capabilities dropped and no privileged mode (#731)
No privilege escalation Implemented allowPrivilegeEscalation: false on all chart-deployed containers
All capabilities dropped Implemented capabilities.drop: ["ALL"] on all chart-deployed containers
No privileged pods in default install Implemented No Privileged: true pod path exists in the codebase. The only one there ever was (unit-runtime + GPIO chardev, opt-in) was deleted along with the GPIO driver in #727 (Path B of #373).
Static binary Implemented CGO_ENABLED=0 produces statically linked binaries
Multi-stage build Implemented Build stage discarded; only binary copied to runtime image
Read-only root filesystem Implemented readOnlyRootFilesystem: true on all chart-deployed containers and the unit-runtime pod (#722; runtime state is written under the hostPath data mount)
Seccomp profile Implemented seccompProfile: RuntimeDefault on all chart-deployed pods and the unit-runtime pod (#722)

Supply Chain Security

Control Status Implementation
Dependency management Implemented Go modules with integrity verification
Image architecture Implemented linux/amd64 only (platforms: linux/amd64 in .github/workflows/release.yml) — x86_64 is the sole supported production architecture; arm64 image builds were dropped 2026-05-11 when ARM/SBC moved to community/experimental tier
Release checksums Implemented SHA256 checksums for all binary artifacts
Container image signing Implemented Cosign keyless signing (Sigstore/Fulcio) in release pipeline
SBOM generation Implemented SPDX SBOM generated by syft; attached to signed images
SLSA provenance Planned No provenance attestation

Encryption at Rest -- Operator Guidance

Cloud-Native DCS does not implement disk-level encryption itself. Encryption at rest is a layered responsibility: the platform (Kubernetes, CSI driver, node OS) provides the primitive, and the operator configures it. The product documents what each data-bearing tier needs and how to verify that the configuration is in place. The table below makes the surface explicit so an auditor reading this document can confirm coverage without grepping through Helm values.

Data-Bearing Tiers

Tier Contents Encryption Mechanism Operator Action
etcd Kubernetes Secrets (mTLS keys, MQTT credentials, OIDC client secret, e-signature HMAC key, runtime bearer tokens), CRDs (AuditRecord, Batch, ProceduralInstance) Kubernetes EncryptionConfiguration on control plane (aescbc or aesgcm) Configure on kube-apiserver per Security Hardening § 8; verify with the procedure below
Historian PostgreSQL / TimescaleDB PVC Batch records, time-series tags, archived AuditRecords (3-year retention) CSI StorageClass with encrypted backing volumes (cloud KMS or LUKS-backed local) Use a StorageClass with provider-side encryption; CNPG inherits via bootstrap.initdb / storage.storageClass
MQTT broker PVC Mosquitto persistent session and retained messages CSI StorageClass with encrypted backing volumes Same as historian; see mqtt.persistence.storageClass
Unit-runtime hostPath Persisted FB networks, ISA-88 state replay buffer (per-device-node) Node-OS disk encryption (LUKS) or immutable encrypted root (Talos) Site-level node hardening; see Threat Model — edge-device physical security is named as a residual risk. The cluster-control-plane Talos pairing (#363) is implementation-complete in cndcs-deploy-demo/; the edge-device Talos path is community/experimental tier and tracked separately
Container images Static Go binaries; no embedded secrets Registry-side encryption (GHCR provides at-rest encryption for tenants) None at deploy time; verify image signatures with cosign (see Supply Chain)
Application audit trail AuditRecord etcd entries + archived rows in PostgreSQL Inherited from etcd + PVC encryption above; record-level HMAC-SHA256 on e-signed entries provides tamper detection independent of disk encryption Configure both etcd encryption and archive PVC encryption; HMAC key managed per 21 CFR Part 11

Verifying etcd Encryption

A site claiming SL 2 or above must verify that etcd is encrypted (at-rest confidentiality selects at SL 2 via SR 4.1 RE(1)) -- the application cannot self-test this because the kube-apiserver shields it from the raw etcd contents. Two checks an auditor or operator runs:

# On a control-plane node, inspect the EncryptionConfiguration:
sudo grep -A2 "providers:" /etc/kubernetes/encryption-config.yaml
# Expect: aescbc or aesgcm before identity

# Confirm new Secrets land encrypted in etcd (k3s example):
sudo etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/var/lib/rancher/k3s/server/tls/etcd/server-ca.crt \
  --cert=/var/lib/rancher/k3s/server/tls/etcd/client.crt \
  --key=/var/lib/rancher/k3s/server/tls/etcd/client.key \
  get /registry/secrets/cloud-native-dcs/dcs-signing-key | hexdump -C | head
# Expect: bytes beginning with k8s:enc:aescbc:v1:<key-name>: prefix, not plain JSON

If the prefix is k8s:enc:, encryption is active. If the value begins with {"kind":"Secret"..., encryption is not active and a site cannot claim the SR 4.1 RE(1) at-rest delegation -- which caps the site's DC level below SL 2.

Verifying PVC Encryption

CSI StorageClass encryption is provider-specific. For the reference cluster (DigitalOcean Block Storage) and most managed-Kubernetes offerings, the encryption posture is published by the provider and applies cluster-wide. For self-managed control planes, an operator typically uses LUKS-on-LVM under a local-path-provisioner or a CSI driver that wraps LUKS.

Verify by inspecting the StorageClass used by the historian and MQTT PVCs:

kubectl get storageclass $(kubectl -n cloud-native-dcs get pvc \
  -l app.kubernetes.io/component=historian \
  -o jsonpath='{.items[0].spec.storageClassName}') -o yaml
# Inspect parameters; provider-specific encryption flag (e.g., do-block-storage
# encrypts at rest by default; aws-ebs requires encrypted: "true").

Edge-Device hostPath Caveat

Unit-runtime pods persist FB-network state and the ISA-88 replay buffer to a hostPath on the device node. This is the one tier where the application's data-at-rest is not protected by an encryption boundary the platform typically provides: edge nodes are SBCs or industrial PCs with locally attached storage, and full-disk encryption is a node-OS configuration the operator must opt into. The threat model names this explicitly under "Physical security of edge devices is out of scope". Sites relying on the SR 4.1 RE(1) at-rest delegation (SL 2 and above) should either (a) deploy unit-runtime nodes with LUKS-encrypted root or (b) restrict device-node placement to a physically secured cabinet. The cluster-control-plane Talos pairing tracked in #363 addresses FR 7 (least functionality) for the control plane, but does not cover edge-device nodes: x86_64 industrial PCs running Modbus TCP / OPC UA over Ethernet are the supported edge surface, and Talos on ARM SBCs remains community/experimental tier (the GPIO driver itself was removed in #727).

Intrusion Detection Narrative

Cloud-Native DCS does not ship an intrusion-detection system. IEC 62443 SR 6.2 ("continuous monitoring") is met at SL 2 by operational telemetry (Prometheus metrics, controller-runtime metrics, audit-record creation rate). The SL-3 / SL-4 expectation of "detect security-relevant anomalies" is delegated to platform and operator-supplied tooling. This is an explicit product decision. An IDS is a separately maintained system with its own update cadence, signature management, and operator-training surface, and bundling one with the DCS would lock customers to whatever signatures and rules the DCS happens to ship.

Defense-in-Depth We Do Ship

The product reduces the surface an IDS would otherwise have to defend:

  • Distroless images, read-only root filesystem, no shell: a compromised container cannot drop a tool, modify a binary, or persist a payload (read-only FS is enforced on the chart-deployed workloads, and the unit-runtime pod relies on the distroless image alone)
  • Static Go binaries (CGO_ENABLED=0): no dynamic loader, no LD_PRELOAD injection vector, no system libraries to swap
  • Non-root + dropped capabilities + seccomp RuntimeDefault: a compromised process cannot escalate to host (enforced on the chart-deployed workloads and the unit-runtime pod (#722), while the runtime still runs hostNetwork, per threat model UR-E-01)
  • NetworkPolicies (IEC 62443 zone/conduit model): lateral movement between Control / Field / External / Field Bus zones is denied by default, and cross-zone conduits are explicit NetworkPolicy allow rules
  • mTLS on all inter-component HTTP: an attacker on the cluster network cannot pretend to be the gateway, an operator, or a unit-runtime without a valid cert-manager-issued certificate
  • Anti-replay nonces on mutating Gateway endpoints: replayed-request attacks return HTTP 409 (see SR 3.8 above)
  • Immutable AuditRecords: a webhook rejects update / delete on audit rows, so an intruder cannot erase their tracks even with API access
  • Image signing (cosign keyless): an attacker cannot replace a registry image without an out-of-band signing-identity compromise

What an Operator Adds for SL-3+ Detection

For sites claiming SL 3 or planning toward SL 4, the operator deploys detection on top of the controls above. Common stacks:

Layer Recommended Tool What it Watches
Pod-level syscall anomaly Falco or Tetragon Unexpected exec in distroless pods, ptrace, raw socket use
Admission anomaly Kyverno or OPA Gatekeeper Image-signature mismatch, privileged pod attempt, ServiceAccount-token mount on non-system pods
Network anomaly Cilium Hubble + Tetragon, or sidecar IDS (Suricata) Cross-zone flows that bypass NetworkPolicy, OPC UA traffic from unexpected source
API anomaly Audit log shipping to SIEM Bulk Secret read by a non-operator identity, sudden manager-role binding change
Authentication anomaly OIDC provider's own analytics (Keycloak event log, Entra Sign-in logs) Failed-login bursts, unusual-location sign-in, group-membership change

The Security Operations guide documents the OIDC and audit-log surfaces a SIEM can subscribe to. The Security Hardening guide lists the controls the product ships that an IDS layers on top of. We do not prescribe a specific IDS vendor. A Falco-equipped k3s droplet and a kube-bench-audited Talos cluster reach the same SL-3 narrative through different means.

Talos Pairing

Talos Linux is a supported deployment path for the cluster control plane as of the #363 refresh (x86_64 only, with ARM SBC at community/experimental tier). The substrate collapses several SL-3 controls onto the node OS: no shell on the node, no package manager, signed kernel and boot chain, encrypted root by default, minimal kernel, talosctl API as the only management surface (SSH disabled by design).

Implementation status (2026-05-12):

  • Terraform module rewritten to provision DigitalOcean droplets from a Talos custom image with a machine-config patch (terraform/modules/digitalocean/ in cndcs-deploy-demo/). Cloud-init is gone.
  • scripts/bootstrap.sh rewritten to drive talosctl gen config, apply-config --insecure, bootstrap, and kubeconfig against the reserved IP, with no SSH involved. Idempotent.
  • Validated end-to-end on a throwaway DigitalOcean droplet running Talos v1.10.9 / Kubernetes v1.33.6 with flannel + kube-proxy + coredns Running.
  • Talos custom image upload procedure documented in terraform/README.md (Image Factory schematic + doctl compute image create against factory.talos.dev).
  • Reference-cluster cutover (parallel-droplet sequence with Postgres dump pre-flight, soak, DNS switchover, and decommission, per the plan in cndcs-deploy-demo/docs/talos-cutover-plan.md) completed 2026-05-12 (#363 closed). The live reference cluster runs Talos v1.10.9 / Kubernetes v1.33.6.

Scope boundary: the cluster-control-plane claim is the only thing this issue closes. Edge-device unit-runtime nodes (Modbus / OPC UA gateways running on x86_64 industrial PCs) remain operator-hardened via standard node-OS controls, and the ARM SBC path is not in the supported tier. The GPIO driver (the only privileged-pod path) was removed in #727, so no privileged pods are ever scheduled. Note that PSA baseline still cannot be enforced on site namespaces hosting unit runtimes, because the runtime pod's hostNetwork + hostPath are forbidden at baseline. Namespace-level enforcement therefore stays at privileged there, with restricted on the control-plane namespace (see Security Hardening §6).

SL-3 Claim -- Operator Checklist

A site claiming IEC 62443 SL 3 compliance for a Cloud-Native DCS deployment should be able to point an auditor at each of the following:

  1. Cluster posture
  2. [ ] etcd EncryptionConfiguration enabled (aescbc or aesgcm) on every control-plane node, verified per Verifying etcd Encryption
  3. [ ] CSI StorageClass for historian and MQTT PVCs uses encrypted backing volumes, verified per Verifying PVC Encryption
  4. [ ] Cluster-control-plane nodes run Talos Linux (recommended) or an operator-hardened baseline. The Talos deployment path is implemented in cndcs-deploy-demo/ and validated end-to-end at v1.10.9 (#363)
  5. [ ] Edge unit-runtime nodes use full-disk encryption (LUKS) on a hardened Linux baseline. x86_64 industrial PCs are the only supported edge tier
  6. [ ] Kubernetes audit logging enabled and shipped to a SIEM
  7. Application posture (from this chart)
  8. [ ] mtls.enabled: true
  9. [ ] networkPolicies.enabled: true
  10. [ ] mqtt.securityProfile: production
  11. [ ] gateway.auth.mode: oidc with group-claim configuration
  12. [ ] webhook.enabled: true (AuditRecord immutability)
  13. [ ] historian.audit.archival.enabled: true
  14. [ ] OPC UA Unit serviceBinding sessions configured with SignAndEncrypt (or Sign minimum) and each serverCertSha256Pin populated for every deployed Unit (the trust-list inbox UX from #301 was removed in #348, and the bulk-approval inbox rebuild is on the backlog)
  15. Detection layer (operator-supplied)
  16. [ ] Pod-level anomaly detection (Falco, Tetragon, or equivalent)
  17. [ ] Admission-time enforcement (Kyverno or OPA Gatekeeper) for image signatures and pod security
  18. [ ] OIDC provider sign-in / failed-auth telemetry shipped to a SIEM
  19. Process posture
  20. [ ] Secret rotation runbook exercised at least annually (see Rotation Runbook)
  21. [ ] Cert-expiry drill exercised at least annually (see Cert Expiry Drill)
  22. [ ] Archive-corruption drill exercised at least annually (see Archive Corruption Drill)

Items 1 and 3 are platform / operator scope. Items 2 and 4 are product scope. An auditor reading this checklist can scope their interview by distinguishing the two.

Gap Analysis

Gap SL Required Priority Description
~~Human user authentication~~ ~~SL 2~~ ~~High~~ ~~Resolved: OIDC authentication for Gateway REST API and Web UI; group-based authorization~~
~~mTLS between components~~ ~~SL 3~~ ~~High~~ ~~Resolved: cert-manager-based mTLS for all inter-component HTTP~~
~~Network segmentation~~ ~~SL 2~~ ~~High~~ ~~Resolved: Helm-managed NetworkPolicies with IEC 62443 zone/conduit model~~
~~Gateway API authentication~~ ~~SL 2~~ ~~High~~ ~~Resolved: OIDC authentication on all API routes; group-based RBAC authorization~~
~~MQTT security~~ ~~SL 2~~ ~~Medium~~ ~~Resolved: Production security profile (TLS + auth + ACL) enabled by default; mqtt.securityProfile: production in Helm values; cert-manager-provisioned TLS; per-role credentials with topic ACLs; Mosquitto connection/auth logging for audit~~
~~Input validation~~ ~~SL 2~~ ~~Medium~~ ~~Resolved: go-playground/validator on all gateway endpoints with typed DTOs, custom k8s-name/ISA-88 validators, unknown-field rejection, and structured error responses~~
~~Container image signing~~ ~~SL 3~~ ~~Medium~~ ~~Resolved: Cosign keyless signing in release pipeline~~
~~SBOM generation~~ ~~SL 2~~ ~~Medium~~ ~~Resolved: SPDX SBOM generated by syft, attached to images~~
~~Read-only root filesystem~~ ~~SL 2~~ ~~Low~~ ~~Resolved: readOnlyRootFilesystem on all Helm-deployed pods~~
~~Seccomp profiles~~ ~~SL 2~~ ~~Low~~ ~~Resolved: seccompProfile RuntimeDefault on all Helm-deployed pods~~
~~Session lifecycle (inactivity lock, remote termination, logout invalidation)~~ ~~SL 1–2 base (SR 2.5, SR 2.6); SL 3 REs (SR 3.8 RE(1)(2))~~ ~~High~~ ~~Resolved (#971): server-side session registry with inactivity termination, logout + admin revocation endpoints, UI idle/manual lock with re-auth resume, session-ID invalidation on termination; Clause 4.2 carve-out documented in Security Hardening~~
~~Deterministic-output configurability~~ ~~SL 1 base (SR 3.6)~~ ~~High~~ ~~Resolved (#972): failState parameter on output blocks (safeValue default / holdLast), de-energized-equivalent defaults, invalid values rejected at Init; ADR 0009 amended; truly-unpowered documented as I/O-hardware watchdog scope~~
Deterministic output on the raw device write path SL 1 base (SR 3.6) High A channel no ControlModule declares can be written through POST .../iomodules/{name}/write, which reaches the field through the io-probe. It has no block behind it, so no failState is declared for it and no software mechanism drives it to a predetermined state on any loss. The device's own comm-loss watchdog is the only thing left. The product declares and applies that since #1683, and because the watchdog acts on the whole node it covers undeclared channels too, so a module with spec.failSafe set is no longer exposed here. The residual risk is a module that declares nothing, which keeps its coupler's default and says so through the FailSafeDeclared condition. The earlier "io-probe performs no output writes" scoping was false (#1691)
~~Backup verification~~ ~~SL 2 (SR 7.3 RE(1))~~ ~~Medium~~ ~~Resolved (#973): sidecar integrity manifests + dcs backup verify pre-restore check, barman delegation for the historian tier (register D13), Secrets excluded by design, drill recorded 2026-07-18~~
~~Gateway auth/audit gap-fills~~ ~~SL 1–2 base (SR 1.11, 1.12, 2.8, 2.10; SR 2.9 capacity behavior)~~ ~~Medium~~ ~~Resolved (#974): login lockout with admin unlock + Clause 4.2 exempt list, pre-auth use notification (login page + CLI), SR 2.8 category completion with documented mapping, per-action-class audit-failure policy + DCSAuditProcessingFailure alert, capacity behavior defined~~
~~Decommission data purge~~ ~~SL 2 (SR 4.2)~~ ~~Low~~ ~~Resolved (#977): Decommissioning and Data Purge runbook across all data-bearing tiers (edge hostPath, historian/MQTT PVCs, etcd, archive buckets), retention-first ordering, NIST SP 800-88 sanitization standards, delegation register entry D14~~
Island mode + fail close SL 3 (SR 5.2 RE(2)/RE(3)) Low Not implemented; only relevant to a full SL 3 zone-boundary claim. Recorded so the SL 2 claim is clean and the SL 3 delta is explicit
Multifactor authentication SL 3 (SR 1.1 RE(2)) Low Not required at SL 2 (verified, 5.3.4). Achievable in deployments via an MFA-enforcing IdP on the OIDC flow, which is a documented delegation and no product control
Encryption at rest SL 2 (SR 4.1 RE(1)) -- delegated Medium Documented Posture: product does not implement disk-level encryption; depends on Kubernetes EncryptionConfiguration for etcd, CSI StorageClass encryption for PVCs, and node-OS disk encryption for unit-runtime hostPath. Per-tier guidance and verification procedures in Encryption at Rest; edge-device hostPath remains an operator-hardened tier (LUKS on a standard Linux baseline)
Intrusion detection SL 3 / SL 4 Low Documented Posture: product does not ship an IDS by design; defense-in-depth via distroless + non-root + read-only FS + NetworkPolicies + mTLS + immutable AuditRecords + cosign signing reduces the surface. Operators layer Falco/Tetragon/OPA/SIEM per Intrusion Detection Narrative
~~Immutable node OS (control plane)~~ ~~SL 3~~ ~~Medium~~ ~~Resolved: Talos Linux deployment path implemented in cndcs-deploy-demo/; reference cluster cutover completed 2026-05-12 (#363 closed) — the reference cluster runs Talos v1.10.9 / Kubernetes v1.33.6~~
~~Formal threat model~~ ~~SL 2~~ ~~Medium~~ ~~Resolved: STRIDE analysis per component and MITRE ATT&CK for ICS mapping with risk assessment matrix~~

Compliance claim history

Auditor-facing changelog for compliance claims that have been weakened or withdrawn since a previous published revision. New strengths are captured by the rows in the matrices above. This section exists so that an auditor comparing a current revision against a prior printout can see why a previously-asserted control posture is no longer reflected verbatim.

Date Clause(s) Change Reason
2026-08-18 SR 3.6 The device comm-loss fail-safe is declarable and applied: spec.failSafe on IOModule, applied by the io-probe, read back into status.failSafe, reported through FailSafeDeclared and FailSafeApplied (ADR 0068, #1683). Stays Partial: the action is node-wide on the reference hardware, and an undeclared module still inherits its coupler's default by design
2026-08-18 SR 3.6, CR 3.6 Withdrew "io-probe performs no output writes (read-only prober) — the requirement attaches to it vacuously", and re-read the row from Implemented (SL-C 2) to Partial. The sentence was never stale. It was false when it was written. The io-probe's POST /api/v1/write route landed 2026-03-09 and the claim 2026-07-18, and the gateway sends every raw channel write for a non-simulation IOModule to it, so the io-probe is the write path for real hardware and the unit runtime is the write path for the simulator. What the vacuous verdict ended was a real question. Every mechanism the row cites is executed by a program that the raw path has no instance of, so an output left driven by dcs io write has no predetermined state the product provides; the device's own comm-loss watchdog is what remains, and the product can neither declare nor apply it (#1683). The program-driven half of the row is unchanged and unaffected, and the FR 3 vector stays at a conditional 2 with the gap named. #1691, found while removing the io-probe read shadow (#1690)
2026-08-11 SR 3.8 RE(1), SR 2.6 Corrected the scope of "enforced on REST and WS/SSE paths". The session registry was consulted when a WebSocket or SSE client connected and never again, so a revoked session kept streaming the whole plant until its socket happened to drop, and an access token's exp was verified once at the upgrade. The prior wording was true of a new connection and silent about one already open — which is the case an auditor asks about, since terminating a session that is doing nothing proves little. A browser reconnects constantly and hid it; a plant SCADA holds one connection for months, and docs/scada-integration.md sells it that transport. It was fixed in place (#1445), with no downgrade. Revocation now closes the streams the session holds, each stream re-checks the identity behind it on its keepalive tick, and both the WebSocket close code and the SSE terminating event name the reason so a client stops reconnecting. The terminal WebSocket, which was in the same position and hands out a shell, is covered on the same mechanism. Row status unchanged (SL-C 3); the evidence shape now covers the live connection #1445, found by the #1425 spike
2026-07-18 4-1 section Replaced the secure-development-lifecycle table with the clause-accurate 4-1 assessment. The old table's "SD 1–SD 7" numbering does not exist in ANSI/ISA-62443-4-1-2018 (the real structure is 8 practices, SM/SR/SD/SI/SVV/DM/SUM/SG, 47 requirements) — its rows asserted conformance against requirement identifiers the standard never defined. The honest per-practice posture (incl. SVV as a structural gap pending an independent V&V engagement) is in the new assessment; public wording is scoped to "aligned to 62443-4-1 practices", with "conformant" ruled out #970, epic #967
2026-07-18 Entire 3-3 matrix Clause-by-clause re-audit against the purchased ANSI/ISA-62443-3-3 (2013) text. Weakened/corrected claims: SR 3.8 "SL 3 Implemented" → Implemented (SL-C 2) — SL 3 selects RE(1) session-ID invalidation after termination, which stateless gateway JWTs do not provide (#971 is the path back). SR 5.2 "SL 3 Implemented" → Implemented (SL-C 2) — SL 3 selects RE(2) island mode and RE(3) fail close, neither of which exists. SR 1.7 rationale "no password-based authentication implemented" was factually wrong (LDAP bind mode is password-based); re-filed as a documented directory/IdP strength delegation. SR 1.9 "SL 3" → SL-C 2 (RE(1) hardware key protection is the SL 3 selection; not claimed). SR 4.1 / encryption at rest re-filed from SL 3 positioning to an SL 2 delegation — RE(1) selects at SL 2 (8.3.4). SR 4.3 and SR 7.7 "SL 3" labels removed — both select the base at every level, so the labels asserted enhancements the standard does not define. Also in this audit (recorded here so marketing can reconcile the public "elements of SL 3" wording; not weakenings): 21 previously missing SR rows added (1.4, 1.6, 1.8, 1.10–1.13, 2.3, 2.7, 2.8, 2.10–2.12, 3.3, 3.6, 3.9, 4.2, 5.3, 5.4, 7.3, 7.5) — no silent omissions remain; SL labels on 1.2, 3.4, 3.7, 7.8 corrected (Not Selected at SL 1; base selects at SL 2); under-claimed levels raised with citations (SR 2.1 → SL-C 2 via RE(1)+(2); SR 6.1 → SL-C 3 via RE(1); SR 7.1 → SL-C 2 via RE(1)); SR 2.12 and SR 3.9 recorded as clause-verified SL 3 capabilities; per-FR SL-C vector added. Public SL 3 wording should now cite: SR 2.12, SR 3.9, SR 6.1 RE(1), SR 3.1 RE(1)-with-exception — and must not cite zone-boundary or session-integrity SL 3 until #971 lands #968, epic #967
2026-07-12 SR 1.5 Added the gateway-owned OPC UA discovery client identity to the SR 1.5 evidence: the gateway now auto-provisions a single self-signed RSA-2048 client cert (dcs-opcua-client Secret, stable ApplicationURI) used as the default channel-layer cert for the transient discovery/wizard path when no explicit credentials are supplied, with the public cert served at GET /api/v1/discovery/opcua/client-identity for server-side trust-listing (#919, direction 2 of #873). Unlike the #511 claim removed below, this identity is genuinely wired (used in buildOPCUAConnConfig, covered by tests) and is explicitly scoped to discovery — it does not auto-issue per-Unit runtime serviceBinding certs, which remain operator-supplied. Row status unchanged (still Partial): rotation is manual and there is no per-Unit lifecycle automation. #919, #873
2026-07-02 SR 7.7, 4-2 Container Hardening, SL 3 narrative Narrowed the pod-hardening claims from "all containers/pods" to the chart-deployed control-plane workloads. The operator-created unit-runtime pod never carried the asserted controls: it runs hostNetwork: true with a hostPath FB-state volume and no explicit securityContext (internal/controller/physical/unit_pod.go) — non-root comes from the distroless image's USER 65532, and capabilities-drop / seccomp / read-only-FS are not enforced; under the opt-in experimental GPIO driver it is privileged root. Also corrected the SL 3 narrative's implication that PSA baseline "applies cleanly to all cluster workloads": baseline forbids hostNetwork/hostPath, so runtime-hosting site namespaces stay at enforce=privileged (control-plane namespace remains restricted-clean). Threat-model rows UR-T-02 / UR-E-01 moved Implemented → Partial to match. The chart-deployed workloads' posture is unchanged and verified. #713
2026-06-10 SR 1.5, SR 1.9 Removed the assertion that OPC UA client certificates are "auto-generated per Unit on first reconcile". The self-signed cert generator (pkg/opcuaclient/certgen.go) was never wired into any reconcile or API path — it had zero callers — so the auto-generation claim never reflected shipped behavior; the dead code was deleted under #511. Actual shipped posture (unchanged by the deletion): client cert + key are operator-supplied via the credentials Secret's clientCert/clientKey keys, with cert/key pairing and validity-window checks enforced at every connect. Row statuses are unchanged. #511
2026-06-09 SR 1.2, SR 1.5, SR 4.1 Removed the assertions that device identity rests on the product's token-based enrollment flow ("token-based device enrollment", "enrollment tokens are single-use", "Kubernetes Secrets for enrollment tokens"). The in-product enrollment subsystem (gateway-issued single-use tokens + dcs-enroll k3s join) was retired by ADR 0004: cluster node join — and the credentials that secure it — moved to the deployment layer (e.g. Talos PKI machine identity), which is stronger than the retired 8-character token but is no longer a product control. Row statuses are unchanged; the evidence shape moved from product mechanism to deployment-layer mechanism. #462, #473, ADR 0004
2026-05-13 SR 1.5, SR 1.9, SL 3 narrative Removed the assertion that pending OPC UA server certificates appear in a gateway "trust-list inbox" for operator approval before sessions establish. The Trust List inbox UX shipped in #301 was deleted as part of the #348 Asset CRD dissolution. Server-cert trust is currently established by an explicit per-Unit serviceBinding.security.serverCertSha256Pin. The SR 1.5 row stays at "Partial" but its evidence shape changed: the operator approval flow is now manifest-level (cert SHA committed in the Unit CR), where it used to be a UI inbox. A reworked bulk-approval inbox keyed on Unit is on the rebuild backlog (see migration guide); landing it would restore the prior evidence shape. #348, docs/migrations/asset-dissolution.md

Roadmap

  1. ~~Gateway authentication~~ -- Implemented: OIDC authentication for REST API and Web UI with group-based authorization
  2. ~~Network policies~~ -- Implemented: Per-component NetworkPolicies enforcing IEC 62443 zone/conduit boundaries (Control, Field, External, Field Bus zones) via Helm templates with networkPolicies.enabled: true
  3. ~~mTLS~~ -- Implemented: cert-manager-based mutual TLS for all inter-component HTTP communication via Helm mtls.enabled: true
  4. ~~MQTT security~~ -- Implemented: TLS encryption, per-role credential authentication, and topic-level ACLs for Mosquitto broker, with the production security profile enabled by default via mqtt.securityProfile: production
  5. ~~Image signing~~ -- Implemented: Cosign keyless signing in release pipeline. Admission-controller verification is a deployment-time option
  6. ~~SBOM and provenance~~ -- Implemented: SPDX SBOM generation via syft, with SLSA provenance attestation planned
  7. ~~Threat model~~ -- Implemented: STRIDE analysis per component with MITRE ATT&CK for ICS mapping and risk assessment matrix, plus the security hardening guide for production deployments
  8. Encryption at rest -- Documented Posture: per-tier operator guidance under Encryption at Rest, with verification procedures for etcd and PVC tiers. It documents the SR 4.1 RE(1) at-rest delegation (an SL 2 obligation per the 2026-07-18 clause audit, formerly mis-filed as SL 3) and the SR 4.3 cryptography posture. The edge-device hostPath tier is an operator-hardened layer (LUKS on a standard Linux baseline), uncoupled from the cluster-control-plane Talos pairing
  9. Intrusion detection narrative -- Documented Posture: the product does not ship an IDS by design. Defense-in-depth via distroless + read-only-FS + non-root + dropped capabilities + NetworkPolicies + mTLS
  10. immutable AuditRecords reduces the surface, and operators layer Falco/Tetragon/OPA/SIEM per Intrusion Detection Narrative
  11. ~~Immutable node OS -- cluster control plane (Talos)~~ -- Implemented: Talos deployment path in cndcs-deploy-demo/. The live reference cluster completed its cutover on 2026-05-12 (#363 closed) and runs Talos v1.10.9 / Kubernetes v1.33.6. Contributes SL-3 narrative for FR 7 (least functionality) at the node-OS layer. Edge-device unit-runtime nodes and ARM SBC support are not in scope for this claim (community/experimental tier).