OpenDecree is configured entirely through environment variables. No config files needed.
| Variable | Description | Default | Required |
|---|---|---|---|
GRPC_PORT |
Port the gRPC server listens on. | 9090 |
No |
HTTP_PORT |
Port for the REST/JSON gateway + Swagger UI. Empty = disabled. | -- | No |
STORAGE_BACKEND |
Storage backend: postgres (default) or memory (no external deps, data not persisted). |
postgres |
No |
DB_WRITE_URL |
PostgreSQL connection string for the primary (read-write) database. Format: postgres://user:pass@host:5432/dbname?sslmode=disable |
-- | Yes (postgres mode) |
DB_READ_URL |
PostgreSQL connection string for the read replica. Used for all read queries. Falls back to DB_WRITE_URL if not set. |
DB_WRITE_URL |
No |
DB_MAX_CONNS |
Maximum number of connections in each pool. Raise if you see connection pool exhausted errors under load; lower if the database server connection limit is tight. |
25 |
No |
DB_MIN_CONNS |
Minimum number of idle connections the pool keeps open. Reduces cold-start latency for bursty traffic. | 2 |
No |
DB_MAX_CONN_LIFETIME |
Maximum wall-clock age of a connection before it is closed and replaced. Format: Go duration (e.g., 30m, 1h). Rotates connections away from a failover. |
30m |
No |
DB_MAX_CONN_IDLE_TIME |
Maximum time a connection may sit idle before being closed. Format: Go duration (e.g., 10m). Reduces idle load on the database server. |
10m |
No |
DB_HEALTH_CHECK_PERIOD |
How often the pool pings idle connections to verify they are still alive. Format: Go duration (e.g., 1m). |
1m |
No |
REDIS_URL |
Redis connection string. Used for config caching and real-time change propagation (pub/sub). Format: redis://host:6379 |
-- | Yes (postgres mode) |
ENABLE_SERVICES |
Comma-separated list of services to enable. Valid values: schema, config, audit. |
schema,config,audit |
No |
LOG_LEVEL |
Log verbosity. One of: debug, info, warn, error. Logs are JSON-formatted to stdout. |
info |
No |
USAGE_TRACKING_ENABLED |
Enable automatic recording of config field reads (GetField, GetConfig, GetFields). Set to false to disable. |
true |
No |
USAGE_FLUSH_INTERVAL |
How often accumulated read counts are flushed to storage. Format: Go duration (e.g., 30s, 1m). |
30s |
No |
GRPC_MAX_RECV_MSG_BYTES |
Maximum size of an inbound gRPC message, in bytes. Requests above this return ResourceExhausted. Set to 0 for the default. |
20971520 (20 MiB) |
No |
GRPC_MAX_SEND_MSG_BYTES |
Maximum size of an outbound gRPC message, in bytes. Responses above this return ResourceExhausted to the client. Set to 0 for the default. |
20971520 (20 MiB) |
No |
SCHEMA_MAX_FIELDS |
Maximum number of fields per schema accepted by CreateSchema and ImportSchema. Requests above this return InvalidArgument. Set to 0 to disable. |
10000 |
No |
SCHEMA_MAX_DOC_BYTES |
Maximum serialized YAML document size accepted by ImportSchema, in bytes. Requests above this return InvalidArgument. Set to 0 to disable. |
5242880 (5 MiB) |
No |
SCHEMA_COMPILE_TIMEOUT |
Wall-clock cap on a single JSON-Schema compile (per-field constraint). Format: Go duration (e.g., 5s, 2s). Set to 0 to disable the timeout. |
5s |
No |
SCHEMA_MAX_REF_DEPTH |
Maximum structural nesting depth of a JSON-Schema constraint document. Schemas deeper than this are rejected before compilation. Set to 0 to disable. |
64 |
No |
SCHEMA_MAX_REMOVE_FIELDS |
Maximum number of fields a single schema update may remove in one request. Requests above this return InvalidArgument. Set to 0 to disable. |
1000 |
No |
SCHEMA_MAX_CONCURRENT_COMPILES |
Maximum number of JSON-Schema compiles running concurrently across requests. Additional compiles block until a slot frees or the compile timeout fires, bounding goroutine growth under malicious input. Set to 0 to disable the cap. |
32 |
No |
CONFIG_MAX_LIST_LEN |
Maximum number of entries in a single batched config request (e.g. GetFields.field_paths, SetFields.updates). Requests above this return InvalidArgument. Set to 0 to disable. |
1000 |
No |
CONFIG_MAX_DOC_BYTES |
Maximum serialized YAML document size accepted by ImportConfig, in bytes. Requests above this return InvalidArgument. Set to 0 to disable. |
5242880 (5 MiB) |
No |
CONFIG_MAX_FIELD_VALUE_BYTES |
Maximum size of an individual field value at ImportConfig, in bytes. Values above this return InvalidArgument. Set to 0 to disable. |
1048576 (1 MiB) |
No |
DECREE_CEL_COST_LIMIT |
DoS guard for CEL validations: rules. cel.CostLimit value used when building each cel.Program; evaluation aborts when the rule's internal cost counter exceeds this limit. |
100000 |
No |
DECREE_CEL_INTERRUPT_FREQ |
How often the CEL cost counter is sampled while a rule's loops or comprehensions run. Lower values catch runaway evaluation sooner at small per-evaluation overhead. | 100 |
No |
DECREE_CEL_AGGREGATE_COST_CAP |
DoS guard bounding the summed CEL cost across all validations: rules evaluated in a single write. Evaluation aborts once the running total exceeds this cap, preventing many cheap rules from collectively exhausting CPU. |
1000000 |
No |
| Variable | Description | Default | Required |
|---|---|---|---|
ENABLE_REFLECTION |
Set to 1 to enable gRPC server reflection. Allows tools like grpcurl to introspect the server's service schema. Never enable in production — reflection exposes the full API to any caller regardless of auth. |
disabled | No |
GRPC_DEFAULT_TIMEOUT |
Server-side deadline applied to every request whose client did not supply one. Format: Go duration (e.g., 30s, 5s). 0 (default) disables the feature. |
-- | No |
TLS is required by default for the gRPC server and the gateway-to-gRPC dial. Set INSECURE_LISTEN=1 to opt out for local dev only.
| Variable | Description | Default | Required |
|---|---|---|---|
INSECURE_LISTEN |
Set to 1 to listen in plaintext and have the gateway dial gRPC in plaintext. Local dev only. |
-- | No |
TLS_CERT_FILE |
Path to the server's PEM-encoded TLS certificate. | -- | Yes (unless INSECURE_LISTEN=1) |
TLS_KEY_FILE |
Path to the server's PEM-encoded private key. | -- | Yes (unless INSECURE_LISTEN=1) |
TLS_CLIENT_CA_FILE |
Path to PEM CA bundle that signs allowed client certificates. When set, the server requires and verifies client certificates (mTLS). | -- | No |
TLS_GATEWAY_CA_FILE |
CA bundle the gateway uses to verify the upstream gRPC server's certificate. When unset, the system root pool is used. | system roots | No |
TLS_GATEWAY_SERVER_NAME |
SNI / verification hostname the gateway expects on the upstream gRPC certificate. | dial address host | No |
TLS_GATEWAY_CLIENT_CERT_FILE |
Client certificate the gateway presents to the upstream gRPC server (when the server requires mTLS). | -- | No |
TLS_GATEWAY_CLIENT_KEY_FILE |
Private key for TLS_GATEWAY_CLIENT_CERT_FILE. Must be set together with the cert file. |
-- | No |
DECREE_TLS_MIN_VERSION |
Minimum TLS version. Set to TLS12 to allow TLS 1.2 clients (enables an explicit cipher suite allowlist; see below). Default is TLS13. |
TLS13 |
No |
Example (TLS, no mTLS):
TLS_CERT_FILE=/etc/decree/tls/server.crt
TLS_KEY_FILE=/etc/decree/tls/server.key
TLS_GATEWAY_CA_FILE=/etc/decree/tls/server.crt # self-signed: trust the server cert
TLS_GATEWAY_SERVER_NAME=localhostExample (mTLS):
TLS_CERT_FILE=/etc/decree/tls/server.crt
TLS_KEY_FILE=/etc/decree/tls/server.key
TLS_CLIENT_CA_FILE=/etc/decree/tls/clients-ca.crt
TLS_GATEWAY_CLIENT_CERT_FILE=/etc/decree/tls/gateway.crt
TLS_GATEWAY_CLIENT_KEY_FILE=/etc/decree/tls/gateway.keyMutual TLS (mTLS) lets the server authenticate clients by certificate in addition to the usual server-to-client authentication. This is the strongest transport-layer auth posture: no valid client cert, no connection.
1. Create a CA, server cert, and client cert
# CA
openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.crt \
-days 3650 -nodes -subj "/CN=decree-ca"
# Server cert (include localhost SAN so the internal gateway dial works)
openssl req -newkey rsa:4096 -keyout server.key -out server.csr \
-nodes -subj "/CN=decree.example.com" \
-addext "subjectAltName=DNS:decree.example.com,DNS:localhost,IP:127.0.0.1"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365 \
-extfile <(printf "subjectAltName=DNS:decree.example.com,DNS:localhost,IP:127.0.0.1")
# Client cert (for the built-in HTTP/JSON gateway)
openssl req -newkey rsa:4096 -keyout gateway.key -out gateway.csr \
-nodes -subj "/CN=decree-gateway"
openssl x509 -req -in gateway.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out gateway.crt -days 3652. Start the server with mTLS
TLS_CERT_FILE=/etc/decree/tls/server.crt
TLS_KEY_FILE=/etc/decree/tls/server.key
TLS_CLIENT_CA_FILE=/etc/decree/tls/ca.crt # require client certs signed by this CA
TLS_GATEWAY_CA_FILE=/etc/decree/tls/ca.crt # gateway trusts the server cert
TLS_GATEWAY_CLIENT_CERT_FILE=/etc/decree/tls/gateway.crt
TLS_GATEWAY_CLIENT_KEY_FILE=/etc/decree/tls/gateway.key3. Verify with grpcurl
grpcurl \
-cacert ca.crt \
-cert gateway.crt \
-key gateway.key \
localhost:9090 grpc.health.v1.Health/CheckA successful response ({"status":"SERVING"}) confirms both the server cert and the client cert are accepted.
Cert rotation. The server reloads TLS_CERT_FILE / TLS_KEY_FILE from disk on every TLS handshake, so cert rotation requires no restart. Replace the files and the next handshake picks up the new cert. TLS_CLIENT_CA_FILE is also reloaded per handshake, so rotating the client CA requires no restart either.
By default the server requires TLS 1.3. Set DECREE_TLS_MIN_VERSION=TLS12 to accept TLS 1.2 clients. When TLS 1.2 is enabled, the server applies an explicit cipher suite allowlist that restricts to ECDHE key exchange and AEAD encryption — RC4, CBC, and static-RSA suites are excluded.
Allowed TLS 1.2 cipher suites:
| Cipher suite | Key exchange | Encryption |
|---|---|---|
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 |
ECDHE | AES-128-GCM |
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
ECDHE | AES-128-GCM |
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 |
ECDHE | AES-256-GCM |
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 |
ECDHE | AES-256-GCM |
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 |
ECDHE | ChaCha20-Poly1305 |
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 |
ECDHE | ChaCha20-Poly1305 |
TLS 1.3 cipher suites are not listed because Go's TLS 1.3 implementation negotiates them automatically and ignores any application-level allowlist.
When the HTTP/JSON gateway is enabled (HTTP_PORT), it dials the gRPC server at localhost:<GRPC_PORT> over TLS. This means the server certificate must include localhost as a Subject Alternative Name (SAN). A certificate issued for only the public hostname (e.g. decree.example.com) will cause a TLS handshake failure at startup:
tls: certificate is valid for decree.example.com, not localhost
Two ways to satisfy this:
-
Include
localhostin the certificate's SANs. Most certificate-generation tools (cfssl, cert-manager, openssl) accept a SAN list:# openssl example openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt \ -days 365 -nodes \ -subj "/CN=decree.example.com" \ -addext "subjectAltName=DNS:decree.example.com,DNS:localhost,IP:127.0.0.1"
-
Override the verification hostname with
TLS_GATEWAY_SERVER_NAME. Use this when you cannot modify the certificate (e.g. a certificate managed by a third party):TLS_GATEWAY_SERVER_NAME=decree.example.com
The gateway then verifies the server certificate against
decree.example.cominstead oflocalhost. This is safe as long as the gRPC server's identity is controlled by the same operator.
Rate limiting is enabled by default using an in-process token-bucket limiter (per-tenant + per-method). The health check endpoint (/grpc.health.v1.Health/*) is always exempt.
| Variable | Description | Default | Required |
|---|---|---|---|
RATE_LIMIT_ENABLED |
Set to false to disable rate limiting entirely. |
true |
No |
RATE_LIMIT_ANON_RPS |
Requests per second for unauthenticated callers (shared global bucket per method). | 10 |
No |
RATE_LIMIT_AUTHED_RPS |
Requests per second per tenant for authenticated callers. | 100 |
No |
RATE_LIMIT_SUPERADMIN_RPS |
Requests per second per superadmin identity. 0 = unlimited. |
0 |
No |
RATE_LIMIT_GLOBAL_RPS |
Requests per second for a single process-wide bucket per method, applied across all callers before role-specific limits. 0 = no global cap. |
0 |
No |
RATE_LIMIT_PREAUTH_RPS |
Requests per second for an anonymous, per-IP limiter applied before authentication runs (protects the auth path itself). 0 = disabled. |
0 |
No |
RATE_LIMIT_TRUSTED_PROXY |
Set to 1 to derive the anonymous caller's IP from the x-forwarded-for header (first hop) instead of the gRPC peer address. |
-- | No |
RATE_LIMIT_BURST |
Token bucket burst size (applies to all role classes). | 10 |
No |
Rejected requests return codes.ResourceExhausted with a RetryInfo detail (1 s retry hint). The in-process limiter does not share state across replicas; for multi-replica deployments, implement the Limiter interface backed by Redis.
Set STORAGE_BACKEND=memory to run without PostgreSQL or Redis. All data is stored in memory and lost on restart. Useful for evaluation, local development, and testing:
INSECURE_LISTEN=1 STORAGE_BACKEND=memory HTTP_PORT=8080 decree-serverSet HTTP_PORT to enable the REST API alongside gRPC. The gateway translates HTTP/JSON requests to gRPC automatically. Swagger UI is available at /docs:
# Enable REST gateway on port 8080
HTTP_PORT=8080 decree-server
# Access Swagger UI
open http://localhost:8080/docs| Variable | Description | Default | Required |
|---|---|---|---|
ENABLE_UI |
Set to 1 to serve the embedded admin GUI at /admin/ (with SPA client-side routing). Requires the REST/JSON gateway (HTTP_PORT) to be enabled. |
disabled | No |
# Enable the admin GUI alongside the gateway
HTTP_PORT=8080 ENABLE_UI=1 decree-server
# Open the GUI
open http://localhost:8080/admin/Setting DB_READ_URL to a read replica offloads read queries from the primary. This is useful in read-heavy deployments where config reads vastly outnumber writes.
Use ENABLE_SERVICES to run different services on different instances:
# Config-only instance (high read traffic)
ENABLE_SERVICES=config
# Schema + audit instance (admin operations)
ENABLE_SERVICES=schema,auditEach instance must have access to the same PostgreSQL database and Redis instance.
| Variable | Description | Default | Required |
|---|---|---|---|
JWT_JWKS_URL |
JWKS endpoint URL for JWT validation. Setting this enables JWT auth mode. When unset, the server uses metadata-based auth. | -- | No |
JWT_ISSUER |
Expected JWT iss claim. When set, tokens with a different issuer are rejected. |
-- | No |
JWT_AUDIENCE |
Expected JWT aud claim. When set, tokens whose audience does not match are rejected. Only applies in JWT auth mode. |
-- | No |
JWT_LEEWAY |
Clock-skew tolerance applied to the exp, nbf, and iat claims. Format: Go duration (e.g., 30s, 1m). When unset, a 60s default applies. Only used in JWT auth mode. |
60s |
No |
DECREE_INSECURE_DEFAULT_SUPERADMIN |
Set to 1 to restore the pre-v0.10 behaviour where a metadata request with no x-role defaults to superadmin instead of user. WARN on startup and on every request that uses the fallback. Never enable in production. See Security-Sensitive Settings. |
-- | No |
When JWT_JWKS_URL is not set, the server operates in metadata auth mode — identity is passed via gRPC metadata headers (x-subject, x-role, x-tenant-id). A missing x-role defaults to the least-privileged user role. See Auth for details on both modes.
A handful of settings weaken the server's default security posture and must only be enabled when you understand the trade-off. Two in particular trust client-supplied input and should never be enabled in production without a correctly configured upstream.
When set to 1, the anonymous rate limiter derives the caller's IP from the x-forwarded-for request header (first hop) instead of the real gRPC peer address. This is correct only when a trusted reverse proxy sits in front of the server and overwrites x-forwarded-for with the true client IP.
Risk: if no such proxy is in place (or it appends rather than overwrites the header), any caller can spoof x-forwarded-for to present a unique IP per request and completely bypass per-IP anonymous rate limiting — including the pre-auth limiter (RATE_LIMIT_PREAUTH_RPS) that protects the authentication path. Leave this unset unless you control the proxy in front of the server.
When set to 1, a metadata-mode request that omits x-role is treated as superadmin instead of the default least-privileged user. This restores the pre-v0.10 behaviour and exists only as a short-term migration aid.
Risk: any caller that can reach the server with no x-role header gains full superadmin privileges. The server logs a WARN on startup and on every request that uses the fallback. This is a migration-window-only escape hatch — never enable it in production, and remove it as soon as all clients send an explicit x-role.
This project is alpha — security defaults and the settings above are subject to change.
All observability flags are opt-in. Set to true or 1 to enable.
| Variable | Description | Default |
|---|---|---|
OTEL_ENABLED |
Master switch. Initializes the OTel SDK, OTLP exporter, and enables slog trace correlation (adds trace_id and span_id to log entries). Required for any other OTel flag to take effect. |
false |
| Variable | What it traces |
|---|---|
OTEL_TRACES_GRPC |
gRPC server spans — one span per RPC call with method, status code, and duration. |
OTEL_TRACES_DB |
PostgreSQL query spans — one span per query/transaction via pgx instrumentation. |
OTEL_TRACES_REDIS |
Redis command spans — one span per Redis command. |
| Variable | What it measures |
|---|---|
OTEL_METRICS_GRPC |
gRPC request count, latency histograms, and message sizes (via otelgrpc). |
OTEL_METRICS_DB_POOL |
Database connection pool gauges: total, acquired, idle, and max connections. |
OTEL_METRICS_CACHE |
Cache hit/miss counters for config value reads. |
OTEL_METRICS_CONFIG |
Config write counter and current version gauge. By default, no tenant_id label is emitted. |
OTEL_METRICS_SCHEMA |
Schema publish counter. |
OTEL_METRICS_TENANT_ALLOWLIST |
Comma-separated list of tenant IDs that receive a tenant_id label on config metrics. Empty by default to prevent cardinality explosion. See Observability for the trade-off. |
OpenDecree respects standard OpenTelemetry SDK environment variables:
| Variable | Description | Default |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP exporter endpoint. | http://localhost:4317 |
OTEL_SERVICE_NAME |
Service name reported in traces and metrics. | decree |
OTEL_RESOURCE_ATTRIBUTES |
Additional resource attributes (e.g., deployment.environment=prod). |
-- |
See Observability for setup instructions and trace viewing.
GRPC_PORT=9090
HTTP_PORT=8080
DB_WRITE_URL=postgres://decree:secret@db-primary:5432/centralconfig?sslmode=require
DB_READ_URL=postgres://decree:secret@db-replica:5432/centralconfig?sslmode=require
REDIS_URL=redis://redis:6379
JWT_JWKS_URL=https://auth.example.com/.well-known/jwks.json
JWT_ISSUER=https://auth.example.com
LOG_LEVEL=info
OTEL_ENABLED=true
OTEL_TRACES_GRPC=true
OTEL_METRICS_GRPC=true
OTEL_METRICS_CONFIG=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317- Auth — auth modes and role system
- Deployment — Docker Compose, Helm, and Kubernetes setup
- Observability — OTel setup and trace viewing