Skip to content

0.10.0

Latest

Choose a tag to compare

@diolektor diolektor released this 11 Jul 21:20

TL;DR

A security-hardening and observability release. The headline is that the official image no longer serves as root out of the box — plus fail-closed configuration validation, encrypted trace export, and full exception data in APM spans.

  • Drops to www-data by defaultoxphp serve / oxphp run started as root now bind their listeners as root and then permanently drop to www-data before serving any traffic or running any PHP, so the official image no longer serves as root. Best-effort and never aborts startup; --user=root restores the old behaviour. → Upgrade from 0.9.0
  • Fail-closed config validation — a half-configured TLS pair, a non-UTF-8 TLS_CERT / TLS_KEY, a malformed ASYNC_*, an invalid SUPERGLOBALS_ENABLED on oxphp run, and a foreign-syntax TLS_MIN_VERSION now abort startup instead of silently falling back to defaults. → Upgrade from 0.9.0
  • OTLP trace export over TLS — an https:// OTLP endpoint is now exported encrypted on both the gRPC and HTTP transports, verified against the system trust store (rustls, OpenSSL-free). → Highlights
  • Full exception data on APM spans — the exception span event now carries exception.message and exception.stacktrace alongside exception.type, so error inboxes (New Relic Errors Inbox, Jaeger, Grafana Tempo) render the message and stack without an in-application subscriber. → Highlights
  • Graceful drain on shutdown — SIGTERM stops accepting, sends GOAWAY to HTTP/2 clients, and ends open long-lived streams promptly and uncatchably; ordinary in-flight requests get the full DRAIN_TIMEOUT_SECONDS window (default lowered 30 → 25 to fit Kubernetes' grace period). → Highlights
  • TLS_MIN_VERSION floor (1.2 / 1.3) and a Buggregator profiler export envelope round out the release.

Upgrading from v0.9.0? See the Upgrade checklist — the default privilege drop and fail-closed validation are the items that can change behaviour.


Highlights

Drop to www-data by default

oxphp serve and oxphp run now drop OS privileges to www-data by default. Started as root on a host where the www-data account exists (as in the official image), the process binds its listeners as root and then permanently drops to www-data before serving any traffic or running any PHP — so the official image no longer serves as root out of the box.

  • The default is best-effort and never aborts startup: started non-root it keeps the current user; started as root without a www-data account it logs a warning and stays root.
  • To restore the previous always-root behaviour, pass --user=root; to drop to another account, pass --user=<name|uid[:gid]>. An explicit --user remains fail-fast (it must start as root).
  • Orchestrator-level drops (docker run --user, Compose user:, Kubernetes securityContext) are unaffected and take precedence — when the container already starts non-root, the self-drop is a no-op.

Fail-closed TLS and config validation, plus TLS_MIN_VERSION

Several previously-silent misconfigurations are now hard errors at oxphp serve / oxphp run startup and at oxphp config --check:

  • a half-configured TLS pair (TLS_CERT set without TLS_KEY, or vice versa) — silently serving plain HTTP on a port meant for HTTPS fails open;
  • a non-UTF-8 TLS_CERT / TLS_KEY value (an exactly-empty value is treated as unset);
  • a malformed ASYNC_WORKERS / ASYNC_QUEUE_CAPACITY / ASYNC_MAX_FIBERS (ASYNC_WORKERS=8x previously collapsed to 0 and disabled the async pool);
  • an invalid SUPERGLOBALS_ENABLED passed to oxphp run (which previously fell back to defaults wholesale, silently re-enabling superglobals an operator had disabled).

New TLS_MIN_VERSION sets the minimum accepted protocol version — 1.2 (default) or 1.3 (a TLS 1.2 ClientHello is rejected at the handshake). It is validated at startup even when TLS is disabled, so a mistyped security floor cannot quietly weaken the configuration; the effective floor is reported as tls_min_version in /config. TLS startup errors now name the variable and the file (TLS_CERT: cannot read /etc/ssl/cert.pem: No such file or directory).

TLS docs

OTLP trace export over TLS

An OTEL_EXPORTER_OTLP_ENDPOINT with an https:// scheme is now exported encrypted on both the gRPC (grpc) and HTTP (http/protobuf) transports, with the collector certificate verified against the system trust store. Previously the default gRPC transport had no TLS support at all — an https:// gRPC endpoint failed to connect — and the HTTP transport's TLS backend was non-deterministic.

Both transports now use rustls with system (native) roots, keeping the build OpenSSL-free. Verification uses the OS trust store, so the runtime image must ship a CA bundle — the official image installs ca-certificates. Custom CA bundles and mutual-TLS client certificates are not yet supported.

Distributed Tracing docs

Full exception data on APM span events

The exception span event now carries exception.message and exception.stacktrace alongside exception.type, matching the OpenTelemetry exception semantic conventions — so error inboxes (New Relic Errors Inbox, Jaeger, Grafana Tempo) render the message and stack without an in-application subscriber.

  • Applies to both the automatic #[OxPHP\Apm\Trace] decorator and the manual oxphp_apm_error($e) SDK call — the latter previously flipped the span to error status without recording any of it. A bare string argument (oxphp_apm_error('gateway timeout')) is recorded as exception.message.
  • Message and stacktrace are captured length-delimited and decoded lossily, so a non-UTF-8 or embedded-NUL message is preserved rather than dropped. The stacktrace comes from getTraceAsString().
  • Both are size-capped so a large payload can't bloat the export: the message by OTEL_APM_MESSAGE_MAX_BYTES (default 4096) and the stacktrace by OTEL_APM_STACKTRACE_MAX_BYTES (default 8192); 0 disables either cap. Over the cap each is truncated from the tail (the #0 throw-site frame is preserved) with a …(truncated) marker.

Graceful drain of long-lived connections

On SIGTERM the server stops accepting, sends GOAWAY to HTTP/2 clients and closes idle HTTP/1.1 keep-alives, and ends every open long-lived stream (SSE and other flush loops) promptly and uncatchablyregister_shutdown_function() callbacks still run and error_get_last()['message'] reads Request cancelled (shutdown); a try/catch (\Throwable) around the streaming loop cannot swallow it.

Ordinary in-flight requests are left alone: they get the whole DRAIN_TIMEOUT_SECONDS window to finish, and only requests still running at the deadline are cancelled, after which the process exits within ~2s. The DRAIN_TIMEOUT_SECONDS default is lowered from 30 to 25 so the full sequence — drain, ~2s unwind, telemetry flush — fits inside Kubernetes' default 30-second grace period. Set your orchestrator's grace period above DRAIN_TIMEOUT_SECONDS + 2s.

Buggregator profiler export

The profiler's HTTP push can now wrap xhprof data in the envelope Buggregator's POST /api/profiler/store expects, so profiles land in a Buggregator instance with correct project grouping and tag filtering. PROFILER_EXPORT_BUGGREGATOR toggles it; when unset it auto-detects an export URL whose path ends in /api/profiler/store. PROFILER_EXPORT_APP_NAME sets the project name and PROFILER_EXPORT_TAGS sets tags from a key=value,… list.

Profiling docs

Fixes

  • Worker-mode per-request cancellation (client abort or request timeout) could target whichever request was most recently accepted on the worker instead of the one being cancelled — so a timeout or disconnect on one multiplexed request could abort an unrelated in-flight request sharing the same worker.
  • Once any request on a worker called oxphp_finish_request(), every other stream multiplexed onto that worker silently dropped the output of its subsequent oxphp_stream_flush() calls.

Upgrade from 0.9.0

Two items can change behaviour on upgrade; the rest are error-on-misconfiguration.

  1. The process drops to www-data by default. Started as root on an image with a www-data account (the official image), oxphp serve / oxphp run now serve as www-data, not root. Action: if you relied on serving as root, pass --user=root. Orchestrator-level user drops still take precedence and are unaffected.

  2. Config is validated fail-closed. A half-configured TLS pair, a non-UTF-8 TLS_CERT / TLS_KEY, a malformed ASYNC_*, an invalid SUPERGLOBALS_ENABLED on oxphp run, and a TLS_MIN_VERSION in a foreign syntax (e.g. TLSv1.2, now validated even without TLS) all abort startup. Action: run oxphp config --check against your environment before rolling out; fix or unset the offending variable. An exactly-empty value still means unset.

  3. A QUERY request without a Content-Type header now returns 400, not 415. This was the only server-generated 415. Action: if you keyed a custom error page on it, rename ERROR_PAGES_DIR/415.html to 400.html.

  4. Profiler export envelope resolution changed for pre-existing configs. PROFILER_EXPORT_XHGUI=false now disables the xhgui envelope even on a /run/import URL, and envelope auto-detection keys only on each tool's canonical endpoint path. Action: if you point PROFILER_EXPORT_URL at a non-standard path, set PROFILER_EXPORT_XHGUI / PROFILER_EXPORT_BUGGREGATOR explicitly.


Full changelog: CHANGELOG.md · v0.9.0...v0.10.0