Files
ScadaBridge/env_variables.md
Joseph Doherty bb138d5254 docs: add env_variables.md — full environment-variable inventory
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.
2026-07-24 05:02:24 -04:00

14 KiB

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 <DbName>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-<siteId>} Yes on Site
ScadaBridge__Communication__SitePsks__<siteId> No in shipped src appsettings (central normally resolves the SB-GRPC-PSK-<siteId> secret); present only in the docker-compose env blocks ScadaBridge:Communication:SitePsks:<siteId> 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-<siteId> 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-<siteId>} 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-<siteId> secret or ScadaBridge__Communication__SitePsks__<siteId>.

Site node: SCADABRIDGE_CONFIG=Site and ScadaBridge__Communication__GrpcPsk (or its ${secret:SB-GRPC-PSK-<siteId>} form). StartupValidator hard-fails without the PSK.