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.
- 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.
| Variable | Default | Build mode | Notes |
|---|---|---|---|
DATABASE_URL |
required | postgres |
Postgres 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/storage |
filesystem | Path for state + delta blobs. |
GUARDIAN_METADATA_PATH |
/var/guardian/metadata |
filesystem | Path 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/keystore |
any | Local Falcon/ECDSA key files (ACK signers and per-account creds). |
GUARDIAN_DB_POOL_MAX_SIZE |
16; 32 when GUARDIAN_ENV=prod |
postgres |
Storage backend pool size. |
GUARDIAN_METADATA_DB_POOL_MAX_SIZE |
matches storage | postgres |
Metadata backend pool size; usually leave equal. |
GUARDIAN_CANONICALIZATION_FAST_PROMOTION_ENABLED |
true |
any | Enables 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_ACCOUNTS |
10; 50 when GUARDIAN_ENV=prod |
any | Accounts 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_SERVER_FEATURES |
build-time | deploy script | Comma list (postgres, evm) the deploy script compiles in. Not read at runtime — controls how the image is built. |
Canonicalization settings apply as follows:
| Setting | Full pass | Fast promotion |
|---|---|---|
check_interval_seconds |
Sets the full-pass cadence. | Stops new fast work when the next full pass is due. |
fast_promotion_enabled |
No effect. | Enables or disables the pass. |
fast_promotion_interval_seconds |
No effect. | Sets its cadence and admission window. Work already in flight may finish after the deadline. |
fast_promotion_window_seconds |
No effect. | Limits eligibility to recently created candidates. |
max_retries |
Controls when repeated verification failures discard a candidate. | Not read or modified. |
submission_grace_period_seconds |
Defers retry consumption for young candidates. | Not read or modified. |
divergence_confirmations |
Controls confirmed-divergence discard decisions. | Not read or modified. |
max_concurrent_accounts |
Bounds 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.
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.
sslmode |
sslrootcert |
Behavior |
|---|---|---|
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), orsslrootcert=system(unsupported — needs libpq ≥16) all abort startup with an actionable error rather than connecting insecurely. - Hostname matching under
verify-fullis 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
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_ENV |
unset | Deployment 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_REGION |
unset | Required 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_SCHEMES |
unset (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_TYPE |
none — required | Miden 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_ENDPOINT |
per-network default | Overrides 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_MS |
10000 |
Per-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_ATTEMPTS |
1 (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=30000to 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):
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_ACK_FALCON_SECRET_ID |
guardian-prod/server/ack-falcon-secret-key |
Secrets Manager name/ARN for the Falcon ACK secret key. |
GUARDIAN_ACK_ECDSA_SECRET_ID |
guardian-prod/server/ack-ecdsa-secret-key |
Secrets Manager name/ARN for the ECDSA ACK secret key. Used only when the ECDSA backend is in-memory. |
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.
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_ACK_SECRET_PROVIDER |
aws when GUARDIAN_ENV=prod, else none |
Source 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_PATH |
unset | Required 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_PATH |
unset | Required 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. |
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.
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):
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_STORAGE_ENCRYPTION_KEY |
unset | Direct 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_FILE |
unset | Key-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_ID |
unset | Secrets 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:
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_STORAGE_ENCRYPTION_KEY_ID |
k1 |
Only 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).
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.
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_ACK_ECDSA_BACKEND |
in-memory |
in-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_ID |
unset | Required 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-memorytoaws-kmsmeans a new keypair, hence a new ECDSApubkey/commitment. Re-establish downstream trust accordingly.
_SECRET_ID(runtime) vs_SECRET_NAME(deploy-side): the server readsGUARDIAN_ACK_*_SECRET_IDat startup, but you typically don't set these by hand. The deploy script acceptsGUARDIAN_ACK_*_SECRET_NAME(see the Deploy script section below), passes it into Terraform asguardian_ack_*_secret_name, and Terraform sets the matching_SECRET_IDenv 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.
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_RATE_LIMIT_ENABLED |
true |
Master 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_SEC |
10; 200 when GUARDIAN_ENV=prod |
Requests 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_MIN |
60; 5000 when GUARDIAN_ENV=prod |
Sustained 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_REPLICAS |
1 (code default); greater of desired count and autoscaling max when enabled, or desired count otherwise, set by Terraform |
Per-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_SEC |
6 |
Fleet-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_MIN |
30 |
Fleet-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_BYTES |
1048576 (1 MB) |
Reject request bodies larger than this. |
GUARDIAN_MAX_PENDING_PROPOSALS_PER_ACCOUNT |
20 |
Account-level cap; hitting it returns pending_proposals_limit. |
GUARDIAN_CORS_ALLOWED_ORIGINS |
unset | Comma-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). |
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_METRICS_ENABLED |
false |
Master switch for the Prometheus integration. When false (default) nothing runs: no metrics listener, no recorder, no storage instrumentation, no background refresher. |
GUARDIAN_METRICS_ADDR |
127.0.0.1:9464 |
Bind 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 |
/metrics |
Path serving the Prometheus text exposition on that listener. Must start with /. |
GUARDIAN_METRICS_REFRESH_INTERVAL_SECS |
30 |
Cadence 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_TOKEN |
unset | Optional 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.
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_OPERATOR_PUBLIC_KEYS_SECRET_ID |
unset | AWS Secrets Manager secret name/ARN holding the operator allowlist JSON. Hot-reloaded on every challenge and authenticated /dashboard/* request. |
GUARDIAN_OPERATOR_PUBLIC_KEYS_FILE |
unset | Local 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_SECS |
300 |
Cadence 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_SECRET |
random per process if unset | 32-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.
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=prodonly enabled the guards and selected Secrets Manager; the tuning values stayed at the development defaults unless set. A deployment that setsGUARDIAN_ENV=prodwithout setting the five variables above (for example the aws-signers guide stack) moves to the production values on upgrade, includingjsonlogs. 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_MINis belowGUARDIAN_MAX_REPLICAS, which would make every replica throttle all traffic. Raise the global limit or lowerGUARDIAN_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.
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.
| Variable | Default | Notes |
|---|---|---|
GUARDIAN_EVM_RPC_URLS |
unset (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_ADDRESS |
0x433709009b8330fda32311df1c2afa402ed8d009 (EntryPoint v0.9) |
Shared EntryPoint address used for finality checks across chains. |
| Variable | Default | Notes |
|---|---|---|
RUST_LOG |
info |
Standard 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_FORMAT |
text; json when GUARDIAN_ENV=prod |
Log 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.
These are read by the deploy script, not by the server itself. The script turns them into Terraform variables or build-time choices.
| Variable | Default | Notes |
|---|---|---|
STACK_NAME |
guardian |
Base name for all AWS resources and Terraform state file. |
DEPLOY_STAGE |
dev |
dev or prod; selects stage profile (autoscaling, RDS Proxy, etc.). |
CPU_ARCHITECTURE |
X86_64 |
X86_64 or ARM64. Picks the Docker buildx platform and the ECS task arch. |
AWS_REGION |
required | All AWS API calls. |
DOMAIN_NAME |
openzeppelin.com |
Root domain for the canonical public hostname. |
SUBDOMAIN |
guardian |
Host portion of the public hostname. The OZ stacks override it with guardian-devnet (devnet) or guardian-testnet (testnet). |
ACM_CERTIFICATE_ARN |
unset | ACM certificate for HTTPS on the canonical hostname. |
ROUTE53_ZONE_ID |
unset | Optional Route 53 hosted zone for an alias record. |
CLOUDFLARE_ZONE_ID |
unset | Optional Cloudflare zone for CNAME management. |
CLOUDFLARE_API_TOKEN |
unset | Required when either primary or secondary Cloudflare DNS management is enabled. |
CLOUDFLARE_PROXIED |
true |
Whether the Cloudflare CNAME should be proxied. |
ALIAS_SUBDOMAIN |
unset | Migration-only legacy subdomain under DOMAIN_NAME; leave unset for normal deployments. DNS may be Terraform-managed or external. |
ALIAS_ACM_CERTIFICATE_ARN |
ACM_CERTIFICATE_ARN |
Migration-only distinct certificate for the legacy hostname, attached through SNI when needed. |
GUARDIAN_ACK_FALCON_SECRET_NAME |
unset → ${STACK_NAME}/server/ack-falcon-secret-key |
Deploy-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_NAME |
unset → ${STACK_NAME}/server/ack-ecdsa-secret-key |
Deploy-side override for the ECDSA ACK secret. Same flow as the Falcon entry above. |
GUARDIAN_OPERATOR_PUBLIC_KEYS_JSON |
unset | Inline 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_ARN |
unset | ARN 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_FILE |
unset | Path 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_IDS |
unset | Comma 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_ARN |
unset | ECS-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_ARN |
unset | Same, for the bookkeeping chain-ID list. |
TF_VAR_* |
unset | Any standard Terraform var override; the script passes through. |
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.
A few things are deliberately compile-time or builder-API only — knowing this saves you from grepping:
- HTTP / gRPC ports.
3000and50051are 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 arefast_promotion_enabledandmax_concurrent_accounts, configurable viaGUARDIAN_CANONICALIZATION_FAST_PROMOTION_ENABLEDandGUARDIAN_CANONICALIZATION_MAX_CONCURRENT_ACCOUNTS(see the environment table above). - Auth timestamp window.
MAX_TIMESTAMP_SKEW_MS = 300_000(5 min) is hard-coded inmetadata/auth/credentials.rs:6.
| I want… | Set |
|---|---|
| Minimum local dev | nothing — docker compose up works |
| Postgres backend locally | DATABASE_URL=… + build with --features postgres |
| EVM support locally | GUARDIAN_EVM_RPC_URLS (allowed chain set derives from its keys) + build with --features evm |
| Use Secrets Manager for ACK keys | GUARDIAN_ENV=prod + AWS_REGION=<region> + secrets pre-created |
| Run the dashboard locally | GUARDIAN_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 AWS | GUARDIAN_ENV=prod (applies the profile) + GUARDIAN_ACK_SECRET_PROVIDER=file + GUARDIAN_STORAGE_ENCRYPTION_KEY_FILE; see the production guide |
| Higher throughput than the prod profile | GUARDIAN_RATE_BURST_PER_SEC, GUARDIAN_RATE_PER_MIN, GUARDIAN_DB_POOL_MAX_SIZE, GUARDIAN_CANONICALIZATION_MAX_CONCURRENT_ACCOUNTS |