Private
Public Access
1
0
Files
linux_patch_manager/config/config.example.toml
Draco-Lunaris-Echo 3bdae4bcc5 fix(security): harden IP allowlist against XFF bypass and spoofing (#3)
Hardens the IP allowlist in require_auth against the two bypasses filed in #3.

1. Bypass via missing X-Forwarded-For (no IP to check, allowlist skipped).
2. Spoofing via attacker-controlled X-Forwarded-For (header trusted unconditionally).

Resolves both by deriving the client IP from the socket peer (ConnectInfo<SocketAddr>) and only honoring X-Forwarded-For when the immediate peer is in a new security.trusted_proxies allowlist (default empty = strict). Fails closed with 403 forbidden_ip when a non-empty allowlist is configured and the client IP cannot be determined. Empty ip_whitelist continues to mean allow all (preserved for dev installs).

27 pm-auth tests pass (12 new resolver + 8 new middleware + 7 existing). Spec: tasks/ip-allowlist-spec.md.
2026-06-02 18:06:43 -05:00

156 lines
6.1 KiB
TOML

# Linux Patch Manager — Example Configuration
# Copy to /etc/patch-manager/config.toml and edit for your environment.
#
# Environment variable overrides follow the pattern:
# PATCH_MANAGER__SECTION__KEY=value
# e.g. PATCH_MANAGER__DATABASE__URL=postgres://...
# ============================================================
# Web Server
# ============================================================
[server]
# Bind address for the HTTPS listener
host = "0.0.0.0"
# HTTPS port (443 for production; 8443 for non-root dev)
port = 443
# Path to compiled React SPA static files
static_dir = "/usr/share/patch-manager/frontend"
# ============================================================
# Database
# ============================================================
[database]
# PostgreSQL connection URL
url = "postgres://patch_manager:CHANGEME@localhost/patch_manager"
# Connection pool sizing
max_connections = 20
min_connections = 2
# Seconds to wait for a connection from the pool
acquire_timeout_secs = 30
# ============================================================
# Background Worker
# ============================================================
[worker]
# Agent health check interval (seconds). Default: 300 = 5 minutes
health_poll_interval_secs = 300
# Agent patch data poll interval (seconds). Default: 1800 = 30 minutes
patch_poll_interval_secs = 1800
# Health check poll interval (seconds). Default: 300 = 5 minutes
# Controls how often configured service/HTTP health checks are evaluated.
health_check_poll_interval_secs = 300
# Maximum concurrent mTLS agent calls (Tokio Semaphore)
max_concurrent_agent_calls = 64
# Worker heartbeat write interval (seconds)
# WS relay HTTP polling fallback interval (seconds). When WebSocket connection to
# an agent fails, the relay falls back to polling the agent's HTTP API at this
# interval. Default: 10
ws_relay_poll_interval_secs = 10
# ============================================================
# Logging
# ============================================================
[logging]
# Log level: trace, debug, info, warn, error
# Override with RUST_LOG environment variable
level = "info"
# Output format: "json" (production) or "pretty" (development)
format = "json"
# ============================================================
# Security
# ============================================================
[security]
# IP whitelist: list of CIDRs or individual IPs allowed to connect.
# IMPORTANT: An empty list allows ALL IPs. Restrict this in production.
# Example: ["10.0.0.0/8", "192.168.1.50"]
ip_whitelist = []
# Trusted reverse proxies: list of CIDRs or individual IPs. When the immediate
# TCP peer is in this list, `X-Forwarded-For` is honored (leftmost untrusted
# hop is used for allowlist enforcement). When this list is EMPTY (the
# default), `X-Forwarded-For` is IGNORED entirely and the socket peer IP is
# used — the strict, fail-closed default.
#
# REQUIRED if you front pm-web with nginx/HAProxy/Cloudflare/etc.: add the
# proxy's egress IP (or CIDR) here, otherwise the allowlist will evaluate
# against the proxy's IP and deny legitimate traffic. If your proxy chain
# has multiple hops, add each hop you control.
# Example: ["10.0.0.0/8"] (corporate egress)
# Example: ["172.16.0.0/12"] (internal load balancer)
trusted_proxies = []
# Ed25519 JWT signing key (private key, PEM format)
# Generate: openssl genpkey -algorithm ed25519 -out /etc/patch-manager/jwt/signing.pem
jwt_signing_key_path = "/etc/patch-manager/jwt/signing.pem"
# Ed25519 JWT verification key (public key, PEM format)
# Generate: openssl pkey -in /etc/patch-manager/jwt/signing.pem -pubout -out /etc/patch-manager/jwt/verify.pem
jwt_verify_key_path = "/etc/patch-manager/jwt/verify.pem"
# JWT access token TTL in seconds (default: 900 = 15 minutes)
jwt_access_ttl_secs = 900
# mTLS client certificate for agent communication
agent_client_cert_path = "/etc/patch-manager/certs/client.crt"
agent_client_key_path = "/etc/patch-manager/certs/client.key"
# Internal CA certificate and private key (must be unencrypted PEM)
# WARNING: Do NOT use password-protected/encrypted keys; the service will fail.
# Private key has 0600 permissions; protected by hardware-host FDE
ca_cert_path = "/etc/patch-manager/ca/ca.crt"
ca_key_path = "/etc/patch-manager/ca/ca.key"
# Web UI TLS certificate (default: self-signed from internal CA)
# Set web_tls_strategy = 'operator_supplied' in system_config and
# point these paths to your certificate/key to use your own cert.
web_tls_cert_path = "/etc/patch-manager/tls/web.crt"
web_tls_key_path = "/etc/patch-manager/tls/web.key"
# Frontend URL to redirect the browser to after Azure SSO callback.
# The backend sends tokens as query parameters to this URL.
# Default: "http://localhost:5173/auth/sso/callback" (Vite dev server)
sso_callback_url = "http://localhost:5173/auth/sso/callback"
# Allowlist of browser `Origin` values permitted to open the
# `/api/v1/ws/jobs` WebSocket upgrade. Each entry is an exact
# `scheme://host[:port]` string (no wildcards, no paths). When this list is
# empty, the server derives a single-entry default from `sso_callback_url`
# at startup (the host of the SSO callback). If the derivation also fails,
# a warning is logged and the WS endpoint rejects all browser upgrades
# (fail-closed).
#
# Add additional origins here if your SPA and API are served from different
# hosts (e.g. SPA on https://app.example.com talking to API on
# https://api.example.com). For typical single-host deployments the derived
# default is correct and this line should be left commented out.
#
# allowed_origins = ["https://patch-manager.example.com"]
# ============================================================
# Rate Limiting
# ============================================================
[rate_limit]
# Enrollment endpoint: requests per minute per IP (default: 5)
enrollment_rpm = 5
# Enrollment burst allowance (default: 3)
enrollment_burst = 3
# Public auth endpoints: requests per minute per IP (default: 20)
auth_rpm = 20
# Auth burst allowance (default: 10)
auth_burst = 10
# Authenticated API: requests per minute per IP (default: 120)
api_rpm = 120
# API burst allowance (default: 30)
api_burst = 30