Highlights
A security-focused minor release: the Go 1.26.3 toolchain clears six stdlib advisories, three silent-downgrade vectors in TLS handling are closed, and tcp+tls:// Docker hosts work again with the cert material the operator declared. MaxRuntime-bounded jobs now properly clean up their containers, and the [global] config story is finally consistent across INI and Docker labels.
๐ Security
- Go toolchain
1.26.2โ1.26.3clearsnet/mail,html/template,net, andnet/httpadvisories reachable from this codebase (GO-2026-4986, -4982, -4980, -4977, -4971, -4918). Post-bump, only the two unfixable upstream moby advisories ondocker/dockerv28.5.2 remain. (#662) - No more silent TLS downgrades in three places where Ofelia previously masked an operator misconfiguration as fail-open: HTTPS Docker daemons with broken cert material, SMTP middleware on servers that don't advertise STARTTLS, and webhook URL allow-lists collapsed to
["*"]by a typo. Each one now fails closed with a typed sentinel and a clear log line at startup. (#660, #646) - โ ๏ธ Operator impact if you relied on
OpportunisticStartTLSfor SMTP relays that don't advertise STARTTLS (legacy local relays, MailHog dev fixtures): set the newsmtp-tls-policy = opportunistic(or= nonefor test fixtures only) to restore the previous behavior. The new default ismandatory. Seedocs/TROUBLESHOOTING.mdfor migration recipes. - Remote preset fetches (
webhook-allow-remote-presets = true) now route through the sameTransportFactory()the webhook stack uses, instead of the implicithttp.DefaultClient. (#624)
๐ณ Docker daemon connectivity
tcp+tls://is back on theDOCKER_HOSTallow-list now that the TLS plumbing landed โDOCKER_CERT_PATH/DOCKER_TLS_VERIFY(and the equivalentClientConfigoverrides) are wired into the HTTP transport, andtcp+tls://without cert material now fails loud instead of silently dialing TLS with the system CA pool. (#625)DOCKER_HOSTschemes are now validated against an allow-list (unix://,tcp://,tcp+tls://,http://,https://,npipe://) and normalized to lowercase. Unsupported values (ssh://,fd://, typos) fail at startup with a clear error instead of falling through to plain-TCP. (#612)DOCKER_HOST=tcp://...plusDOCKER_CERT_PATHnow actually negotiates TLS end-to-end โ the SDK URL is silently upgraded tohttps://so the configured cert and pinned CA apply on the wire (mirroring the docker CLI). Previously the cert was loaded but the SDK still spoke plain TCP. (#647)- Startup is no longer hostage to a wedged daemon:
NegotiateAPIVersion, periodic health/ready pings, the doctor diagnostic, and remaining unbounded Docker SDK calls are all wrapped incontext.WithTimeout. (#611, #636)
๐งน Correctness
MaxRuntimecancellation now stops and removes the container or swarm service. The deadline-wiring fix in #651 returned control to the wrapper, but the deferred cleanup reused the already-cancelled parent context โ so stop/remove API calls were rejected before they reached the daemon. The cleanup path now uses a freshcontext.WithTimeout(context.Background(), jobCleanupTimeout)andRunServiceJobmirrors the same fix. Operators previously seeingExitedcontainers piling up should see them properly cleaned after this release. (#659)[global]Docker label keys now reach the live config across every subsystem. Setting e.g.ofelia.smtp-host=mail.example.comon a service container previously decoded into a scratch struct that was discarded after the per-job merge โ Slack, Mail, Save, scheduling, and the runtime knobs (log-level,notification-cooldown) all silently kept their INI defaults. New per-subsystem helpers and anapplyAllowListedGlobalsaggregator wire every allow-listed non-webhook global through both the boot and reconcile paths with the same "INI wins when set; label only fills empty/default" precedence asmergeWebhookGlobals. (#661)- Pervasive nil-guard sweep across the Docker adapter family. Every public method on every
*ServiceAdapter(Container,Exec,Image,Event,Network,Swarm,System) returns a typed sentinel (ErrNilDockerClient,ErrNilExecConfig,ErrNilContainerConfig) instead of panicking if its internals are misconstructed; everyconvertTo*/convertFrom*helper nil-guards its pointer argument and returns a zero value. (#626, #639, #648, #658)
๐ Docs and DX
- Webhook config has one source of truth now:
c.WebhookConfigs.Globalaliases&c.Global.WebhookGlobalConfigonce inNewConfig(), eliminating the dual-store anti-pattern that needed a hand-rolledsyncGlobalWebhookConfigshim. INI live-reload ofwebhook-allowed-hostsnow also re-runsWebhookManager.InitManager()so the URL validator picks up changes at runtime. (#637) - Docker label
ofelia.webhooksdeprecated in favor ofofelia.webhook-webhooksto match the documented INI[global]key name. The legacy form still works and logs a one-shot deprecation warning per process โ migrate before the next major release. (#620) - Documentation reconciled with reality across Slack middleware (removed never-supported keys), Save middleware (documented
restore-history), webhook globals (webhook-webhooks,webhook-trusted-preset-sources,webhook-preset-cache-dir), the poll-interval split, andweb-trusted-proxies. NewTestConfigGlobalKeysAreDocumentedwalks theConfig.Globalreflection tree and asserts everymapstructurekey is mentioned in at least one operator-facing docs file, so this drift class is now caught mechanically. (#621, #635, #656) - Unified Docker host / scheme resolution behind a single
resolveDockerHostseam, with the dispatch table and the allow-list derived from one source so they cannot drift. (#629)
Dependencies
Go toolchain 1.26.2 โ 1.26.3. Direct deps: docker/cli 29.4.2 โ 29.4.3, golang.org/x/crypto 0.50 โ 0.51, golang.org/x/term 0.42 โ 0.43, golang.org/x/text 0.36 โ 0.37. Indirect-graph refresh via go get -u all (otelhttp 0.67 โ 0.68, grpc 1.80 โ 1.81, x/sys/mod/net/tools bumps, genproto date refresh, docker-credential-helpers 0.9.5 โ 0.9.7, morikuni/aec 1.0 โ 1.1, plus tertiary bumps).
Thanks
Special thanks to the external reporters whose issues drove substantive fixes in this release:
- @techsolo12 for #604 โ reporting that
webhook-allow-remote-presets(and the other documentedwebhook-*[global]keys) emitted "Unknown configuration key" warnings and silently fell back to defaults. Fixed in #618. - @groknt for #605 โ reporting that
DOCKER_HOST=tcp://...configurations failed to start because the HTTP transport's dialer was hard-pinned tounix:///var/run/docker.sock. The trail from that report cascaded into the wider TLS/scheme rework that landed across #606, #612, #613, and #629.
Container image
ghcr.io/netresearch/ofelia:0.25.0
ghcr.io/netresearch/ofelia:0.25
ghcr.io/netresearch/ofelia:0
Verify your download
Per-asset cosign bundles. Verify any single file:
cosign verify-blob \
--bundle ofelia-linux-amd64.bundle \
--certificate-identity-regexp "https://github.com/netresearch/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ofelia-linux-amd64Each binary also ships an SPDX SBOM (.spdx.json) with its own attached cosign bundle.