coordd — Setup & Configuration¶
This document covers how to run and configure the coordd server in both development and production environments.
Newly released
seedward-chaincoord v1.0.0 is the first stable release. It has not had an external security audit — review the threat model and verify on your own setup before high-value use.
Configuration¶
coordd resolves configuration from three sources, in order of precedence (highest first):
- CLI flags (
--listen-addr,--db-path, …) - Environment variables (
COORD_*) - Config file (
config.yaml— searched in.,$HOME/.coordd,/etc/coordd)
Most options are available through all three sources, but a few have no CLI flag and must come from an env var or
the config file: the signing keys (audit_private_key/_file, jwt_private_key/_file), admin_addresses,
launch_policy, and insecure_no_ssrf_check (see the Flag column in the reference table below). Environment variables
are the recommended approach for production deployments; the config file is convenient for local development.
Development¶
Minimal config.yaml¶
listen_addr: ":8080"
db_path: "./data/coord.db"
audit_log_path: "./data/audit.jsonl"
files_path: "./data/genesis"
log_level: "debug"
cors_origins: "http://localhost:3000"
audit_private_key_file: "./data/audit_key"
jwt_private_key_file: "./data/jwt_key"
Generate the two key files first:
mkdir -p data
bin/coordd keygen > data/audit_key
bin/coordd keygen > data/jwt_key
chmod 600 data/audit_key data/jwt_key
log_level: debug enables the human-readable ConsoleWriter output instead of JSON, which is easier to read during
development.
CORS¶
The web app (Next.js) runs on http://localhost:3000 by default. Set cors_origins to that origin so the browser
allows cross-origin requests (required for the SSE stream, which connects directly from the browser):
Or via environment variable:
Multiple origins are comma-separated:
Do not use
*in development unless you have no other option —AllowCredentials: trueis set on the server, and browsers will reject credentialed requests to a wildcard origin. Use the exact origin instead.
Running the server¶
# Build
make build-server
# Run migrations first
./bin/coordd migrate --config config.yaml
# Start
./bin/coordd serve --config config.yaml
Production¶
TLS¶
coordd supports three deployment modes:
| Mode | Description |
|---|---|
| Native TLS | coordd terminates TLS itself using --tls-cert / --tls-key |
| Infra TLS | TLS is terminated at a load balancer, ingress, or reverse proxy; coordd binds plain HTTP on a private interface |
| Local dev | Plain HTTP on loopback; TLS not needed |
Native TLS¶
Pass paths to a PEM certificate and private key:
Or via environment variables:
Both must be set together or both left empty. Setting only one is a configuration error.
For local HTTPS testing (e.g. browser wallet testing that requires a secure context):
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
-keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"
coordd serve --tls-cert cert.pem --tls-key key.pem ...
Infra TLS (reverse proxy / load balancer)¶
When TLS is terminated upstream, coordd binds plain HTTP. Set COORD_INSECURE_NO_TLS=true to suppress the startup
warning — this makes the intent self-documenting in your deploy config:
Minimal Caddy config (automatic HTTPS via Let's Encrypt):
For nginx, include proxy_set_header X-Real-IP $remote_addr; so IP-based rate limiting on POST /api/v1/auth/challenge sees
the real client address rather than the proxy address.
Local dev¶
Plain HTTP on loopback (127.0.0.1 or ::1) suppresses the warning automatically — no flag needed.
Observability¶
Health check — GET /healthz¶
Unauthenticated. Probes liveness dependencies and returns:
200 {"status":"ok"}when the database is queryable (SELECT 1) and the audit log file is present.503 {"status":"unavailable"}when either fails — a DB error, or the audit log path can't bestat'd (unmounted, deleted, or unreadable). It is an existence probe, not a write test, so a mounted-but-full disk still passes.
Failure detail is written to the server log, not the response body, so an unauthenticated caller learns only up/down. Point your orchestrator's liveness/readiness probe at it.
Access lines are leveled by response class: a successful request (< 400, including this frequent 200 probe)
logs at debug, a failed one (>= 400) at info. So at the normal info level the access log shows only
failures — a healthy /healthz stays silent, while a failing one (503) is visible — and a live switch to debug
(see Log level) turns on full request tracing.
Metrics — GET /metrics¶
Unauthenticated Prometheus endpoint exposing the default Go runtime and process metrics (goroutines, heap, GC, open file descriptors, CPU). Network-restrict it — it is not behind auth, so keep it off the public interface and scrape it from your monitoring network.
Log level¶
The verbosity is set at startup by log_level and can be changed live, without a restart
(admin only):
GET /api/v1/admin/log-level→{"level":"info"}— the current level.POST /api/v1/admin/log-levelwith{"level":"debug"}— set it. Acceptstrace,debug,info,warn,error(fatal,panic, anddisabledare rejected — they would silence the log).
The change is in-memory: it takes effect immediately and reverts to the configured log_level on restart. Only
the threshold changes — the console-vs-JSON output format is fixed at startup (JSON unless the server started at
debug). The change itself is logged at warn, recording the old and new level and the admin who made it. A handy pairing
with the access-log leveling above: drop to debug to trace all requests (successful ones and health probes
included) live, then back to info to see only failures again.
Security response headers¶
Every response carries defensive headers:
| Header | Value | When |
|---|---|---|
X-Content-Type-Options |
nosniff |
always |
X-Frame-Options |
DENY |
always |
Strict-Transport-Security |
max-age=63072000; includeSubDomains |
when coordd terminates TLS |
HSTS is added only when coordd terminates TLS itself (native TLS — tls_cert/tls_key set). Behind an
upstream TLS proxy (infra TLS mode), add HSTS at the proxy instead.
Configuration Reference¶
| Key | Env var | Flag | Default | Required |
|---|---|---|---|---|
listen_addr |
COORD_LISTEN_ADDR |
--listen-addr |
:8080 |
No |
db_path |
COORD_DB_PATH |
--db-path |
— | Yes |
audit_log_path |
COORD_AUDIT_LOG_PATH |
--audit-log-path |
— | Yes |
files_path |
COORD_FILES_PATH |
--files-path |
— | Yes |
audit_private_key |
COORD_AUDIT_PRIVATE_KEY |
— | — | Yes¹ |
audit_private_key_file |
COORD_AUDIT_PRIVATE_KEY_FILE |
— | — | Yes¹ |
jwt_private_key |
COORD_JWT_PRIVATE_KEY |
— | — | Yes² |
jwt_private_key_file |
COORD_JWT_PRIVATE_KEY_FILE |
— | — | Yes² |
log_level |
COORD_LOG_LEVEL |
--log-level |
info |
No |
cors_origins |
COORD_CORS_ORIGINS |
--cors-origins |
(disabled) | No |
admin_addresses |
COORD_ADMIN_ADDRESSES |
— | (none) | No |
launch_policy |
COORD_LAUNCH_POLICY |
— | restricted |
No |
genesis_host_mode |
COORD_GENESIS_HOST_MODE |
--genesis-host-mode |
false |
No |
genesis_max_bytes |
COORD_GENESIS_MAX_BYTES |
--genesis-max-bytes |
734003200 (700 MiB) |
No |
tls_cert |
COORD_TLS_CERT |
--tls-cert |
— | No |
tls_key |
COORD_TLS_KEY |
--tls-key |
— | No |
insecure_no_tls |
COORD_INSECURE_NO_TLS |
--insecure-no-tls |
false |
No |
insecure_no_rate_limit |
COORD_INSECURE_NO_RATE_LIMIT |
--insecure-no-rate-limit |
false |
No |
insecure_no_ssrf_check |
COORD_INSECURE_NO_SSRF_CHECK |
— | false |
No |
rehearsal_ops_token |
COORD_REHEARSAL_OPS_TOKEN |
— | (bridge disabled) | No |
rehearsal_ops_token_file |
COORD_REHEARSAL_OPS_TOKEN_FILE |
— | (bridge disabled) | No |
rehearsal_lease_ttl |
COORD_REHEARSAL_LEASE_TTL |
— | 45m |
No |
rehearsal_gate |
COORD_REHEARSAL_GATE |
— | off |
No |
audit_startup_verify |
COORD_AUDIT_STARTUP_VERIFY |
— | full |
No |
¹ Exactly one of audit_private_key (inline base64) or audit_private_key_file (path) must be set.
² Exactly one of jwt_private_key (inline base64) or jwt_private_key_file (path) must be set.
audit_private_key / audit_private_key_file¶
Ed25519 seed for signing audit log entries. Generate with coordd keygen. In production, prefer _FILE so the raw seed
never appears in environment variable listings or container inspection output:
coordd keygen | docker secret create audit_key -
# then set COORD_AUDIT_PRIVATE_KEY_FILE=/run/secrets/audit_key
jwt_private_key / jwt_private_key_file¶
Ed25519 seed for signing session JWTs. Must be different from the audit key. Same file-based pattern applies.
admin_addresses¶
Comma-separated list of operator addresses that have admin privileges (/api/v1/admin/* endpoints). If empty, no address has
admin access.
launch_policy¶
Controls who may create new launches:
restricted(default) — only addresses on the coordinator allowlist (/api/v1/admin/coordinators) may create a launchopen— any authenticated address may create a launch
genesis_host_mode¶
When true, coordd accepts raw genesis file uploads (POST /api/v1/launch/:id/genesis) and serves them directly from disk.
When false (the default), only attestor mode is available — committee members register an external URL and SHA-256
hash.
genesis_max_bytes¶
Maximum raw genesis upload size in bytes when host mode is enabled. Defaults to 700 MiB. Ignored when
genesis_host_mode is false.
cors_origins¶
Comma-separated list of allowed origins for cross-origin requests. Only needed when a browser-based client (the
validator web app) connects to coordd from a different origin.
- Leave empty to disable CORS headers entirely (default — safe for API-only or same-origin deployments).
- Set to the exact origin(s) of the web app in both dev and prod. Wildcards are not supported when credentials are involved.
tls_cert / tls_key¶
Paths to a PEM-encoded TLS certificate and private key. Both must be set together, or both left empty. When set,
coordd calls ListenAndServeTLS and handles TLS termination itself (native TLS mode). See the TLS section
above for all deployment modes.
insecure_no_tls¶
Suppresses the startup warning when TLS is not configured and the listen address is not loopback. Set this when TLS is
terminated upstream (load balancer, ingress, reverse proxy) and coordd binds plain HTTP on a private network
interface. The Docker Compose file sets this automatically.
insecure_no_rate_limit¶
Disables all rate limiters: the HTTP per-IP middleware on POST /api/v1/auth/challenge (10 req/IP/min) and validator write
endpoints (60 req/IP/min), and the storage-layer per-operator limit on challenge issuance (5 req/operator/5 min). Only
for automated test environments — do not enable in production.
insecure_no_ssrf_check¶
Disables DNS-resolution and private-IP validation on user-supplied RPC URLs (monitor_rpc_url) and genesis attestor
URLs. Only enable this in trusted environments — for example, the smoke-test Docker network, where RPC hostnames are
internal container names that would fail the SSRF check. Do not enable in production.
rehearsal_ops_token / rehearsal_ops_token_file¶
Shared bearer token authenticating the rehearsal bridge (ops plane) endpoints under
/api/v1/bridge/* — a
headless service-to-service credential, not a wallet. It is an arbitrary secret you generate yourself
(any high-entropy string, e.g. openssl rand -hex 32), configured identically on both sides. When set,
the rehearsal service presents it as
Authorization: Bearer <token> to pull the approved input set and post signed results. Leave unset to
disable the bridge (all bridge requests are rejected, fail-closed). Deployment-wide, not per-launch;
prefer the _file variant (secret manager) over the plain env var. Rotation is "swap the secret + reload."
Deploy the /api/v1/bridge/* endpoints on an internal network only (e.g. an ingress rule restricting
the prefix), since the ops plane must not be internet-reachable.
rehearsal_lease_ttl¶
How long a claimed rehearsal run (POST /api/v1/bridge/launches/{id}/rehearsal-claim) holds its single-writer
lease before it is treated as stale and re-claimable. A crashed runner self-heals after this window without
operator intervention; set it comfortably above your longest rehearsal. Accepts a Go duration string
(45m, 1h, 90m). Defaults to 45m when unset. For an immediate override of a stuck lease, a committee member can
call
POST /api/v1/launch/{id}/rehearsal/{attempt_id}/reset instead of waiting for expiry.
rehearsal_gate¶
Opt-in policy for whether a launch may finalize genesis (WINDOW_CLOSED → GENESIS_READY) only after a
passing rehearsal. Default off — coordd runs fully standalone; rehearsal is an optional bolt-on, never a
hard dependency.
off(default) — the gate is never consulted. A deployment with no rehearsal service is unaffected.advisory— the gate is evaluated and, when unsatisfied, recorded in the audit log, but never blocks.required— publishing genesis is rejected (409) unless the launch's latest rehearsal fact isPASSand current (itsinput_set_hashstill matches the present approved set). Enforced when thePUBLISH_GENESISproposal is raised, with a re-check when it executes.
required needs the rehearsal bridge enabled (a rehearsal_ops_token) — coordd refuses to start with
rehearsal_gate=required and no ops token. It also requires a per-launch trusted rehearsal service pubkey
(set via PATCH /api/v1/launch/{id}); a required launch with no configured service is rejected at publish time.
Independent of this gate, coordd always enforces that a published genesis matches the approved validator set it was assembled from (the set can change in
WINDOW_CLOSEDvia approve/remove) — a correctness invariant, not an opt-in.
log_level¶
Controls verbosity at startup. Accepted values: debug, info, warn, error (trace also works but is rarely
needed).
debug— human-readable console output (stderr), verbose. Use in development only.infoand above — structured JSON to stdout. Use in production.
Changeable at runtime without a restart via POST /api/v1/admin/log-level; a runtime change is in-memory and
reverts to this configured value on restart.
audit_startup_verify¶
Depth of the boot-time audit-log integrity check (see Audit Log → Startup and live integrity checks):
full(default) — scan the whole log on startup (Ed25519 signatures, hash-chain, timestamps) in addition to the cheap chain-tip check. Tamper or corruption refuses startup; a backward timestamp only warns.tail— only the cheap chain-tip check. For operators whose log has grown large; pair it with a scheduledcoordd audit verify.