From bb138d525468cfe983c874ca0b6eb39acb4730ce Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 24 Jul 2026 05:02:24 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20add=20env=5Fvariables.md=20=E2=80=94=20?= =?UTF-8?q?full=20environment-variable=20inventory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deep-scan catalog of every environment variable ScadaBridge reads (Host, CLI, DelmiaNotifier, EF design-time tooling, test/CI harness) plus supporting infra containers. Each entry carries scope (Runtime/Design-time/Test/Infra), whether a shipped appsettings key already exists for it, consumer, purpose, potential values, and required-ness. Compiled from GetEnvironmentVariable call sites, the Host AddEnvironmentVariables() source, docker/docker-env2 compose, install.ps1, and all appsettings*.json. --- env_variables.md | 169 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 env_variables.md diff --git a/env_variables.md b/env_variables.md new file mode 100644 index 00000000..2f419661 --- /dev/null +++ b/env_variables.md @@ -0,0 +1,169 @@ +# Environment Variables + +A deep-scan inventory of every environment variable ScadaBridge reads — the Host, the CLI, the +DelmiaNotifier client, EF design-time tooling, and the test/CI harness — plus the supporting infra +containers. Compiled by scanning `Environment.GetEnvironmentVariable` call sites, the Host's +`AddEnvironmentVariables()` config source, `docker/` + `docker-env2/` compose files, +`deploy/wonder-app-vd03/install.ps1`, and every `appsettings*.json`. + +**Scan date:** 2026-07-24 · **Branch:** `main` + +Two columns qualify each variable: + +- **Scope** — where it is read: **Runtime** (the running product — Host / CLI / DelmiaNotifier), + **Design-time** (`dotnet ef` tooling), **Test** (unit / integration / E2E / perf harness only, never + the shipped product), or **Infra** (a sidecar container, not ScadaBridge code). +- **In appsettings?** — whether a shipped `appsettings*.json` already carries the same config key + (so the env var is an *override* or *secret substitute*), versus being env-only. `${secret:...}` + means the key is present but its value is a secret token, not a literal. + +## How configuration resolves (read this first) + +The Host builds its configuration in `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs` in this precedence +order (later overrides earlier): + +1. `appsettings.json` +2. `appsettings.{SCADABRIDGE_CONFIG}.json` (role file — `Central` or `Site`) +3. **`.AddEnvironmentVariables()`** ← every variable below in the "config-override" table +4. `.AddCommandLine(args)` + +Then a **pre-host secrets expander** resolves any `${secret:NAME}` tokens left in the merged config, +reading the encrypted secrets store whose master key comes from **`ZB_SECRETS_MASTER_KEY`**. Because +step 3 runs *before* expansion, a whole-key env override (e.g. `ScadaBridge__Database__ConfigurationDb`) +wins over — and short-circuits — the corresponding `${secret:...}` token. That is the deliberate +"rollback / dev" path the docker rigs use. + +**Config-key → env-var mapping:** replace each `:` in a config path with `__` (double underscore). +So `ScadaBridge:Communication:GrpcPsk` is set via `ScadaBridge__Communication__GrpcPsk`. Any config +key in the app can be overridden this way; the table below lists only the ones with an established +purpose (secrets kept off disk, or dev-rig wiring). + +--- + +## 1. Role & host environment selection + +| Variable | Scope | In appsettings? | Consumed by | Purpose | Potential values | Required | +|---|---|---|---|---|---|---| +| `SCADABRIDGE_CONFIG` | Runtime | No (it *selects* the `appsettings.{value}.json` file) | Host (`Program.cs:42`) | Selects the role-specific overlay. **Primary role switch.** | `Central`, `Site` | Effectively yes (falls back to `DOTNET_ENVIRONMENT`, then `Production`) | +| `DOTNET_ENVIRONMENT` | Runtime | No | Host (`Program.cs:43`) | Fallback role selector when `SCADABRIDGE_CONFIG` is unset; generic .NET environment name. | `Central`, `Site`, `Development`, `Production` | No | +| `ASPNETCORE_ENVIRONMENT` | Runtime | No | ASP.NET Core + `DisableLoginGuard` (`Program.cs:438`) | Standard ASP.NET environment. `DisableLogin` is refused unless this is `Development`. | `Development`, `Production` | No (defaults to `Production` behavior) | +| `ASPNETCORE_URLS` | Runtime | No (Kestrel binding; not in these appsettings) | Kestrel | HTTP bind addresses. Docker sets `http://+:5000`; install.ps1 sets `http://+:{WebPort}` (8085). | e.g. `http://+:5000`, `http://+:8085` | No | + +> `SCADABRIDGE_CONFIG` and `ASPNETCORE_ENVIRONMENT` are independent. The docker rig runs +> `SCADABRIDGE_CONFIG=Central` **and** `ASPNETCORE_ENVIRONMENT=Development` simultaneously. + +## 2. Secrets store master key (KEK) + +| Variable | Scope | In appsettings? | Consumed by | Purpose | Potential values | Required | +|---|---|---|---|---|---|---| +| `ZB_SECRETS_MASTER_KEY` | Runtime | Name only (`Secrets:MasterKey:EnvVarName` in `appsettings.json`; the value is never committed) | Host secrets expander | The KEK that decrypts the `ZB.MOM.WW.Secrets` store, from which every `${secret:...}` token resolves. A missing reference fails closed (`SecretNotFoundException`) before any SQL/LDAP/cluster wiring. | an opaque base64/hex master key | Yes on any node whose appsettings uses `${secret:...}` (shipped Central + Site do). Docker rigs avoid it with whole-key overrides. | + +The variable *name* is itself configurable via `Secrets:MasterKey:EnvVarName` (defaults to +`ZB_SECRETS_MASTER_KEY`). See `docs/operations/2026-07-16-secrets-clustered-master-key.md`. + +## 3. Config-override env vars (`ScadaBridge__*` whole-key) + +Map onto config keys via the `:`→`__` rule. Keep secrets off disk (production, via a secret manager) +or wire the dev rigs without a KEK. The `__site-x` / `__site-a` leaf is the literal `siteId`. +All are **Runtime** scope (the Host reads them; Host integration-test fixtures set the same names, but +that is the same variable, not a separate one). + +| Variable | In appsettings? | Config key | Purpose | Potential values | Required | +|---|---|---|---|---|---| +| `ScadaBridge__Database__ConfigurationDb` | Yes — `${secret:sql/scadabridge/configdb-connection}` in `appsettings.Central.json` | `ScadaBridge:Database:ConfigurationDb` | Central config DB connection string (MS SQL). Central-only. | `Server=host,1433;Database=ScadaBridgeConfig;User Id=...;Password=...;TrustServerCertificate=true` | Yes on Central — via env **or** the `${secret}` | +| `ScadaBridge__Database__MachineDataDb` | No in shipped src appsettings; present in the deploy overlay (`deploy/wonder-app-vd03/appsettings.Central.json`, as `${...}`). install.ps1 derives it as `MachineData`. | `ScadaBridge:Database:MachineDataDb` | Central machine-data DB connection string. Central-only. | connection string | Yes on Central | +| `ScadaBridge__Security__JwtSigningKey` | Yes — `${secret:security/scadabridge/jwt-signing-key}` in `appsettings.Central.json` | `ScadaBridge:Security:JwtSigningKey` | HMAC-SHA256 signing key for the cookie-embedded session JWT. | ≥32-char secret | Yes on Central | +| `ScadaBridge__Security__Ldap__ServiceAccountPassword` | Yes — `${secret:ldap/scadabridge/service-account-password}` in `appsettings.Central.json` | `ScadaBridge:Security:Ldap:ServiceAccountPassword` | LDAP bind-account password. (Renamed from the old flat `...__LdapServiceAccountPassword`.) | the bind password | Yes on Central if LDAP bind is used | +| `ScadaBridge__InboundApi__ApiKeyPepper` | No — env/secret only (not in any shipped appsettings) | `ScadaBridge:InboundApi:ApiKeyPepper` | Per-environment pepper for the inbound API-key peppered-HMAC verifier. Hard Central startup requirement since 2026-06-03. | ≥16-char secret, unique per environment | Yes on Central | +| `ScadaBridge__Communication__GrpcPsk` | Yes — `${secret:SB-GRPC-PSK-site-1}` in `appsettings.Site.json` | `ScadaBridge:Communication:GrpcPsk` | **Site side** of the per-site gRPC control-plane PSK. StartupValidator refuses to boot a Site node without it; both pair nodes carry the same value. | per-site secret (e.g. `dev-grpc-psk-docker-site-a`); prod `${secret:SB-GRPC-PSK-}` | Yes on Site | +| `ScadaBridge__Communication__SitePsks__` | No in shipped src appsettings (central normally resolves the `SB-GRPC-PSK-` secret); present only in the docker-compose env blocks | `ScadaBridge:Communication:SitePsks:` | **Central side** of the same key: the value central presents when dialling that site. Must equal the site's `GrpcPsk`. Override for hosts without a master secret store. | matches the site's `GrpcPsk` | On Central, either this **or** the `SB-GRPC-PSK-` store secret | + +Docker dev values (dev-only-insecure, committed in `docker/docker-compose.yml`): +`ScadaBridge__Communication__SitePsks__site-a = dev-grpc-psk-docker-site-a` (also `site-b`, `site-c`); +`docker-env2` uses `SitePsks__site-x = dev-grpc-psk-docker-env2-site-x`. + +> Any other config key is overridable the same way (e.g. `ScadaBridge__Cluster__SplitBrainResolverStrategy`, +> `ScadaBridge__Logging__MinimumLevel`) — these are the standard .NET config binder, not "special" env +> vars. Prefer `appsettings` files for non-secret values. + +### `${secret:...}` tokens (resolved from the store, NOT env vars) + +Not environment variables — config values resolved by the secrets expander using `ZB_SECRETS_MASTER_KEY`. +All are **Runtime** scope and, by definition, present **In appsettings** as tokens. + +| Token (in `appsettings`) | Meaning | +|---|---| +| `${secret:sql/scadabridge/configdb-connection}` | Central config DB connection string | +| `${secret:ldap/scadabridge/service-account-password}` | LDAP bind password | +| `${secret:security/scadabridge/jwt-signing-key}` | JWT signing key | +| `${secret:SB-GRPC-PSK-}` | Per-site gRPC PSK (site side); central resolves `SB-GRPC-PSK-{siteId}` at channel-build time | + +## 4. CLI (`scadabridge` / `ZB.MOM.WW.ScadaBridge.CLI`) + +Resolution order per value: command-line flag → env var → `~/.scadabridge/config.json`. The CLI does +**not** read `appsettings.json` — its config file is `~/.scadabridge/config.json` (a separate schema). + +| Variable | Scope | In appsettings? | Purpose | Potential values | Required | +|---|---|---|---|---|---| +| `SCADABRIDGE_MANAGEMENT_URL` | Runtime (CLI) | No — CLI config file has `managementUrl` instead | Management API base URL. Overridden by `--url`; overrides the config file. Missing all three → exit `1`, `NO_URL`. | e.g. `http://localhost:9000` | One of flag/env/config | +| `SCADABRIDGE_FORMAT` | Runtime (CLI) | No — CLI config file has `defaultFormat` instead | Default output format. Overridden by `--format`. | `json`, `table` | No (defaults to `json`) | +| `SCADABRIDGE_USERNAME` | Runtime (CLI) | No | LDAP username fallback when `--username` absent. | e.g. `multi-role` | No (some commands need auth) | +| `SCADABRIDGE_PASSWORD` | Runtime (CLI) | No | LDAP password fallback when `--password` absent. **Preferred over `--password`** — avoids leaking into process listings / shell history. | the user's password | No (some commands need auth) | + +## 5. DelmiaNotifier client (`WWNotifier.exe`) + +| Variable | Scope | In appsettings? | Purpose | Potential values | Required | +|---|---|---|---|---|---| +| `SCADABRIDGE_API_KEY` | Runtime (client) | No — the notifier's `appsettings.json` holds only `BaseUrls`/`TimeoutSeconds`/`LogPath`; the key is **env-only by design** | Inbound API key sent as `X-API-Key`. Read only from the environment, never a file. Missing/empty → prints `API key not configured (SCADABRIDGE_API_KEY)`, exit `-1`, no HTTP attempt. | e.g. `sbk_...` | Yes | + +> This is also why `System.Environment` is wholly forbidden in the script trust model — it would let a +> site/inbound script exfiltrate `SCADABRIDGE_API_KEY` and other host env secrets. + +## 6. EF Core design-time tooling + +| Variable | Scope | In appsettings? | Purpose | Potential values | Required | +|---|---|---|---|---|---| +| `SCADABRIDGE_DESIGNTIME_CONNECTIONSTRING` | Design-time | Fallback for `ScadaBridge:Database:ConfigurationDb` — the factory reads that appsettings key first, then this env var | Connection string for `dotnet ef` migrations tooling when the Host's appsettings isn't the source. No hardcoded fallback — missing → actionable failure. | a config DB connection string | Only for `dotnet ef` when appsettings is unavailable | + +## 7. Test / CI-only + +Not read by the running product — the test harness only. Most default gracefully (skip, or use a +local docker value) when unset. + +| Variable | Scope | In appsettings? | Consumed by | Purpose | Potential values | +|---|---|---|---|---|---| +| `SCADABRIDGE_MSSQL_TEST_CONN` | Test | No | `MsSqlMigrationFixture`, AuditLog migration tests | Admin (sa) connection to a running MS SQL; absent → local docker sa default; drives `Skip.If`. | sa connection string | +| `SCADABRIDGE_PLAYWRIGHT_DB` | Test | No | `PlaywrightDbConnection` | DB connection for Playwright E2E seeding; absent → `localhost:1433`. | connection string | +| `SCADABRIDGE_CLI_DLL` | Test | No | `CliRunner` | Absolute path to the built CLI dll for Playwright when auto-probing fails. | path to `...CLI.dll` | +| `SCADABRIDGE_AUDIT_FILTER_4KB_P95_US` | Test | No | `HotPathLatencyTests` | Override P95 latency threshold (µs) for the 4 KB audit-filter perf test. | positive number (default 5) | +| `SCADABRIDGE_AUDIT_FILTER_RAW_P95_US` | Test | No | `HotPathLatencyTests` | Override P95 threshold for the raw-capture perf test. | positive number | +| `SCADABRIDGE_AUDIT_FILTER_NOOP_P95_US` | Test | No | `HotPathLatencyTests` | Override P95 threshold for the no-op fast-path perf test. | positive number (default 5) | +| `HOME` / `USERPROFILE` | Test | No | `CliConfig` tests | Redirected to locate `~/.scadabridge/config.json` under test. | temp dir path | + +The Host test fixtures (`CentralDbTestEnvironment`, `ScadaBridgeWebApplicationFactory`, +`StartupValidationTests`) set the **section-3 config-override vars** (`ScadaBridge__Database__*`, +`ScadaBridge__InboundApi__ApiKeyPepper`, etc.) and `DOTNET_ENVIRONMENT=Central` at runtime; those are +the same variables already documented above, not new ones. + +## 8. Supporting infrastructure containers (not the app) + +Set on the sidecar/infra containers in `infra/docker-compose.yml`, consumed by those images, not by +ScadaBridge code — included for completeness when standing up a rig. + +| Variable | Scope | In appsettings? | Container | Purpose | Value used | +|---|---|---|---|---|---| +| `MSSQL_SA_PASSWORD` | Infra | No | `scadabridge-mssql` | SQL Server `sa` password. | `ScadaBridge_Dev1#` (dev-only) | +| `MSSQL_PID` | Infra | No | `scadabridge-mssql` | SQL Server edition. | `Developer` | + +--- + +## Quick reference: minimum sets to boot a real deploy + +**Central node** (via env, or the `${secret:...}` equivalents + `ZB_SECRETS_MASTER_KEY`): +`SCADABRIDGE_CONFIG=Central`, `ScadaBridge__Database__ConfigurationDb`, +`ScadaBridge__Database__MachineDataDb`, `ScadaBridge__Security__JwtSigningKey`, +`ScadaBridge__Security__Ldap__ServiceAccountPassword`, `ScadaBridge__InboundApi__ApiKeyPepper`, and +for each managed site either the `SB-GRPC-PSK-` secret or `ScadaBridge__Communication__SitePsks__`. + +**Site node:** `SCADABRIDGE_CONFIG=Site` and `ScadaBridge__Communication__GrpcPsk` (or its +`${secret:SB-GRPC-PSK-}` form). StartupValidator hard-fails without the PSK.