Security Hardening Guide

Security is a primary motivation of Praxis, not an afterthought. This guide covers the secure defaults and operational hardening for production deployments.
On this page

Security is a primary motivation of Praxis, not an afterthought. This guide covers the secure defaults and operational hardening for production deployments.

Default Security Posture

Praxis ships secure by default and fails closed on ambiguous configuration:

  • A listener address is required; there is no implicit default, so a listener never binds to an interface you did not name. Bind to 127.0.0.1 rather than 0.0.0.0 when a listener should not be externally reachable.
  • TLS certificate verification is enabled by default for upstream connections.
  • Admin endpoints are restricted to loopback; non-loopback binding is a validation error unless insecure_options.allow_public_admin is set.
  • A loopback admin listener rejects requests whose Host is not a loopback name, blocking DNS rebinding from the operator’s browser (see Admin DNS Rebinding).
  • unsafe_code = "deny" in workspace lints; no unsafe Rust in the Praxis codebase.
  • Rustls for TLS (no OpenSSL, no C FFI in the TLS path).
  • TLS certificate and key paths reject directory traversal (..).
  • Health check targets reject loopback, link-local, and cloud metadata addresses (SSRF protection).
  • Upstream hostnames that resolve to private or reserved addresses are refused at connection time on both the TCP and HTTP data planes, so a DNS record that rebinds after startup cannot steer traffic to loopback, RFC 1918, or 169.254.169.254. Set insecure_options.allow_private_upstreams when upstream DNS names legitimately resolve into private space.
  • Policy engine outbound calls (JWKS, token exchange, CIBA backchannel) share the proxy’s sub-request connector. Private DNS answers (loopback, RFC 1918, link-local, cloud metadata, and CGNAT) are skipped; calls with no public answer are refused. Resolution happens once to prevent rebinding. Use allow_private_idp for an in-cluster provider.
  • Policy engine TLS verifies against the platform trust store, including SSL_CERT_FILE and SSL_CERT_DIR; certificate and hostname verification are always on. Cluster tls settings do not apply, so private-CA and mTLS providers are unsupported and cannot share cluster-TLS connections.
  • Root execution (UID 0) rejected by default.
  • Supply chain audited via cargo audit and cargo deny.
  • Reserved internal headers (x-praxis-* and AI extension prefixes x-ext-protocol-*, x-ext-agent-*) are rejected from client requests, stripped before forwarding to backends, and stripped from backend responses before reaching clients.
  • --dump redacts credential injection literal values as [REDACTED] to prevent accidental secret exposure in config dumps.

Network Security

  • Bind public-facing listeners to specific interfaces rather than 0.0.0.0.
  • Place Praxis behind a firewall. Expose only the ports your listeners require.
  • Use separate listeners for public traffic and internal admin or health-check endpoints.
  • Restrict admin and metrics endpoints to internal networks or loopback addresses.
  • Restrict admin endpoints (including KV store API) to internal networks or loopback addresses. The KV admin API allows runtime modification of routing and transformation data.

Admin DNS Rebinding

