ABOUTME: Explicit Environment, Infisical, and Development/Testing User Secrets authority. ABOUTME: Covers bootstrap selection, safe diagnostics, health monitoring, and rotation boundaries.
Explore.Secrets resolves values from exactly one deployment-selected authority.
Environment and Infisical are supported for deployments. UserSecrets is a
local Development/Testing authority. A missing, unsupported, disallowed, or failed
authority stops required startup work and never falls back to another source.
The Keycloak BFF client secret is deployment-owned runtime material. Event
resolves it through ISecretResolver from exactly one selected authority and
never stores, rotates, returns or logs it. A reviewed create receipt records
only a value-free binding generation; it does not persist plaintext, provider
coordinates, or a secret-derived hash. If that generation changes before
apply, the plan is rejected before Keycloak administrator authentication or
provider mutation.
The resolved secret may seed a proven-absent confidential BFF client exactly
once. Bearer-only API client payloads omit secret. Existing-client secret
endpoints are outside application authority. Rotation is coordinated externally:
update Keycloak and the selected environment/Infisical authority, restart all
affected replicas, run read-only inspection, then verify a fresh user sign-in.
Event provides this guidance but performs no live rotation or fallback copy.
The eight INSTANCE_BOOTSTRAP_* keys follow the same single-authority rule as
every other value here. They come from the deployment environment or from the
one selected secret authority, never from source defaults, appsettings
fallbacks, or a second provider. A missing or unreadable key fails startup
closed.
The subject, DID, issuer pairing and generation are selectors, not authentication
proof. External identities still require a real sign-in with the exact provider
claim. Configured Local bootstrap additionally resolves
authentication.local.bootstrap_password from
INSTANCE_BOOTSTRAP_LOCAL_PASSWORD (Infisical /api/bootstrap or /api); it is instance-only,
bootstrap-classified and has no live-rotation path or source default.
That secret creates only the initial temporary credential during incomplete setup. The native receipt coordinates Identity creation with application linkage, administrator grants and setup completion. Private first-use replacement and a fresh login are required before ordinary session authority. Completed-state reconciliation does not reread or replay leftover bootstrap passwords; use normal credential administration rather than editing this secret to reset an established account.
The opt-in Development-only AgentBrowser profile resolves
authentication.local.agent_browser_persona_password from
AGENT_BROWSER_PERSONA_PASSWORD through the same selected authority (Infisical
/api, Environment, or Development User Secrets). The instance-scoped,
bootstrap-classified value is required only while a persona's native first-use
replacement is unfinished. The configured administrator additionally needs
INSTANCE_BOOTSTRAP_LOCAL_PASSWORD until its own replacement completes, and
the Local JWT signing authority remains required on every launch. Once all
personas are complete, startup does not reread initialization passwords or
restore changed credentials. Production Setup neither activates nor exports
the agent-only credential.
The agent profile's dedicated Redis container also resolves
redis.agent_browser_password (AGENT_BROWSER_REDIS_PASSWORD) from the selected
authority's /api Infisical folder, explicit environment injection, or
Development User Secrets. It is a separate instance-scoped secret, not a
derivative of the persona or database password, and has no source default.
Diagnostics stay value-free. Logs, health output, and support evidence carry status and reason codes, never the configured subject, DID, email, profile names, or any fingerprint value.
| Provider | Enum | Status | Auth Method |
|---|---|---|---|
| Environment | 0 |
Implemented | Explicit environment injection |
| Infisical | 1 |
Implemented | Universal Auth (ClientId + ClientSecret) |
| User Secrets | 2 |
Development/Testing only | Shared local .NET store |
Vault, Azure Key Vault, and AWS Secrets Manager have no enum, options, factory, or deployment scaffolding. Any such configured string is invalid and fails closed.
When Provider = Environment, secrets come exclusively from process environment
variables. When Provider = UserSecrets, secrets come exclusively from the shared
event-shared-secrets .NET store and the process must run as Development or Testing.
When Provider = Infisical, only Infisical results are authoritative. Missing or
failed values never fall through to appsettings, environment, User Secrets, or a
different provider.
Optional Photon address geocoding does not use an API key, password, token, or client credential. Its endpoint and bounded runtime settings are ordinary startup configuration, not secrets. Do not create a Photon secret binding or place address queries, normalized provider records, coordinates, selection tokens, tenant/user identifiers, or dataset contents in any secret provider. If a future contracted geocoder requires credentials, add a separate reviewed secret definition and resolver in that provider's approved implementation phase; never reuse the Photon configuration surface.
{
"SecretProvider": {
"Provider": "Environment",
"FailFast": true,
"Infisical": {
"Url": "",
"ProjectId": "",
"ClientId": "",
"ClientSecret": "",
"Environment": "dev",
"Paths": ["/"]
}
}
}Copy .env.example to the ignored repository-root .env. Select exactly one
SECRET_PROVIDER: Environment, Infisical, or UserSecrets. Environment mode
reads the documented process variables. Infisical mode uses INFISICAL_* process
environment variables or, in Development and Testing environments, can read bootstrap
Universal Auth credentials (Infisical:Url, Infisical:ProjectId, Infisical:ClientId,
Infisical:ClientSecret, Infisical:Environment) directly from the shared User Secrets store
owned by src/Explore.Secrets/Explore.Secrets.csproj. In Development/Testing, if SECRET_PROVIDER
is omitted from process variables or .env, it also falls back to SECRET_PROVIDER configured in
User Secrets. User Secrets mode reads all application secrets directly from the shared store
through your IDE or the dotnet user-secrets --project src/Explore.Secrets CLI.
Both User Secrets mode and Infisical bootstrap via User Secrets are rejected unless the
host environment is Development or Testing. In Production, Infisical bootstrap credentials
must strictly come from process environment variables.
AppHost loads the repository .env, so Aspire profiles honor
SECRET_PROVIDER=UserSecrets directly. Direct project launches do not load that
file. For direct Standalone development, export only the non-secret selector and
host environment before launch:
export SECRET_PROVIDER=UserSecrets
export DOTNET_ENVIRONMENT=Development
export ASPNETCORE_ENVIRONMENT=Development
dotnet run --project src/Event.StandaloneEF design-time factories follow the same boundary. After populating all structured
database fields required by the selected provider in the shared store, run the EF
command from a shell with SECRET_PROVIDER=UserSecrets and
DOTNET_ENVIRONMENT=Development. A clean shell defaults to Production and rejects
User Secrets by design.
User Secrets is loaded once with reloadOnChange=false. Any value addition,
replacement, or removal requires a full restart of AppHost and every affected child
or direct host. The five-minute resolver cache bound does not reload the backing
User Secrets file.
Important
The contributor default Aspire profile is local-full. It starts local infrastructure and uses Environment unless UserSecrets is explicitly selected, so contributors should not need Infisical credentials.
Maintainer profiles intentionally differ:
local-corestarts local PostgreSQL/Redis and uses the authority selected in.envfor external platform values.local-litestarts the migration worker, API, and Blazor and uses the same selected authority. To switch away from Infisical, explicitly selectEnvironmentand inject every required value through.envor the deployment platform.
The repository root .env.example mirrors the supported Infisical folder layout and documents which service consumes each key. Copy it to .env for local Compose runs; .env is intentionally ignored by git.
Docker Compose uses .env for interpolation before starting containers. The Compose file then passes explicit environment: entries into each service. Do not rely on a broad env_file: .env import because it would place unrelated secrets into containers that do not need them.
Set SECRET_PROVIDER=Environment or SECRET_PROVIDER=Infisical in .env; do
not configure a secondary source. Validate and start each supported topology with
the normal deployment-owned inputs:
# Split Compose
docker compose config --quiet
docker compose up -d
# Aspire local profiles
dotnet run --project src/Explore.AppHost/Explore.AppHost.csproj --launch-profile local-full
# Direct Standalone image after building it
docker run --env-file .env islamu-event-standaloneFor an Infisical-backed Aspire profile, select Infisical in .env and provide
the documented INFISICAL_URL, INFISICAL_PROJECT_ID, INFISICAL_CLIENT_ID,
INFISICAL_CLIENT_SECRET, and INFISICAL_ENV inputs before starting the AppHost.
For Environment mode, leave every INFISICAL_* value blank and provide each
required consumer variable directly.
If startup reports secret_authority_unauthorized, secret_authority_invalid, or
secret_authority_unavailable, repair the selected deployment authority and restart
the affected host. Do not print provider responses, run commands that echo secret
values, or add a lower-source value to make startup appear healthy. A mode change is
an explicit deployment cutover and requires a full restart of the affected replicas.
| Secret authority | Direct Standalone | Aspire Standalone | Aspire Split | Single-replica Compose | Multi-replica Split |
|---|---|---|---|---|---|
Environment |
Supported with explicit process injection | Supported in local-core/local-lite; local-full defaults to local Environment mode |
Supported in local-core/local-lite; local-full defaults to local Environment mode |
Supported through the explicit Compose allow-list | Supported only when the deployment injects one consistent value set and one explicit shared setup secret into every replica |
UserSecrets |
Development/Testing only | Supported for local Development profiles | Supported for local Development profiles | Rejected | Rejected |
Infisical |
Supported with all five INFISICAL_* bootstrap inputs |
Supported in local-core/local-lite |
Supported in local-core/local-lite |
Supported with the same explicit bootstrap inputs | Supported when every replica uses the same project/environment authority and deployment-owned setup secret |
| Vault / Azure Key Vault / AWS Secrets Manager | Fail closed | Fail closed | Fail closed | Fail closed | Fail closed |
SECRET_PROVIDER has no deployment default. Compose and direct hosts reject a
missing or unsupported value. Environment mode clears Infisical bootstrap inputs;
Infisical mode requires URL, project, client ID, client secret, and environment.
Runtime and migrator processes receive only their role-specific database credentials.
The checked-in Compose topology is single-replica; a multi-replica split deployment
must additionally set Hosting:ReplicaCount and provide one shared SETUP_SECRET.
Delivery requires explicit email.delivery_enabled=true governance; SMTP credentials
do not enable it. Disabled transports resolve no credentials. Instance-hosted SMTP uses
instance bindings; tenant-owned hosts use exact tenant bindings through the existing
binding repository and ResolveTenantBindingAsync, never the tenant-to-instance fallback.
The materialized credential scope is checked again before constructing the transport.
Username and password must either both be absent (anonymous SMTP) or both be present.
Authority failures yield degraded capability and do not permit anonymous fallback.
Only bounded state, enabled intent, and ownership scope enter EmailDeliveryCapability.
No extra SMTP credential cache sits above the selected secret authority. The shared
resolver's documented freshness and coordinated-restart requirements below still apply.
Runtime bindings return one of five value-free outcomes. Resolved is the only
outcome carrying secret material in process memory. Unconfigured means the
selected authority has no usable binding/value; Unavailable means the selected
provider could not be reached; Unauthorized means provider authentication or
authorization failed; and Invalid means the binding metadata or selected source
is incompatible with the deployment authority. Provider failures never become a
clean miss and never activate another source.
Required capabilities fail closed for every non-Resolved outcome. Optional
integrations remain disabled or degraded within that capability and expose only the
bounded outcome in health data. Logs and metrics contain source/status categories,
not values, exception bodies, environment names, paths, keys, project identifiers,
binding identifiers, or tenant identifiers.
Successful runtime resolutions use the existing process-local memory cache for at most five minutes. Cache identity includes setting scope, tenant/instance identity, qualifier, selected source, and binding identity. A safe binding metadata mutation invalidates the matching entries immediately; provider failures and authorization failures are never cached as successful absence. Each replica owns its cache, so a deployment must allow the five-minute freshness bound or restart/drain replicas after an urgent authority change. Rotation activation and replica acknowledgement are documented separately with their owning runbook.
The secret-resolver readiness check reports providerState=available for the
selected Environment/User Secrets authority after its environment gate passes, or
after successful Infisical authentication. A
selected Infisical authority that is incomplete or cannot authenticate is
Unhealthy with the value-free providerState=unavailable; it is never load-balancer
ready as an optional/unconfigured provider. Follow Secret provider unavailable
without printing provider responses or testing with commands that echo credentials.
The database contract is structured; do not store or inject a raw connection
string. Endpoint metadata uses DATABASE_PROVIDER, DATABASE_HOST,
DATABASE_PORT, DATABASE_NAME, DATABASE_SCHEMA, DATABASE_TLS_MODE, and
DATABASE_TRUST_SERVER_CERTIFICATE. MariaDB and MySQL automatically infer
server flavor from DATABASE_PROVIDER and default to modern LTS versions (11.4
for MariaDB, 8.4 for MySQL); operators on custom engine versions can optionally
supply DATABASE_SERVER_VERSION.
DATABASE_SCHEMA is non-secret namespace metadata. PostgreSQL and SQL Server
use it as the application and Data Protection schema and keep clean table names
such as users. SQLite, MariaDB, and MySQL always apply the fixed ie_ prefix
and do not use this field for table placement. Prefer a separate SQLite file or
MariaDB/MySQL database for each deployment instance; never invent or store a
configurable prefix secret.
| Compose key | Direct .NET key | Consumer |
|---|---|---|
DATABASE_RUNTIME_USERNAME, DATABASE_RUNTIME_PASSWORD |
Database:Runtime:Username, Database:Runtime:Password |
API/runtime processes only |
DATABASE_MIGRATOR_USERNAME, DATABASE_MIGRATOR_PASSWORD |
Database:Migrator:Username, Database:Migrator:Password |
Event.MigrationService only |
SQLite has no database credentials. Its DATABASE_NAME value is a
persisted local file path and is deployment configuration, not a secret. Do not
give runtime services the migrator role, and never expose either role to the
Blazor client.
| Compose / Infisical key | Direct .NET key | Consumer |
|---|---|---|
ERASURE_DATABASE_RUNTIME_USERNAME, ERASURE_DATABASE_RUNTIME_PASSWORD (flat .env or Infisical /database/erasure) |
Database:Erasure:Runtime:Username, Database:Erasure:Runtime:Password |
API only, and only for ExternalDatabase |
ERASURE_DATABASE_MIGRATOR_USERNAME, ERASURE_DATABASE_MIGRATOR_PASSWORD (flat .env or Infisical /database/erasure) |
Database:Erasure:Migrator:Username, Database:Erasure:Migrator:Password |
Event.MigrationService only |
For ExternalDatabase, endpoint metadata uses the same ERASURE_DATABASE_* names in both authorities: ERASURE_DATABASE_HOST, PORT, NAME, TLS_MODE, and TRUST_SERVER_CERTIFICATE (or structured PrivacyErasureAuthorityDatabase:*); the provider is fixed to PostgreSQL. The ERASURE_DATABASE_ prefix remains the supported contract inside /database/erasure. Recursive startup reads use each response item's actual secretPath, not the requested parent path. Child values remain in their own section and cannot publish primary database flat aliases, including when /database/identity is also requested explicitly. Missing or out-of-scope response provenance fails closed. Use separate
roles: runtime receives only authority append/read/state/evaluate function
execution and still has zero table or sequence access, while the migrator owns
schema, lifecycle functions, grants, and destructive compaction execution. The
usernames must be different; configuration binding and PostgreSQL provisioning
both fail closed when one login is shared across these trust boundaries.
Never pass either
authority credential to
Explore.Blazor or Explore.Blazor.Client. Rotate the migrator credential
independently of runtime. These values are unused in EmbeddedSqlite topology;
that mode has no database username/password and protects its dedicated local
file with filesystem permissions. Its nonsecret deployment fields are
PrivacyErasureAuthorityEmbedded:Path (default
/app/data/privacy_erasure_authority.db), WriterReplicaCount=1, and
BusyTimeoutSeconds=30.
There is one Infisical bootstrap schema: SecretProvider:Provider=Infisical
selects the authority and SecretProvider:Infisical:* (projected from the documented
INFISICAL_* deployment inputs, or in Development/Testing loaded from User Secrets) supplies secret-zero Universal Auth credentials.
API composition projects the validated provider selection and the actual authenticated
source's URL, project, client, credential, environment and requested paths into
runtime SecretProviderOptions. Runtime binding does not silently append a root
path or select a provider supplied by vault contents. Bootstrap credentials still
come from the process environment in Production, not arbitrary merged configuration.
Development/Testing additionally supports bootstrap configuration and shared User Secrets.
API composition validates obsolete PrivacyErasure:Durability:Mode inputs before
projecting either bootstrap configuration or the selected Environment, Infisical
or development User Secrets authority. Its double-underscore form is also rejected.
Operators must explicitly retain the intended supported topology through
ERASURE_DATABASE_TOPOLOGY or PrivacyErasure:Authority:Topology; deleting an obsolete selector without choosing
the intended authority is not a migration. Follow the existing privacy-erasure
backup/restore/reset policy before intentionally changing topology. This repair
performs no automatic authority migration or database schema change.
The isolated Blazor Infisical provider admits only /keycloak, /blazor and
/atproto roots and their frontend subpaths. Backend/root paths and database
configuration keys (including KEYCLOAK_DB_PASSWORD) fail closed, even when placed
in a frontend folder. Restrict the BFF machine identity independently of the API
and provisioning identities. Supply database/container credentials only to their
backend consumers; do not expose them through a BFF-readable folder. Nested
secrets use their actual folder namespace rather than a global flat alias.
This is provider admission enforcement, not a claim that process environment
variables or a standalone host's shared backend configuration are scrubbed.
For full local runs, keep SECRET_PROVIDER=Environment and leave INFISICAL_* blank
so local structured DATABASE_*, Keycloak, Cerbos, and storage values remain
authoritative. Infisical loads primary database configuration directly from /database
using DATABASE_* keys mapped to the structured Database:* configuration section.
| Key | Default | Purpose |
|---|---|---|
Enabled |
true |
Enable periodic secret refresh |
RefreshInterval |
00:05:00 |
Polling interval |
InitialDelay |
00:00:10 |
Delay before first refresh |
BaseBackoffDelay |
00:00:05 |
Base delay for exponential backoff |
MaxBackoffDelay |
00:05:00 |
Maximum backoff cap |
JitterFactor |
0.1 |
Randomization factor to prevent thundering herd |
UnhealthyThreshold |
3 |
Consecutive failures before unhealthy status |
Backoff formula: BaseBackoffDelay × 2^(failures - 1), capped at MaxBackoffDelay, plus jitter.
| Key | Purpose |
|---|---|
CurrentKeyVersion |
Active encryption key version (integer) |
KeyVersions |
Dictionary of version → key material |
MasterKeyEnvironmentVariable |
Env var holding the master key |
Encryption uses AES-256-GCM with 12-byte nonce and 16-byte authentication tag.
AT Protocol authentication has three purpose-separated, instance-only key rings:
| Secret key | Consumer | Purpose |
|---|---|---|
auth.atproto.oauth_client_private_jwks |
Blazor BFF | P-256/ES256 OAuth private_key_jwt assertions and short-lived bootstrap assertions. |
auth.atproto.session_encryption_keyring |
Infrastructure | AES-256-GCM encryption of the complete persisted CarpaNet OAuth session. |
auth.atproto.session_jwt_private_jwks |
API | P-256/ES256 signing and validation of short-lived first-party ATProto session JWTs. |
Never reuse a key between these purposes. Key IDs may be persisted or advertised where the protocol requires them; private key values, OAuth tokens, DPoP keys, and decrypted session JSON must remain inside the owning server process.
auth.atproto.session_encryption_keyring is an instance-only secret used exclusively for durable CarpaNet OAuth sessions. Its value is strict JSON with one active AES-256 key and zero or more retired read keys:
{"keys":[{"kid":"2026-07","k":"<base64url-encoded-32-byte-key>","status":"active"},{"kid":"2026-06","k":"<base64url-encoded-32-byte-key>","status":"retired"}]}Key IDs are persisted as metadata; key material is never stored in the application database. The encrypted envelope contains the complete token set and private DPoP JWK. AES-GCM associated data binds the ciphertext to the tenant, user, provider, subject DID, normalized PDS URI, OAuth client signing-key ID, and envelope version, so copying a row across any of those boundaries fails authentication.
To rotate, publish a new active key and mark the previous active key retired. A successfully restored session is rewritten under the active key. Keep each retired key available until no session row references its key ID; removing it earlier forces affected users to authenticate again. Unknown keys, malformed key rings, ciphertext tampering, or binding mismatches fail closed as reauthentication and must never be repaired by inventing session data or restoring plaintext credential columns.
OAuth client and session-JWT signing rings follow the same overlap rule: exactly one active signing key, with previous keys retained as retired verification/session keys until their pinned OAuth sessions or issued JWTs have expired or been revoked. Rotate one purpose at a time, verify readiness, and only then remove an unused retired key. Removing an in-use session-encryption or OAuth-client key deliberately invalidates the affected sessions and requires those users to sign in again.
Only the BFF OAuth-client signing ring is evaluated by the atproto-authentication readiness check. Validate the encryption and session-JWT rings with a controlled sign-in/session refresh before removing retired keys; their consumers otherwise fail closed when persisting/restoring a session or issuing/validating a first-party JWT.
Promotion lookup uses the dedicated registry key promotions.code_lookup_hmac_key. It is instance-only, server-only, non-bootstrap secret material; it must not share a key with encryption, signing, webhook, payment, or provider credentials. Each usable key requires a qualified SecretBinding whose qualifier is exactly v{version}, such as v1. The default initial source coordinates are /promotions/PROMOTIONS_CODE_LOOKUP_HMAC_KEY for Infisical and PROMOTIONS_CODE_LOOKUP_HMAC_KEY for environment-variable bindings. Overlapping versions must point to distinct immutable source coordinates; do not repoint both v1 and v2 bindings at one environment variable whose value is replaced.
The value must be standard Base64 that decodes to at least 32 bytes. Infrastructure normalizes the attendee-entered code, scopes the input to tenant and event, and computes HMAC-SHA256. The database stores only the digest and positive lookup-key version as private persistence metadata. Management and checkout read contracts never expose either value or an internal promotion-code identifier; they expose only a masked display label. Organizer code creation and code rotation return the organizer-entered plaintext once, and the Studio component clears it after acknowledgement, context change, failure, or disposal.
Rotate the lookup trust root in this order:
- Create the new secret value at a distinct external coordinate and add its instance binding under the next qualifier, for example
v2, without changing or deletingv1. - Set
Promotions:CodeLookup:ActiveKeyVersionto the new positive version and restart the API replicas so new publish/code-rotation writes pin that version. - Confirm controlled create/apply behavior without copying raw codes, digests, binding coordinates, or secret material into logs, screenshots, health output, or support artifacts.
- Retain every older qualified key while any active promotion-code row references its
LookupKeyVersion. Remove an old binding only after an authoritative database/administrative check shows no active code uses it.
Changing the HMAC key value in place under an existing qualifier invalidates lookup for every active code pinned to that version and is not a supported rotation. A missing, malformed, or too-short qualified key fails closed: code publishing/rotation or application does not fall back to another source or compute an unkeyed digest.
| Key | Default | Purpose |
|---|---|---|
Enabled |
false |
Enable only the existing options-driven local HTTP/database rotation boundaries |
GracePeriod |
00:00:30 |
Keep the previous local HTTP client available for in-flight work after validated activation |
MaxConcurrentRotations |
5 |
Parallel rotation limit |
Rotation is a deployment protocol, not a successful options callback. Each local HTTP/database activation returns a value-free acknowledgement containing an opaque attempt ID, replica ID, consumer category, bounded status, and timestamp. It never revokes provider credentials and never claims that other replicas converged.
| Consumer family | Mode | Candidate/activation evidence | Revoke and rollback rule |
|---|---|---|---|
| Promotion/admission HMAC versions and ATProto key rings | overlap-rollout |
Publish a distinct version, validate it, activate locally, and require the same attempt acknowledgement from every declared replica. | Keep the previous version valid until all replicas acknowledge and pinned data/sessions no longer reference it; reactivate the previous version on any failure. |
| Generic options-driven HTTP clients | overlap-rollout (local boundary only) |
Validate the candidate client before atomic local swap; deployment tooling must collect every replica acknowledgement. | Keep the old provider credential valid through the grace/acknowledgement window; rollback locally on validation failure. |
| Primary database, Keycloak database, Stripe, Svix, SMTP, S3, analytics, localization, Cerbos, registration providers, Listmonk, AI, and managed control-plane credentials | coordinated-restart |
Validate at the source, enter maintenance, restart every consumer with one deployment attempt, and require readiness from every replica. | Restore the previous deployment value and restart on failure; revoke only after all old processes are stopped and readiness converges. |
| First-run setup secret | unsupported-live |
Complete/lock setup or replace the deployment-owned value while setup remains active, then restart. | Never infer live rotation from a process callback; use the setup recovery contract. |
For overlap providers, missing or rejected acknowledgement leaves the rollout
Pending; at the declared stale deadline it becomes FailedClosed, and the stale
replica must be drained before recovery. Providers without overlap require a
coordinated maintenance restart. One healthy replica is never convergence evidence.
Break glass means restoring the previous provider value and restarting/draining the
declared replica set; it does not mean adding a fallback source or revoking the old
credential early.
Infisical uses SCREAMING_SNAKE_CASE with path-based sections. The provider maps bidirectionally:
| Infisical Path + Key | .NET Configuration Key |
|---|---|
/atproto/ATPROTO_OAUTH_CLIENT_PRIVATE_JWKS |
auth.atproto.oauth_client_private_jwks; resolves to BFF Atproto:OAuthClientPrivateJwks |
/atproto/ATPROTO_SESSION_ENCRYPTION_KEYRING |
auth.atproto.session_encryption_keyring; consumed by Infrastructure session-envelope protection |
/atproto/ATPROTO_SESSION_JWT_PRIVATE_JWKS |
auth.atproto.session_jwt_private_jwks; consumed by API first-party session JWT signing/validation |
/keycloak/REALM_NAME |
Keycloak:RealmName |
/keycloak/KEYCLOAK_CLIENT_ID |
Nonsecret browser/BFF client metadata mapped to Keycloak:ClientId for API onboarding detection. |
/keycloak/KEYCLOAK_BLAZOR_CLIENT_SECRET |
Deployment-owned Blazor BFF runtime credential resolved through the selected authority; Event never persists or rotates it |
/api/CONTROL_PLANE_REGISTRATION_CREDENTIALS |
management.control_plane_registration_credentials |
/api or /cerbos + AUTHORIZATION_PROVIDER |
Non-secret Authorization:Provider deployment intent. Blank keeps manual Local-first onboarding; local or cerbos makes the provider deployment-owned and skips the choice page. |
root or AI path + AI_TOOL_PROPOSALS_ENABLED |
AiProvider:ToolProposalsEnabled |
/database/DATABASE_PROVIDER |
Primary database provider: PostgreSql, Sqlite, SqlServer, MariaDb, MySql |
/database/DATABASE_HOST |
Primary database host |
/database/DATABASE_PORT |
Primary database port |
/database/DATABASE_NAME |
Primary database name or SQLite file path |
/database/DATABASE_SCHEMA |
Application schema (default: islamu_event; PostgreSQL and SQL Server) |
/database/DATABASE_TLS_MODE |
TLS mode: Prefer, Required, Disabled |
/database/DATABASE_TRUST_SERVER_CERTIFICATE |
false (default: strict CA verification) or true (accept self-signed certs) |
/database/DATABASE_SERVER_VERSION |
Optional override for MariaDB/MySQL (defaults: MariaDB 11.4, MySQL 8.4; e.g. 10.11, 8.0) |
/database/DATABASE_RUNTIME_USERNAME |
Runtime database username |
/database/DATABASE_RUNTIME_PASSWORD |
Runtime database password |
/database/DATABASE_MIGRATOR_USERNAME |
Migrator database username |
/database/DATABASE_MIGRATOR_PASSWORD |
Migrator database password |
/database/ERASURE_DATABASE_TOPOLOGY |
Privacy erasure topology: EmbeddedSqlite, CoLocated, ExternalDatabase |
/database/IDENTITY_DATABASE_TOPOLOGY |
Identity database topology: colocated or external |
/database/erasure/ERASURE_DATABASE_PROVIDER |
External authority provider (fixed to PostgreSql) |
/database/erasure/ERASURE_DATABASE_HOST |
External authority PostgreSQL host |
/database/erasure/ERASURE_DATABASE_PORT |
External authority PostgreSQL port (default: 5432) |
/database/erasure/ERASURE_DATABASE_NAME |
External authority PostgreSQL database name |
/database/erasure/ERASURE_DATABASE_TLS_MODE |
External authority TLS mode: Prefer, Required, Disabled |
/database/erasure/ERASURE_DATABASE_TRUST_SERVER_CERTIFICATE |
false (default: strict CA verification) or true (accept self-signed certs) |
/database/erasure/ERASURE_DATABASE_RUNTIME_USERNAME |
External authority runtime username (function-execution role) |
/database/erasure/ERASURE_DATABASE_RUNTIME_PASSWORD |
External authority runtime password |
/database/erasure/ERASURE_DATABASE_MIGRATOR_USERNAME |
External authority migrator username (schema/admin role) |
/database/erasure/ERASURE_DATABASE_MIGRATOR_PASSWORD |
External authority migrator password |
storage path + STORAGE_S3_* |
Storage:S3* (for example /storage/STORAGE_S3_ENDPOINT → Storage:S3Endpoint) |
/smtp/MAIL_SMTP_HOST |
smtp.host secret binding default; Development seed maps it to email.smtp_host when no SMTP setting exists |
/smtp/MAIL_SMTP_PORT |
smtp.port secret binding default; Development seed maps it to email.smtp_port when no SMTP setting exists |
/smtp/MAIL_SMTP_USERNAME |
smtp.username secret-bearing SMTP username |
/smtp/MAIL_SMTP_PASSWORD |
smtp.password secret-bearing SMTP password |
/smtp/MAIL_SMTP_FROM_ADDRESS |
smtp.from_address secret binding default; Development seed maps it to email.from_address when no SMTP setting exists |
/smtp/MAIL_SMTP_FROM_NAME |
smtp.from_name secret binding default; Development seed maps it to email.from_name when no SMTP setting exists |
/cerbos/CERBOS_USE_POLICY_SCOPE |
Cerbos:UsePolicyScope |
/reporting/OSPREY_API_KEY |
reporting.osprey_api_key tenant Osprey provider credential |
/reporting/OSPREY_WEBHOOK_SECRET |
reporting.osprey_webhook_secret tenant Osprey callback/signing secret when a deployment uses one |
/reporting/COOP_API_KEY |
reporting.coop_api_key tenant Coop provider credential |
/reporting/COOP_WEBHOOK_SECRET |
reporting.coop_webhook_secret tenant Coop callback HMAC secret |
/stripe/STRIPE_PLATFORM_SECRET_KEY |
payments.stripe.platform_secret_key instance/server-only Stripe platform secret owned by each self-hoster |
/stripe/STRIPE_WEBHOOK_SECRET |
payments.stripe.webhook_secret instance/server-only Stripe webhook signing secret owned by each self-hoster |
/promotions/PROMOTIONS_CODE_LOOKUP_HMAC_KEY |
promotions.code_lookup_hmac_key instance/server-only HMAC material; each binding uses a positive v{version} qualifier |
/registration-providers/REGISTRATION_PROVIDER_API_TOKEN |
registration_provider.api_token tenant provider API/OAuth credential binding. Google Forms uses this for the OAuth access token or refresh-token envelope. |
/registration-providers/REGISTRATION_PROVIDER_WEBHOOK_SECRET |
registration_provider.webhook_secret tenant callback signing secret binding for providers that use shared callback secrets. Google Forms Pub/Sub does not use it because callback authentication is Google OIDC. |
/integrations/listmonk/LISTMONK_API_USERNAME |
integrations.listmonk.api_username |
/integrations/listmonk/LISTMONK_API_KEY |
integrations.listmonk.api_key |
/registration-providers/REGISTRATION_PROVIDER_API_TOKEN |
registration_provider.api_token |
/registration-providers/REGISTRATION_PROVIDER_WEBHOOK_SECRET |
registration_provider.webhook_secret |
/api/VAPID_SUBJECT |
WebPush:VapidSubject |
/api/VAPID_PUBLIC_KEY |
WebPush:VapidPublicKey |
/api/VAPID_PRIVATE_KEY |
WebPush:VapidPrivateKey |
Environment authority + STORAGE_S3_* |
consumed by the S3 resolver only when Environment is selected |
The three ATProto rows use the same uppercase name as their default environment-variable name as well as their Infisical key. Private credentials use only the authoritative registry names; there are no .NET double-underscore credential aliases. Storage accepts the documented STORAGE_S3_* variables only under Environment authority. Primary database bootstrap uses discrete structured fields rather than a connection string. SMTP uses the authoritative MAIL_SMTP_* names. Registration-provider credentials are tenant-scoped secret definitions and must be bound through SecretBinding; use the bounded Qualifier field when several tenant connections need distinct API tokens or webhook secrets for the same key. Browser contracts never carry secret values or provider source coordinates.
Stripe secrets are instance-scoped, server-only, and optional while paid events are disabled. Payments:Stripe:Mode=Test requires a platform key beginning sk_test_; Live requires sk_live_. The Connect endpoint uses only the dedicated webhook binding, never the platform key or an outgoing-webhook secret. Rotate platform and endpoint secrets deliberately with the matching Stripe mode and endpoint configuration; retain no secret value in logs, support artifacts, browser DTOs, OpenAPI, or the DBML reference.
Keycloak itself consumes KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD to
create its initial administrator. Event never reads them as application
runtime secrets; do not store them in governance settings or copy them into
support artifacts.
The checked-in Keycloak realm export contains no client secret and is not mounted or imported by normal Compose/AppHost startup. Compose and local Aspire pass the deployment value to API and BFF only. If an operator approves creation of an absent confidential client, the API resolves that value for the one foreground Admin REST request. AppHost never generates, persists or renders a replacement value.
External-Keycloak privileged inspection accepts a one-time Keycloak administrator
username/password through the advanced setup or administration form. Treat that
credential as operator input for exactly one foreground request, not as a
platform-managed secret. Event must not read KEYCLOAK_ADMIN*, Infisical
administrator entries, User Secrets or application configuration to satisfy an
empty form, and must not save the input to appsettings, environment variables,
Infisical paths, database governance settings, logs, traces, screenshots or
support bundles.
Paid-event hosted onboarding never returns payment platform secrets, provider account identifiers, or connection identifiers to the browser. Payments:OrganizerDirect:ProviderCode and ConnectPlatformId are server configuration, not secret values, but are still omitted from browser readiness contracts.
Keycloak onboarding and administrator reads always redact the runtime client secret. They may expose only configured/source/editability metadata plus nonsecret authority and client ID. Secret values remain in the selected deployment authority; database rows contain only binding metadata, and setup writes do not persist a replacement value.
Keycloak runtime connection resolution reads the endpoint, realm, BFF client ID and BFF client secret from the selected deployment authority once for the request. There is no database-secret reader, source fallback or application-managed rotation branch. Rotate an existing client credential in the deployment authority and Keycloak under an operator-controlled maintenance window, restart affected replicas, and then re-inspect. Event never writes the replacement to application storage.
The /setup route is a pre-authentication operator gateway, not a browser-owned secret workflow. Browser-supplied setup, tenant, authorization, and provider-administration headers are stripped before proxying. The BFF/server replaces them only from trusted server-owned state, and the API validates the resulting authority again.
The following values must never be persisted in browser storage, returned in browser-facing DTOs, or copied into logs, traces, screenshots, diagnostics, or support artifacts:
- access, refresh, and provider tokens;
- the setup secret or a recoverable derivative;
- temporary provider administrator usernames/passwords or service-account credentials;
- raw provider request or response bodies.
An explicit SETUP_SECRET is always authoritative until onboarding completes and locks setup mode. When it is empty on a declared single replica, the API writes one random secret to SETUP_SECRET_FILE with 0600 permissions and logs only a docker cp retrieval instruction. Hosting:ReplicaCount > 1 fails startup unless one deployment-owned explicit SETUP_SECRET is present. The platform default is /tmp/islamu-event/setup-secret; split Compose overrides it to the setup_data volume at /app/bootstrap/setup-secret, and standalone uses /app/data/setup-secret. The file is deleted when onboarding completes; setting a non-empty SETUP_SECRET also removes and overrides any generated file. After validation, the BFF keeps the secret in a protected, HttpOnly 30-minute rolling session and requires re-entry after 30 minutes without setup activity.
Retrieve the generated fallback only from the Docker host, copy it to an owner-protected local file, enter it at /setup, then remove the local copy. Do not include the generated file in backups. An unmounted temp file may be replaced with a new secret after container recreation; the old value immediately stops working. Read-only containers and rolling or multi-replica API deployments must use an explicit shared SETUP_SECRET from their platform secret manager.
Rerunning verification or completion does not grant permission to read a stored secret back. Application-managed credentials remain write-only and are rotated through the owning server operation. Deployment-managed credentials remain authoritative in their configured environment/secret provider; rotate them there, refresh or restart as required, and confirm only through configured/readiness metadata. Do not overwrite a deployment-managed value from onboarding to repair drift.
See SELF_HOSTING.md for the operator flow and TROUBLESHOOTING.md for recovery without disclosing credentials.
The sole human terminal target is Event.SetupAssistant.Terminal; the machine
CLI has no interactive or secret-entry mode. The Terminal.Gui target accepts no
secret arguments, environment transport, redirected standard streams, clipboard
paste, history, or alternate console renderer. It owns one bounded mutable
buffer, masks the visible field, clears state across every terminal path, and
returns only diagnostic code, readiness counts, and artifact digest.
The Terminal.Gui field stores bullet count only; the real input never enters
its Text or undo history. Terminal composition sends a nonsecret placeholder
through the authoritative string-based Core workflow, then replaces it with
clearable URL-safe UTF-8 bytes. Mutable secret buffers are zeroed after the
write attempt. .NET, the operating system, and storage hardware still cannot
guarantee physical erasure of every runtime, kernel, or device-level copy; do
not treat this process as resistant to a compromised local account, debugger,
memory dumper, or crash-dump collector.
On Linux, macOS, and FreeBSD it writes a new owner-only 0600 file with
create-new/no-overwrite semantics. Unsupported protected-write platforms fail
closed. The generated file is deployment input: move its values into the
selected deployment secret authority, then remove the local file when it is no
longer required.
Secrets use a platform-wide ownership model so environment variables and external secret providers are not permanent live overrides by accident. The ownership source is metadata; browser-facing DTOs expose only configured/source/editability flags and never raw values, ciphertext, provider tokens, or resolved secret coordinates.
| Mode | Source types | UI behavior | Runtime meaning |
|---|---|---|---|
| Deployment-managed | EnvironmentVariable or Infisical metadata binding |
Read-only configured/source/status metadata; rotate outside the app | Values remain in the selected deployment authority and changes require provider refresh or redeploy/restart. |
| Request/job bootstrap | Purpose-bound credential supplied for one setup request or job | Never persisted or readable through status UI | The credential expires with the request/job and cannot become a runtime fallback. |
Application databases store only non-secret binding/reference metadata. The
SecretResolver dispatches through one selected source and never falls back to a
second source after absence or failure. APIs and UI expose only value-free status;
they do not prefill, persist, or read back deployment secret values.
Reporting provider secrets are server-side tenant settings. API keys and webhook secrets for Osprey and Coop must never be returned in browser DTOs, HAL links, health checks, logs, metrics, traces, screenshots, issue templates, or support bundles; browser/control-plane surfaces may expose only configured/source/editability metadata. Routing update actions are write-only for secret values: supplying a new Osprey/Coop API key or webhook secret rotates that tenant value, while omitting the field or sending it blank preserves the currently stored secret. There is no implicit clear-secret endpoint and no readback path; confirm rotation through configured flags, provider readiness checks, and secret-provider audit trails.
Current migrated surface: Cerbos authorization settings expose endpoint and Admin API credential ownership metadata. AUTHORIZATION_PROVIDER is non-secret deployment intent, while CERBOS_ADMIN_USERNAME and CERBOS_ADMIN_PASSWORD are server-side deployment secrets resolved from environment configuration or Infisical. The browser normally sees only configured flags and ownership metadata. During an explicit setup sync, an operator may instead submit a complete one-time pair; it exists only in the Blazor server circuit and request pipeline, overrides deployment credentials for that call, is cleared after the call, and is never written to SystemSetting, returned by an API, or logged. CERBOS_ADMIN_PASSWORD_HASH is the Cerbos server verifier and cannot authenticate an Admin API client; keep the matching plaintext password only in deployment secrets or enter it for one sync. Reporting provider secret keys are registered as sensitive hierarchical settings for the moderation routing foundation. Listmonk API username/key values are registered server-side secret bindings; admin updates are write-only and browser DTOs expose configured flags only. Stripe payments.stripe.platform_secret_key and payments.stripe.webhook_secret are instance/server-only definitions for self-hoster-owned platform credentials. Promotion lookup resolves the qualified instance-only promotions.code_lookup_hmac_key binding for every digest operation. SMTP, S3, OAuth, localization/TMS, and AI keys still have area-specific storage/UI paths and must not be documented as fully migrated until their resolvers use the shared ownership metadata consistently.
Web Push VAPID keys are deployment configuration. Infisical /api/webpush (or legacy /api) PRIVATE_KEY / VAPID_PRIVATE_KEY maps to WebPush:VapidPrivateKey; it is a server-only secret and must never appear in browser configuration, API responses, HAL links, logs, traces, health data, screenshots, or support artifacts. VAPID_PUBLIC_KEY is intentionally public and is returned by GET /vapid-public-key as plain text and by GET /api/notification/web-push/config. Browser subscription endpoints and p256dh/auth material are stored tenant-scoped and are never echoed by subscription status DTOs.
| Method | Returns | Purpose |
|---|---|---|
InitializeAsync |
Task |
One-time provider setup |
GetSecretAsync |
Task<string?> |
Single secret by key |
GetSecretWithMetadataAsync |
Task<SecretWithMetadata?> |
Secret with version and timestamps |
GetSecretsByPathAsync |
Task<IDictionary> |
All secrets under a path |
RefreshAsync |
Task |
Force refresh from provider |
GetHealthAsync |
Task<HealthCheckResult> |
Provider health status |
Properties: ProviderType, SupportsRefresh.
Registered as secret_provider with tag secrets.
| Status | Condition |
|---|---|
| Healthy | Fewer than UnhealthyThreshold consecutive failures |
| Degraded | 1–2 consecutive failures |
| Unhealthy | ≥ UnhealthyThreshold consecutive failures |
Health data includes: provider type, supports refresh, consecutive failure count, last successful refresh timestamp.
Meter name: Explore.Secrets
| Instrument | Type | Purpose |
|---|---|---|
secrets_refresh_total |
Counter | Total refresh attempts |
secrets_refresh_failures_total |
Counter | Failed refresh attempts |
secrets_refresh_duration_seconds |
Histogram | Refresh operation duration |
secrets_consecutive_failures |
UpDownCounter | Current consecutive failure count |
secrets_last_refresh_timestamp_seconds |
Gauge | Unix timestamp of last successful refresh |
A BackgroundService using PeriodicTimer that:
- Waits
InitialDelaybefore first poll. - Calls
ISecretProvider.RefreshAsync()at eachRefreshInterval. - On success, calls
IConfigurationRoot.Reload()to propagate changes. - On failure, applies exponential backoff with jitter.
- Logs structured warnings with correlation IDs.
Wraps ISecretProvider for value-free provider lifecycle mutations only. Secret reads do not create a persistent audit trail.
Audit entries track: Operation, ProviderType, KeyPattern (redacted), Timestamp, UserId (extracted via sub → nameidentifier → sid fallback), CorrelationId.
| Method | Purpose |
|---|---|
AddSecretProvider |
Core provider registration |
AddSecretManagement |
Full setup (provider + refresh + health + metrics) |
AddSecretObservability |
Health checks + metrics |
AddSecretMetrics |
Prometheus metrics only |
AddSecretHealthCheck |
Health check only |
AddSecretRefreshService |
Background refresh service |
AddRotationAwareHttpClientFactory |
HttpClient with rotating credentials |
AddRotationAwareDbContextFactory<T> |
DbContext with rotating connection strings |
AddConnectionRotation<T> |
Generic connection rotation |
Typical startup: services.AddSecretManagement(configuration) registers everything.
CONFIGURATION_MANIFEST_MODE, CONFIGURATION_MANIFEST_PATH, and
CONFIGURATION_MANIFEST_HOST_DIRECTORY are non-secret deployment metadata. Do not
store the manifest body in .env, Infisical, command-line arguments, or image
environment layers. Mount the JSON file separately as read-only configuration.
The .env path and host-directory values select where the owning process sees
the file; they neither mount the file nor carry instance or tenant business
values.
The independent instance and tenant catalogs exclude credentials, tokens,
connection strings, encryption/signing material, provider secrets, secret
references, PII, and payment operational state. Keep those values in Infisical
or the documented .env bindings. Unknown, sensitive, or non-catalog keys fail
closed before any instance or tenant state is written. Export remains a
secret-free configuration artifact, never a database or secret backup.
Setup Live can submit a value only after the target reports the exact binding
as Ready. The target writes through its already-selected authority; the
current live-write authority is Infisical. Environment and User Secrets are not
live-write providers, and absence or provider failure never falls back to
appsettings, another provider, or stored coordinates.
The enrollment capability, raw value, and target-local Infisical coordinates
are never saved in a profile, database row, portable artifact, log, trace,
metric, response, or support bundle. The API exposes value-free readiness and
receipts only and has no readback route. Release activation remains disabled by
eng/setup-assistant/generated/setup-live-release-capabilities.json.
- CONFIGURATION.md — application settings
- SELF_HOSTING.md — environment variable reference
- OPERATIONS.md — health checks and metrics