Security
Security considerations and hardening guidance for production mroki deployments.
See Configuration for the full environment variable reference.
API Key Authentication
Every request to the mroki API must include a valid API key via the Authorization header:
Authorization: Bearer <key>The key is configured with the MROKI_APP_API_KEY environment variable and must be at least 16 characters. Keys are compared using crypto/subtle.ConstantTimeCompare, preventing timing side-channel attacks.
Generating a strong key:
# 32-byte random key, base64-encoded (44 characters)
openssl rand -base64 32Unauthenticated or invalid requests receive an RFC 7807 error response with HTTP 401.
Field Redaction
mroki replaces sensitive field values with [REDACTED] before storage. The following fields are redacted by default:
headers.Authorizationheaders.Cookieheaders.Set-Cookieheaders.X-Api-Key
Fields use gjson path notation with a headers. or body. prefix (e.g. body.user.password).
Redacted fields are automatically excluded from diff computation so they don't produce false positives.
Adding fields per gate (API mode):
# Add extra redacted fields to an existing gate
curl -X PATCH /gates/{id} \
-d '{"redacted_fields": ["headers.X-Internal-Token", "body.secret"]}'Adding fields in standalone proxy mode:
# Comma-separated list — adds to the default set
MROKI_APP_REDACTED_FIELDS=headers.X-Internal-Token,body.user.passwordRate Limiting
The API enforces a token-bucket rate limit of 1000 requests per minute per IP (configurable via MROKI_APP_RATE_LIMIT). When exceeded, the API responds with:
- HTTP
429 Too Many Requests Retry-Afterheader indicating when to retry
Metrics Endpoint
When MROKI_APP_METRICS_ENABLED=true (the default), both components expose a Prometheus /metrics endpoint without authentication, mirroring the health probes:
- mroki-api —
GET /metricson the API port (MROKI_APP_PORT), outside the API-key middleware chain. - mroki-proxy —
GET /metricson the admin port (MROKI_APP_ADMIN_PORT), isolated from proxied traffic.
The endpoint exposes operational data, including per-gate series labelled with gate UUIDs and latency distributions. Treat it as internal:
- Keep the API and proxy admin ports on a trusted network (private subnet, same Kubernetes pod) and do not expose
/metricspublicly. Scope it to your Prometheus scrapers via firewall rules or aNetworkPolicy. - If you front the API with a reverse proxy, do not route external traffic to
/metrics. - Set
MROKI_APP_METRICS_ENABLED=falseto disable the endpoint entirely where it is not needed.
TLS / Network Security
mroki does not terminate TLS itself. Use a reverse proxy or load balancer (nginx, Caddy, cloud LB) to terminate HTTPS in front of the API.
Example nginx snippet:
server {
listen 443 ssl;
server_name mroki.example.com;
ssl_certificate /etc/ssl/certs/mroki.crt;
ssl_certificate_key /etc/ssl/private/mroki.key;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}The internal network between the proxy and the API should be trusted (e.g. same Kubernetes pod, private subnet) or secured with mutual TLS (mTLS). Proxy-to-API mTLS is planned but not yet implemented.
Security Headers
The API emits standard security response headers on every response — including unauthenticated endpoints such as /health and /metrics, and error responses. The following headers are always sent:
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options | nosniff | Stops browsers from MIME-sniffing responses away from the declared Content-Type |
X-Frame-Options | DENY | Prevents the responses from being framed (clickjacking protection) |
Referrer-Policy | no-referrer | Prevents the Referer header from leaking API URLs to third parties |
HSTS (Strict-Transport-Security)
Strict-Transport-Security is off by default because mroki does not terminate TLS. Enabling it while the API is reachable over plain HTTP can make clients unreachable, so only turn it on once a TLS-terminating reverse proxy is confirmed in front of the API. mroki never auto-detects TLS from the request; the header is emitted solely based on the config toggle.
# Only enable behind TLS termination
MROKI_APP_HSTS_ENABLED=true
# max-age advertised to browsers (Go duration, default 8760h = 365d)
MROKI_APP_HSTS_MAX_AGE=8760hWhen enabled, the API sends:
Strict-Transport-Security: max-age=31536000; includeSubDomainsMROKI_APP_HSTS_MAX_AGE must be positive when HSTS is enabled, otherwise the API rejects the configuration at startup.
Hub (static server) security headers
The hub is a static Vue SPA served by Caddy (build/package/mroki-hub/Caddyfile). It emits security response headers on every SPA response (the /health endpoint is left untouched):
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options | nosniff | Stops browsers from MIME-sniffing responses away from the declared Content-Type |
X-Frame-Options | DENY | Prevents the hub from being framed (clickjacking protection), paired with CSP frame-ancestors 'none' |
Referrer-Policy | no-referrer | Prevents the Referer header from leaking URLs to third parties |
Content-Security-Policy | see below | Restricts the origins the SPA may load resources from and connect to |
The Content-Security-Policy is:
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' {$MROKI_APP_API_BASE_URL}; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'style-src 'unsafe-inline'is required because Vue/reka-ui emit runtime inlinestyleattributes for positioning.connect-srcis widened at container start with the deploy-time API origin (MROKI_APP_API_BASE_URL) so the SPA'sfetch/XHR calls to mroki-api are allowed. If unset, it falls back toconnect-src 'self'.
Unlike the API, the hub emits Strict-Transport-Security: max-age=31536000; includeSubDomains unconditionally. This is safe because browsers ignore HSTS over plain HTTP and only honor it over HTTPS — it assumes TLS is terminated by an upstream reverse proxy (the hub, like the rest of mroki, does not terminate TLS itself).
Database Security
# Use a strong, randomly generated password
POSTGRES_PASSWORD=$(openssl rand -base64 32)
# Enable SSL for the database connection
MROKI_APP_DATABASE_URL=postgres://user:pass@host:5432/mroki?sslmode=require
# Restrict network access in pg_hba.conf
host mroki mroki 10.0.0.0/8 scram-sha-256Additional recommendations:
- Create a dedicated database user with only the permissions mroki requires (SELECT, INSERT, UPDATE, DELETE on its tables)
- Store database credentials in a secrets manager (Kubernetes Secrets, AWS Secrets Manager, etc.)
CORS
Cross-origin requests are controlled via MROKI_APP_CORS_ORIGINS. Set it to a comma-separated list of allowed origins:
MROKI_APP_CORS_ORIGINS=https://hub.example.com,https://admin.example.comWhen configured, the API sets:
Access-Control-Allow-Origin: <configured origin>
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400If MROKI_APP_CORS_ORIGINS is empty or unset, CORS is disabled entirely — no Access-Control-* headers are sent.
The wildcard origin * is not permitted. Because the API always allows the Authorization header, a wildcard origin would let any site drive authenticated cross-origin requests. This misconfiguration is rejected at startup with a clear error — set an explicit list of trusted origins (e.g. https://hub.example.com) instead.
Production Checklist
- Strong API key — at least 16 characters, randomly generated (
openssl rand -base64 32) - TLS termination — place a reverse proxy or load balancer in front of the API
- Enable HSTS — once TLS termination is confirmed, set
MROKI_APP_HSTS_ENABLED=true(see Security Headers) - Database SSL — append
?sslmode=requiretoMROKI_APP_DATABASE_URL - Restrict network access — firewall the API and database; deploy the proxy in an isolated network
- Configure CORS origins — set
MROKI_APP_CORS_ORIGINSto only the domains that need access - Restrict the metrics endpoint —
/metricsis unauthenticated; firewall it to your scrapers or disable it withMROKI_APP_METRICS_ENABLED=falsewhere unused - Enable field redaction — review default redacted fields; add application-specific fields per gate or via
MROKI_APP_REDACTED_FIELDS - Monitor logs — watch for 401/429 responses and unusual traffic patterns