The admin API has no authentication; loopback binding is its access control. A web page the operator visits can rebind its own DNS name to 127.0.0.1, after which the browser treats the admin API as same-origin and can read /api/pipelines, /api/stats, and KV values, or send PUT/DELETE to /api/log-level and /api/kv/*.

Every such request still carries the attacker’s name in Host. When admin.address is a loopback address, every admin route (including /healthy, /ready, and /metrics) answers 421 Misdirected Request with {"error":"misdirected request"} unless each Host header (and any absolute-form request authority) is one of:

  • a loopback IP literal: 127.0.0.0/8, [::1], or IPv4-mapped loopback, with or without a port
  • localhost, in any case, optionally with a trailing dot and a port

A request with no Host at all (HTTP/1.0) is served: browsers always send Host, so only a client that already reaches the socket directly can omit it. Probe and scrape the admin port as 127.0.0.1, [::1], or localhost; a local DNS alias for the admin address is rejected.

The check is skipped when the admin listener binds a non-loopback address (allow_public_admin), because operators may legitimately reach it by DNS name. Such a listener is still reachable through loopback on the same host, so protect it with network controls rather than relying on the bind address.

TLS Best Practices

  • Set certificate and key file permissions to 0600, owned by the Praxis process user.
  • Use min_version: tls13 in TLS configuration. TLS 1.2 can be used if required, but TLS 1.0 and 1.1 are deprecated and Praxis will not negotiate them.
  • Rotate certificates before expiration. Single-cert listeners hot-reload certificates automatically (see tls.md). Multi-cert listeners require a restart.
  • Use separate certificate entries with server_names for multi-domain deployments (SNI routing).
  • Enable CRL checking for mTLS listeners by adding crl_paths to the client_ca block. CRL paths reject directory traversal (..). See tls.md for configuration details.

Access Control

  • IP ACLs: Use the ip_acl filter to restrict access by source IP. Use either allow or deny, not both (mutually exclusive). An allow-list implicitly denies all non-matching IPs.
  • Rate Limiting: Configure rate_limit filters to bound request volume per client or globally. Tune limits based on expected traffic patterns.
  • CORS: Use the cors filter with explicit allow_origins rather than wildcards. Restrict allow_methods and allow_headers to what your application requires.
  • CSRF: Use the csrf filter with explicit trusted_origins. The enforce_percentage field enables gradual rollout; enforcement sampling is randomized per-request to prevent attackers from predicting unenforced windows.
  • Connection limits: Set max_connections on listeners to cap concurrent connections. HTTP listeners reject excess requests with 503 and Retry-After; TCP listeners close immediately.
  • Path-based gating is not a boundary against normalizing upstreams: filter conditions (when: { path_prefix: … }) and router route matches compare the raw request path. Praxis does not resolve dot-segments (/./, /../), collapse duplicate slashes (//), or percent-decode the path before matching, and it forwards the path to the upstream verbatim (this is deliberate — see the %2f/// passthrough behavior). A request such as //admin or /%2e/admin will therefore not match a path_prefix: /admin gate, yet an upstream that normalizes the path may still treat it as /admin. Do not rely on path-prefix gating of basic_auth, ip_acl, or csrf as the sole access control in front of a backend that normalizes paths. Prefer gating on a classifier-promoted x-praxis-* header (the “classify → route → branch” pattern), or terminate sensitive paths at the proxy.

Resource Limits

  • Memory pressure: Set runtime.max_memory_bytes to a process RSS ceiling. When exceeded, the proxy rejects new requests with 503 to prevent OOM. See configuration.md for details.
  • File descriptors: Praxis raises its open file limit at startup and sheds requests with 503 before descriptors run out. Set downstream_keepalive_timeout_ms on listeners so idle clients cannot pin descriptors, and see capacity-planning.md for sizing the limit and raising the hard limit.
  • Payload size: Set body_limits.max_request_bytes and body_limits.max_response_bytes to bound buffered payload sizes. Requests exceeding the limit receive 413.

Deployment

Container Security

  • Run the container as a non-root user. The official image uses a dedicated praxis user.
  • Mount the filesystem read-only where possible. Configuration and TLS materials can be mounted as read-only volumes.
  • Drop all Linux capabilities except those required for binding to privileged ports (if needed).
  • Use a minimal base image to reduce attack surface.

Kubernetes

  • Set runAsNonRoot: true and readOnlyRootFilesystem: true in the pod security context.
  • Use NetworkPolicy to restrict traffic.
  • Store TLS certificates in Kubernetes Secret objects and mount them read-only.
  • Set resource limits to prevent resource exhaustion.

Insecure Configuration Options

The following options weaken security. Use them only in development:

  • verify: false on upstream TLS: Disables certificate verification. Acceptable only for local development with self-signed certs.
  • Binding to 0.0.0.0: Exposes the listener on all interfaces. Use specific addresses in production.
  • Wildcard CORS origins ("*"): Allows any origin. Use explicit origin lists in production.
  • Empty IP ACL allowlists: An empty allowlist permits all traffic. When possible, use the principle of least privilege and only allow access from the networks that require it.