Skip to content

Latest commit

 

History

History
337 lines (250 loc) · 21.2 KB

File metadata and controls

337 lines (250 loc) · 21.2 KB

Server Configuration

OpenDecree is configured entirely through environment variables. No config files needed.

Server

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

gRPC Server Options

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

Transport Security (TLS)

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

Example (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.key

mTLS Setup

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

2. 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.key

3. Verify with grpcurl

grpcurl \
  -cacert ca.crt \
  -cert gateway.crt \
  -key  gateway.key \
  localhost:9090 grpc.health.v1.Health/Check

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

TLS 1.2 Compatibility

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.

Gateway TLS and the localhost SAN requirement

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:

  1. Include localhost in 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"
  2. 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.com instead of localhost. This is safe as long as the gRPC server's identity is controlled by the same operator.

Rate Limiting

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. ⚠️ Security-sensitive — see Security-Sensitive Settings. -- 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.

In-Memory Mode

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

REST/JSON Gateway

Set 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

Admin GUI

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/

Split Read/Write Database

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.

Selective Service Enablement

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

Each instance must have access to the same PostgreSQL database and Redis instance.

Authentication

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. ⚠️ Insecure — migration window only. Logs a 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.

Security-Sensitive Settings

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.

RATE_LIMIT_TRUSTED_PROXY

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.

DECREE_INSECURE_DEFAULT_SUPERADMIN

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.

Observability (OpenTelemetry)

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

Trace Flags

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.

Metric Flags

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.

Standard OTel Variables

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.

Example: Minimal Production Config

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

Related

  • Auth — auth modes and role system
  • Deployment — Docker Compose, Helm, and Kubernetes setup
  • Observability — OTel setup and trace viewing