Files
ScadaBridge/docker/site-c-node-a/appsettings.Site.json
T
Joseph Doherty ff8354478c fix(dcl): OPC UA secure client presents a real application certificate (#121)
Sign/SignAndEncrypt sessions failed before activation with "ApplicationCertificate
for the security profile ... cannot be found" (BadConfigurationError): both
RealOpcUaClient.ConnectAsync and the verify probe built an empty CertificateIdentifier.

- New OpcUaApplicationIdentity builds the client ApplicationConfiguration for both
  paths. Secured modes load the application certificate from a configurable own
  store (subject + ApplicationUri), auto-creating a self-signed RSA-2048 cert only
  when none exists. A persisted cert is reused across reconnect/restart and never
  silently rotated: one that no longer validates (e.g. ApplicationUri mismatch)
  fails closed. First-time creation is serialised per own store and logged at
  Warning with the thumbprint. None mode is unchanged and never touches the store.
- New OpcUaGlobalOptions: ApplicationUri, ApplicationCertificateSubject,
  OwnCertificateStorePath, AutoCreateApplicationCertificate,
  ApplicationCertificateLifetimeMonths (validated).
- Owned resources: the private-key cert is disposed on disconnect, failed connect
  and at probe end; sessions are now disposed after close, and a reconnect on the
  same instance closes the previous session first.
- CertStoreActor wrote trusted server certs to the store ROOT, which the SDK never
  reads (it uses {store}/certs), so an operator Trust never affected validation and
  SDK-rejected certs never listed. It now uses certs/ and migrates legacy root files
  on start.
- Rig: all site nodes (docker/, docker-env2/) put the four PKI stores on the
  persistent /app/data volume with a pinned ApplicationUri.
- Tests: in-process strict-trust StandardServer (test-only Opc.Ua.Server 1.5.378.106)
  exercising the real adapter through ConnectAsync and VerifyEndpointAsync for Sign
  and SignAndEncrypt: allow + read + reconnect + restart on one identity, deny when
  the client doesn't trust the server (BadCertificateUntrusted), deny when the server
  doesn't trust the client (BadSecurityChecksFailed), None untouched, concurrent
  provisioning creates one cert, URI mismatch fails without rotating, auto-create off.
  Against the pre-fix client these tests fail with the exact #121 error.
2026-11-10 13:56:47 -05:00

95 lines
3.8 KiB
JSON

{
"ScadaBridge": {
"Node": {
"Role": "Site",
"NodeName": "node-a",
"NodeHostname": "scadabridge-site-c-a",
"SiteId": "site-c",
"RemotingPort": 8082,
"GrpcPort": 8083,
"MetricsPort": 8084
},
"Cluster": {
"SeedNodes": [
"akka.tcp://scadabridge@scadabridge-site-c-a:8082",
"akka.tcp://scadabridge@scadabridge-site-c-b:8082"
],
"SplitBrainResolverStrategy": "auto-down",
"StableAfter": "00:00:15",
"HeartbeatInterval": "00:00:02",
"FailureDetectionThreshold": "00:00:10",
"MinNrOfMembers": 1,
"_bootstrapGuard": "Gitea #33 guard ENABLED on the docker rig (2026-08-02): deploy.sh recreates all containers simultaneously, which twice split site pairs into two 1-node clusters on 2026-08-01. Lower host:port founds self-first; the higher node TCP-probes then joins peer-first.",
"BootstrapGuard": {
"Enabled": true,
"PartnerProbeSeconds": 25,
"PartnerProbeIntervalMs": 500,
"ProbeConnectTimeoutMs": 1000
}
},
"OpcUa": {
"ApplicationUri": "urn:scadabridge-site-c-node-a:ScadaBridge-DCL",
"OwnCertificateStorePath": "/app/data/pki/own",
"TrustedPeerStorePath": "/app/data/pki/trusted",
"TrustedIssuerStorePath": "/app/data/pki/issuers",
"RejectedCertificateStorePath": "/app/data/pki/rejected"
},
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
"WriteTimeout": "00:00:30",
"SeedReadTimeout": "00:00:30"
},
"Communication": {
// DEV-ONLY control-plane preshared key — NOT a real secret. Must be
// IDENTICAL on both nodes of the pair and match the central-side entry in
// ScadaBridge__Communication__SitePsks__<siteId> (docker-compose.yml).
// Production supplies this as ${secret:SB-GRPC-PSK-<siteId>}. Without it the
// node fails StartupValidator: the gate is fail-closed, so an unset key would
// refuse every SiteStream call while the node still looked healthy.
"GrpcPsk": "dev-grpc-psk-docker-site-c",
"CentralGrpcEndpoints": [
"http://scadabridge-central-a:8083",
"http://scadabridge-central-b:8083"
],
"DeploymentTimeout": "00:02:00",
"LifecycleTimeout": "00:00:30",
"QueryTimeout": "00:00:30",
"TransportHeartbeatInterval": "00:00:05",
"TransportFailureThreshold": "00:00:15"
},
"HealthMonitoring": {
"ReportInterval": "00:00:30",
"OfflineTimeout": "00:01:00"
},
"SiteEventLog": {
"RetentionDays": 30,
"MaxStorageMb": 1024,
"_comment": "Retention uses the configured PurgeInterval delay after each purge; no wall-clock cron schedule is supported."
},
"Notification": {},
"Logging": {
"MinimumLevel": "Information"
}
},
// Consolidated site database (LocalDb Phase 1): OperationTracking + site_events.
// On the mounted /app/data volume so it survives container recreate - unlike the
// legacy site-tracking.db / site_events.db, which defaulted to CWD-relative paths
// outside the volume and were lost on every recreate.
// Replication is opt-in and configured separately; absent = local-only.
"LocalDb": {
"Path": "/app/data/site-localdb.db"
},
// arch-review remediation WP1.2: the site hot-path audit writer's SQLite file has no
// default path (mirrors LocalDb:Path above) and StartupValidator now requires it for
// Site nodes — an unset value used to fall back to a bare "auditlog.db" resolved
// relative to CWD, i.e. the container's ephemeral overlayfs, so the pending audit
// backlog was silently discarded on every recreate. On the mounted /app/data volume,
// same as LocalDb:Path, so it survives container recreate.
"AuditLog": {
"SiteWriter": {
"DatabasePath": "/app/data/auditlog.db"
}
}
}