Skip to content

Security: SimpleDaemons/simple-httpd

Security

docs/security.md

Security

simple-httpd is a static file server with optional extras. The threat model is: untrusted clients on the listen socket, a trusted config and document root on disk. CGI, FastCGI, SSI, and reverse proxy stay off unless you enable them; turning them on means the process runs or forwards application traffic.

Transport

Enable HTTPS with a PEM chain and key:

ssl_cert = /etc/simple-httpd/tls/fullchain.pem
ssl_key  = /etc/simple-httpd/tls/privkey.pem
tls_min_version = 1.2
hsts = true
hsts_max_age = 31536000

TLS 1.2 is the default floor; set tls_min_version = 1.3 to refuse older handshakes. Cipher suites prefer ECDHE (PFS) and TLS 1.3 AEAD suites. HSTS is sent only on TLS connections. OCSP stapling is not implemented — terminate TLS at a front proxy if you need stapling today. There is no HTTP→HTTPS redirect on a second port.

Permissions: private key 600, owned by the service user. Prefer binding as root (or with CAP_NET_BIND_SERVICE) then user / group to drop privileges after listen.

Filesystem

  • Paths are resolved under the selected document root. .. segments that would escape return 400 or 403.
  • Symlinks are refused unless follow_symlinks = true.
  • Directory listing is off by default. Turn it on only for trees you are willing to publish.

Do not point document_root at /, $HOME, or a tree that contains secrets.

Response headers

Always (from the static handler): X-Content-Type-Options: nosniff.

When security_headers = true (the default):

Header Default
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy camera=(), microphone=(), geolocation=()

Content-Security-Policy is sent only when csp is non-empty. The browser enforces it; the daemon does not parse the policy.

The built-in directory listing uses an inline <style> block. Use something like:

csp = default-src 'self'; style-src 'self' 'unsafe-inline'

if listing might be enabled. A strict default-src 'self' will leave listings unstyled.

Methods

allow_methods defaults to GET, HEAD, OPTIONS. Anything else against a static path gets 405 and an Allow header. Proxy, CGI, and FastCGI prefixes skip that filter so backends can accept POST.

HTTP Basic

auth_basic_realm = Restricted
auth_basic_file = /etc/simple-httpd/htpasswd

File format is plaintext user:password, one per line (# comments). Copy htpasswd.example, change the password, chmod 600. There is no digest, no bcrypt htpasswd, and no per-path realm — auth covers the whole listener except /healthz and /metrics.

Missing or empty auth files fail startup rather than running open.

Rate limiting

rate_limit_enabled = true
rate_limit_requests = 60
rate_limit_window_seconds = 60

Counters are per client IP. Over-limit responses are 429 with Retry-After. The table is capped (~20k IPs) so it cannot grow without bound. This is a courtesy throttle, not a DDoS product.

Health and metrics paths are not counted.

IP allow / deny and connection limits

allow_ips = 10.0.0.0/8,192.168.0.0/16
deny_ips = 10.0.0.5
max_connections_per_ip = 32
connection_rate_limit_enabled = true
connection_rate_limit = 30
connection_rate_window_seconds = 60

Deny is evaluated first. An empty allow_ips means any non-denied peer is accepted. Connections that fail the lists or connection-rate / per-IP caps are closed at accept (and counted as rejected). Request-path checks still return 403 if a peer somehow reaches the pipeline.

Security audit log

security_log = /var/log/simple-httpd/security.log

Events include ip_denied, auth_failed, admin_auth_failed, rate_limited, connection_rate_limited, connection_limit, admin_config, and admin_reload.

Compliance checklist (operator)

Use this as a lightweight control list — not a certification:

  • TLS with current certificates; prefer tls_min_version = 1.3 where clients allow
  • HSTS on public HTTPS
  • Document root excludes secrets; symlinks off; listing off
  • Basic auth or network ACL for non-public trees and admin_path
  • Rate limits and (where needed) IP allow/deny
  • Privilege drop (user/group) after bind; non-root in containers
  • Logs rotated; security_log retained for review
  • Config and htpasswd backed up with restricted permissions

Rewrites

Prefix rewrites happen before auth and static mapping. Do not rewrite /healthz onto a protected file if you still want probes unauthenticated — health is matched after rewrite.

Optional extras

CGI scripts must be executable files under the document root. FastCGI talks to a TCP process you already run. SSI include paths cannot escape the document root; exec is disabled. Reverse proxy backends are cleartext HTTP only — do not point proxy at an untrusted origin. WebSocket is an HTTP/1.1 byte tunnel after 101, not a framed protocol implementation.

What this daemon does not do

  • Client certificate authentication
  • Digest / OAuth / JWT
  • HTTP/3
  • HTTPS reverse-proxy origins
  • FastCGI unix sockets or php-fpm process management
  • SSI exec / a template language beyond SSI
  • GraphQL or a general application plugin API
  • Cloud object-store backends (S3/GCS) as document roots
  • OCSP stapling
  • Request-body inspection beyond size and framing
  • chroot or seccomp

Those belong in front (firewall, load balancer, CDN) or remain intentionally out of scope for a static-file daemon. See ROADMAP.md.

Suggested production baseline

Start from config/examples/security.conf.example:

  • TLS + HSTS (tls_min_version 1.2 or 1.3)
  • No directory listing, no symlink follow
  • Tight max_request_size / header count / timeouts
  • CSP + security headers
  • allow_methods = GET, HEAD, OPTIONS
  • Rate limiting on; consider max_connections_per_ip
  • Optional Basic auth for non-public trees; admin_path only with auth
  • user / group after bind
  • Logs to files at warn or info, plus security_log

There aren't any published security advisories