Files
sencho/.env.example
Anso 966cf7255b fix(security): make ignored forwarded headers loud (#2069)
* fix(security): make ignored forwarded headers loud

Behind a reverse proxy, forwarding headers are accepted only from peers
listed in SENCHO_TRUSTED_PROXY_CIDRS. When the list is unset or invalid,
that failure was silent: client addresses, secure-cookie detection, SSO
callback URLs, Pilot enrollment URLs, and rate-limit keys all fell back
to the proxy, and an invalid list (for example a bare IP) was ignored
without a word.

Log the effective policy at boot, naming every rejected entry and the
reason. Warn once per untrusted peer when a request carries forwarding
headers the policy is ignoring, with the exact CIDR to set. Document the
behavior in the reverse-proxy and troubleshooting pages. The fail-closed
semantics of the allowlist are unchanged.

The login limiter test now drives the real createApp pipeline instead of
setting trust proxy by hand, which is what hid this regression.

* fix(security): keep forwarding headers out of the pilot tunnel

The tunnel bridge framed the hub's incoming request headers as-is, so a
reverse proxy in front of the hub had its X-Forwarded-* headers relayed
to the agent. The agent replays them to its loopback listener, which saw
them from 127.0.0.1 and warned about an untrusted proxy, suggesting the
operator trust loopback. Strip forwarding headers when framing http_req
and ws_open so the tunnel carries no unverifiable client address; the
hub's own pipeline has already applied its trusted-proxy policy to the
original request. Forwarded (RFC 7239) is stripped defensively.

Also narrow the reverse-proxy docs to the limiters that actually share a
bucket (sign-in, SSO, and other unauthenticated requests; signed-in
requests stay per user or per token), assert the boot log wiring, and
word the runtime warning as "add" when the list is configured, or point
at the rejected entries when it is invalid.

* docs(security): qualify trusted-proxy attribution and warning scope

QA follow-ups on the previous commit. The reverse-proxy paragraph now
says the proxy transport is the one where a remote can name the
operator's address if it trusts the forwarding instance; Pilot agents
receive tunneled requests without forwarding headers, so their local
audit trail records the tunnel ingress and the hub's audit log names the
operator. The runtime warning is documented as HTTP-only, since
WebSocket upgrades bypass Express middleware. The warning itself now
says "a reverse proxy or another Sencho instance" because a proxy-mode
remote's peer is the hub.
2026-10-03 10:35:43 -04:00

194 lines
8.5 KiB
Bash

# Sencho Configuration
# Copy this file to .env and update the values for production
# Directory containing docker-compose files
COMPOSE_DIR=/path/to/your/compose/files
# Database and state directory inside the container (default: /app/data)
DATA_DIR=/app/data
# Optional path to a host-installed Trivy binary. Sencho first looks for a
# managed install under DATA_DIR/bin/trivy; if absent and TRIVY_BIN is set,
# Sencho uses that path; otherwise it falls back to `trivy` on PATH. Once a
# managed install is present, the managed copy takes precedence over this.
TRIVY_BIN=
# Node environment (set automatically in Docker image; only change for local dev)
NODE_ENV=production
# Frontend URL for CORS in production (leave empty for same-origin setups)
FRONTEND_URL=
# Base URL for the Hardened Build assurance API
SENCHO_ASSURANCE_API=https://sencho.io
# Non-production/test-only entitlement stub (1, entitled, unauthorized, unavailable).
# Ignored when NODE_ENV=production.
SENCHO_ASSURANCE_ENTITLEMENT_STUB=
# Global API rate limit (requests per minute per user session, production only)
# Authenticated requests are keyed by user ID; unauthenticated by IP.
# Internal node-to-node traffic (node_proxy tokens) bypasses this limit entirely.
API_RATE_LIMIT=200
# Polling endpoint rate limit (requests per minute, production only)
# Applies to dashboard/status polling endpoints that are exempt from the global limit.
# Increase for environments with many concurrent browser sessions behind shared NAT.
API_POLLING_RATE_LIMIT=300
# Public URL of THIS primary instance, used to bake an externally-reachable
# SENCHO_PRIMARY_URL into pilot enrollment YAML. Set this when the primary
# sits behind a reverse proxy or Cloudflare Tunnel so pilots on a different
# network can dial the public hostname instead of the request Host header
# the admin happens to be on (loopback, LAN, etc.). Must be http(s)://; no
# trailing slash. When native TLS is enabled, use https://.
SENCHO_PUBLIC_URL=
# Native TLS on the control instance (optional). When both cert and key
# files are set, Sencho listens with HTTPS instead of HTTP. Use this when
# there is no TLS-terminating reverse proxy. Partial configuration (only
# one of the two) refuses to start. Ignored when SENCHO_MODE=pilot; the
# agent loopback listener stays HTTP. The certificate must include an
# iPAddress SAN when agents dial by IP; a DNS-only cert fails Node
# verification.
# SENCHO_TLS_CERT_FILE=/app/certs/fullchain.pem
# SENCHO_TLS_KEY_FILE=/app/certs/privkey.pem
# Optional CA bundle copied into Pilot enrollment so agents trust a
# private or self-signed issuer. Leave unset for publicly trusted certs.
# SENCHO_TLS_CA_FILE=/app/certs/ca.pem
# Optional passphrase for an encrypted private key.
# SENCHO_TLS_KEY_PASSPHRASE=
# Comma-separated CIDRs of reverse proxies trusted to set forwarding headers.
# Set a single IPv4 proxy as a /32, a single IPv6 proxy as a /128, or use the
# proxy network CIDR when Sencho is behind one (for example, 192.168.1.50/32).
# Unset or invalid values make Sencho ignore forwarded client and scheme data.
# Sencho logs the effective policy at startup and warns once per unlisted peer
# that sends forwarding headers.
SENCHO_TRUSTED_PROXY_CIDRS=
# ─── Pilot agent (remote host only) ──────────────────────────────
# These three vars are required ONLY on a remote host running as a
# pilot-agent reverse-tunnel container. The primary instance does not
# read them. See docs/features/pilot-agent.mdx for the full setup.
# Switches the container into agent mode. The primary instance leaves
# this unset.
SENCHO_MODE=
# WebSocket-capable URL of the controlling Sencho instance. Use https://
# scheme; the agent rewrites it to wss:// for the tunnel upgrade.
SENCHO_PRIMARY_URL=
# Single-use, 15-minute enrollment token issued by the primary when the
# pilot-agent node is created. After the first successful connect the
# agent persists a long-lived tunnel credential at /app/data/pilot.jwt
# and ignores this var on subsequent restarts.
SENCHO_ENROLL_TOKEN=
# Optional: path inside the agent container to a PEM CA bundle the
# agent should trust when validating the primary's TLS cert. Use this
# for self-hosted deployments terminating TLS with an internal CA.
# Leave unset to fall back to the system trust store. There is no
# escape hatch to disable TLS verification.
SENCHO_PILOT_CA_FILE=
# ─── SSO / LDAP Configuration ────────────────────────────────────
# LDAP / Active Directory
SSO_LDAP_ENABLED=false
SSO_LDAP_URL=ldap://ldap.example.com:389
SSO_LDAP_BIND_DN=cn=readonly,dc=example,dc=com
SSO_LDAP_BIND_PASSWORD=
SSO_LDAP_SEARCH_BASE=ou=users,dc=example,dc=com
SSO_LDAP_SEARCH_FILTER=(uid={{username}})
SSO_LDAP_ADMIN_GROUP_DN=
SSO_LDAP_DEFAULT_ROLE=viewer
SSO_LDAP_DISPLAY_NAME=LDAP
SSO_LDAP_TLS_REJECT_UNAUTHORIZED=true
# Google OIDC
SSO_OIDC_GOOGLE_ENABLED=false
SSO_OIDC_GOOGLE_CLIENT_ID=
SSO_OIDC_GOOGLE_CLIENT_SECRET=
# GitHub OAuth
SSO_OIDC_GITHUB_ENABLED=false
SSO_OIDC_GITHUB_CLIENT_ID=
SSO_OIDC_GITHUB_CLIENT_SECRET=
# Okta OIDC
SSO_OIDC_OKTA_ENABLED=false
SSO_OIDC_OKTA_ISSUER_URL=
SSO_OIDC_OKTA_CLIENT_ID=
SSO_OIDC_OKTA_CLIENT_SECRET=
# Custom OIDC (Keycloak, Authentik, Authelia, Zitadel, etc.)
SSO_OIDC_CUSTOM_ENABLED=false
SSO_OIDC_CUSTOM_DISPLAY_NAME=Custom OIDC
SSO_OIDC_CUSTOM_ISSUER_URL=
SSO_OIDC_CUSTOM_CLIENT_ID=
SSO_OIDC_CUSTOM_CLIENT_SECRET=
SSO_OIDC_CUSTOM_SCOPES=openid email profile
SSO_OIDC_CUSTOM_ID_CLAIM=
SSO_OIDC_CUSTOM_USERNAME_CLAIM=
SSO_OIDC_CUSTOM_EMAIL_CLAIM=
# Role mapping (shared across OIDC providers)
SSO_OIDC_ADMIN_CLAIM=groups
SSO_OIDC_ADMIN_CLAIM_VALUE=sencho-admins
SSO_DEFAULT_ROLE=viewer
# External base URL for OAuth callback URLs (required behind reverse proxy)
SSO_CALLBACK_URL=
# Idle teardown for the persistent mesh proxy tunnel central opens to
# Distributed API (proxy-mode) remotes. The tunnel stays open for the
# life of the WebSocket. Set a positive number of milliseconds to
# enable idle teardown after that many ms of zero active mesh streams;
# the next mesh dial re-opens it. Default 0 (persistent).
SENCHO_MESH_PROXY_TUNNEL_IDLE_MS=0
# Mesh network subnet for this node. Leave unset to let Sencho pick the first
# free /24 from its candidate list (172.30.0.0/24, 172.31.0.0/24, 10.42.0.0/24,
# 10.43.0.0/24), or adopt the existing sencho_mesh network's subnet when it is
# already present. Set a specific CIDR only when you need to avoid an overlap
# with another network on this host. Configured independently per node.
# SENCHO_MESH_SUBNET=10.42.0.0/24
# Maximum bytes a single Git Source clone may download from the Git host before
# it is aborted. This bounds network transfer and abuse, not the decompressed
# on-disk size. A shallow clone of a compose repo is tiny; raise this only if
# you track compose files in a legitimately large repository. Default
# 104857600 (100 MB).
GITSOURCE_MAX_CLONE_BYTES=104857600
# Complete-project materialization bounds (Git Sources)
GITSOURCE_MAX_MATERIALIZED_FILES=10000
GITSOURCE_MAX_MATERIALIZED_BYTES=536870912
GITSOURCE_MAX_BUILD_CONTEXT_BYTES=268435456
GITSOURCE_MAX_PATH_DEPTH=64
GITSOURCE_MAX_FILE_BYTES=10485760
# Idle-output backstop for deploy and update compose steps (pull/recreate). If a
# step produces no output for this long while still running, Sencho treats it as
# stalled and stops it, so a hung image pull surfaces a clear failure (and the
# in-app recovery actions) instead of spinning. Conservative by default because a
# working pull can be briefly silent while a large layer extracts; raise it on
# slow links or heavy local builds. Default 600000 (10 minutes).
# SENCHO_COMPOSE_STALL_TIMEOUT_MS=600000
# Container-side path to the OpenZFS ARC stats file, for ZFS-aware host memory.
# On ZFS hosts (TrueNAS SCALE, Proxmox, ZFS on Ubuntu/Debian) the ARC cache is
# reclaimable but the kernel reports it as used, which can trigger false
# host-memory alerts. When ARC stats are readable, Sencho adds reclaimable ARC
# back into available memory. Sencho checks this path first, then
# /host/proc/spl/kstat/zfs/arcstats, then /proc/spl/kstat/zfs/arcstats. Set this
# only if your ARC stats live at a non-standard path inside the container. If no
# ARC stats are readable, host memory reporting is unchanged.
# SENCHO_ZFS_ARCSTATS_PATH=
# Path inside the container to /proc/meminfo, for VM memory ballooning awareness.
# Sencho checks this path first, then /host/proc/meminfo, then /proc/meminfo.
# Set it only when your meminfo lives at a non-standard path inside the container.
# SENCHO_PROC_MEMINFO_PATH=