Phase 3 PR 28 — Admin UI cert-trust management page #27

Merged
dohertj2 merged 1 commits from phase-3-pr28-cert-trust into v2 2026-04-18 14:42:53 -04:00
Owner

Adds /certificates Admin UI route (FleetAdmin-only) that surfaces the OPC UA server's PKI store rejected + trusted cert folders and gives operators Trust / Delete / Revoke actions so rejected client certs can be promoted without touching disk.

Service

CertTrustService reads $PkiStoreRoot/{rejected,trusted}/certs/*.der directly via X509CertificateLoaderno Opc.Ua dependency in the Admin project. The Opc.Ua stack uses a Directory-typed store on disk so the file layout is stable, and the Admin process can manage it without needing the Server's assemblies loaded. Typical deployment has Admin + Server side-by-side on the same machine; CertTrustOptions.PkiStoreRoot defaults to %ProgramData%\OtOpcUa\pki to match OpcUaServerOptions.PkiStoreRoot's default, so a vanilla install needs no override.

Operator actions

  • TrustFile.Move(rejected/certs/x.der → trusted/certs/x.der, overwrite: true). Idempotent, tolerates a concurrent operator doing the same move.
  • Delete rejected — wipes the file.
  • Revoke trust — removes from trusted/certs/. The Opc.Ua stack re-reads the Directory store on each new client handshake, so no explicit reload signal is needed; operators retry the rejected client's connection after trusting.

UX details

  • Thumbprint match is case-insensitive because X509Certificate2.Thumbprint is upper-case hex but operators often copy-paste from logs that lowercase it.
  • Malformed .der files in the store are logged + skipped — a single bad file can't take the whole management page offline.
  • Missing store directories (pristine install, Server never run) produce empty lists rather than exceptions.
  • Each action Serilog-logs user + thumbprint + action so the filesystem-op log in CertTrustService correlates back to the authenticated admin. DB-level ConfigAuditLog persistence is deferred because that schema is cluster-scoped and cert actions are cluster-agnostic.

Tests

CertTrustServiceTests — 9 new unit cases:

  • ListRejected parses subject / thumbprint / store kind from a self-signed test cert.
  • Rejected and trusted stores stay separate.
  • TrustRejected moves the file; ListRejected empty afterwards.
  • TrustRejected with a missing thumbprint returns false without touching trusted.
  • DeleteRejected removes the file.
  • UntrustCert removes from trusted only.
  • Thumbprint match is case-insensitive.
  • Missing store directories produce empty lists instead of throwing.
  • Junk .der in the store is logged + skipped; valid certs still surface.

Full Admin.Tests Unit suite: 23 pass / 0 fail (14 prior + 9 new). Full Admin build clean — 0 errors, 0 warnings.

Docs

lmx-followups.md item #3 marked DONE. Deferred: flipping AutoAcceptUntrustedClientCertificates to false as the production default — that's a deployment-config follow-up, not a code gap. The Admin UI is now ready to be the trust gate.

Closes

LMX follow-up #3.

Adds `/certificates` Admin UI route (FleetAdmin-only) that surfaces the OPC UA server's PKI store `rejected` + `trusted` cert folders and gives operators Trust / Delete / Revoke actions so rejected client certs can be promoted without touching disk. ## Service `CertTrustService` reads `$PkiStoreRoot/{rejected,trusted}/certs/*.der` directly via `X509CertificateLoader` — **no `Opc.Ua` dependency** in the Admin project. The Opc.Ua stack uses a Directory-typed store on disk so the file layout is stable, and the Admin process can manage it without needing the Server's assemblies loaded. Typical deployment has Admin + Server side-by-side on the same machine; `CertTrustOptions.PkiStoreRoot` defaults to `%ProgramData%\OtOpcUa\pki` to match `OpcUaServerOptions.PkiStoreRoot`'s default, so a vanilla install needs no override. ## Operator actions - **Trust** — `File.Move(rejected/certs/x.der → trusted/certs/x.der, overwrite: true)`. Idempotent, tolerates a concurrent operator doing the same move. - **Delete rejected** — wipes the file. - **Revoke trust** — removes from `trusted/certs/`. The Opc.Ua stack re-reads the Directory store on each new client handshake, so no explicit reload signal is needed; operators retry the rejected client's connection after trusting. ## UX details - Thumbprint match is **case-insensitive** because `X509Certificate2.Thumbprint` is upper-case hex but operators often copy-paste from logs that lowercase it. - Malformed `.der` files in the store are logged + skipped — a single bad file can't take the whole management page offline. - Missing store directories (pristine install, Server never run) produce empty lists rather than exceptions. - Each action Serilog-logs `user + thumbprint + action` so the filesystem-op log in `CertTrustService` correlates back to the authenticated admin. DB-level `ConfigAuditLog` persistence is deferred because that schema is cluster-scoped and cert actions are cluster-agnostic. ## Tests `CertTrustServiceTests` — 9 new unit cases: - `ListRejected` parses subject / thumbprint / store kind from a self-signed test cert. - Rejected and trusted stores stay separate. - `TrustRejected` moves the file; `ListRejected` empty afterwards. - `TrustRejected` with a missing thumbprint returns false without touching trusted. - `DeleteRejected` removes the file. - `UntrustCert` removes from trusted only. - Thumbprint match is case-insensitive. - Missing store directories produce empty lists instead of throwing. - Junk `.der` in the store is logged + skipped; valid certs still surface. Full Admin.Tests Unit suite: **23 pass / 0 fail** (14 prior + 9 new). Full Admin build clean — 0 errors, 0 warnings. ## Docs `lmx-followups.md` item #3 marked DONE. Deferred: flipping `AutoAcceptUntrustedClientCertificates` to `false` as the production default — that's a deployment-config follow-up, not a code gap. The Admin UI is now ready to be the trust gate. ## Closes LMX follow-up #3.
dohertj2 added 1 commit 2026-04-18 14:42:43 -04:00
Phase 3 PR 28 — Admin UI cert-trust management page. New /certificates route (FleetAdmin-only) surfaces the OPC UA server's PKI store rejected + trusted certs and gives operators Trust / Delete / Revoke actions so rejected client certs can be promoted without touching disk. CertTrustService reads $PkiStoreRoot/{rejected,trusted}/certs/*.der files directly via X509CertificateLoader — no Opc.Ua dependency in the Admin project, which keeps the Admin host runnable on a machine that doesn't have the full Server install locally (only needs the shared PKI directory reachable; typical deployment has Admin + Server side-by-side on the same box and PkiStoreRoot defaults match so a plain-vanilla install needs no override). CertTrustOptions bound from the Admin's 'CertTrust:PkiStoreRoot' section, default %ProgramData%\OtOpcUa\pki (matches OpcUaServerOptions.PkiStoreRoot default). Trust action moves the .der from rejected/certs/ to trusted/certs/ via File.Move(overwrite:true) — idempotent, tolerates a concurrent operator doing the same move. Delete wipes the file. Revoke removes from trusted/certs/ (Opc.Ua re-reads the Directory store on each new client handshake, so no explicit reload signal is needed; operators retry the rejected connection after trusting). Thumbprint matching is case-insensitive because X509Certificate2.Thumbprint is upper-case hex but operators copy-paste from logs that sometimes lowercase it. Malformed files in the store are logged + skipped — a single bad .der can't take the whole management page offline. Missing store directories produce empty lists rather than exceptions so a pristine install (Server never run yet, no rejected/trusted dirs yet) doesn't crash the page. ed88835d34
Razor page layout: two tables (Rejected / Trusted) with Subject / Issuer / Thumbprint / Valid-window / Actions columns, status banner after each action with success or warning kind ('file missing' = another admin handled it), FleetAdmin-only via [Authorize(Roles=AdminRoles.FleetAdmin)]. Each action invokes LogActionAsync which Serilog-logs the authenticated admin user + thumbprint + action for an audit trail — DB-level ConfigAuditLog persistence is deferred because its schema is cluster-scoped and cert actions are cluster-agnostic; Serilog + CertTrustService's filesystem-op info logs give the forensic trail in the meantime. Sidebar link added to MainLayout between Reservations and the future Account page.
Tests — CertTrustServiceTests (9 new unit cases): ListRejected parses Subject + Thumbprint + store kind from a self-signed test cert written into rejected/certs/; rejected and trusted stores are kept separate; TrustRejected moves the file and the Rejected list is empty afterwards; TrustRejected with a thumbprint not in rejected returns false without touching trusted; DeleteRejected removes the file; UntrustCert removes from trusted only; thumbprint match is case-insensitive (operator UX); missing store directories produce empty lists instead of throwing DirectoryNotFoundException (pristine-install tolerance); a junk .der in the store is logged + skipped and the valid certs still surface (one bad file doesn't break the page). Full Admin.Tests Unit suite: 23 pass / 0 fail (14 prior + 9 new). Full Admin build clean — 0 errors, 0 warnings.
lmx-followups.md #3 marked DONE with a cross-reference to this PR and a note that flipping AutoAcceptUntrustedClientCertificates to false as the production default is a deployment-config follow-up, not a code gap — the Admin UI is now ready to be the trust gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dohertj2 merged commit 6fdaee3a71 into v2 2026-04-18 14:42:53 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#27