# Environment Variables — SCADA/OT family > Cross-project audit of every environment variable used or read by the sister projects > and the shared `ZB.MOM.WW.*` libraries. Compiled **2026-06-03** by sweeping C# reads, > Docker/compose, `launchSettings.json`, shell/PowerShell scripts, and CI for each repo. > This is a **summary index** — when a value matters operationally, confirm against the > cited `file:line` in the owning repo (paths below are relative to each project root). ## Scope | Project | Root | Covered | |---|---|---| | OtOpcUa | `~/Desktop/OtOpcUa` | Host, Galaxy/Historian drivers, docker-dev, tests, CI | | MxAccessGateway | `~/Desktop/MxAccessGateway` | Server (x64), Worker (x86), client CLI, tests, pack script | | ScadaBridge | `~/Desktop/ScadaBridge` | Host (Central/Site), CLI, `docker/`, `docker-env2/`, `infra/`, tests | | Shared libs | `~/Desktop/scadaproj/ZB.MOM.WW.*` | Auth, Theme, Health, Telemetry, Configuration, Audit (code + build scripts) | ## How env vars reach these apps All four .NET apps call `AddEnvironmentVariables()`, so **any** configuration key is overridable from the environment using the **double-underscore (`__`) → colon (`:`)** convention (`ScadaBridge__InboundApi__ApiKeyPepper` overrides `ScadaBridge:InboundApi:ApiKeyPepper`). Array indices use a trailing `__0`, `__1` (`Cluster__SeedNodes__0`). Because *every* options key is technically settable this way, the tables below split into: - **Direct reads / operationally-set** — `Environment.GetEnvironmentVariable(...)` in code, or values actually set in compose/launchSettings/scripts. These are the ones you'll really touch. - **Config keys overridable via `__`** — the validated/notable options that are normally in `appsettings*.json` but are commonly (or required to be) supplied via environment in containers. Not every options key is reproduced — only validated, secret, or container-set ones. > **Secrets:** rows marked 🔒 are secrets. Per the Auth/Config normalization, peppers/keys/passwords > are **per-environment secrets injected out-of-band** (secret store / orchestrator), never committed. > The dev-only values that *do* appear in compose are explicitly insecure placeholders for the local > clusters — see `scadabridge-local-deploy-gotchas` and `docs/operations/inbound-api-key-reissue.md`. --- ## 1. OtOpcUa ### 1.1 Direct reads / operationally-set (runtime) | Variable | Where | Purpose | Req? / default | Process | |---|---|---|---|---| | `OTOPCUA_ROLES` | `src/Server/.../Host/Program.cs:31` | Comma-list of roles (admin, driver) for conditional wiring | optional | Host | | `ASPNETCORE_ENVIRONMENT` | `Host/Properties/launchSettings.json:9` | ASP.NET Core environment | optional / `Production` | Host | | `ASPNETCORE_URLS` | `docker-dev/docker-compose.yml` | Kestrel bind address/port | optional | Host | | `GALAXY_MXGW_API_KEY` 🔒 | `docker-dev/docker-compose.yml`; resolved in `GalaxyDriver.cs:466` | mxaccessgw API key for the Galaxy driver | required if Galaxy driver deployed | Driver (Galaxy) | | `OTOPCUA_CONFIG_CONNECTION` 🔒 | `Configuration/DesignTimeDbContextFactory.cs:25` | SQL connection for EF design-time (`dotnet ef`) | required for migrations tooling | build-time | | `OTOPCUA_HISTORIAN_PIPE` | `Driver.Historian.Wonderware/Program.cs:32` | Named-pipe name for the Historian sidecar IPC | required | Historian sidecar | | `OTOPCUA_ALLOWED_SID` | `…/Program.cs:34` | Windows SID allowed to connect to the sidecar | required | Historian sidecar | | `OTOPCUA_HISTORIAN_SECRET` 🔒 | `…/Program.cs:36` | Shared secret for named-pipe auth | required | Historian sidecar | | `OTOPCUA_HISTORIAN_ENABLED` | `…/Program.cs:48` | Init the Historian SDK (else pipe-only) | optional / `false` | Historian sidecar | | `OTOPCUA_HISTORIAN_SERVER` | `…/Program.cs:89` | Wonderware Historian host | optional / `localhost` | Historian sidecar | | `OTOPCUA_HISTORIAN_PORT` | `…/Program.cs:90` | Historian port | optional / `32568` | Historian sidecar | | `OTOPCUA_HISTORIAN_INTEGRATED` | `…/Program.cs:91` | Use Windows Integrated Security | optional / `true` | Historian sidecar | | `OTOPCUA_HISTORIAN_USER` | `…/Program.cs:92` | SQL user (when not integrated) | optional | Historian sidecar | | `OTOPCUA_HISTORIAN_PASS` 🔒 | `…/Program.cs:93` | SQL password (when not integrated) | optional | Historian sidecar | | `OTOPCUA_HISTORIAN_TIMEOUT_SEC` | `…/Program.cs:94` | SQL command timeout (s) | optional / `30` | Historian sidecar | | `OTOPCUA_HISTORIAN_MAX_VALUES` | `…/Program.cs:95` | Max values per read query | optional / `10000` | Historian sidecar | | `OTOPCUA_HISTORIAN_COOLDOWN_SEC` | `…/Program.cs:96` | Failure cooldown before retry (s) | optional / `60` | Historian sidecar | | `OTOPCUA_HISTORIAN_SERVERS` | `…/Program.cs:99` | Comma-list of historian servers for failover | optional | Historian sidecar | | `OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED` | `…/Program.cs:125` | Enable alarm-event writer to historian | optional / `true` when enabled | Historian sidecar | > The Galaxy API key supports prefix resolution (`GalaxyDriver.cs:466`): `env:VAR` (read another > env var), `file:PATH` (read a file), `dev:LITERAL` (dev only), or an unprefixed literal. ### 1.2 Config keys overridable via `__` (set in `docker-dev/docker-compose.yml`) | Variable | Purpose | Req? | |---|---|---| | `ConnectionStrings__ConfigDb` 🔒 | SQL connection for the OtOpcUa config DB | required | | `Cluster__Hostname` / `Cluster__Port` / `Cluster__PublicHostname` | Akka remoting bind/advertise | required | | `Cluster__SeedNodes__0` | Initial seed node URL | required | | `Cluster__Roles__0` (`__1`) | Cluster role assignment (admin/driver) | required | | `Security__Jwt__SigningKey` 🔒 | JWT signing key (≥32 bytes) | required | | `Security__Jwt__Issuer` / `Security__Jwt__Audience` | JWT claims | required | | `Security__Ldap__*` | LDAP host/bind (in `appsettings.admin*.json`) | per-deploy | | `Authentication__Ldap__DevStubMode` | Dev LDAP stub (any user → FleetAdmin) — **removed from docker-dev**; `docker-dev` now binds the shared GLAuth on `10.100.0.35:3893` | optional / `false` | ### 1.3 Docker infra & test fixtures (OtOpcUa) - **docker-dev SQL:** `ACCEPT_EULA=Y`, `SA_PASSWORD` 🔒 (`OtOpcUa!Dev123` dev-only), `MSSQL_PID=Developer`. - **docker-dev seed (`seed/entrypoint.sh`):** `SQL_HOST` / `SQL_USER` / `SQL_PASSWORD` 🔒 / `SQL_DATABASE` (defaults `sql`/`sa`/`OtOpcUa!Dev123`/`OtOpcUa`). - **Integration compose:** SQL (`SA_PASSWORD` 🔒 `OtOpcUa!Harness123`) + OpenLDAP (`LDAP_ROOT`, `LDAP_ADMIN_USERNAME`, `LDAP_ADMIN_PASSWORD` 🔒, `LDAP_USERS`, `LDAP_PASSWORDS` 🔒, `LDAP_USER_DC`) — this OpenLDAP instance is **integration-test-only**; the standard DEV auth is the shared GLAuth at `10.100.0.35:3893` (`dc=zb,dc=local`, see `scadaproj/infra/glauth/`). - **Test-fixture overrides (all optional, default to the shared host `10.100.0.35`):** `OPCUA_SIM_ENDPOINT`, `MODBUS_SIM_ENDPOINT`, `MODBUS_SIM_PROFILE`, `AB_SERVER_ENDPOINT`, `AB_SERVER_PROFILE`, `S7_SIM_ENDPOINT`, `OTOPCUA_FOCAS_SIM_ENDPOINT`, `OTOPCUA_FOCAS_SIM_PROFILE`, `TWINCAT_HOST`, `TWINCAT_NETID`, `TWINCAT_PORT`, `AB_LEGACY_ENDPOINT`, `AB_LEGACY_CIP_PATH`, `AB_LEGACY_COMPOSE_PROFILE`, `OTOPCUA_CONFIG_TEST_SERVER`, `OTOPCUA_CONFIG_TEST_SA_PASSWORD` 🔒, `OTOPCUA_HARNESS_USE_SQL`, `OTOPCUA_HARNESS_USE_LDAP`, `MXGW_ENDPOINT`, `D1_SMOKE_OUT`. - **CI (`.github/workflows/v2-*.yml`):** `DOTNET_NOLOGO=1`, `DOTNET_CLI_TELEMETRY_OPTOUT=1`. --- ## 2. MxAccessGateway > Ships **no** Docker/compose assets — it's a Windows-native app (x64 .NET 10 Server + x86 .NET 4.8 Worker). > The Server spawns the Worker via `ProcessStartInfo.Environment`, passing the two `MXGATEWAY_WORKER_*` vars below. ### 2.1 Direct reads / operationally-set (runtime) | Variable | Where | Purpose | Req? / default | Process | |---|---|---|---|---| | `MXGATEWAY_WORKER_NONCE` 🔒 | Server `WorkerProcessLauncher.cs:180` → Worker `WorkerOptionsParser.cs:78` | Per-session handshake nonce (kept off the command line) | required (generated per session) | Server→Worker | | `MXGATEWAY_WORKER_PIPE_CONNECT_ATTEMPT_TIMEOUT_MS` | Server `WorkerProcessLauncher.cs:181` → Worker `WorkerPipeClient.cs:255` | Per-attempt named-pipe connect timeout | optional / `2000` | Server→Worker | | `ASPNETCORE_CONTENTROOT` | `GatewayApplication.cs:130` | Content-root override (logs/wwwroot) | optional / auto | Server | | `ASPNETCORE_ENVIRONMENT` | `Server/Properties/launchSettings.json:10` | Environment selector | optional / `Production` | Server | ### 2.2 Config keys overridable via `__` (notable) | Variable | Purpose | Req? | |---|---|---| | `MxGateway__ApiKeyPepper` 🔒 | HMAC pepper for API-key secrets in the SQLite auth DB | required when auth Mode=ApiKey | | `MxGateway__Authentication__Mode` / `__SqlitePath` / `__PepperSecretName` | Auth mode, auth DB path, pepper config-key name | per-deploy | | `MxGateway__Worker__ExecutablePath` / `__WorkingDirectory` / `__StartupTimeoutSeconds` / `__PipeConnectAttemptTimeoutMilliseconds` | x86 worker launch config | per-deploy | | `MxGateway__Sessions__*` / `MxGateway__Events__QueueCapacity` | Session pool & event queue tuning | optional | | `MxGateway__Galaxy__ConnectionString` 🔒 | SQL connection for Galaxy browse RPCs | per-deploy | | `MxGateway__Alarms__*` / `MxGateway__Dashboard__*` | Alarm monitor & dashboard config | optional | | `MxGateway__Telemetry__Exporter` / `__OtlpEndpoint` | OpenTelemetry exporter selection / OTLP endpoint | optional | | `MxGateway__Tls__SelfSignedCertPath` | Self-signed PFX path | optional | | `Kestrel__Endpoints__Http__Url` / `__Protocols`, `Kestrel__Endpoints__Dashboard__Url` | gRPC (h2c) + dashboard endpoints | per-deploy | ### 2.3 Client CLI, tests & build script (MxGateway) - **Client CLI / smoke tests:** `MXGATEWAY_ENDPOINT` (default `http://localhost:5000`), `MXGATEWAY_API_KEY` 🔒 (`MxGatewayClientCli.cs:289`). - **Live-test opt-in gates (set to `1` to enable; otherwise skipped):** `MXGATEWAY_RUN_LIVE_MXACCESS_TESTS`, `MXGATEWAY_RUN_LIVE_LDAP_TESTS`. - **Live-test params (optional):** `MXGATEWAY_LIVE_MXACCESS_WORKER_EXE`, `_ITEM`, `_CLIENT_NAME`, `_EVENT_TIMEOUT_SECONDS`, `_WRITE_SECURED_USER`, `_WRITE_SECURED_PASSWORD` 🔒, `MXGATEWAY_LIVE_GALAXY_CONN` 🔒. - **Pack/publish (`scripts/pack-clients.ps1`):** `GITEA_USERNAME`, `GITEA_TOKEN` 🔒 (required with `-Publish`), `JAVA_HOME` (Java client build). --- ## 3. ScadaBridge > Role is selected by `SCADABRIDGE_CONFIG` (`Central`|`Site`), which picks `appsettings.{role}.json` > (falls back to `DOTNET_ENVIRONMENT`, then `Production`). The pre-host `StartupValidator` / > `ConfigPreflight` enforces the **required** keys below and fails fast if any are missing/invalid. ### 3.1 Direct reads (C#) | Variable | Where | Purpose | Req? / default | Scope | |---|---|---|---|---| | `SCADABRIDGE_CONFIG` | `Host/Program.cs:31` | Role selector → appsettings file | optional (→ `DOTNET_ENVIRONMENT` → `Production`) | both | | `DOTNET_ENVIRONMENT` | `Host/Program.cs:32` | Fallback role/env selector | optional | both | | `ASPNETCORE_ENVIRONMENT` | `Host/Program.cs:245` | Dev-mode check | optional | both | | `SCADABRIDGE_DESIGNTIME_CONNECTIONSTRING` 🔒 | `ConfigurationDatabase/DesignTimeDbContextFactory.cs:48` | EF tooling connection (`dotnet ef`) | optional (build-time) | design-time | | `SCADABRIDGE_MANAGEMENT_URL` | `CLI/CliConfig.cs:72` | Management API URL for the CLI | optional | CLI | | `SCADABRIDGE_FORMAT` | `CLI/CliConfig.cs:76` | CLI default output format | optional / `json` | CLI | | `SCADABRIDGE_USERNAME` | `CLI/CliConfig.cs:81` | CLI LDAP username (safer than `--password`) | optional | CLI | | `SCADABRIDGE_PASSWORD` 🔒 | `CLI/CliConfig.cs:85` | CLI LDAP password | optional | CLI | ### 3.2 Config keys required at startup (`__` form, enforced by `StartupValidator`) | Variable | Purpose | Constraint | Scope | |---|---|---|---| | `ScadaBridge__Node__Role` | `Central` or `Site` | required | all | | `ScadaBridge__Node__NodeHostname` | Hostname advertised to cluster | required, non-empty | all | | `ScadaBridge__Node__RemotingPort` | Akka remoting TCP port | required, 1–65535 (default 8081) | all | | `ScadaBridge__Cluster__SeedNodes__0` / `__1` | Akka seed addresses | required, ≥2 entries | all | | `ScadaBridge__Database__ConfigurationDb` 🔒 | SQL config-DB connection | required | Central | | `ScadaBridge__Security__JwtSigningKey` 🔒 | Cookie-JWT HMAC key | required, ≥32 chars | Central | | `ScadaBridge__Security__Ldap__Server` | LDAP host | required | Central | | `ScadaBridge__InboundApi__ApiKeyPepper` 🔒 | Peppered-HMAC inbound API-key pepper | **required, ≥16 chars** | Central | | `ScadaBridge__Node__SiteId` | Site identifier | required | Site | | `ScadaBridge__Database__SiteDbPath` | Site-local SQLite path | required | Site | | `ScadaBridge__Node__GrpcPort` | gRPC streaming port | default 8083; ≠ remoting/metrics | Site | | `ScadaBridge__Node__MetricsPort` | Prometheus `/metrics` port | default 8084; ≠ remoting/grpc | Site | > LDAP service-account fields (`ScadaBridge__Security__Ldap__ServiceAccountDn`, > `__ServiceAccountPassword` 🔒, `__SearchBase`) are validated **post-host** by `LdapOptionsValidator` > for Central, not in the fail-fast pre-host pass. ### 3.3 Config keys overridable via `__` (optional, not validated) Large surface — all bound from the role appsettings and overridable via env. Grouped by module (each prefixed `ScadaBridge__`): - **Node:** `Node__NodeName`. **Database:** `Database__MachineDataDb` 🔒, `Database__SkipMigrations`. - **Security:** `Security__Ldap__Port|Transport|AllowInsecure`, `Security__JwtExpiryMinutes`, `Security__IdleTimeoutMinutes`, `Security__JwtRefreshThresholdMinutes`, `Security__RequireHttpsCookie`. - **Cluster (SBR):** `Cluster__SplitBrainResolverStrategy|StableAfter|HeartbeatInterval|FailureDetectionThreshold|MinNrOfMembers|DownIfAlone`. - **Communication:** `Communication__DeploymentTimeout|LifecycleTimeout|QueryTimeout|TransportHeartbeatInterval|TransportFailureThreshold`, `Communication__CentralContactPoints__0…` (Site→Central ClusterClient). - **HealthMonitoring:** `HealthMonitoring__ReportInterval|OfflineTimeout|CentralOfflineTimeout`. - **InboundApi:** `InboundApi__DefaultMethodTimeout|MaxRequestBodyBytes`. - **Notification (SMTP):** `Notification__SmtpServer|SmtpPort|AuthMode|FromAddress|ConnectionTimeoutSeconds|MaxConcurrentConnections`. - **NotificationOutbox (Central):** `NotificationOutbox__DispatchInterval|DispatchBatchSize|StuckAgeThreshold|TerminalRetention|PurgeInterval|DeliveredKpiWindow`. - **Transport (Central):** `Transport__SourceEnvironment|BundleSessionTtlMinutes|MaxBundleSizeMb|MaxBundleEntryDecompressedMb|MaxBundleEntryCount|MaxBundleEntryCompressionRatio|MaxUnlockAttemptsPerSession|MaxUnlockAttemptsPerIpPerHour|Pbkdf2Iterations`. - **Logging:** `Logging__MinimumLevel`. - **DataConnection (Site):** `DataConnection__ReconnectInterval|TagResolutionRetryInterval|WriteTimeout|StableConnectionThreshold`. - **StoreAndForward (Site):** `StoreAndForward__SqliteDbPath|ReplicationEnabled|DefaultRetryInterval|DefaultMaxRetries`. - **SiteEventLog (Site):** `SiteEventLog__RetentionDays|MaxStorageMb|DatabasePath|PurgeInterval`. - **SiteRuntime (Site):** `SiteRuntime__StartupBatchSize|StartupBatchDelayMs|MaxScriptCallDepth|ScriptExecutionTimeoutSeconds|StreamBufferSize|ScriptExecutionThreadCount`. - **Telemetry (Site):** `Telemetry__Exporter|OtlpEndpoint`. ### 3.4 Docker / infra / build (ScadaBridge) - **`docker/docker-compose.yml`** (3-site cluster) — Central: `SCADABRIDGE_CONFIG=Central`, `ASPNETCORE_ENVIRONMENT=Development`, `ASPNETCORE_URLS=http://+:5000`, `ScadaBridge__InboundApi__ApiKeyPepper` 🔒 = `dev-only-insecure-pepper-docker-cluster-0001`; Sites: `SCADABRIDGE_CONFIG=Site`. - **`docker-env2/docker-compose.yml`** (1-site cluster) — same shape, pepper 🔒 = `dev-only-insecure-pepper-env2-cluster-0001` (distinct per environment). - **`infra/docker-compose.yml`** — MSSQL (`ACCEPT_EULA=Y`, `MSSQL_SA_PASSWORD` 🔒 `ScadaBridge_Dev1#`, `MSSQL_PID=Developer`), Mailpit SMTP (`MP_SMTP_AUTH_ACCEPT_ANY=1`, `MP_SMTP_AUTH_ALLOW_INSECURE=1`, `MP_MAX_MESSAGES=500`), REST API (`API_NO_AUTH=0`, `PORT=5200`). - **`docker/Dockerfile` build args:** `NUGET_GITEA_USER` / `NUGET_GITEA_PASS` 🔒 → injected as `NuGetPackageSourceCredentials_dohertj2-gitea`. **`docker/build.sh`** reads `MXGW_NUGET_USER` / `MXGW_NUGET_PASS` 🔒 from host env (blank ⇒ anonymous feed). - **Seed/init scripts** (`docker*/init-db.sh`, `seed-sites.sh`) hardcode the dev SA password 🔒 `ScadaBridge_Dev1#`. - **`launchSettings.json`** profiles set `DOTNET_ENVIRONMENT`, `ASPNETCORE_ENVIRONMENT`, `SCADABRIDGE_CONFIG` (`Central`/`Site`). --- ## 4. Shared libraries (`ZB.MOM.WW.*`) The libraries deliberately read almost nothing from the environment directly — config flows through strongly-typed options bound by the **consuming** app. Notable exceptions: | Variable | Where | Purpose | Req? / default | |---|---|---|---| | `GITEA_NUGET_SOURCE` | `ZB.MOM.WW.{Auth,Theme,Audit}/build/push.sh:16` | Gitea NuGet feed URL for `dotnet nuget push` | required to publish | | `GITEA_NUGET_KEY` 🔒 | `…/build/push.sh:17` | Gitea token (`package:write`) | required to publish | | `ZB_LDAP_IT` | `ZB.MOM.WW.Auth/tests/.../GLAuthIntegrationTests.cs:48` | Gate flag (`1`) to run the live LDAP test | optional (skipped if unset) | | `ZB_LDAP_SERVER` / `_PORT` / `_BASE` / `_SVC_DN` / `_SVC_PW` 🔒 / `_USER` / `_PW` 🔒 / `_USERATTR` | `…/GLAuthIntegrationTests.cs:52-59` | Live LDAP test connection params | optional (defaults: `localhost`/`3893`/`dc=zb,dc=local`/…); point at the shared GLAuth (`10.100.0.35:3893`, `dc=zb,dc=local`) for the live test | **Telemetry:** `ZB.MOM.WW.Telemetry` does **not** read standard `OTEL_*` env vars — OTel identity/exporter come from `ZbTelemetryOptions` passed to `AddZbTelemetry()`. It only reads system properties (`Environment.MachineName`, `Environment.ProcessId` in `ZbResource.cs:20`) to form `service.instance.id` / `host.name`. Consuming apps that want OTLP wire it via their own `…Telemetry__OtlpEndpoint` config key (see MxGateway §2.2, ScadaBridge §3.3). **Health, Configuration, Audit, Theme:** no direct environment-variable reads (code or build). --- ## 5. Cross-cutting cheat sheet ### Standard framework vars (honored everywhere) `ASPNETCORE_ENVIRONMENT`, `ASPNETCORE_URLS`, `ASPNETCORE_CONTENTROOT`, `DOTNET_ENVIRONMENT`, `DOTNET_NOLOGO`, `DOTNET_CLI_TELEMETRY_OPTOUT`. ### Build/publish (Gitea NuGet feed) — naming differs per repo | Repo | Vars | |---|---| | Shared libs | `GITEA_NUGET_SOURCE`, `GITEA_NUGET_KEY` 🔒 | | MxGateway pack | `GITEA_USERNAME`, `GITEA_TOKEN` 🔒 | | ScadaBridge image build | `MXGW_NUGET_USER`, `MXGW_NUGET_PASS` 🔒 (→ `NUGET_GITEA_USER`/`NUGET_GITEA_PASS` build args) | ### Secrets inventory (🔒) — inject out-of-band, never commit real values - **Peppers:** `ScadaBridge__InboundApi__ApiKeyPepper` (≥16, Central-only), `MxGateway__ApiKeyPepper`. - **Signing/JWT keys:** `Security__Jwt__SigningKey` (OtOpcUa), `ScadaBridge__Security__JwtSigningKey`. - **API keys / nonces:** `GALAXY_MXGW_API_KEY`, `MXGATEWAY_API_KEY`, `MXGATEWAY_WORKER_NONCE`, `OTOPCUA_HISTORIAN_SECRET`. - **DB / LDAP / SMTP passwords:** all `*SA_PASSWORD`/`MSSQL_SA_PASSWORD`, `ConnectionStrings__ConfigDb`, `ScadaBridge__Database__ConfigurationDb`, `*Ldap*Password`, `SCADABRIDGE_PASSWORD`, `OTOPCUA_HISTORIAN_PASS`. - **Feed tokens:** `GITEA_NUGET_KEY`, `GITEA_TOKEN`, `MXGW_NUGET_PASS`/`NUGET_GITEA_PASS`. > The only secret-typed values that legitimately appear in source are the **dev-only, insecure** > local-cluster placeholders in `docker*/docker-compose.yml` and the dev SA passwords in the > `infra`/seed scripts — usable for the local stacks only, never as real secrets.