Releases: netresearch/ofelia
v0.25.1
Highlights
A patch release that closes the docker hijack regression class that crept in alongside v0.25.0's TLS/scheme rework, plus a webhook multi-instance dedup bug and the long-standing url-only-webhook documentation gap. Anyone running run_exec jobs through tecnativa/docker-socket-proxy or any plain-TCP/HTTP Docker host should upgrade. Anyone wiring more than one webhook per job (typically one for success, one for error) needs the multi-webhook fix. Anyone who tried to configure a webhook with only a url = ... since v0.16.0 will find that it actually works now.
🐳 Docker daemon connectivity
DOCKER_HOST=tcp://...(plain HTTP)run_execjobs work again. v0.25.0 regressed plain-tcp://hijack APIs (ContainerExecAttach,ContainerAttach,ContainerLogs --follow) with the unhelpful errortls: first record does not look like a TLS handshake. Container discovery kept working because the regular HTTP path was unaffected; onlyrun_execand friends failed. Root cause was a Go stdlib × Docker SDK interaction:net/httplazily auto-configures HTTP/2 on the first request and allocatesTLSClientConfigin place for ALPN — and the SDK's hijack dialer reads that as "TLS required" and dialstls.Dialagainst your plaintext daemon. The fix suppresses the lazy auto-config on non-TLS transports. TLS Docker hosts (https://,tcp+tls://) are unaffected — ALPN h2 negotiation still works there. (#681, fixes #668)DOCKER_HOST=http://...hijack now works for the same APIs. Same SDK dialer, different failure mode:net.Dial("http", addr)is rejected by Go'snetpackage ("http"isn't a valid network name) so the call surfaced asdial http: unknown network http. The fix installs an explicit TCPDialContextso the SDK picks our dialer instead of reaching the broken fallback. Path-bearing hosts (http://daemon:port/v1.43) and IPv6 literals (http://[::1]:2375) round-trip cleanly viaurl.Parse, matching how the SDK already handlestcp://. (#686, fixes #682)
🔁 Webhooks
- Url-only webhooks finally work out of the box. The
docs/webhooks.md"Custom Webhooks" section has been promising since v0.16.0 that you could declare a webhook with justurl = ...and have Ofelia POST a JSON payload to it — but the code path actually returnedpreset specification cannot be emptyand refused to attach. v0.25.1 ships a new bundledjson-postpreset and a[global] webhook-default-presetselector that ships withjson-postas the default fallback. A webhook with onlyurl = ...(or Docker labelofelia.webhook.<name>.url: ...) now attaches and fires, no custom preset YAML required.- Audit-and-pin upgrade impact: a stale URL-only config that previously sat inert will now actually POST to whatever's in
url. Pinwebhook-allowed-hoststo a specific allow-list if you want to restrict egress; setwebhook-default-preset =(empty) to keep pre-upgrade behavior and require every webhook to declarepresetexplicitly. Three-state semantics: nil = "operator did not set" (use bundled fallback), non-nil empty = "explicit opt-out", non-nil non-empty = "operator's chosen fallback name". (#677, fixes #676)
- Audit-and-pin upgrade impact: a stale URL-only config that previously sat inert will now actually POST to whatever's in
- Jobs that reference more than one webhook now fire every listed webhook. The middleware container deduplicated middlewares by Go type, so handing it two
*Webhookinstances (e.g. one withtrigger: success, one withtrigger: error) kept only the first and silently dropped the rest — the error webhook attached to nothing and never ran. The same dedup also shadowed scheduler-level webhooks against per-job webhooks during propagation, so any job declaringwebhooks:silently lost global notifications too. The fix attaches a single per-job composite carrying the union of[global] webhook-webhooksand the job's ownwebhooksselector, deduplicated by name. The previously-silent failure path (unknown webhook name, preset-load failure, missing required variable) now emits anslog.Errorkeyed by job name and webhook list so misconfigurations show up in the log. (#671, fixes #670) - Webhook retries no longer hold the daemon hostage at shutdown.
(*Webhook).sendWithRetry's inter-attempt backoff used a baretime.Sleepthat ignored ctx cancellation, soSIGTERMmid-retry pinned a goroutine for up toretry-delay × retry-count. The in-flight HTTP request had the same shape — its context was derived fromcontext.Background(), so even a hung POST blocked shutdown for the fulltimeout. Both halves now drain on scheduler cancellation. Worst-case shutdown contribution from a single webhook drops fromretry-delay × retry-count + timeout(multi-minute on aggressive retry budgets) to roughly one network round-trip on cancel. Callers see an error chain wrappingcontext.Canceled/context.DeadlineExceededrather than the previous "all N attempts failed" message after the full timeout elapsed. (#685, fixes #673)
📖 Docs
- New
docs/TROUBLESHOOTING.mdsections for both hijack failure modes (tls: first record does not look like a TLS handshakeontcp://,dial http: unknown network httponhttp://), slotted alongside the existing #605 socket-proxy section so operators scanning by symptom land on the right entry. docs/webhooks.mdretry-config table now documents the shutdown drain semantics —retry-count × retry-delay + timeoutbounds the worst-case daemon-shutdown contribution from any single webhook, and the retry loop observes scheduler cancellation since this release.- Webhook lifecycle flowchart added to
docs/webhooks.mdfor the trigger / dedup / retry / shutdown story. (#680)
Dependencies
No direct dependency bumps in this release. The Go toolchain and module graph stay at v0.25.0's pinned versions.
Thanks
Special thanks to the external reporters whose issues drove the bugfixes in this release:
- @groknt for #668 — reporting that
run_execjobs failed with the cryptic TLS handshake error on plain-tcp://socket-proxy setups, and following up with the exact diagnostic dump (env, debug logs, container labels, full compose file) that let us reproduce the bug in a self-contained Go program and isolate it to the Go std-lib × Docker SDK interaction. The reproducer also surfaced the siblinghttp://failure mode that ships in this release as #682 / #686. - @techsolo12 for #670 — reporting that error-trigger webhooks were silently being dropped from jobs that also declared a success-trigger webhook. The repro pointed straight at the type-keyed dedup in
middlewareContainer.Use, which had been a latent footgun waiting for the first N-instance middleware to land.
Container image
ghcr.io/netresearch/ofelia:0.25.1
ghcr.io/netresearch/ofelia:0.25
ghcr.io/netresearch/ofelia:0
ghcr.io/netresearch/ofelia:latest
Verification
All release assets (binaries, SBOMs, checksums) are cosign-signed with keyless OIDC and include SLSA Level 3 build provenance. Verify with:
cosign verify-blob \
--bundle ofelia-linux-amd64.bundle \
--certificate-identity-regexp 'https://github\.com/netresearch/(\.github|ofelia)' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ofelia-linux-amd64
Full Changelog: v0.25.0...v0.25.1
v0.25.0
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 onOpportunisticStartTLSfor 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.
v0.24.0
Highlights
This release fixes Docker Compose job naming so it actually works as documented, adds a real end-to-end test harness that runs the compiled binary, and ships a Go 1.26.2 build with stdlib security patches.
🚨 Breaking: Docker Compose job naming
If you reference jobs across containers in a Compose stack, the documented Cross-Container Job References (Docker Compose) feature is now functional for the first time. Ofelia previously stripped the com.docker.compose.service label before it reached the naming code, so jobs ended up with their fallback names instead of the documented Compose service names.
After upgrading, jobs in Compose setups may appear under different names — those documented in docs/CONFIGURATION.md. Configurations that worked around the bug by referencing the old (incorrect) names need to be updated. (#597)
End-to-end testing
The test suite gains a subprocess-based harness that runs the actual compiled ofelia binary and exercises real schedules, real Alpine containers via Docker, the validate command, and graceful shutdown on SIGTERM/SIGINT. This catches integration regressions that unit tests miss. (#581)
Security
- Go bumped to 1.26.2 for stdlib security fixes (#557)
Other improvements
log-level=...errors now list every accepted level instead of a generic "invalid value" message (#599)make lintworks again —golangci-lintis now installed via the v2 module path (#600).envrchooks detection works correctly inside git worktrees (#598).gitignore/ofeliapattern is anchored so it cannot shadow source files (#574)- Stabilized flaky tests for scheduler shutdown, retry backoff, and rate limiter (#582, #601)
Build & supply chain
The release pipeline migrated to the unified netresearch/.github reusable workflow (#566, #587), with auto-merge moved to an org-level reusable workflow (#567) and the standalone integration.yml retired in favor of go-check (#579). Checksum signing now uses cosign --bundle (#547).
Dependencies
Routine dependency bumps: github.com/netresearch/go-cron 0.13.1 → 0.14.0, github.com/docker/cli 29.3.0 → 29.4.0, github.com/docker/go-connections 0.6.0 → 0.7.0, github.com/go-viper/mapstructure/v2 2.4.0 → 2.5.0, golang.org/x/{crypto,term,text} updates, OpenTelemetry exporter 1.42 → 1.43, Alpine base image refresh, GitHub Actions group bumps.
Thanks
Special thanks to @smitsyn for diagnosing and fixing the Compose label filtering issue (#597) — a long-standing gap between the documentation and the actual behavior.
Container image
ghcr.io/netresearch/ofelia:0.24.0
ghcr.io/netresearch/ofelia:0.24
ghcr.io/netresearch/ofelia:0
Verify your download
Per-asset signatures are bundled. 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-amd64Verify checksums against the signed manifest:
cosign verify-blob \
--bundle checksums.txt.bundle \
--certificate-identity-regexp "https://github.com/netresearch/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
checksums.txt
sha256sum -c checksums.txt --ignore-missingVerify build provenance:
gh attestation verify <artifact> --repo netresearch/ofeliaVerify container image:
cosign verify ghcr.io/netresearch/ofelia:0.24.0 \
--certificate-identity-regexp "https://github.com/netresearch/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
gh attestation verify oci://ghcr.io/netresearch/ofelia:0.24.0 --repo netresearch/ofeliaFull changelog: v0.23.1...v0.24.0
v0.23.1
What's Changed
Security
- Migrate
go-viper/mapstructurev1 to v2.4.0 — fixes GO-2025-3787 and GO-2025-3900 (sensitive information leak in logs when processing malformed data) (#544)
Fixed
- Release pipeline migrated from
slsa-github-generatortoactions/attest-build-provenancevia org-wide reusable workflow atnetresearch/.github(#542). This fixes the v0.23.0 release which had no binaries due to SHA-pinning conflict.
Note: v0.23.0 was released without binaries due to the pipeline issue fixed in this release. Users should use v0.23.1 instead.
v0.23.0
What's Changed
Environment File & Container Env Support
Load environment variables from external sources for all job types (#540, closes #314, #336, #351):
env-file: LoadKEY=VALUEpairs from files, like Docker's--env-file. Supports multiple files, quoted values,exportprefix, and special characters.env-from: Copy environment variables from a running Docker container at job execution time.
Merge order (last wins): env-file < env-from < environment (explicit always wins).
Bug Fixes
- Environment variable values containing
#or;are no longer truncated by INI comment parsing (#539, fixes #538) - Environment variable expansion now works in webhook config values, section names, and the
log-levelpre-parse path (#539)
Security
- SHA-pin all GitHub Actions and add Dependabot for actions updates (#536)
v0.22.0
What's Changed
Environment Variable Substitution in INI Config
Support ${VAR} and ${VAR:-default} syntax in INI configuration files (#532, closes #362).
Keep secrets out of version-controlled config files:
[global]
smtp-password = ${SMTP_PASS}
[job-run "backup"]
image = ${BACKUP_IMAGE:-postgres:15}
command = pg_dump ${DB_NAME:-mydb}${VAR}— replaced if defined and non-empty; kept literal if undefined (typos visible)${VAR:-default}— uses default when undefined or empty$VAR(no braces) — not substituted, keeping cron expressions and shell commands safe
Thanks to @nut-neek for describing their use case.
Dependencies
- Bump OpenTelemetry modules to v1.42.0 (#533)
- Bump
step-security/harden-runnerto v2.16.0 (#533) - Bump
aquasecurity/trivy-actionto v0.35.0 (#532) - Bump
google.golang.org/grpcto v1.79.3 (#531)
Full Changelog: v0.21.5...v0.22.0
Verification
All binaries include SLSA Level 3 provenance attestations.
Verify binary provenance
slsa-verifier verify-artifact ofelia-linux-amd64 \
--provenance-path ofelia-linux-amd64.intoto.jsonl \
--source-uri github.com/netresearch/ofeliaVerify checksums signature
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-identity "https://github.com/netresearch/ofelia/.github/workflows/release-slsa.yml@refs/tags/v0.22.0" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
checksums.txtIncluded in this release
v0.21.5
What's Changed
Two new features for this release.
Thanks to @sethlinnkuleuven for reporting #527.
Added
ofelia versioncommand (#528): Print version information viaofelia versionorofelia --version. Release builds show tag and commit; dev builds fall back to Go build info.- Volume support for
job-service-run(#529, closes #527): Mount host directories and named volumes into swarm service containers using thevolumeconfig key. Samesource:target[:ro|rw]format asjob-run.
[job-service-run "backup"]
schedule = @daily
image = postgres:15
network = cluster
volume = /host/script.sh:/script.sh
volume = backups:/backups:rw
command = /script.shFull Changelog: v0.21.4...v0.21.5
Verification
All binaries include SLSA Level 3 provenance attestations.
Verify binary provenance
slsa-verifier verify-artifact ofelia-linux-amd64 \
--provenance-path ofelia-linux-amd64.intoto.jsonl \
--source-uri github.com/netresearch/ofeliaVerify checksums signature
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-identity "https://github.com/netresearch/ofelia/.github/workflows/release-slsa.yml@refs/tags/v0.21.5" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
checksums.txtIncluded in this release
v0.21.4
What's Changed
Fix job-service-run network attachment being silently dropped — services now correctly join the specified network.
Thanks to @sethlinnkuleuven for reporting the issue.
Fixed
- Service network not attached (#524):
convertToSwarmSpecnow reads networks from bothServiceSpec.NetworksandTaskTemplate.Networks, fixing a mismatch introduced during the domain/adapter refactor - Service inspect missing fields:
convertFromSwarmServicenow converts Mounts, RestartPolicy, Resources, Networks, Mode, Placement, LogDriver, and EndpointSpec — all were previously lost silently
Added
- Swarm service adapter now converts Placement, LogDriver, and EndpointSpec in both directions — these domain types existed but had no conversion code
- 13 round-trip tests verifying every service spec field survives the domain→swarm→domain conversion cycle
Full Changelog: v0.21.3...v0.21.4
Verification
All binaries include SLSA Level 3 provenance attestations.
Verify binary provenance
slsa-verifier verify-artifact ofelia-linux-amd64 \
--provenance-path ofelia-linux-amd64.intoto.jsonl \
--source-uri github.com/netresearch/ofeliaVerify checksums signature
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-identity "https://github.com/netresearch/ofelia/.github/workflows/release-slsa.yml@refs/tags/v0.21.4" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
checksums.txtIncluded in this release
v0.21.3
What's Changed
Wire missing container spec fields that the Docker API already supports but job types didn't expose, fixing environment not working for job-service-run (#519).
Thanks to @sethlinnkuleuven for reporting the issue.
Fixed
- job-service-run:
environment,hostname, anddirconfig keys now work — passed through to Docker Swarm ContainerSpec - job-run:
working-dirconfig key now sets the container working directory;volumes-fromwas defined in the struct but never wired to Docker — now functional - job-exec:
privilegedconfig key now enables privileged exec mode - Corrected misleading documentation that claimed
job-service-runinherits fromRunJob(it embedsBareJob) - Removed non-existent swarm fields (replicas, placement-constraints, resource limits) from documentation examples
Full Changelog: v0.21.2...v0.21.3
Included in this release
v0.21.2
Highlights
This security-focused release addresses 5 vulnerabilities and 6 stability issues discovered during a comprehensive code review.
Security hardening
- Credential leak prevention:
/api/configno longer exposesWebPasswordHashandWebSecretKey - CSRF bypass removed: The
X-Requested-Withheader bypass has been eliminated - Rate limiter DoS fix: Stale entries are now cleaned up to prevent unbounded memory growth
- IP spoofing prevention:
X-Forwarded-ForandX-Real-IPheaders are only trusted from loopback or explicitly configured proxies - Configurable trusted proxies: New
web-trusted-proxiesoption for deployments behind reverse proxies in non-loopback networks
Stability improvements
- Context propagation to Docker API calls — scheduler shutdown, job removal, and max-runtime cancellation now reach Docker containers
- Double-close panic on daemon done channel fixed with
sync.Once - Concurrent map access crash in Config protected with mutex
- Shutdown hooks execute in priority groups instead of all concurrently
- Shutdown timeout now enforced even when hooks ignore context cancellation
- Swarm services correctly return
NonZeroExitErrorfor non-zero exit codes
Changes
Security
- fix(security): hide WebPasswordHash and WebSecretKey from /api/config (#511)
- fix(security): remove CSRF bypass via X-Requested-With header (#511)
- fix(security): implement rate limiter cleanup to prevent memory DoS (#511)
- fix(security): only trust forwarded headers from trusted proxies (#511)
- fix(security): make trusted proxies configurable (#511)
- fix(security): also check X-Real-IP in rate limiter middleware (#511)
Bug Fixes
- fix: propagate context to Docker API calls for cancellation support (#511)
- fix: prevent double-close panic on daemon done channel (#511)
- fix: add mutex to Config to prevent concurrent map access crash (#511)
- fix: execute shutdown hooks in priority groups (#511)
- fix: enforce shutdown timeout even when hooks ignore context (#511)
- fix: return NonZeroExitError for non-zero Swarm service exit codes (#511)
Dependencies
- chore(deps): bump golang.org/x/crypto from 0.48.0 to 0.49.0 (#512)
- chore(deps): bump github.com/netresearch/go-cron from 0.13.0 to 0.13.1 (#514)
- chore(deps): bump golang.org/x/time from 0.14.0 to 0.15.0 (#515)
Verification
All binaries include SLSA Level 3 provenance attestations.
Verify binary provenance
slsa-verifier verify-artifact ofelia-linux-amd64 \
--provenance-path ofelia-linux-amd64.intoto.jsonl \
--source-uri github.com/netresearch/ofeliaVerify checksums signature
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-identity "https://github.com/netresearch/ofelia/.github/workflows/release-slsa.yml@refs/tags/v0.21.2" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
checksums.txt