Skip to content

Latest commit

 

History

History
417 lines (343 loc) · 36.9 KB

File metadata and controls

417 lines (343 loc) · 36.9 KB

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

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.

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.

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), 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

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=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):

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.

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.

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.

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.

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).

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.

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-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

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).

Runtime — metrics (Prometheus)

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.

Runtime — dashboard

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.

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.

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.

Logging

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.

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.

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.

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 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