Added
CLI
ferron directivessubcommand: new CLI subcommand that prints every registered configuration directive as structured JSON, grouped by section. Useful for tooling, editor integrations, and inspecting the directive schema of the running binary.- FIPS build indication: a build compiled with the
fipsfeature printsThis build is configured to use FIPS-certified cryptography.in its version output.
Build & packaging
- FIPS-certified cryptography builds: the
fipsCargo feature enables FIPS-certified cryptography via AWS-LC and rustls. When enabled, OCSP stapling, TLS cipher suites and key exchange groups are restricted to FIPS-approved algorithms, and HTTP basic auth password verification supports only PBKDF2 (Argon2 and scrypt hashes are rejected). You can enable the feature withcargo build --features=fips, or through the build tooling:just build fips=true,cross-build/build.sh --fips, theFIPS=1Docker build argument, and the FIPS packaging scripts for archive, Debian, RPM, and Windows installer artifacts. FIPS artifacts are named with a+fipssuffix (for example,ferron+fips-<version>-<target>.tar.gz), and FIPS Docker images use the-fipstag suffix. - SBOMs for release archives: every release now ships a software bill of materials (SBOM) archive next to the binary archives, named
ferron-<version>-<target>-sbom.tar.gz(or.zipfor Windows). Each archive contains CycloneDX documents in JSON and XML format that list the third-party Rust crates compiled into the binaries. FIPS releases ship matching SBOM archives with the+fipssuffix. You can generate SBOMs locally withjust package-sbom <target> [fips=true].
Observability & tracing
- Configuration drift hints: new
ferron.admin.config_driftGauge metric andconfig_drift/config_drift_hints_enabledfields on the/statusAdmin API endpoint detect when configuration source files have changed on disk but have not been reloaded. Drift is detected via periodic lightweight mtime comparison (no re-parsing). Enabled by default; disable withdrift_hints falsein config adapter params (e.g.,--config-params "drift_hints=false"). A warn-level log is emitted when drift is detected, and an info-level log when drift resolves after reload. - StatsD metrics export: new
provider statsdinobservabilityblocks sends Ferron metrics to a StatsD server over UDP, withhost,port(default127.0.0.1:8125),prefix, anddatadogdirectives. The optionaldatadog truetoggle enables DogStatsD extensions: metric tags rendered from metric attributes (and control-plane metadata) and thehhistogram type. Without it, histograms are sent asmstimers (with seconds-to-milliseconds conversion). Theprefixdirective prepends a namespace to every metric name (e.g.,myapp.ferron.http.server.request_count:1|c). Each metric event is sent as a separate UDP datagram. (observability-statsd) - OTLP metric exemplars: the OTLP observability sink now supports exemplar attachments on metric data points, allowing the last sampled value to be attached as an exemplar. This allows for better trace correlation and debugging by attaching relevant historical context. Previously, this wasn't supported due to OpenTelemetry SDK limitations, but the limitation was removed by replacing OpenTelemetry SDK with a custom OTLP exporter implementation.
- OTLP export batching tuning: the
logsandtracesOTLP signal sub-blocks now acceptexport_interval <duration>andexport_batch_size <number>, and themetricssub-block acceptsread_interval <duration>. This controls how often partially full batches flush and how many finished items trigger an export. - OTLP gzip compression: the
logs,metrics, andtracesOTLP signal sub-blocks now acceptgzip [bool](defaultfalse). When enabled, export requests are compressed with gzip (HTTPContent-Encoding: gzip, gRPC gzip compression). - OTLP exemplar toggle: the
metricssub-block now acceptsexemplars [bool](defaulttrue). Set it tofalseto stop attaching the last sampled measurement per series as an exemplar. - OTLP native histogram toggle: the
metricssub-block now acceptsnative_histograms [bool](defaulttrue). Set it tofalseto aggregate histograms with explicit bucket boundaries instead of the exponential layout. - Variable interpolation in log filenames: the
access_loganderror_logdirectives now support{{variable}}interpolation in file paths. Access log filenames use theaccesslog.prefix (e.g.{{accesslog.header_host}}) and resolve against access log event fields. Application log filenames use thelog.prefix (e.g.{{log.level}},{{log.error.type}}) and resolve against log event attributes. Environment variables are also available via theenv.prefix. This enables use cases like per-host access logs (access_log /var/log/ferron/{{accesslog.header_host}}/access.log). (GitHub issue)
HTTP server core
- Variable interpolation in map results: variable interpolations (
{{name}}) in map result values are now resolved at runtime. request.uri.query.<param>variables: therequest.uri.query.<param>variables are now available for string interpolations, so to not manually create a regular expression for extracting query parameter value from a query string.request.cookie.<name>variables: therequest.cookie.<name>variables are now available for string interpolations, so to not manually extract cookie values from the request.- CORS origin interpolations: CORS origin values can now be interpolated using
{{name}}variable syntax (previously they were declared invalid). - Request
Cache-Controldirectives: Ferron now honors the RFC 9111 §5.2.1 request directives on cache lookups.max-age=<n>andmin-fresh=<n>revalidate a stored response that does not satisfy the requested freshness window, andonly-if-cachedreturns504 Gateway Timeouton a miss instead of contacting the origin.no-transformis accepted and has no effect.no-cache(andPragma: no-cache) already forced revalidation;no-storealready bypassed the cache. - Canary deployments without a reverse proxy: new
canarydirective (http-canary) assigns each request a weighted variant with consistent-hash sticky affinity, for canary rollouts and A/B testing of static content. Affinity isipby default and can be based on a cookie, a request header, or a built-in request variable (hash). With theset_cookiesub-directive, Ferron generates a random sticky key and writes it to the affinity cookie itself when the request has none, so assignments survive client IP changes without help from the web application. Variants are exposed ascanary.variant,canary.weight, andcanary.keyvariables, so a document root likeroot "/srv/www/{{canary.variant}}"serves each variant its own content. Weights and variant lists can change on reload; clients near variant boundaries may move, other clients keep their variant. Observability includes theferron.canary.requestscounter (withferron.canary.nameandferron.canary.variantattributes), span attributes on theferron.stage.canaryspan, and theferron.canary.variantcustom access log field. (http-canary) - HTTP/3 protocol configuration directives: added several new
h3_*subdirectives inhttpdirective for fine-tuning HTTP/3 protocol configuration.
HTTP caching
- On-disk cache persistence: the HTTP cache can now survive process restarts. Set
persist <dir>in a cache block to write cache mutations to a journal and periodic snapshots under<dir>/<zone>. On startup, Ferron replays the snapshot and journal back into memory.persist_interval <duration>(default30s, minimum1s) controls how often Ferron flushes queued mutations. By default only public entries are persisted;persist_privateopts into persisting private entries. Deletions are always persisted. The persistence directory is single-process only — do not share it between instances. A SIGHUP config reload does not touch the on-disk cache.
Changed
HTTP server core
- PGO for GNU/Linux and some Linux with musl: PGO (profiled-guided optimization) is now enabled for GNU/Linux targets and some Linux targets with musl libc (64-bit x86, ARM64), for pre-built Ferron binaries. This mainly improves tail (p90/p99/max) latency for web requests.
.ferronfile extension:.ferronfiles are now supported as an alternative to.conf(which is a generic file extension) files for configuration (GitHub issue).- Panic hook improvements: panic hook has been simplified (removing backtraces that are likely unhelpful), and it now logs the Ferron version and build target.
- HTTP/2 and HTTP/3 implementations: HTTP/2 and HTTP/3 functionality in Ferron now depends on
vibeio-http's in-house protocol implementations instead ofh2andh3from Hyperium. New HTTP/2 implementation features higher space savings for headers (~95%, versus ~91% with the previous implementation for a "Hello World" application), while new HTTP/3 implementation features improved protocol compliance (47/49 onh3spec, versus 44/49 with the previous implementation). - QUIC performance optimizations: QUIC "endpoints" are now per-thread instead of single-threaded, improving throughput and tail (p90, p99) latency for HTTP/3 requests.
Observability & tracing
- Non-existent directory handling for log files: when a log file path contains a non-existent parent directory, Ferron now creates it automatically before writing to the log file.
Configuration validation
- Diagnostic span location improvements: diagnostic span location in error messages is now more accurate, showing the exact line and column where the error occurred in the configuration file.
Container images
- "Slim" Debian-based Docker images:
ferronserver/ferron:3-debian(and other Debian-base image tags) now are based on a "slim" variant of Debian instead of the full Debian image, making the image smaller and removing unnecessary packages. The slim variant is still compatible with the full Debian image, so it should not break existing deployments.
Access control
- Interpolated strings as password hashes (HTTP basic auth): interpolated strings can now be used as password hashes for HTTP basic authentication (for example when moving from a static password to an environment variable).
- Password hashing backends for HTTP basic auth: password verification no longer depends on the
password-authcrate. PBKDF2 verification now uses aws-lc-rs, scrypt uses AWS-LC (EVP_PBE_scrypt), and Argon2 uses argon2-rs (the Argon2 C reference implementation) with constant-time comparison. Newly supported hash formats:$pbkdf2-sha384$and$pbkdf2-sha512$. Base64 salt and hash fields now accept both padded and unpadded encodings. PBKDF2 hashes accept a bare iteration count ($pbkdf2-sha256$600000$...) or the PHC parameter form ($pbkdf2-sha256$i=600000,l=32$...).
Fixed
HTTP server core
- Trailing slash redirects: trailing slash redirection logic now correctly handle index files (e.g.,
index.html) and redirects to the correct URL (with trailing slash) rather than serving two URLs (both with and without trailing slash) - Connection accept fix for
poll: previously, connections at TCP listeners (including HTTP/1.x and HTTP/2) accepted only a single connection when usingpoll(notepoll), stalling afterwards. This has been fixed to be able to accept multiple connections (seevibeiochangelog). - HTTP/2 host header handling correctness: previously, if both
hostand:authorityHTTP/2 headers were present, the web server would return a 400 error, which might not be correct according to the HTTP/2 specification (RFC 9113, section 8.3.1). This has been fixed to overrideHostheader value with:authorityheader value instead of appending a new value. - Symlink ownership check: previously, symlink ownership check (
disable_symlinks if_not_owner) was effectively a stub that effectively disabled all symlinks. This has been replaced with a proper implementation. - Conflicting Alt-Svc headers for HTTP/3: the server now rewrites Alt-Svc header values to remove conflicting HTTP/3 port definitions.
HTTP caching
- RFC 9111 compliance: updated freshness lifetime precedence, 304 revalidation header merging,
must-revalidatehandling,no-cache="field-names"support, and206 Partial Contentcacheability to align with RFC 9111. - Cache key & scope: cache keys now use resolved vhosts, normalize whitespace/sorting in
Varyvalues, and exclude client IPs for private responses. Added cardinality bounds for private keys and query string stripping from fingerprints to prevent cache fragmentation and information leakage. - Purge & security: scoped
PURGErequests to requesting hosts, added constant-timeX-Purge-Secretauthentication for propagated purges, and ensuredSet-Cookieheaders are never stored verbatim. - SWR & performance: improved
stale-while-revalidatewith SWR leader fresh response delivery, singleflight coalescing with timeouts, and throttled expired-entry sweeps. - Request handling:
no-storerequests now serve fresh entries (but don't store), hop-by-hop headers are stripped, and fresh hits now answer client conditionals (e.g.,If-None-Match) with local 304s. - Metrics & diagnostics: corrected
Cache-Statuslabels forstale-if-errorand fixed double-counting in cache request metrics.
Observability
- OTLP gRPC
no_verificationTLS handshake: previously, when an OTLP gRPC endpoint was configured withno_verification trueand used HTTPS, TLS handshakes failed because the custom certificate verifier rejected TLS 1.2/1.3 handshake signatures. This has been fixed to return assertion-based verification results (matching the HTTP exporter behavior). ferron.ocsp.stapling.hit_totalmetric emission fix: previously, theferron.ocsp.stapling.hit_totalmetric emission didn't function at all. It has been fixed to emit the metric to global observability sinks correctly.- ACME TLS resolution errors: previously, TLS resolution errors were always logged into the console when using automatic TLS via ACME. This has been changed to log them into configured observability sinks.
- Spurious abrupt connection termination error logs: earlier, some abrupt connections termination log were logged (
Reverse proxy: HTTP upgrade tunneling failed: peer closed connection without sending TLS close_notify: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof), even if the connection was idle. This has been fixed along with an update to the HTTP server library used by Ferron (seevibeio-httpchangelog).
Configuration validation
- JSON configuration parse error reporting: previously, configuration parse errors were reported using human-readable error messages (even if the
--jsonflag was used). This has been fixed to report parse errors in JSON when configured to do so.
Reverse proxy
- Config reload cleanup: on configuration reload, old reverse proxy health check probe tasks are now aborted and per-config caches (resolved upstreams, retry budgets, unhealthy backend counters) are invalidated. Previously these accumulated on every reload, leaking background tasks and memory.
- Upstream resolution cache fix - in earlier versions of Ferron 3 beta, stale upstream resolution cache (with infinite TTL) could have caused upstream connection errors, due to drift between cached state and actual DNS state. This has been fixed by removing the infinite-TTL internal upstream resolution cache.
X-Forwarded-ForandForwardedheader value fix: previously,X-Forwarded-ForandForwardedheader values were truncated to 256 bytes maximum, which could lead to incorrect (or even malformed) client IP values in the proxy request. This has been fixed to set the header values correctly without truncation.
Static file serving
Rangerequest correctness: previously, weak ETags were included with206 Partial Contentresponses (and some other responses withRangerequest header), which is a violation of HTTP spec (RFC 7232, RFC 7233). This has been fixed to remove ETags from responses to range requests.
CORS
- CORS
Varyheader correctness - previously, if responseVaryheader was set upstream, it would be overwritten byVary: originheader, which might lead to incorrect caching. This has been fixed to appendoriginto the list of header names inVaryheader value instead. - CORS
Vary: Originheader: theVary: Originheader is now always added to CORS responses (instead of selectively). - CORS
Originrequest header fix: previously, ifcorsdirective contained non-*origin, Ferron didn't add CORS headers at all. This has been fixed to add CORS headers for non-*origins as well.
CLI utilities
ferron-fmtduration string formatting fix: previously,ferron-fmtformatted short duration strings (like1s) without quotes, which caused subsequent parse errors. This has been fixed to always quote this kind of strings.