Skip to main content
Version: Next (unstable)

Configuration Reference

Every environment variable Guardian honours, in one place.

Use this as a lookup table — pair it with LOCAL_DEV.md for which combinations make sense locally and SERVER_AWS_DEPLOY.md for how the deploy script sets them in production.

For Terraform variables (everything under infra/), see infra/README.md — those are a separate surface; the deploy script translates between Terraform vars and the runtime env vars in this document.

Conventions​

  • Required means the server will refuse to start if the variable is missing in the relevant build/feature combo.
  • Default is what the server picks when the variable is unset.
  • Build mode indicates which Cargo feature gate consumes the value. Defaults builds are the no-feature filesystem build unless stated.

Runtime — server identity and storage​

VariableDefaultBuild modeNotes
DATABASE_URLrequiredpostgresPostgres connection string. Server panics at startup if unset under --features postgres. TLS verification is controlled by the standard sslmode/sslrootcert parameters — see Database TLS.
GUARDIAN_STORAGE_PATH/var/guardian/storagefilesystemPath for state + delta blobs.
GUARDIAN_METADATA_PATH/var/guardian/metadatafilesystemPath for accounts, auth credentials, network config. Holds .metadata/accounts.json (account records) and .metadata/auth_state.json (replay-protection timestamps). Back up and restore the two files together: the server refuses to start if auth_state.json is missing while migrated accounts exist, because empty replay state would re-accept previously seen requests (see Troubleshooting).
GUARDIAN_KEYSTORE_PATH/var/guardian/keystoreanyLocal Falcon/ECDSA key files (ACK signers and per-account creds).
GUARDIAN_DB_POOL_MAX_SIZE16; 32 when GUARDIAN_ENV=prodpostgresStorage backend pool size.
GUARDIAN_METADATA_DB_POOL_MAX_SIZEmatches storagepostgresMetadata backend pool size; usually leave equal.
GUARDIAN_CANONICALIZATION_FAST_PROMOTION_ENABLEDtrueanyEnables the additional promotion-only pass for recent candidates. Set to false to use only the full canonicalization interval; AWS deployments can set Terraform variable guardian_canonicalization_fast_promotion_enabled = false.
GUARDIAN_CANONICALIZATION_MAX_CONCURRENT_ACCOUNTS10; 50 when GUARDIAN_ENV=prodanyAccounts one canonicalization pass processes in parallel; 1 = fully sequential. Each account holds a DB connection only during its short fenced transactions (the dominant cost is a connectionless chain RPC), so this may exceed GUARDIAN_DB_POOL_MAX_SIZE; simultaneous write bursts just queue briefly at the pool.
GUARDIAN_CANONICALIZATION_RETAINED_TTL_SECONDS86400anyHow long a give-up candidate is kept as retained for background reconciliation; 0 disables retention (historical delete-on-give-up).
GUARDIAN_CANONICALIZATION_RECONCILE_INTERVAL_SECONDS60anyCadence of the reconcile pass over retained deltas.
GUARDIAN_RELEASE_SWEEP_ENABLEDtrueanyRuns the chain-driven release sweep (issue #434), a background task that walks every unreleased Miden account against the chain and releases those whose guardian switch never reached this server (offline switches, failed pushes, a network-dead old operator). A switch is recognised either through a pending proposal (any label) or an unpromoted switch delta whose post-state the chain reached, at the head or in the account's transaction history (works for private accounts, even after they transacted again), or, for public accounts, through the guardian key in the account's published on-chain storage. It also writes a push-path release whose own write failed. A key counts as a switch only when it is neither this server's nor the one the account was stored with, so a server restarted under a different ack key releases nothing. Set to false to leave release detection to the push path alone. All GUARDIAN_RELEASE_SWEEP_* values are trimmed; one that does not parse, or lies outside the range given for it, stops startup. AWS deployments set the switch, the rotation and the rate through Terraform variables guardian_release_sweep_enabled, guardian_release_sweep_rotation_seconds and guardian_release_sweep_max_rate_per_second.
GUARDIAN_RELEASE_SWEEP_ROTATION_SECONDS21600 (6 h)anyTarget time for one full walk of the fleet: every eligible account is visited once per rotation, with the visits spread evenly over the window. A switch executed through a proposal pending on this server is released within one rotation. 1 to 2592000 (30 days).
GUARDIAN_RELEASE_SWEEP_MAX_RATE_PER_SECOND5anyUpper bound on account visits per second, rotation and confirmation re-checks together, i.e. the sweep's share of chain-node RPC capacity. A visit is one GetAccount; only for an account whose chain state moved past the stored one it adds a storage read and, per candidate that moves the guardian key away, a chain-tip read and at least one SyncTransactions page. A fleet too large to cover in one rotation at this rate simply takes longer. 1 to 1000.
GUARDIAN_RELEASE_SWEEP_PAGE_SIZE100anyAccounts fetched from the metadata store per listing page during the walk. A batching detail only: the walk's cursor advances per visited account, never per page. 1 to 10000.
GUARDIAN_RELEASE_SWEEP_RECHECK_SECONDS60anyDelay between confirmation re-checks of an account whose published storage showed a foreign guardian key. Re-checks share the rotation's pacing, so they never raise the visit rate. 1 to 86400.
GUARDIAN_RELEASE_SWEEP_CONFIRMATIONS2anyObservations of the same foreign guardian key in published storage, each at a strictly later block, required before releasing on that evidence (the rotation visit is the first, re-checks follow). A read of a state older than the stored one never counts. A pending proposal or unpromoted delta whose post-state is found on chain is proof and releases at once. 1 to 100.
GUARDIAN_SERVER_FEATURESbuild-timedeploy scriptComma list (postgres, evm) the deploy script compiles in. Not read at runtime — controls how the image is built.

Canonicalization settings apply as follows:

SettingFull passFast promotion
check_interval_secondsSets the full-pass cadence.Stops new fast work when the next full pass is due.
fast_promotion_enabledNo effect.Enables or disables the pass.
fast_promotion_interval_secondsNo effect.Sets its cadence and admission window. Work already in flight may finish after the deadline.
fast_promotion_window_secondsNo effect.Limits eligibility to recently created candidates.
max_retriesControls when repeated verification failures discard a candidate.Not read or modified.
submission_grace_period_secondsDefers retry consumption for young candidates.Not read or modified.
divergence_confirmationsControls confirmed-divergence discard decisions.Not read or modified.
max_concurrent_accountsBounds concurrent accounts.Bounds concurrent accounts. Candidates remain sequential within each account.

Fast promotion is promotion-only: a miss, absent account, RPC failure, invalid claim, or reconstruction mismatch does not change candidate status, retry count, or divergence count. The full pass remains responsible for every retry, defer, divergence, and discard decision.

Database TLS​

TLS behavior is driven entirely by the standard libpq parameters in DATABASE_URL; there is no Guardian-specific TLS env var. To migrate a deployed AWS stack to verified TLS, see runbooks/enable-db-tls.md. The same parameters govern both the synchronous startup-migration connection and the asynchronous runtime pools, so they always behave identically.

sslmodesslrootcertBehavior
omitted / disable(any)Plaintext, no TLS.
require(none)Encrypted, certificate not verified.
require<path>Encrypted + certificate chain verified (promoted to verify-ca, matching libpq).
verify-ca<path>Encrypted + chain verified (hostname not checked).
verify-full<path>Encrypted + chain and hostname verified. Recommended for managed providers.
  • sslrootcert=<path> points at a PEM CA bundle file the server can read; the whole bundle must parse (a malformed entry fails startup). It may contain multiple roots.
  • Verifying modes fail closed: a missing/unreadable/empty CA bundle, an unknown sslmode, allow/prefer (which permit plaintext fallback), or sslrootcert=system (unsupported — needs libpq ≥16) all abort startup with an actionable error rather than connecting insecurely.
  • Hostname matching under verify-full is strict SAN-based; certificates without a matching Subject Alternative Name (including Common-Name-only certs) are rejected.

Per-provider examples:

# AWS RDS (managed; recommended). Mount a combined bundle of the Amazon RDS CA
# roots AND the Amazon Trust Services roots — the RDS Proxy presents an ACM
# certificate chaining to Amazon Trust Services, a direct instance chains to the
# RDS CA roots.
DATABASE_URL=postgres://USER:PW@HOST:5432/guardian?sslmode=verify-full&sslrootcert=/etc/guardian/tls/rds-combined-ca.pem

# Another managed provider (GCP Cloud SQL / Azure / Supabase / Neon …)
DATABASE_URL=postgres://USER:PW@HOST:5432/guardian?sslmode=verify-full&sslrootcert=/etc/guardian/tls/provider-ca.pem

# Local docker compose (no TLS)
DATABASE_URL=postgres://guardian:guardian@localhost:5432/guardian

Runtime — ACK signing and network​

VariableDefaultNotes
GUARDIAN_ENVunsetDeployment stage. prod turns on the prod-stage startup guards, switches the runtime defaults to the production profile (rate limits 200/5000, pool size 32, canonicalization concurrency 50, json logs; an explicitly set variable always wins), and selects AWS Secrets Manager as the default ACK secret source. Anything else (or unset) keeps the development defaults and ephemeral filesystem ACK keys, regenerated each restart. Override the ACK source explicitly with GUARDIAN_ACK_SECRET_PROVIDER (below), e.g. file for a stable identity without AWS.
AWS_REGIONunsetRequired whenever an AWS-backed source is selected: the default aws ACK provider under GUARDIAN_ENV=prod, GUARDIAN_ACK_ECDSA_BACKEND=aws-kms, GUARDIAN_STORAGE_ENCRYPTION_KEY_SECRET_ID, or GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ID. Not needed on the no-AWS path (GUARDIAN_ACK_SECRET_PROVIDER=file with file-backed keys), even in prod.
GUARDIAN_ALLOWED_ACCOUNT_SCHEMESunset (falcon,ecdsa)Comma-separated signature schemes that new accounts may register with: falcon, ecdsa, case-insensitive. A /configure for a new account with any other scheme is rejected with stable code signature_scheme_not_allowed (HTTP 403, gRPC PERMISSION_DENIED; meta.scheme and meta.allowed_schemes name the rejected scheme and the accepted set). Accounts already in this Guardian's metadata are never affected: the scheme is fixed on-chain at creation and every later request is verified with the scheme stored for that account, so re-configuring a known Falcon account still works with ecdsa alone. An on-chain Falcon account that is not in the metadata (re-onboarding after a metadata restore, or a SwitchGuardian from another Guardian) counts as new and is rejected; allow falcon,ecdsa for the duration of such a migration. Both ACK signers stay loaded regardless. Unset or blank allows every scheme; a value naming an unknown scheme fails startup. The check runs before signature verification so a rejected registration costs no chain call; the only information it reveals to an unauthenticated caller is the policy itself (meta.allowed_schemes) and whether an account id is already registered here. The startup banner reports the set as account_schemes on the ack signers line. New production deployments should set ecdsa (Falcon is second-class).
GUARDIAN_NETWORK_TYPEnone — requiredMiden network identifier: MidenLocal (local), MidenTestnet (testnet), MidenDevnet (devnet); case-insensitive. Pins the network identity (bech32 address prefixes, dashboard rendering) and the default Miden RPC endpoint. The server refuses to start when it is unset or unrecognized — there is no fallback network.
GUARDIAN_MIDEN_RPC_ENDPOINTper-network defaultOverrides the Miden node RPC endpoint (self-hosted node, private RPC, sidecar container) without changing network identity. Must be an origin-only http(s) URL (scheme://host[:port]); userinfo, non-root paths, queries, and fragments are rejected because tonic does not send them as authentication. An invalid value fails startup. Startup logs the validated origin. Combining an override with MidenTestnet/MidenDevnet logs a warning (legitimate for a mirror). A configured endpoint never falls back to the network default.
GUARDIAN_MIDEN_RPC_TIMEOUT_MS10000Per-request deadline on the node channel (the same 10s default every Miden RPC surface uses). Positive integer; 0 or a malformed value fails startup.
GUARDIAN_MIDEN_RPC_MAX_ATTEMPTS1 (retries off)Attempt budget for eligible idempotent node reads outside canonicalization. Canonicalization performs exactly one node read per observation regardless of this value — transient failures there are recovered by the next scheduled pass, so raising this budget never makes a canonicalization pass hold its lease longer. Transaction submission is never retried regardless of this value. Endpoint failover is not currently implemented. Retry activity is visible as guardian_miden_rpc_retries_total (never incremented by canonicalization reads).

Upgrade note: this release reduces the default Miden RPC deadline from 30 seconds to 10 seconds for existing deployments. The deadline is applied at the channel level, so it caps submissions as well as reads even though submissions remain single-attempt. Set GUARDIAN_MIDEN_RPC_TIMEOUT_MS=30000 to retain the previous deadline.

ACK secret IDs are configurable. The server reads two env vars at startup and falls back to fixed defaults when they're unset (crates/server/src/ack/secrets_manager.rs:10-13):

VariableDefaultNotes
GUARDIAN_ACK_FALCON_SECRET_IDguardian-prod/server/ack-falcon-secret-keySecrets Manager name/ARN for the Falcon ACK secret key.
GUARDIAN_ACK_ECDSA_SECRET_IDguardian-prod/server/ack-ecdsa-secret-keySecrets Manager name/ARN for the ECDSA ACK secret key. Used only when the ECDSA backend is in-memory.

ACK secret provider (stable identity without AWS)​

Outside prod the default (GUARDIAN_ACK_SECRET_PROVIDER=none) generates a fresh ACK keypair on every restart, which changes the Guardian's on-chain ack-key commitment and freezes accounts that pinned the old one. Set the provider to file to load fixed keys from local files instead — a stable identity without AWS Secrets Manager. Each file holds the hex string emitted by ack-keygen (shipped in the image as /app/ack-keygen; --out-dir writes both files directly) (identical to what Secrets Manager stores). See the Secrets runbook.

VariableDefaultNotes
GUARDIAN_ACK_SECRET_PROVIDERaws when GUARDIAN_ENV=prod, else noneSource of the ACK signing keys: aws (Secrets Manager), file (local files), or none (ephemeral, dev only). An unrecognized value fails startup; none is rejected when GUARDIAN_ENV=prod.
GUARDIAN_ACK_FALCON_SECRET_PATHunsetRequired when GUARDIAN_ACK_SECRET_PROVIDER=file. Path to a file holding the hex-encoded Falcon ACK secret key. On Unix the file must be owner-only (mode 0600) or startup fails.
GUARDIAN_ACK_ECDSA_SECRET_PATHunsetRequired when GUARDIAN_ACK_SECRET_PROVIDER=file, unless GUARDIAN_ACK_ECDSA_BACKEND=aws-kms (then the ECDSA key comes from KMS and this file is never read). Path to the hex-encoded ECDSA ACK secret key; same 0600 requirement.

Storage encryption at rest​

Application-layer encryption of the sensitive stored payloads (account state, delta and proposal payloads). It is opt-in by key-source presence — configure a key and the server encrypts; configure none and it stores plaintext exactly as before. Routing/index fields (account id, nonce, commitments, status, timestamps) always stay plaintext, including each stored state's nonce, which reveals the account's transaction count.

Which variable do I set? Choose one key source: the direct key for local work, the key-document file for self-managed deployments without Secrets Manager, or the Secrets Manager secret where AWS is available. The file and the secret hold the same {active, keys} document, so both support multi-key rotation. You never set more than one (doing so is a startup error). GUARDIAN_STORAGE_ENCRYPTION_KEY_ID is not a key: it is an optional label, and most users leave it unset.

Key sources (set exactly one; presence is what turns encryption on):

VariableDefaultNotes
GUARDIAN_STORAGE_ENCRYPTION_KEYunsetDirect key source. The actual 32-byte key, base64-encoded (openssl rand -base64 32). Single key: no multi-key rotation reads, and it lives in the process environment. For local development; self-managed production should prefer the key-document file below.
GUARDIAN_STORAGE_ENCRYPTION_KEY_FILEunsetKey-document file source. Path to a file holding the structured { "active": kid, "keys": { kid: base64-32-bytes } } document (one or more keys), read once at startup. The file must be owner-only (0600; a Kubernetes secret mount needs defaultMode: 0400) or startup fails, as for the ACK key files. This is the self-managed counterpart of the Secrets Manager source, with the same rotation procedure (see the production guide).
GUARDIAN_STORAGE_ENCRYPTION_KEY_SECRET_IDunsetSecrets Manager key source. AWS Secrets Manager name/ARN of a secret holding a structured { "active": kid, "keys": { kid: base64-32-bytes } } document (one or more keys). Reuses AWS_REGION.

Optional label:

VariableDefaultNotes
GUARDIAN_STORAGE_ENCRYPTION_KEY_IDk1Only used with the direct key source. Sets the key id (kid) recorded on each encrypted record so the key can be identified later. The default is fine for almost everyone: leave it unset unless you specifically need the dev key's id to match an id used elsewhere. (With a key document, from the file or from Secrets Manager, the ids come from its keys/active fields instead, so this variable is ignored.)

Every encrypted record stores the kid of the key that wrote it, and on read the key is resolved by that id. This is what lets a key document (in Secrets Manager or in a mounted file) hold several keys and rotate them (add a new key, repoint active, keep the old key so existing records still decrypt). The direct key source holds a single key, so it cannot do multi-key rotation reads.

Rules: configure exactly one key source (more than one set → startup error). When a key source is set the server validates it at startup and fails fast on a missing/malformed/wrong-length key — it never silently falls back to plaintext. Encryption is fixed for a populated store: the server records a marker on the first encrypted write and refuses to mix plaintext and ciphertext, so enable it against an empty store (e.g. after the Miden 0.16 reset, see MIDEN_COMPATIBILITY.md). Switching an existing store requires an explicit re-encryption migration (not yet provided).

Hosted ECDSA signer backend​

The ECDSA ACK signer can keep its private key outside the process in a hosted backend. Falcon is unaffected and always uses the in-memory path.

VariableDefaultNotes
GUARDIAN_ACK_ECDSA_BACKENDin-memoryin-memory (filesystem keystore, or Secrets Manager when GUARDIAN_ENV=prod) or aws-kms. An unrecognized value fails startup listing the supported values.
GUARDIAN_ACK_ECDSA_KMS_KEY_IDunsetRequired when GUARDIAN_ACK_ECDSA_BACKEND=aws-kms. KMS key id, ARN, or alias. The key must be ECC_SECG_P256K1 with usage SIGN_VERIFY.

With aws-kms, the server holds only the key handle; the private key never enters the process. At startup it fetches the public key, validates the key spec, and performs a sign probe to confirm kms:Sign permission — failing fast otherwise. The ECDSA secret in Secrets Manager (GUARDIAN_ACK_ECDSA_SECRET_ID) is not read on this path. Credentials resolve through the standard AWS chain (the ECS task role in production); required IAM is kms:GetPublicKey and kms:Sign on the key.

Switching an existing deployment from in-memory to aws-kms means a new keypair, hence a new ECDSA pubkey/commitment. Re-establish downstream trust accordingly.

_SECRET_ID (runtime) vs _SECRET_NAME (deploy-side): the server reads GUARDIAN_ACK_*_SECRET_ID at startup, but you typically don't set these by hand. The deploy script accepts GUARDIAN_ACK_*_SECRET_NAME (see the Deploy script section below), passes it into Terraform as guardian_ack_*_secret_name, and Terraform sets the matching _SECRET_ID env var on the ECS task. Same value, three places — see the Secrets runbook for the full override chain.

In the reference AWS deploy, Terraform sets both _SECRET_ID env vars on the ECS task to ${stack_name}/server/ack-{falcon,ecdsa}-secret-key so multi-stack deployments get scoped IDs.

Runtime — request safety​

VariableDefaultNotes
GUARDIAN_RATE_LIMIT_ENABLEDtrueMaster kill-switch for rate limiting on both transports (HTTP and gRPC; there is no per-transport toggle). Set false only in test environments. Client identity for keying comes from the ingress (rightmost X-Forwarded-For entry, then X-Real-IP, then the socket peer); deployments not behind the reference ALB must forward the client address on both listeners, strip any client-supplied X-Forwarded-For if they identify callers with X-Real-IP, and restrict direct access to the server ports; see PRODUCTION.md.
GUARDIAN_RATE_BURST_PER_SEC10; 200 when GUARDIAN_ENV=prodRequests allowed in any one-second window, keyed per IP and endpoint, where the endpoint is the HTTP path or the gRPC method. Both transports are metered from one store, but because their endpoint names differ, a burst bucket is never shared across transports.
GUARDIAN_RATE_PER_MIN60; 5000 when GUARDIAN_ENV=prodSustained rate, keyed per IP only, so HTTP and gRPC calls from one client draw on the same allowance. This is the cross-transport limit: deployments sized for HTTP-only traffic should re-check it, since gRPC traffic (the Rust SDK's default transport) counts against it since the transport-bypass fix.
GUARDIAN_MAX_REPLICAS1 (code default); greater of desired count and autoscaling max when enabled, or desired count otherwise, set by TerraformPer-replica rate-limit divisor used by global HTTP+gRPC and dashboard per-commitment limits. Each replica enforces global / GUARDIAN_MAX_REPLICAS, keeping the aggregate at or below the configured limit through steady-state autoscaling. During a rolling deployment the temporary aggregate may rise by up to deployment_maximum_percent / 100 (2× by default). Drives rate-limiting only — coordination mode is backend-derived. Running below the steady-state maximum over-throttles; HTTP keep-alive can pin a client to one replica. An override is clamped up to steady-state capacity by Terraform. Must be a positive integer when set: an invalid value fails startup in prod and is treated as 1 with a warning elsewhere. See runbooks/horizontal-scaling.md.
GUARDIAN_DASHBOARD_COMMITMENT_RATE_BURST_PER_SEC6Fleet-wide dashboard challenge and verification burst budget for one operator commitment. Divided by GUARDIAN_MAX_REPLICAS and clamped to at least 1 per replica. A custom value below the divisor can therefore exceed its nominal fleet-wide budget.
GUARDIAN_DASHBOARD_COMMITMENT_RATE_PER_MIN30Fleet-wide dashboard challenge and verification sustained budget for one operator commitment. Divided by GUARDIAN_MAX_REPLICAS and clamped to at least 1 per replica. A custom value below the divisor can therefore exceed its nominal fleet-wide budget.
GUARDIAN_MAX_REQUEST_BYTES1048576 (1 MB)Reject request bodies larger than this.
GUARDIAN_MAX_PENDING_PROPOSALS_PER_ACCOUNT20Account-level cap; hitting it returns pending_proposals_limit.
GUARDIAN_CORS_ALLOWED_ORIGINSunsetComma-separated explicit origins. Unset → permissive Any origin / Any methods / Any headers, credentials disabled (suitable for local dev). Set → strict allowlist with allow_credentials(true) (required for production browser clients).

Runtime — metrics (Prometheus)​

VariableDefaultNotes
GUARDIAN_METRICS_ENABLEDfalseMaster switch for the Prometheus integration. When false (default) nothing runs: no metrics listener, no recorder, no storage instrumentation, no background refresher.
GUARDIAN_METRICS_ADDR127.0.0.1:9464Bind address of the dedicated metrics listener (separate from the API port). Loopback by default; set 0.0.0.0:9464 in containers so a Prometheus sidecar/agent can reach it. Invalid values fall back to the default with a warning.
GUARDIAN_METRICS_PATH/metricsPath serving the Prometheus text exposition on that listener. Must start with /.
GUARDIAN_METRICS_REFRESH_INTERVAL_SECS30Cadence of the background task that refreshes slow aggregate gauges (delta status counts, in-flight proposals, account count). Scrapes never query storage directly.
GUARDIAN_METRICS_BEARER_TOKENunsetOptional shared-secret scrape token. When set, scrapes must send Authorization: Bearer <token> (Prometheus authorization.credentials in the scrape config); anything else gets 401. Compared in constant time, held as a non-loggable secret wrapper in process.

The metrics endpoint is intentionally not part of the main API router: it bypasses rate limiting and CORS and is protected by network isolation first (loopback default / private network / security group), the bearer token second, and proxy-terminated TLS where transport encryption is required. Never expose it to a public network. The exposed metric taxonomy and cardinality rules are documented in spec/api.md; see the Observability guide for scraping and a one-command Grafana dashboard stack.

Runtime — dashboard​

VariableDefaultNotes
GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_IDunsetAWS Secrets Manager secret name/ARN holding the operator allowlist JSON. Hot-reloaded on every challenge and authenticated /dashboard/* request.
GUARDIAN_OPERATOR_PUBLIC_KEYS_FILEunsetLocal JSON path for the same payload; re-read on every challenge and authenticated request. For local dev and for self-managed deployments without Secrets Manager (see the production guide); protect it like the ACK key files.
GUARDIAN_DASHBOARD_STATS_REFRESH_INTERVAL_SECS300Cadence at which the holder of the dashboard_stats lease starts a new inventory walk for the GET /dashboard/stats aggregate (which also backs the cross-account fields of GET /dashboard/info). One replica walks; every replica reads the published result from the shared store. Not a bound on snapshot age — a slow or failed walk keeps the previous publication and as_of reports the real age. Operators with stats:refresh can request an out-of-cycle walk via POST /dashboard/stats/refresh (60 s cooldown). Must be a positive integer. See DASHBOARD.md.
GUARDIAN_DASHBOARD_CURSOR_SECRETrandom per process if unset32-byte hex HMAC key for dashboard pagination cursors. Pin a shared value across replicas so cursors validate everywhere. The prod Terraform profile injects a pre-created Secrets Manager value into every task. If unset outside that profile, the server warns and generates an ephemeral per-process key and still boots (in every stage); an ephemeral key breaks pagination cursors across replicas and restarts — the dashboard feeds and the client GET /delta/history endpoint — nothing else, so it is not a startup guard.

GET /dashboard/info.environment is derived from GUARDIAN_NETWORK_TYPE (testnet, devnet, or local) rather than configured separately.

Prod-stage startup guards & HA behavior​

When GUARDIAN_ENV=prod, the server switches its runtime defaults to the production profile: GUARDIAN_RATE_BURST_PER_SEC=200, GUARDIAN_RATE_PER_MIN=5000, GUARDIAN_DB_POOL_MAX_SIZE=32 (the metadata pool follows it), GUARDIAN_CANONICALIZATION_MAX_CONCURRENT_ACCOUNTS=50, and GUARDIAN_LOG_FORMAT=json. These are the values the AWS Terraform profile injects explicitly, so a self-managed deployment gets them from the stage alone; a variable you set always wins. GUARDIAN_MAX_REPLICAS is deliberately not part of the profile (it is your topology, not a stage).

Upgrade note: before this release GUARDIAN_ENV=prod only enabled the guards and selected Secrets Manager; the tuning values stayed at the development defaults unless set. A deployment that sets GUARDIAN_ENV=prod without setting the five variables above (for example the aws-signers guide stack) moves to the production values on upgrade, including json logs. Set any of them explicitly to keep the previous value. Terraform-managed stacks are unaffected because the profile injects every value explicitly.

It also fails fast on misconfigurations that are silently broken across replicas:

  • the filesystem storage backend is refused (single-instance only — use the Postgres image with DATABASE_URL);
  • a rate limit that partitions to 0 requests per replica is refused — i.e. the global GUARDIAN_RATE_BURST_PER_SEC/GUARDIAN_RATE_PER_MIN is below GUARDIAN_MAX_REPLICAS, which would make every replica throttle all traffic. Raise the global limit or lower GUARDIAN_MAX_REPLICAS. (Non-prod only warns.)

On the Postgres backend, operator/EVM sessions, login challenges, and the canonicalization lease are shared across replicas (backend-derived — no tunable disables this). If the database is briefly unavailable, authentication fails closed (rejected, never bypassed) and recovers automatically. See the horizontal-scaling runbook.

Allowlist payload shapes and enrollment flow: docs/DASHBOARD.md.

Runtime — EVM (feature-gated)​

These take effect only when the server is built with --features evm. The server reads only the two variables in this table; the allowed chain set is derived from the keys of GUARDIAN_EVM_RPC_URLS rather than a separate variable.

VariableDefaultNotes
GUARDIAN_EVM_RPC_URLSunset (treated as an empty registry)Comma list chain_id=rpc_url. E.g. 1=https://…,11155111=https://…. Allowed chain IDs are the keys of this map. Required for usable EVM chains — when unset, the server starts but the EVM registry is empty and every chain ID will be rejected.
GUARDIAN_EVM_ENTRYPOINT_ADDRESS0x433709009b8330fda32311df1c2afa402ed8d009 (EntryPoint v0.9)Shared EntryPoint address used for finality checks across chains.

Logging​

VariableDefaultNotes
RUST_LOGinfoStandard tracing-subscriber filter. Module-scoped filters work: RUST_LOG=server::jobs::canonicalization=debug. Hot-path request events (get_state, get_delta, push_delta, proposal create/sign, lookup_account) are debug; use RUST_LOG=server=debug or RUST_LOG=server::services=debug to see them. At info each request instead emits a single span-close line carrying the span's fields (account ID, nonce, commitment, signer/match counts) plus time.busy / time.idle. That line is emitted on the success and the error path, which is what correlates the centralized 5xx log (see below) back to an account.
GUARDIAN_LOG_FORMATtext; json when GUARDIAN_ENV=prodLog output format. text: human-readable (ANSI when TTY, plain otherwise). json: flattened JSON with span context for CloudWatch Logs Insights. compact: single-line text. Value is trimmed and case-insensitive; unknown values fall back to text with a stderr warn (emitted before the tracing subscriber is installed). Unset follows the stage default; ECS sets it explicitly (see infra/variables.tf guardian_log_format).

The 5xx and 4xx lines emitted from GuardianError's HTTP IntoResponse and tonic::Status conversions run after the service span has closed, so they carry only code and detail, never account fields. The preceding span-close line is what supplies the account context for them.

Useful filters during debugging — see TROUBLESHOOTING.md.

Deploy script (scripts/aws-deploy.sh)​

These are read by the deploy script, not by the server itself. The script turns them into Terraform variables or build-time choices.

VariableDefaultNotes
STACK_NAMEguardianBase name for all AWS resources and Terraform state file.
DEPLOY_STAGEdevdev or prod; selects stage profile (autoscaling, RDS Proxy, etc.).
CPU_ARCHITECTUREX86_64X86_64 or ARM64. Picks the Docker buildx platform and the ECS task arch.
AWS_REGIONrequiredAll AWS API calls.
DOMAIN_NAMEopenzeppelin.comRoot domain for the canonical public hostname.
SUBDOMAINguardianHost portion of the public hostname. The OZ stacks override it with guardian-devnet (devnet) or guardian-testnet (testnet).
ACM_CERTIFICATE_ARNunsetACM certificate for HTTPS on the canonical hostname.
ROUTE53_ZONE_IDunsetOptional Route 53 hosted zone for an alias record.
CLOUDFLARE_ZONE_IDunsetOptional Cloudflare zone for CNAME management.
CLOUDFLARE_API_TOKENunsetRequired when either primary or secondary Cloudflare DNS management is enabled.
CLOUDFLARE_PROXIEDtrueWhether the Cloudflare CNAME should be proxied.
ALIAS_SUBDOMAINunsetMigration-only legacy subdomain under DOMAIN_NAME; leave unset for normal deployments. DNS may be Terraform-managed or external.
ALIAS_ACM_CERTIFICATE_ARNACM_CERTIFICATE_ARNMigration-only distinct certificate for the legacy hostname, attached through SNI when needed.
GUARDIAN_ACK_FALCON_SECRET_NAMEunset → ${STACK_NAME}/server/ack-falcon-secret-keyDeploy-side override for the Falcon ACK secret. Passed into Terraform as guardian_ack_falcon_secret_name and set on the ECS task as the runtime GUARDIAN_ACK_FALCON_SECRET_ID.
GUARDIAN_ACK_ECDSA_SECRET_NAMEunset → ${STACK_NAME}/server/ack-ecdsa-secret-keyDeploy-side override for the ECDSA ACK secret. Same flow as the Falcon entry above.
GUARDIAN_OPERATOR_PUBLIC_KEYS_JSONunsetInline JSON array of operator pubkeys; Terraform creates the secret from this. Mutually exclusive with GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ARN.
GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ARNunsetARN of an externally-managed operator pubkeys secret. When set, Terraform does not create one and the task reads from this ARN instead.
GUARDIAN_EVM_CHAIN_CONFIG_FILEunsetPath to a JSON file the deploy script reads to derive GUARDIAN_EVM_RPC_URLS (and the bookkeeping GUARDIAN_EVM_ALLOWED_CHAIN_IDS Terraform variable). Not read by the server.
GUARDIAN_EVM_ALLOWED_CHAIN_IDSunsetComma list of chain IDs used only by Terraform for bookkeeping / secret naming. The server itself derives allowed chains from GUARDIAN_EVM_RPC_URLS keys.
GUARDIAN_EVM_RPC_URLS_SECRET_ARNunsetECS-injection only: when set, the ECS task reads GUARDIAN_EVM_RPC_URLS from this Secrets Manager ARN at task start (the server still sees a plain env var).
GUARDIAN_EVM_ALLOWED_CHAIN_IDS_SECRET_ARNunsetSame, for the bookkeeping chain-ID list.
TF_VAR_*unsetAny standard Terraform var override; the script passes through.

Secrets in env vars — scope of in-process protection​

Secret-bearing env vars (DATABASE_URL, GUARDIAN_DASHBOARD_CURSOR_SECRET, GUARDIAN_EVM_RPC_URLS) are wrapped at the point of read into zero-on-drop, no-Display/no-Serialize types inside the server process, so they cannot accidentally appear in logs, panic messages, or serialized responses. This does not protect the OS process environment block — /proc/<pid>/environ, coredumps, fork-inherited env, and ECS task-definition environment fields all remain visible at the OS layer regardless. For the highest-sensitivity material (ACK signing keys) prefer the AWS Secrets Manager runtime-fetch path (GUARDIAN_ACK_FALCON_SECRET_ID / GUARDIAN_ACK_ECDSA_SECRET_ID), which is already how production loads those keys.

What's not env-configurable​

A few things are deliberately compile-time or builder-API only — knowing this saves you from grepping:

  • HTTP / gRPC ports. 3000 and 50051 are builder defaults (builder/mod.rs:68); configurable through the Rust builder but not via env. ECS pins these in the task definition.
  • Storage backend choice. Cargo feature postgres (or its absence), not an env var. See Storage modes.
  • EVM support. Cargo feature evm. If the binary wasn't built with it, no env var will turn it on.
  • Canonicalization knobs (check_interval_seconds, fast_promotion_interval_seconds, fast_promotion_window_seconds, max_retries, submission_grace_period_seconds, divergence_confirmations). Currently hard-coded in the canonicalization worker; require a code change to alter. The exceptions are fast_promotion_enabled and max_concurrent_accounts, configurable via GUARDIAN_CANONICALIZATION_FAST_PROMOTION_ENABLED and GUARDIAN_CANONICALIZATION_MAX_CONCURRENT_ACCOUNTS (see the environment table above).
  • Auth timestamp window. MAX_TIMESTAMP_SKEW_MS = 300_000 (5 min) is hard-coded in metadata/auth/credentials.rs:6.

Quick combos​

I want…Set
Minimum local devnothing — docker compose up works
Postgres backend locallyDATABASE_URL=… + build with --features postgres
EVM support locallyGUARDIAN_EVM_RPC_URLS (allowed chain set derives from its keys) + build with --features evm
Use Secrets Manager for ACK keysGUARDIAN_ENV=prod + AWS_REGION=<region> + secrets pre-created
Run the dashboard locallyGUARDIAN_OPERATOR_PUBLIC_KEYS_FILE=/path/to/allowlist.json
Multi-replica (HA)Postgres backend + GUARDIAN_DASHBOARD_CURSOR_SECRET=<64 hex> pinned across tasks + GUARDIAN_MAX_REPLICAS=<steady-state max capacity>. The prod Terraform profile sets these after bootstrap-dashboard-cursor-secret creates the required Secrets Manager entry.
Production defaults without AWSGUARDIAN_ENV=prod (applies the profile) + GUARDIAN_ACK_SECRET_PROVIDER=file + GUARDIAN_STORAGE_ENCRYPTION_KEY_FILE; see the production guide
Higher throughput than the prod profileGUARDIAN_RATE_BURST_PER_SEC, GUARDIAN_RATE_PER_MIN, GUARDIAN_DB_POOL_MAX_SIZE, GUARDIAN_CANONICALIZATION_MAX_CONCURRENT_ACCOUNTS