From e2d149089cbccd68dbc6784cf089598796f9c8c6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 19 Aug 2026 14:39:09 -0400 Subject: [PATCH] infra(docker-env2)+infra(docker): init:true + secrets posture parity for env2; regen-proto EXIT trap (arch-review 2026-08-19 P8-2 + P8-3 + P8-4) --- .../central-node-a/appsettings.Central.json | 17 ++++--- .../central-node-b/appsettings.Central.json | 17 ++++--- docker-env2/docker-compose.yml | 45 +++++++++++++++++++ docker/regen-proto.sh | 28 ++++++++---- 4 files changed, 89 insertions(+), 18 deletions(-) diff --git a/docker-env2/central-node-a/appsettings.Central.json b/docker-env2/central-node-a/appsettings.Central.json index f4f6ba5e..31d66d41 100644 --- a/docker-env2/central-node-a/appsettings.Central.json +++ b/docker-env2/central-node-a/appsettings.Central.json @@ -20,20 +20,27 @@ "MinNrOfMembers": 1 }, "Database": { - "ConfigurationDb": "Server=scadabridge-mssql,1433;Database=ScadaBridgeConfig2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true", - "MachineDataDb": "Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" + "_comment": "ConfigurationDb/MachineDataDb (which carry the dev SQL password) are supplied as ScadaBridge__Database__* whole-key env overrides in docker-env2/docker-compose.yml (dev-only-insecure, mirroring docker/docker-compose.yml and the ApiKeyPepper convention). The same dev password already ships committed for the sibling scadabridge-mssql container (infra/docker-compose.yml MSSQL_SA_PASSWORD) — this is consolidation, not new exposure. Env overrides layer over JSON before the ${secret:} expander runs, so the dev cluster boots with no KEK/secret store. Real/prod config uses ${secret:} tokens in src/.../appsettings.Central.json (T4)." }, "Security": { "Ldap": { - "Server": "10.100.0.35", + "Server": "host.docker.internal", "Port": 3893, + "FallbackServers": [ + "host.docker.internal:3894" + ], + "_comment_FallbackServers": "Redundant local GLAuth pair (scadaproj/infra/glauth-redundant): zb-ldap-primary :3893 + zb-ldap-backup :3894 on the Docker host — mirrors docker/central-node-a/appsettings.Central.json. Replaces the shared 10.100.0.35 GLAuth, whose serviceaccount password was rotated (SEC-36) away from the rig's dev bind creds; env2 hit this directly (every login failing) before this fix.", "Transport": "None", "AllowInsecure": true, "SearchBase": "dc=zb,dc=local", "ServiceAccountDn": "cn=serviceaccount,dc=zb,dc=local", - "ServiceAccountPassword": "serviceaccount123" + "_comment_ServiceAccountPassword": "Supplied as ScadaBridge__Security__Ldap__ServiceAccountPassword env override in docker-env2/docker-compose.yml (dev-only-insecure)." }, - "JwtSigningKey": "scadabridge-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long", + "Auth": { + "DisableLogin": false, + "User": "multi-role" + }, + "_comment_JwtSigningKey": "Supplied as ScadaBridge__Security__JwtSigningKey env override in docker-env2/docker-compose.yml (dev-only-insecure).", "JwtExpiryMinutes": 15, "IdleTimeoutMinutes": 30, "RequireHttpsCookie": false, diff --git a/docker-env2/central-node-b/appsettings.Central.json b/docker-env2/central-node-b/appsettings.Central.json index 8850c3ca..b8b588cd 100644 --- a/docker-env2/central-node-b/appsettings.Central.json +++ b/docker-env2/central-node-b/appsettings.Central.json @@ -20,20 +20,27 @@ "MinNrOfMembers": 1 }, "Database": { - "ConfigurationDb": "Server=scadabridge-mssql,1433;Database=ScadaBridgeConfig2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true", - "MachineDataDb": "Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" + "_comment": "ConfigurationDb/MachineDataDb (which carry the dev SQL password) are supplied as ScadaBridge__Database__* whole-key env overrides in docker-env2/docker-compose.yml (dev-only-insecure, mirroring docker/docker-compose.yml and the ApiKeyPepper convention). The same dev password already ships committed for the sibling scadabridge-mssql container (infra/docker-compose.yml MSSQL_SA_PASSWORD) — this is consolidation, not new exposure. Env overrides layer over JSON before the ${secret:} expander runs, so the dev cluster boots with no KEK/secret store. Real/prod config uses ${secret:} tokens in src/.../appsettings.Central.json (T4)." }, "Security": { "Ldap": { - "Server": "10.100.0.35", + "Server": "host.docker.internal", "Port": 3893, + "FallbackServers": [ + "host.docker.internal:3894" + ], + "_comment_FallbackServers": "Redundant local GLAuth pair (scadaproj/infra/glauth-redundant): zb-ldap-primary :3893 + zb-ldap-backup :3894 on the Docker host — mirrors docker/central-node-b/appsettings.Central.json. Replaces the shared 10.100.0.35 GLAuth, whose serviceaccount password was rotated (SEC-36) away from the rig's dev bind creds; env2 hit this directly (every login failing) before this fix.", "Transport": "None", "AllowInsecure": true, "SearchBase": "dc=zb,dc=local", "ServiceAccountDn": "cn=serviceaccount,dc=zb,dc=local", - "ServiceAccountPassword": "serviceaccount123" + "_comment_ServiceAccountPassword": "Supplied as ScadaBridge__Security__Ldap__ServiceAccountPassword env override in docker-env2/docker-compose.yml (dev-only-insecure)." }, - "JwtSigningKey": "scadabridge-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long", + "Auth": { + "DisableLogin": false, + "User": "multi-role" + }, + "_comment_JwtSigningKey": "Supplied as ScadaBridge__Security__JwtSigningKey env override in docker-env2/docker-compose.yml (dev-only-insecure).", "JwtExpiryMinutes": 15, "IdleTimeoutMinutes": 30, "RequireHttpsCookie": false, diff --git a/docker-env2/docker-compose.yml b/docker-env2/docker-compose.yml index 6d0ec16c..0ef6e111 100644 --- a/docker-env2/docker-compose.yml +++ b/docker-env2/docker-compose.yml @@ -11,6 +11,10 @@ x-secrets-store-env: &secrets-store-env services: central-a: image: scadabridge:latest + # An init process (tini) as PID 1, so a crashed dotnet process actually dies + # instead of wedging the container at 100% CPU with the restart policy never + # firing — mirrors docker/docker-compose.yml's init: true (ScadaBridge#34). + init: true # CoordinatedShutdown needs cluster-leave (15s budget) + cluster-exiting + # actor-system-terminate + Serilog flush; the 10s SIGTERM default SIGKILLed # mid-drain, turning every redeploy into the crash path (review 01 [Medium]). @@ -27,6 +31,25 @@ services: # pepper per the "different per environment" guidance; real deployments inject a # true secret out-of-band, never from source control. Both Central nodes share it. ScadaBridge__InboundApi__ApiKeyPepper: "dev-only-insecure-pepper-env2-cluster-0001" + # DEV-ONLY local-cluster secrets — NOT real secrets. Relocated out of the mounted + # appsettings.Central.json (mirrors docker/docker-compose.yml's G-4 T5 secrets + # cleanup) so that app-config file carries no plaintext credentials. Env overrides + # layer over JSON BEFORE the ${secret:} pre-host expander runs. The SQL password is + # the same value already committed for the sibling scadabridge-mssql container + # (infra/docker-compose.yml MSSQL_SA_PASSWORD) and for docker/'s central nodes — + # consolidation, not new exposure; env2's Config2/MachineData2 databases live on + # that SAME shared mssql container. Real deployments use ${secret:} tokens + + # a seeded store. + ScadaBridge__Database__ConfigurationDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeConfig2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" + ScadaBridge__Database__MachineDataDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" + # LDAP bind credential for the redundant local GLAuth pair (scadaproj/infra/glauth-redundant, + # host.docker.internal:3893/:3894 — see central-node-a/appsettings.Central.json). Same + # value docker/'s central nodes use for the SAME local pair; the shared 10.100.0.35 + # GLAuth's serviceaccount password was rotated (SEC-36) and is no longer valid. + ScadaBridge__Security__Ldap__ServiceAccountPassword: "serviceaccount123" + # DEV-ONLY local-cluster secret — NOT a real secret. Distinct from docker/'s JWT key + # so sessions cannot cross environments (see docker-env2/README.md). + ScadaBridge__Security__JwtSigningKey: "scadabridge-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long" # DEV-ONLY gRPC control-plane preshared key for site-x — NOT a real secret. # Must match ScadaBridge:Communication:GrpcPsk in site-x-node-*/appsettings.Site.json. # Production seeds SB-GRPC-PSK- into the secret store instead. @@ -47,6 +70,7 @@ services: central-b: image: scadabridge:latest + init: true # PID-1 crash wedge, ScadaBridge#34 — see central-a # CoordinatedShutdown needs cluster-leave (15s budget) + cluster-exiting + # actor-system-terminate + Serilog flush; the 10s SIGTERM default SIGKILLed # mid-drain, turning every redeploy into the crash path (review 01 [Medium]). @@ -63,6 +87,25 @@ services: # pepper per the "different per environment" guidance; real deployments inject a # true secret out-of-band, never from source control. Both Central nodes share it. ScadaBridge__InboundApi__ApiKeyPepper: "dev-only-insecure-pepper-env2-cluster-0001" + # DEV-ONLY local-cluster secrets — NOT real secrets. Relocated out of the mounted + # appsettings.Central.json (mirrors docker/docker-compose.yml's G-4 T5 secrets + # cleanup) so that app-config file carries no plaintext credentials. Env overrides + # layer over JSON BEFORE the ${secret:} pre-host expander runs. The SQL password is + # the same value already committed for the sibling scadabridge-mssql container + # (infra/docker-compose.yml MSSQL_SA_PASSWORD) and for docker/'s central nodes — + # consolidation, not new exposure; env2's Config2/MachineData2 databases live on + # that SAME shared mssql container. Real deployments use ${secret:} tokens + + # a seeded store. + ScadaBridge__Database__ConfigurationDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeConfig2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" + ScadaBridge__Database__MachineDataDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" + # LDAP bind credential for the redundant local GLAuth pair (scadaproj/infra/glauth-redundant, + # host.docker.internal:3893/:3894 — see central-node-b/appsettings.Central.json). Same + # value docker/'s central nodes use for the SAME local pair; the shared 10.100.0.35 + # GLAuth's serviceaccount password was rotated (SEC-36) and is no longer valid. + ScadaBridge__Security__Ldap__ServiceAccountPassword: "serviceaccount123" + # DEV-ONLY local-cluster secret — NOT a real secret. Distinct from docker/'s JWT key + # so sessions cannot cross environments (see docker-env2/README.md). + ScadaBridge__Security__JwtSigningKey: "scadabridge-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long" # DEV-ONLY gRPC control-plane preshared key for site-x — NOT a real secret. # Must match ScadaBridge:Communication:GrpcPsk in site-x-node-*/appsettings.Site.json. # Production seeds SB-GRPC-PSK- into the secret store instead. @@ -83,6 +126,7 @@ services: site-x-a: image: scadabridge:latest + init: true # PID-1 crash wedge, ScadaBridge#34 — see central-a # CoordinatedShutdown needs cluster-leave (15s budget) + cluster-exiting + # actor-system-terminate + Serilog flush; the 10s SIGTERM default SIGKILLed # mid-drain, turning every redeploy into the crash path (review 01 [Medium]). @@ -111,6 +155,7 @@ services: site-x-b: image: scadabridge:latest + init: true # PID-1 crash wedge, ScadaBridge#34 — see central-a # CoordinatedShutdown needs cluster-leave (15s budget) + cluster-exiting + # actor-system-terminate + Serilog flush; the 10s SIGTERM default SIGKILLed # mid-drain, turning every redeploy into the crash path (review 01 [Medium]). diff --git a/docker/regen-proto.sh b/docker/regen-proto.sh index ca5ecbcf..a96d51a5 100755 --- a/docker/regen-proto.sh +++ b/docker/regen-proto.sh @@ -50,12 +50,25 @@ if [[ ! -f "$CSPROJ" ]]; then exit 1 fi -# Backup so we can always restore the comment state on failure. Leaving the -# csproj with an active Protobuf item is the one outcome that breaks Docker, so -# every exit path restores this copy. +# Backup so we can always restore the comment state, however this script ends. +# Leaving the csproj with an active Protobuf item is the one outcome that breaks +# Docker, so every exit path restores this copy — success, a failed `dotnet +# build` (ERR), or Ctrl-C (INT/TERM) mid-build. An EXIT trap covers all of +# those (bash runs it on any shell termination, signals included), and the +# guard on $BACKUP's existence makes it idempotent: the normal success path +# below restores explicitly for prompt feedback, so by the time the trap fires +# on a clean exit $BACKUP is already gone and it's a no-op instead of a second +# restore/rm. BACKUP="$(mktemp)" cp "$CSPROJ" "$BACKUP" -trap 'cp "$BACKUP" "$CSPROJ"; rm -f "$BACKUP"; echo "Restored csproj from backup."' ERR +restore_csproj() { + if [[ -f "$BACKUP" ]]; then + cp "$BACKUP" "$CSPROJ" + rm -f "$BACKUP" + echo "Restored csproj from backup." + fi +} +trap restore_csproj EXIT # 1. Inject an ItemGroup holding just the selected protos, immediately before # the closing . The documented commented-out block is left alone. @@ -117,10 +130,9 @@ if [[ "$TARGET" == "sitecommand" || "$TARGET" == "all" ]]; then fi # 5. Restore the backed-up csproj — i.e. drop the injected ItemGroup — so Docker -# builds keep working. -cp "$BACKUP" "$CSPROJ" -rm -f "$BACKUP" -trap - ERR +# builds keep working. (Also covered by the EXIT trap above; calling it here +# too just gives the success path its "Restored..." feedback before "Done".) +restore_csproj echo "" echo "Done. Review and commit:"