# Changelog All notable changes to **Caddy UI** are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · Versioning follows [Semantic Versioning](https://semver.org/). --- ## [Unreleased] --- ## [2.68.8] - 2026-11-10 - One "Strip response headers" field instead of two (#173) ### Fixed - **The host form showed two "Strip response headers" inputs — the response-side twin of #167 (#173).** `strip_resp_headers` (v2.9.35) and `strip_response_headers` (v2.9.168) were two database columns with identical behaviour, so which one a host used came down to which of two boxes the user typed in. Both emitted a *deferred* Caddy response-delete handler ahead of `reverse_proxy`; two delete lists are commutative, so the columns were genuinely interchangeable — and filling both silently emitted **two** strip-response-headers directives. Only `strip_resp_headers` was read by the Caddyfile exporter, so a value in the other column stripped headers in the live config but **was missing from an export**. - **`strip_resp_headers` is now canonical.** The choice was not automatic: the legacy column owned two integrations the canonical one lacked — the Settings → **Globally stripped response headers** setting, and the precedence that stops the **Security Headers** bundle re-setting a header the host strips. Those were *moved onto* the canonical field rather than dropped, because the exporter gap is a correctness bug while the wiring is mechanical, because `strip_resp_headers` canonicalises header names (`http.CanonicalHeaderKey`) and the v2.9.168 path did not, and because #167 had already made the paired `strip_req_headers` canonical — picking the other column would have left request and response on opposite sides. - **Both ported behaviours are preserved and now better.** The global strip list is still applied ahead of each host's own list, but is **deduplicated** against it: a header named in both the global setting and a host's own field used to be emitted twice. The security-bundle precedence is unchanged and remains case-insensitive, so stripping `x-frame-options` in lowercase still stops the bundle putting `SAMEORIGIN` back. - **Existing values are migrated, not dropped.** On upgrade, anything stored in `strip_response_headers` is folded into `strip_resp_headers`: a legacy-only value moves over verbatim, and when both columns are set the header names are **merged and deduplicated** case-insensitively, canonical order kept and new names appended. The legacy column is drained only after its value has been written, in one statement per row, so an interrupted upgrade cannot lose data and a re-run is a no-op. - **The old `strip_response_headers` form value is still accepted for one release**, merged into the canonical field, so an old bookmarked form or an API client posting the v2.9.168 name does not silently lose data. Marked for removal in v2.70 along with the column. - The merge itself is now one shared helper (`models.MergeHeaderNameLists`) used by both the request- and response-side consolidations, rather than two copies of the same logic. - The Caddy config now carries **exactly one** strip-response-headers directive, and an export round-trips header names typed into either box. ### Changed - **Dependency updates (#172).** `aws-sdk-go-v2` 1.47.4 → 1.47.8, `aws-sdk-go-v2/credentials` 1.20.10 → 1.20.16, `aws-sdk-go-v2/service/route53` 1.70.5 → 1.70.9, `golang.org/x/oauth2` 0.37.0 → 0.38.0 and `modernc.org/sqlite` 1.61.0 → 1.62.0. All patch bumps except `oauth2`, which is a minor. ### Notes - **This release contains a data migration** (the response-strip column merge described above). It is idempotent, discards nothing, and is covered by tests for the legacy-only carry-over, the merge case, case-insensitive dedupe, idempotency across a real database reopen, and a missing legacy column. **Back up the database before upgrading** — the migration rewrites every stored `proxy_hosts` row on first start. The same caution applied to v2.68.7, and Settings → Backup can now do this from the UI. - `go test ./...` was run uncached against merged `main` for this release, rather than relying on the per-PR CI runs. - `applegater/caddyui-caddy` is **not** rebuilt for this release: `Dockerfile.caddy` is unchanged since v2.68.6, so its module set is untouched and `:stable` keeps pointing at the v2.68.5 build. Only `applegater/caddyui` is republished. - **If you are still on v2.68.6 or earlier, this release carries both column merges** — the request-side one from v2.68.7 (#167) and the response-side one here — so upgrading straight to v2.68.8 runs both migrations in order and skips nothing. --- ## [2.68.7] - 2026-11-10 - Scheduled backups can finally be configured from the UI (#166); one "Strip request headers" field instead of two (#167) ### Fixed - **Settings → Backup saved nothing, and the scheduled backups shipped in v2.52.0 could not be turned on from the UI at all (#166).** Reported by @BurkhardFS: clicking **Save settings** on the Backup page did nothing — no error, no saved value. The Backup card sat *after* the closing `` of the settings form, so in HTML terms its inputs and both its buttons belonged to no form at all. The reporter's own diagnosis was exact: `button.form === null`. Every value typed there — the schedule, the retention count, the target directory — was discarded on submit, and because the schedule was one of those values, the scheduled-backup feature released in v2.52.0 had been **unreachable from the interface since it shipped**. The Notifier status card had drifted out of the form the same way. - **Both cards now render inside the `action="/settings"` form**, so they post the `settings_section` marker and the CSRF token like every other section. That token is the reason this is not a cosmetic fix: a POST without it is refused with `403`, so the controls had to be inside the form, not merely adjacent to it. - **"Back up now" works again too.** It stays a submit button and keeps its `formaction="/settings/backup/run"`, which overrides the form's action for that one button — outside a form, that attribute had nothing to override. - **A regression test now walks the rendered page and asserts that no named control or submit button is left outside the form**, so a card added after `` in future fails the build instead of silently losing data. Verified end to end against a running instance as well: before the fix all five Backup controls reported `form === null`; after it, all five are owned by `/settings`, values persist across a reload, and **Back up now** produces a valid SQLite backup. - **The host form showed two identical-looking "Strip request headers" inputs, and they were not the same setting (#167).** Reported by @AlexGuld: `strip_req_headers` (v2.9.35) and `strip_request_headers` (v2.9.250) were two database columns with identical behaviour, so which one a host used came down to which of two boxes the user happened to type in. That was a data-correctness bug, not just a confusing form: only `strip_req_headers` was read by the Caddyfile exporter, so a value in the other column stripped headers in the live config but **was missing from an export**, and filling both silently emitted **two** strip-request-headers directives. - **`strip_req_headers` is now canonical** — it owns the header-name canonicalising helper, the exporter support, and the paired "Strip response headers" field above it in the form. The duplicate input is gone and the surviving one keeps the form's `ink-*` design tokens, which the v2.9.250 block did not use. - **Existing values are migrated, not dropped.** On upgrade, anything stored in `strip_request_headers` is folded into `strip_req_headers`: a legacy-only value moves over verbatim, and when both columns are set the header names are **merged and deduplicated** (case-insensitively, since HTTP header names are case-insensitive) with the canonical order kept and new names appended. Nothing is discarded, and re-running the migration is a no-op. - **The old `strip_request_headers` form value is still accepted for one release**, merged into the canonical field, so an old bookmarked form or an API client posting the v2.9.250 name does not silently lose data. It is marked for removal in v2.70 along with the column. - The Caddy config now carries **exactly one** strip-request-headers directive, and an export round-trips header names typed into either box. - **A public health check leaked an idle connection on every probe.** Contributed by @kernaxis (#163): each probe opened a connection and left it in the idle pool, so a fleet with several monitored upstreams accumulated sockets for as long as the process ran. Idle connections are now closed once the probe is done. - **The top bar's actions stayed reachable with a long server name.** Contributed by @Thibault-MTZ (#159): a long name pushed the action buttons off the edge on narrow and mobile viewports, so they could not be tapped. ### Security - **`go-jose` moves to 4.1.6, which fixes GHSA-mgcc-cww5-fp2q.** CaddyUI pulls go-jose in through `go-oidc`, so this is on the **OIDC single sign-on** path specifically. - **Behaviour change worth knowing about if you use SSO:** 4.1.6 adds a cap on the RSA modulus accepted when parsing a JWK, `MaxRSAModulusBits = 4096`. The check rejects a key only when it is *larger* than that (`n.BitLen() > MaxRSAModulusBits`), so a 4096-bit identity-provider signing key — GitLab's size, for example — still parses. An IdP signing with something larger than 4096 bits would be refused; none of the common providers do. ### Changed - **Spanish (es) interface translation.** Contributed by @gnacho (#162) — 316 strings. - **The Security settings and sign-in screens are translatable.** Contributed by @Thibault-MTZ (#147): 127 strings on those two screens were hardcoded English and could not be localised. Keys a translation has not caught up with fall back to English rather than rendering blank. - **Dependency updates.** `go-chi/chi` 5.4.0 → 5.5.0, `modernc.org/sqlite` 1.60.1 → 1.61.0, `modernc.org/libc` 1.77.1 → 1.79.0 and the AWS SDK / Route 53 modules to their current patches (#170, relanding #157). - **CI pins every action to a commit SHA, and BuildKit to a digest (#152).** A moving tag on a workflow action is a supply-chain risk for a workflow that holds a read/write Docker Hub token. ### Notes - **This release contains a data migration.** On first start, anything stored in the legacy `strip_request_headers` column is folded into `strip_req_headers` as described above. It is idempotent and discards nothing, and it was verified against a real database by writing to both columns and re-running the upgrade. As always, a backup before upgrading is cheap insurance — and as of this release you can actually configure one from the UI. - `go test ./...` was run uncached against merged `main` for this release, rather than relying on the per-PR CI runs. - `applegater/caddyui-caddy` is **not** rebuilt for this release: `Dockerfile.caddy` is unchanged since v2.68.6, so its module set is untouched and `:stable` keeps pointing at the v2.68.5 build. Only `applegater/caddyui` is republished. - **The `:preview` Docker tag is gone** — it was deleted from Docker Hub in October 2026. If anything still references it, move to `:stable` or pin a version. --- ## [2.68.6] - 2026-10-25 - Source Push sends the wildcard, not the apex (#153); a stale "Issuance error" clears once the CA recovers (#151) ### Fixed - **Source Push shipped the apex certificate to targets when a Managed ACME definition listed both a wildcard and its apex, breaking HTTPS for every subdomain on those targets (#153).** Caddy stores `*.example.com` and `example.com` as two separate certificates. CaddyUI picked the one with the latest expiry, and adding the apex to an existing wildcard definition meant the apex was obtained last — so targets received a certificate presenting only `example.com`, which covers no subdomain at all. Selection is now by **coverage** of the requested names, with expiry only breaking a tie, and a wildcard outweighs a concrete name because it protects everything behind it. Wildcard matching follows TLS rules: `*.example.com` covers one leftmost label, and never the bare apex. - **A target certificate row holds exactly one keypair, so a single push still cannot carry both certificates.** For a `*.example.com` + `example.com` definition, the push now sends the wildcard and records in the activity log which names it does not cover, naming them. To serve the apex on a target as well, give it its own certificate definition with its own push targets. Nothing about this release makes one push deliver two certificates. - **A certificate kept showing "Issuance error" after the CA came back, and Refresh would not clear it (#151).** A transient DNS failure during an ACME Renewal Information (ARI) update was recorded as an issuance failure, and two things then kept it on screen: a later successful ARI update was itself classified as an error rather than as a healthy certificate, and a certificate already in `error` or `retrying` was not re-probed, so the recorded failure was never revisited. A successful ARI update is now recognised as success, and a stale failure is re-probed, so the status clears on its own once issuance is actually working again. ### Notes - Both fixes are on the certificate path, so they were verified by reverting each cause against the new regression tests and confirming the tests fail without the fix — including the wildcard weighting, without which apex and wildcard tie on coverage and the expiry tie-break reintroduces the original bug. --- ## [2.68.5] - 2026-10-25 - The Caddy image clears its three High CVEs ### Security - **Trivy no longer flags `applegater/caddyui-caddy` for three High CVEs that have fixes.** Two were in Go code that is not compiled into Caddy, which scanners cannot tell; the third was in the image's `curl`. All three are now on fixed versions: - **CrowdSec 1.6.3 (CVE-2026-44982).** CrowdSec's AppSec engine skipped request-body rules for chunked and HTTP/2 requests. That code lives in the CrowdSec engine and is not compiled into Caddy, since the bouncer only uses CrowdSec's API client, but scanners flag the whole module. The newest bouncer release, v0.14.1, cannot build against CrowdSec 1.7. The image now pins the upstream bouncer commit that adds 1.7 support, whose only code change since v0.14.1 is that fix, and builds against CrowdSec 1.7.8. **If you run the CrowdSec engine with AppSec, update that container to 1.7.8 or later too**: that is where the bug actually is. - **golang.org/x/net 0.59.0 (CVE-2026-78669).** A malicious HTTP/2 peer could burn CPU with many small SETTINGS frames. Caddy is built with Go 1.27, where x/net's HTTP/2 client and server both hand off to the standard library's own HTTP/2 code, which Go 1.27.2 already fixed. The vulnerable x/net code was therefore never in the binary. It is now 0.61.0, which also clears four Medium x/net CVEs. - **brotli-libs 1.2.0 (CVE-2026-111497).** An out-of-bounds write in the Brotli decoder. Caddy does not use this library; the base image's `curl` does. The image now runs `apk upgrade`, so it gets Alpine's fixes even when the upstream `caddy:alpine` base is older. This build also picks up curl 8.23.0. - **go-chi moves to 5.4.0 (CVE-2026-114437, Medium).** The affected middleware is not used by Caddy. Trivy now reports no Critical, High or Medium findings in the image. The one remaining advisory, GO-2026-5932 (`x/crypto/openpgp` is unmaintained), has no fix, and that package is not in the binary. - **The security pins can't quietly downgrade a plugin.** xcaddy sets each pinned version exactly, so the pins come before every plugin, and the build now fails if `go get` ever reports a downgrade. - **Behaviour is unchanged.** The old and new images gave identical results for: a CrowdSec ban and unban in stream and live mode against a CrowdSec 1.8.1 Local API, the Coraza WAF with the OWASP Core Rule Set (SQL injection and XSS blocked, clean requests passed), rate limiting, and an HTTP/2 (h2c) reverse proxy. Caddy stays at 2.11.7 and `caddy list-modules` is identical. - **Coraza's JSON-schema library moves from 0.4.6 to 0.7.7.** CrowdSec 1.7.8 requires `kaptinlin/jsonschema` 0.7.7, and Go builds one copy of each library, so Coraza's `@validateSchema` operator gets it too. CaddyUI never generates that operator, so only hand-written WAF directives use it. Coraza's own `@validateSchema` tests pass with 0.7.7. The old and new images also gave identical results on 13 schema cases, covering required fields, types, extra properties, nested `$ref` objects, enums and array limits. ### Notes - CaddyUI itself has no code changes. This release publishes the rebuilt Caddy image. Pull `applegater/caddyui-caddy:stable` (or `:v2.68.5`) and recreate the Caddy container to get it. --- ## [2.68.4] - 2026-10-24 - Go toolchain 1.27.3 distrusts compromised intermediate certificates ### Security - **The published binaries were built with Go 1.27.2, which govulncheck reports as affected by GO-2026-6809 (CVE-2026-111943).** Go 1.27.3, released on 2026-10-19, adds a denylist to `crypto/x509` so that `Certificate.Verify` refuses the intermediate certificates that are no longer trusted after the eMudhra intermediate CA compromise. This is the same incident behind Node.js adding those certificates to its own trust-store blocklist this month. CaddyUI calls `Certificate.Verify` when it checks certificates and connects to upstreams and DNS providers over TLS. The pinned toolchain is now **Go 1.27.3**, and govulncheck reports no vulnerabilities that CaddyUI calls. - **The `applegater/caddyui` Docker images were not affected.** They build on `golang:1.27-alpine`, which already resolved to 1.27.3 for v2.68.3. Only the native binaries from the GitHub releases page carried the old runtime, so anyone running a downloaded binary should take v2.68.4. - **`applegater/caddyui-caddy` is rebuilt as well.** The published image was compiled with Go 1.27.1. The upstream `caddy:2.11.7-builder` image now ships Go 1.27.3, so the rebuild picks up the fix. Caddy stays at 2.11.7, the latest release. --- ## [2.68.3] - 2026-10-24 - Fleet changes reach every node (#150); analytics stays inside each viewer's own hosts; Force SSL redirects survive a catch-all route (#149) ### Security - **Saving Settings re-syncs every managed server, not only the selected one (#150).** Reported by @hieronymousch: fleet settings were not propagated. Settings are global. The fleet-wide IP blocklist, global maintenance mode, the fallback page and status, stripped response headers and the ACME DNS resolvers all go into every server's config. But a save re-synced only the server picked in the top bar, unless it was the Integrations page. On a real two-node fleet, an address added to the blocklist under Settings → Security was blocked on one node and still served by the other. That lasted until something else happened to sync that node. Every managed server is now synced after a save, the selected one first. - **Disabling a host now disables its deployed copies too (#150).** The quick enable/disable toggle, the maintenance toggle and the bulk actions on proxy hosts, redirections and advanced routes changed only the source server. A host switched off there kept serving, unchanged, on every automatic deployment target. These changes now follow the automatic targets as a form save does, and as Layer4 proxies already did. A bulk action syncs each target once, not once per host. Node-local resources still stay on their own node. A bulk action on hosts from several servers now also syncs each of those servers, not only the first. - **A save that does not reach a deployment target now says so (#150).** When a target refused a copy, because it is registered as monitoring-only, because one of the host's names is already claimed there by a different host, or because its Caddy could not be reached, the reason went only to the Activity Log, and that entry did not even include it. The save looked like it had worked, while the target kept serving its old version of the host, without whatever protection had just been added on the source. Saving a proxy host, redirection or advanced route now ends on a notice naming each target that did not get the change and why, and the Activity Log entry carries the reason. - **The request explorer's counts are scoped to the viewer's own hosts.** Searching with no host picked listed only your own requests, but the *Total* and the status-mix chips counted every host on the fleet. A non-admin could therefore read the size and error rate of other tenants' traffic. This is exactly what a bar on the fleet chart opens since v2.68.2. Total, status mix and host facets now come from a query scoped to the viewer's hosts, wildcards included, rather than from rows filtered afterwards. That also fixes a quieter bug: when other hosts were busier, your own matching requests could be pushed out of the 200 rows shown. - **A non-admin who can see no hosts no longer gets the whole fleet.** For such an account, the analytics scope came back as "no list" instead of "an empty list". Every caller read that as *admin, no filter*. The account was shown the full `/analytics` overview, including other hosts' names, traffic and charts, and the explorer listed their requests with paths and client IPs. This affects every signed-in role, since Analytics is open to all of them. It could also happen to a real user, for example a new account or one whose hosts were all removed. Host-page drill-downs and CSV export were not affected: they check for an admin explicitly. ### Fixed - **A Force SSL host requested over plain HTTP gets its `308` redirect again, not a 404.** Reported by @AlexGuld: on CaddyUI's own `:80` server the HTTPS redirect was appended after every other route, so a catch-all Advanced route served on `:80` answered first, and every Force SSL host received an empty `404`. Deleting the route through Caddy's admin API fixed it until the next sync re-added it. The redirect now goes right after the last host-matched route and ahead of any catch-all that follows. That is where Caddy's own automatic HTTPS puts the redirects it adds to an existing `:80` server ([caddyserver/caddy#3212](https://github.com/caddyserver/caddy/issues/3212)). Exact hosts still beat wildcards, and a path-scoped route for a forced host still runs before that host's redirect (#129). - **Import from Caddy no longer copies CaddyUI's own generated config.** In the report above, the catch-all was CaddyUI's own fallback, stored as an Advanced route (`srv0 route[37] (no host match)`) by importing from a Caddy that CaddyUI already managed. The importer now skips the `caddyui_http` server, which CaddyUI rebuilds from its database on every sync. It also skips the fallback route on any server, with or without the CrowdSec guard, and lists it under *Skipped* along with the reason. Before this change, importing that server also turned the Force SSL redirect into a junk Redirection host that claimed the forced hosts. - **Adding an automatic deployment target now says what it does not copy (#150).** A target receives what is saved on the source from then on. Hosts, redirections and advanced routes that already existed were not copied, and nothing said so, which made ticking a target look broken. Saving a server that gains a target now reports how many existing resources were left behind, and that one **Sync from current** on the target copies them. The server form and README no longer list toggles and bulk actions among the things that don't deploy. ### Notes - Hosts that a toggle or bulk action changed on a source before this release still differ on its targets. Run **Sync from current** once on each target, with the source selected, to bring them back in line. - Existing installs that already hold an imported fallback should delete it from **Advanced routes**. It is the `(no host match)` entry whose JSON is only an `error` / `404` handler. CaddyUI adds its own fallback on every sync, and the stored copy also hides the custom fallback page over HTTPS. Until then, this release keeps it behind the redirects on `:80`. --- ## [2.68.2] - 2026-10-22 - Request charts separate 4xx from 5xx, and the fleet chart drills through (#145) ### Fixed - **The request chart no longer lumps client errors in with server errors.** Reported by @BenRLange: v2.66.0 drew 4xx and 5xx as a single red band, so a scanner throwing 404s looked exactly like the backend falling over — the two things you most need to tell apart. Bars are now stacked by response class using the same colours as the status-mix chips everywhere else: **2xx** green, **3xx** blue, **4xx** amber, **5xx** red. The hover text names the counts too, as *"73 requests · 2×4xx · 28×5xx"*. ### Added - **Bars on the main Analytics chart are clickable**, as the per-host ones already were (#145). A spike there now answers *"which hosts, and were they errors?"* without first guessing a host to drill into. Each bar opens the request explorer bounded to its own hour. - The fleet chart is coloured by response class as well, so both charts read the same way. ### Notes - The fleet chart's colouring respects who you are: a non-admin sees the status mix of their own hosts only, scoped exactly as the chart above it already was. - Buckets older than the per-bucket status data fall back to a single neutral bar rather than vanishing. --- ## [2.68.1] - 2026-10-22 - Blocked requests return a readable 403 instead of an empty one (#148) ### Fixed - **A refused request now carries a body and a `Content-Type`.** Reported by @AlexGuld: an IP allowlist answered with a bare `403` — no body, no content type — and while that is legal HTTP, a browser is given nothing to render and nothing to go on. Mobile Safari and Edge react by offering the empty response as a **`document.txt` download** instead of showing a denial page. Blocked visitors now get `403 Forbidden` as `text/plain; charset=utf-8`. - **Every refusal CaddyUI generates was fixed, not just the allowlist.** The same bare-status pattern appeared at **twenty** places — IP allowlists and blocklists, the global blocklist, blocked and disallowed methods, blocked paths, blocked user agents, empty user agents, private-IP blocking and the tenant route guard. They now share one helper, so the next refusal added cannot reintroduce this. - Four of those already had a body but **no content type**, which is the half that actually causes the download. A grep for a missing body would have missed them; they were caught by a test that walks the generated configuration and asserts no refusal is bodiless or typeless. - **Redirects and `204 No Content` are deliberately left alone.** A `301`/`302` carries a `Location` and a `204` means "no content" — giving either a body would be the opposite mistake, and a `204` with a body is malformed. There is a test for that too. ### Notes - Verified against Caddy 2.11.7 end to end, not only in unit tests: before, a blocked request returned `Content-Length: 0` with no content type; after, it returns `403 Forbidden` with `Content-Type: text/plain; charset=utf-8`. - Nothing about **who** is blocked changed — this only affects what a blocked visitor is shown. --- ## [2.68.0] - 2026-10-22 - Users shows how each account signs in ### Added - **A Sign-in column on the Users page.** Each account shows whether it signs in through your identity provider, with a password, or both — so "who is using Google?" is answerable at a glance instead of by inspecting the database. - The provider is recorded on **every** SSO sign-in rather than inferred from how the account was created. An account added by hand that later signs in with Google shows **both** badges, which is the truthful answer; guessing from the creation path would have got that wrong. - The badge is named after the provider — *Google*, *Authentik*, whatever your issuer is — and the full issuer URL and last sign-in time are in its tooltip, so an abbreviation is never the only thing on screen. - **Accounts with no usable password are flagged `SSO only`.** An account the provider created has no password it is possible to match, so it depends entirely on SSO: disable single sign-on, or remove the person at the provider, and they lose access until an administrator sets a password. That is worth seeing before it happens rather than afterwards. ### Notes - Existing accounts show no provider badge until their next SSO sign-in — nothing is backfilled, because there is no record of past sign-ins to backfill from. Password accounts are unaffected and display immediately. --- ## [2.67.0] - 2026-10-22 - Restrict SSO to your own email domains ### Added - **Allowed email domains for SSO**, under *Settings → Security → SSO*. List the domains that may sign in and every other verified address is refused. Leave it blank and nothing changes, so upgrading restricts nobody. This exists because of a distinction that is easy to miss. With a **private** identity provider — Authelia, Authentik, Keycloak — "anyone my provider authenticates" means your own staff. Point CaddyUI at a **public** one such as **Google** and it means every account in the world; with *Auto-create accounts* switched on, that is a CaddyUI account for anyone who finds your login page, at whatever default role you set. Listing your domains closes that. - The list gates **sign-in itself**, not just account creation. Gating creation alone would leave an account from outside the listed domains still able to sign in through a provider the administrator believes is restricted. - Matching is on the exact domain after `@`, case-insensitively, and a leading `@` in an entry is tolerated. **Subdomains are not implied** — listing `example.com` does not admit `user@mail.example.com`, which must be listed separately. - **Local password sign-in is unaffected**, so this cannot lock anyone out of the product. - The refusal page does not name the permitted domains; the login page is reachable by anyone and the list is configuration. ### Docs - **Signing in with Google** is now written up in the in-app documentation: the exact Issuer URL, the redirect URL to register, and the warning above. Google needs no special support — it is an ordinary OIDC provider and CaddyUI has worked with it since SSO shipped — but two things are worth knowing. Group-to-role mapping does **not** work with plain Google, whose ID tokens carry no `groups` claim, so every auto-created user lands on the default role; and Google always returns a verified email address, which CaddyUI requires. --- ## [2.66.1] - 2026-10-20 - Russian interface translation (#146) ### Added - **Russian (`ru`) interface translation**, contributed by @tkkost — all 314 catalog keys, with placeholders and technical terms preserved. Pick it under **Profile → Language**, or set it as the site-wide default under Settings. CaddyUI now ships **English, Français, Русский and 简体中文**, three of them community-contributed inside a fortnight. ### Notes - The four colour-theme names under Settings → Appearance (*Forest Green*, *Deep Indigo*, *Carbon Orange*, *Rose / Crimson*) are deliberately left in English in this catalog, the contributor treating them as names of predefined themes rather than ordinary labels. The French and Chinese catalogs translate them; both readings are defensible. The contributor has also offered to keep the Russian translation current as new strings land. --- ## [2.66.0] - 2026-10-19 - Request explorer, latency percentiles and French (#145, #142) ### Added - **A request explorer at Analytics → Search requests** (#145). One filter over host, response status, user agent, path, client IP and time window, listing the matching requests with a status mix and the hosts they came from. @BenRLange's own examples — *all 500 errors from today*, *user-agent activity for a host yesterday afternoon*, *4xx for one host over a week* — are each a single view now rather than a chain of drill-downs that each dead-ended. - **The fleet-wide status mix is clickable** (#145). The per-host page's status chips have always been links; the ones on the main Analytics page were plain text, so the most obvious starting point for "what are these errors?" went nowhere. Each class now opens the explorer filtered to it. - **Request-chart bars are split by response class and drill through to their own period** (#145). A rise in 4xx/5xx is visible in the bar itself, and clicking any bar lists exactly that bucket's requests — so "what happened during that spike" no longer means reconstructing a time range by hand. - **Exact user agents per host** (#145). The host page had a *browser* breakdown, which buckets agents into Chrome/Firefox/… and hides precisely what you want when a bot is hammering a host. The verbatim agent strings now appear with request and visitor counts, each linking into the explorer. - **p50, p95 and p99 response times** on the host page (#145), alongside min/max and the sample count. A single percentile can't distinguish a slow tail from a broad shift. - **French (`fr`) interface translation** (#142, #144), contributed by @Thibault-MTZ — all 314 catalog keys. CaddyUI now ships English, Français and 简体中文. ### Fixed - **The response-time panel had been showing nothing.** `AVG(duration_ms)` returns a floating-point value, and the query scanned it straight into an integer — which fails, since an average duration is essentially never a whole number. The panel returned a scan error and all-zero statistics on any real data set. Found while adding the percentiles above and confirmed against a 188,000-event database, where it reported zeros before the fix and a 284 ms average after it. This is why latency looked absent rather than merely sparse. ### Notes - **Upstream time cannot be separated from total time.** Issue #145 asked to distinguish proxy time from backend time; Caddy's access log emits a single `duration` for the whole request, so there is nothing to split. Reporting a guess would be worse than reporting nothing. Everything else in that request is covered. - The explorer reads raw access events, so it reaches back as far as your access-event retention — the same bound the existing drill-downs have, not the longer daily rollup. - Non-admins see only hosts they own: a hand-edited `?host=` for someone else's host returns 404 rather than 403, so the set of hosts stays unguessable. --- ## [2.65.1] - 2026-10-16 - Go toolchain 1.27.2 closes 12 standard-library vulnerabilities ### Security - **The published binaries were built with Go 1.26.8, which govulncheck reports as affected by 12 standard-library vulnerabilities.** Mostly denial-of-service and request-desynchronisation issues in `net/http` — an HTTP/2 HPACK encoder race, double flow-control refunds, memory exhaustion via trailers, unbounded `Range` header parsing, HTTP/1 desync after a CONNECT response — plus a MIME header memory-limit bypass in `net/textproto`, malformed ECH handling in `crypto/tls`, and two `html/template` context-tracking issues reachable from CaddyUI's own page rendering. The pinned toolchain is now **Go 1.27.2** and govulncheck reports zero. - **The Docker images were never affected.** They build on `golang:1.27-alpine`, which resolves to 1.27.2 — already the fixed release for that line. Only the native binaries from the GitHub releases page carried the vulnerable runtime, so **anyone running `applegater/caddyui` had nothing to do**; anyone running a downloaded binary should take v2.65.1. - This also closes a gap where one release was compiled two different ways: the binaries from the `toolchain` line in `go.mod` and the images from the Dockerfile's base image. Both now use 1.27.2. --- ## [2.65.0] - 2026-10-16 - Admin allowlist matches the real visitor; SSO roles from group claims (#139, #140) ### Security - **The admin panel IP allowlist did not restrict what it appeared to.** It compared the raw connection address, ignoring *Trusted proxy IPs/CIDRs* entirely, so behind any reverse proxy it only ever saw the proxy. Two symmetrical failures followed, and @BenRLange hit the first: - An allowlist naming **your own address locked you out**, because your address never reached the check. - The obvious workaround — allowlisting **the proxy** — then admitted **every** visitor who arrived through it, at any IP, while Settings still read as restricted. An admin could believe the panel was limited to their home address while it was reachable by anyone who could reach the proxy. The allowlist now matches the same trusted-proxy-aware address the login rate limiter uses: a forwarded header is believed only when the connection came from a trusted proxy, and the **right-most** `X-Forwarded-For` entry is taken — the one your proxy appended — so a visitor cannot prepend a chosen address and be believed. Same class of bug as #129, which v2.63.2 fixed for the Caddy-side allowlists; this was CaddyUI's own panel and was missed at the time. **If you use this allowlist, re-check it after upgrading.** A list that "worked" because it named the proxy now restricts by real visitor address, as it always claimed to. ### Added - **Settings → Security shows the address you are connecting from**, and whether it was read from the connection or from a forwarded header via a trusted proxy (#140). The allowlist is checked against exactly that address, so an entry that would lock you out is visible before you save it rather than after. If it names the proxy instead of you, the hint says to configure *Trusted proxy IPs/CIDRs* first. - **`CADDYUI_ADMIN_ALLOWLIST` recovers a lockout from compose** (#140). Set it in your compose file and restart: it overrides the stored list, and `CADDYUI_ADMIN_ALLOWLIST=off` disables the gate entirely while you get back in. The saved list is left untouched, so removing the variable restores it. An empty value counts as not set, so a blank entry in compose cannot lock you out by itself. - **Auto-created SSO accounts can be given a role** (#139). *Settings → Security → SSO* gains a **default role** for accounts created on first sign-in — still `view` unless you change it, so upgrading widens nobody's access — plus optional mapping from a **groups claim**: name the claim your IdP sends and write `group|role` rules, one per line, first match wins. Group names match case-insensitively, since IdPs are inconsistent about casing. A rule naming a role CaddyUI does not have is refused when you save, with the line number, rather than stored and silently ignored. - The claim is read defensively: an array of strings, a single string, or a space/comma-separated string all work, and any other shape means "no groups" and a sign-in on the default role rather than a failure. - Roles are applied **at account creation only**. An existing user's role is never changed by signing in, so a mapping cannot demote or escalate someone already set up under *Users*. ### Fixed - **The SSO callback URL is spelled out in full** (#139). The field's placeholder was the complete `…/auth/oidc/callback`, but the input is too narrow to show the end of it, so it read as a recommendation to register the bare `/auth/oidc` path — and registering that causes the login to fail at the provider. The exact URL, built from the host you are using, is now shown under the field as selectable text where it cannot be clipped. --- ## [2.64.2] - 2026-10-15 - Declining two-factor auth no longer looks like unfinished setup (#138) ### Fixed - **The readiness banner counted a recommendation as a requirement.** Reported by @tkkost: with two-factor auth deliberately not in use, Operations showed *"Environment readiness · 3 of 4 required"* and **Continue setup** permanently, so a considered decision looked like an install someone had abandoned half-done. The onboarding page labels that same step *"Recommended"*, so the banner was contradicting it. Readiness now counts the three foundations that genuinely are required — your administrator account, a connected Caddy, and a published service — and reaches a finished state without TOTP. The step is still listed, and the security recommendation still appears; neither claims the install is incomplete. - Worth knowing if you go looking: that counter was driven by **your own account's** TOTP, not the site-wide policy. Toggling the policy never moved it. - **The "two-factor authentication is not required" recommendation can be dismissed.** Choosing not to require TOTP across the board is a legitimate decision, and a recommendation that cannot be acknowledged is just a permanent nag. There is now a **Dismiss** next to it, following the same pattern as the unused-certificates recommendation. Turning the policy on clears the dismissal, so switching it off again later raises the recommendation afresh rather than leaving it silently suppressed by a decision made months earlier. ### Notes - Nothing about enforcement changed — this release only affects what the dashboard claims about your setup. If you require TOTP site-wide, it is still required. - Simplified Chinese: the readiness count and body text are updated, and **Dismiss** is translated. --- ## [2.64.1] - 2026-10-15 - One two-factor policy setting instead of two (#138) ### Fixed - **Settings › Security had two toggles for one policy.** *Require 2FA for all users* and *Require TOTP for all users* were separate settings whose descriptions said nearly the same thing, and reported by @tkkost as impossible to tell apart. They were: both sent a user without TOTP to the setup page, and the dashboard's security recommendation already treated them as interchangeable. There is now a single **Require two-factor authentication (TOTP) for everyone**. - **Nothing is switched off by the upgrade.** An install that had set only one of the pair still enforces the policy — both stored keys are honoured, and saving writes both, so downgrading to an earlier release keeps enforcing it too. - The two old toggles enforced the policy with *different* exclusion lists: the TOTP one would answer a session-authenticated `/api/` request with a redirect to an HTML page, while the 2FA one let it through. The surviving check keeps the wider list, so JSON callers get JSON. - They also used different checkbox conventions and one saved immediately while the other waited for **Save**. One toggle, one behaviour. - **The per-user control was hard to find.** The user-menu entry is now **My two-factor authentication** rather than the ambiguous *Two-factor auth*, and the policy setting says in so many words that it is site-wide, that it enrols nobody, and that individual accounts are managed from that menu. @tkkost had reported TOTP as impossible to disable before finding this entry — the disable itself was working correctly all along. - **The policy applies to the admin who turns it on**, which is easy to walk into: with no TOTP on your own account, saving it sends you to enrolment and you must finish before reaching Settings again, including to switch it back off. That was already true; the setting now warns before you save. The enrolment page stays reachable, so this strands nobody. ### Notes - Simplified Chinese: the renamed user-menu entry is translated (*我的两步验证*). The new policy description is English until a translation lands. --- ## [2.64.0] - 2026-10-14 - Client certificate authentication (mTLS) on proxy hosts (discussion #137) ### Added - **A proxy host can require a client certificate.** *Proxy Hosts → edit → Security* gains **Client certificate authentication (mTLS)**: a mode and a list of trusted CA certificate files. Caddy then asks the visitor's browser or client for a certificate during the TLS handshake and refuses the connection unless it checks out. The four modes are Caddy's own — *Require and verify* (the usual meaning of mTLS), *Verify if given* (optional certificate, validated when sent), and *Require* / *Request*, which accept any certificate the client offers for cases where something behind Caddy does the checking. - The trusted CA files are read **by Caddy, inside the Caddy container**, exactly like certificate files are — so the path is the one Caddy sees, and the file does not need mounting into CaddyUI. - Your backend reads the certificate through the usual placeholders. For EJBCA and friends, add `SSL_CLIENT_CERT: {http.request.tls.client.certificate_der_base64}` under *Custom headers* on the same page. - Verified end to end against Caddy 2.11.7 with a throwaway CA: no certificate is refused with *certificate required*, a certificate signed by the trusted CA gets through, and a self-signed one is refused with *unknown ca*. - **A verifying mode with no trusted CA is refused at save time.** Caddy accepts that combination and then turns away every client, because there is nothing to verify against — the save now explains this instead, and suggests *Request* / *Require* for the no-verification case. - **`client_auth_mode` and `client_auth_ca_files` on the REST API** (`/api/v1/proxy-hosts`), with the same validation. On `PATCH`, send `"client_auth_mode": "off"` to switch mTLS off — an empty string leaves it unchanged, as it does for every other field there. - **Export to Caddyfile writes the `tls { client_auth { … } }` block.** Leaving it out would make an exported site look open to anyone. ### Fixed - **An Advanced route carrying `tls { client_auth … }` no longer discards it in silence.** This is what prompted the discussion: Caddy keeps client authentication in the server's TLS connection policies, not in the route, and an Advanced route stores routes only — so the block vanished during import, the generated JSON contained the `reverse_proxy` headers but no `client_auth`, and the site came up serving everyone while looking configured. Importing such a block is now refused, naming the hostnames affected and pointing at the proxy-host setting above, which does reach Caddy. ### Notes - Existing hosts are unaffected: the new mode defaults to off, so nothing starts asking for client certificates after the upgrade. - Not to be confused with **Upstream mTLS Client Certificate / Key** further down the same form — those are Caddy presenting a certificate *to your backend*, the opposite direction. Both can be used at once. --- ## [2.63.2] - 2026-10-10 - IP allowlists work behind a load balancer (#129) ### Fixed - **IP allowlists and blocklists now honour Settings › Security → Trusted proxy IPs/CIDRs.** Every IP check CaddyUI generates matched Caddy's `remote_ip`, which is always the address of the machine that connects to Caddy. Behind a load balancer, reverse proxy or CDN, that is the balancer, never the visitor, so an allowlist either let everyone in (balancer inside the list) or locked everyone out (balancer outside it), whatever the trusted-proxy setting said. The checks now use Caddy's `client_ip`: - **No trusted proxies configured:** `client_ip` is exactly the connection's address, so nothing changes and a forged `X-Forwarded-For` header is still ignored. - **Trusted proxies configured:** the visitor's real address is taken from `X-Forwarded-For` (or the configured *Client IP headers*), but only on connections that come from a trusted proxy. - This covers proxy-host and redirection allowlists, IP blocklists, *Block private IPs*, the global blocklist and the maintenance-mode bypass list. Verified on Caddy 2.11.7 through a load balancer: with the balancer trusted, an allowed visitor gets through, an outside visitor gets 403, and a forged header sent straight to Caddy is refused. - The allowlist and trusted-proxy fields explain this, including that anything listed as a trusted proxy can claim any client address. ### Notes - **Behind a load balancer?** Add its address under **Settings › Security → Trusted proxy IPs/CIDRs**. If it forwards TCP/TLS without HTTP (passthrough), it cannot pass the visitor's address in a header, and IP allowlists cannot tell visitors apart. - `client_ip` needs **Caddy 2.7 or newer** (the `applegater/caddyui-caddy` image ships 2.11.7). --- ## [2.63.1] - 2026-10-10 - Simplified Chinese covers the Dashboard, Profile and Settings › General (#131–#135) ### Added - **The Simplified Chinese (`zh-CN`) translation now covers everything on the catalog**, contributed again by @chongfenglaosiji in five pull requests: Settings › General (#131), the Settings navigation, post-apply checks and colour themes (#132), the Dashboard (#133), Profile (#134), and the health-dot tooltips and shared status words (#135). Every one of the 313 keys is translated, so these pages are fully Chinese with no English left over. The *Follow each browser* option was also reworded slightly. Thank you! --- ## [2.63.0] - 2026-10-10 - Dashboard, Profile and Settings › General can be translated (#128) ### Added - **250 more interface strings are on the translation catalog**, so a translation can now cover the whole **Dashboard** (banners, server and route summary, traffic, system health, *Needs attention*, the fleet cards, recent activity and the proxy-host / advanced-route tables, including the health-dot tooltips drawn by its script), the whole **Profile** page (including its saved/error messages), the **Settings navigation** (every section's name and description, the page heading and the *Save … settings* button) and the full **Settings › General** page (general options, timezone and post-apply checks). English output is unchanged. - **Messages built on the server can be translated too** — the Profile page's confirmations and errors are now looked up in the reader's language before the redirect. - Shared status words (*Online*, *Offline*, *Enabled*, *Disabled*, …) have their own `status.*` keys, ready for the list pages to reuse. ### Notes - Translations simply show English for the new keys until they are updated — nothing breaks in the meantime. The remaining Settings pages, the list pages and the forms follow in the next batches; the large proxy-host form comes last. - A new test keeps the Settings navigation (defined in Go) and its English catalog text identical, and another renders the Dashboard, Profile and Settings › General pages in a second language. --- ## [2.62.2] - 2026-10-10 - The default language setting now takes effect ### Fixed - **Settings → General → Default language did nothing for most people.** The language was chosen as: your own profile choice, then the **browser's** language, then the site default — and since every English-language browser matches the English catalog, the site default was never reached. Choosing Simplified Chinese as the default visibly changed nothing. The order is now: your own **Profile → Language** choice, then the **site default** when an administrator has set one, then the browser, then English. Leaving the default on *Follow each browser* keeps the old browser-based behaviour. The setting's help text says so (in English and Chinese). --- ## [2.62.1] - 2026-10-10 - Simplified Chinese (#130) ### Added - **Simplified Chinese (`zh-CN`) interface translation**, contributed by @chongfenglaosiji (#130) — every key currently in the catalog: the navigation and its sections, the top bar and *Create* menu, the user menu, the command-palette search, the list-page titles and the language settings. Pick it under **Profile → Language**; browsers that ask for Chinese (`zh`, `zh-CN`, `zh-TW`, …) get it automatically. Parts of the interface not yet moved onto the catalog stay in English and will follow release by release. Thank you! --- ## [2.62.0] - 2026-10-09 - Interface languages: the localization plumbing (#128) Requested by @chongfenglaosiji, who has offered to translate CaddyUI into Simplified Chinese. ### Added - **CaddyUI can be shown in other languages.** Translations are flat JSON catalogs in `web/i18n/` — one per language, named by its code (`en.json`, `zh-CN.json`, …) and embedded in the binary — with stable dotted keys such as `nav.proxy_hosts`. **English is the source of truth and the fallback**: any key a translation lacks (or leaves empty) shows in English, so a partial translation never breaks a page, and a key missing everywhere shows as itself rather than blank. Texts can carry named placeholders like `{count}`. - **Each person chooses their own language** under **Profile → Language**, available to every role; the default is *Use my browser's language*. Otherwise the browser's `Accept-Language` decides (a related variant such as `zh-TW` or plain `zh` uses the closest available catalog), then **Settings → General → Default language**, then English. Only languages that have a catalog can be chosen. The page's `` follows the choice. - **Templates and scripts share one catalog**: templates use `{{t $ "key"}}`, and keys starting with `js.` reach the browser as `window.CADDYUI_I18N` with a small `caddyuiT(key, vars)` helper. - **Guard rails, as tests:** every key a template or script uses must exist in `en.json`, and every translation may only use English's keys and must keep the same placeholders — so a typo or drift fails the build instead of showing a raw key. - **First strings on the catalog:** the navigation and its section titles, the top bar, the *Create* menu, the user menu, the command-palette search, the list-page titles for proxy hosts, redirections, advanced routes and certificates, and the new language settings. English output is unchanged. The rest of the interface moves onto keys over the next releases, the large proxy-host form last. - **[docs/TRANSLATING.md](docs/TRANSLATING.md)** explains the format and how to send a translation as a data-only pull request. ### Notes - This release ships English only; the first translation (Simplified Chinese) is coming from the community. New column `users.locale` and setting `default_locale` are created automatically. --- ## [2.61.3] - 2026-10-09 - Security: a wildcard host could bypass a specific host's IP allowlist (#129) Reported by @hieronymousch: after upgrading, sites with an IP allowlist accepted every client, and attaching a middleware profile made no difference. ### Security - **A wildcard proxy host could take over a more specific host's traffic, skipping its IP allowlist and every other protection on that host's route.** Caddy runs the first route that matches a request. CaddyUI emitted host routes in list order — manual sort order, then newest first — so a wildcard host such as `*.example.com` created *after* `app.example.com` came first, and requests for `app.example.com` were served by the wildcard's route. Everything configured on `app.example.com` itself was then skipped: its **IP allowlist**, Basic Auth, forward auth, the middleware profile attached to it and its WAF. Reproduced on a real Caddy: a client outside `app.example.com`'s allowlist got `200` instead of `403`. - **Fix:** host routes are now ordered by specificity, the way Caddy's own Caddyfile does it: exact hostnames before wildcards, more specific wildcards (`*.a.example.com`) before broader ones (`*.example.com`), and a path-scoped route before the same host's catch-all. A route that also carries a wildcard counts as a wildcard route. Applied to the HTTPS server, the plain-HTTP server (where a forced host's HTTPS redirect now always beats a wildcard host) and the managed wildcard-certificate routes. Only routes that match on hostnames move, and only among themselves: global maintenance, an Advanced route without a host matcher and the fallback keep their exact positions, and the manual order still decides between routes that are equally specific. After the fix the same request gets `403`, and other names still reach the wildcard host. - **Upgrade if you have both wildcard and specific proxy hosts**, especially when the specific ones rely on an IP allowlist, Basic Auth, forward auth or a WAF profile. The fix applies on the next sync (a restart with sync-on-start, or any save). --- ## [2.61.2] - 2026-10-08 - Certificate export: a permission problem is reported as one (discussion #127) Reported by @devacc337: *Export failed — Caddy has not stored a certificate for \*.domain.com yet (looked in /caddy-data)*, with the certificate plainly present in the volume. ### Fixed - **A permission error was reported as a missing certificate.** Caddy keeps its data directory private to the user it runs as — mode `0700`, owned by root in the official image — while the CaddyUI container runs as an unprivileged user (uid 10001), so it could not even list `caddy/certificates`. The lookup swallowed that "permission denied" and fell through to *"Caddy has not stored a certificate … yet"* (with `looked in /caddy-data` and no sign of the `certificates` directory — the tell-tale), sending people to look for a certificate that was sitting right there. Reproduced as uid 10001 against a root-only tree; the same lookup succeeds as root. The error now says what is wrong: *CaddyUI is not allowed to read Caddy's certificate storage (permission denied on …), CaddyUI runs as uid N — run the caddyui container as that user, with the official images `user: "0:0"`, the data volume can stay read-only.* A certificate that really is missing still reports as missing. The same lookup serves *Sync from current* for shared managed certificates, which gets the clearer message too. - **Export-directory write failures get the same treatment.** A permission error creating or writing the export directory (a fresh Docker volume is root-owned, and CaddyUI runs unprivileged) now says CaddyUI is not allowed to write there and how to fix it. ### Docs - To use **Export to a directory**, run the `caddyui` service as root — `user: "0:0"` — so it can read Caddy's private data volume (mount it read-only) and write to a fresh export volume. Added to the example `docker-compose.yml` (commented, next to the export mounts), the Data directory help on Caddy Fleet → edit server, and the Documentation page. --- ## [2.61.1] - 2026-10-08 - The Caddy `command` in the example compose files works now (#7) Reported again on #7 by @devacc337. ### Fixed - **The example `command` for the Caddy container crashed it.** The snippet added for #7 (`command: >- mkdir -p /config/caddy; [ -f … ] || echo '{}' > …; exec caddy run …`) is not run through a shell, so Compose started `mkdir` on its own and the container exited immediately with code 0 — with `restart: unless-stopped`, a restart loop, and Caddy never started. (Reproduced with `docker compose` on a clean volume.) It is now `command: [/bin/sh, -c, "…"]`, which seeds an empty `{}` `autosave.json` on first boot and leaves an existing one alone, then resumes from it; the same container starts and resumes normally. Fixed in `docker-compose.yml`, the README (both copies), `DOCKERHUB.md` and `docs/dockerhub.md`. - **If you copied the old snippet** since it was published, replace the `command:` block of your `caddy` service with the new one and run `docker compose up -d --force-recreate caddy`. Setups that mount their own `autosave.json`, or that never used this snippet, are unaffected. - The README note now explains why the shell wrapper is needed. No application code changed. --- ## [2.61.0] - 2026-10-07 - Coraza WAF in middleware profiles, and Layer4 on a shared port (#126) ### Added - **Coraza web application firewall in middleware profiles.** A profile can now enable [Coraza](https://coraza.io) with the OWASP Core Rule Set for every host that uses it: **Detection only** (logs what it would block — visible under Server Logs — and blocks nothing; the recommended first step) or **Block** (answers 403), a **paranoia level** from 1 to 4, a switch for the Core Rule Set itself, and **custom SecLang directives** applied last (for example `SecRuleRemoveById 942100` to silence one rule). The WAF runs first in the host's route, ahead of redirects, forward auth and the upstream, and is added when the config is built — the host row never stores it, and detaching the profile removes it. Verified on a real Coraza build of Caddy 2.11.7: SQL injection and XSS probes got `403` in Block mode, passed (and were logged) in Detection mode, and a host without the profile was untouched. Hosts that share a profile share one rule set (memory stayed flat from 1 to 10 hosts). - **`applegater/caddyui-caddy` now includes the Coraza module** (`github.com/corazawaf/coraza-caddy/v2` v2.6.x, `http.handlers.waf`) alongside the DNS providers, CrowdSec bouncer, rate limiting and `caddy-l4`. The image is about 5 MB larger. Existing installs keep working unchanged; **to use the WAF, pull the new image and recreate your Caddy** (the module only exists in the new build, so that is a brief restart of Caddy itself). - **A Caddy without the module is refused up front.** Saving a WAF profile, editing one that is in use, or attaching one to a host first asks every affected server's Caddy whether it has the module (through `/adapt`, which applies nothing) and names the ones that do not, instead of letting Caddy reject that server's entire config and block every later sync. If a rejection still happens, the error now says what to do. - **Layer4 on a shared port (#126).** A Layer4 proxy can ride a port that is already in use instead of binding its own — for example SSH for `ssh.example.com` over `:443` beside normal HTTPS. Pick *Share the HTTPS / HTTP port* on the Layer4 proxy form, then what to take: connections with given **TLS SNI** names (HTTPS port) or **HTTP hostnames** (HTTP port), or an **SSH**, **RDP** or **PostgreSQL** connection. CaddyUI writes a `caddy-l4` **listener wrapper** on the HTTPS (`srv0`) or HTTP (`caddyui_http`) server; everything that matches no shared proxy carries on to Caddy's normal TLS and HTTP handling. An SNI route can pass the TLS connection through to the upstream (default) or terminate TLS in Caddy and send plain TCP. TCP only — wrapping UDP is not offered. Shared proxies deploy to fleet targets, follow automatic deployment targets and the node-local switch like any other Layer4 proxy. ### Changed - The wrapper is written in the order Caddy needs: any wrapper you configured yourself (PROXY protocol, for example) first, then CaddyUI's layer4 wrapper, then an explicit `tls` wrapper. Found on a real Caddy: with only the layer4 wrapper listed Caddy applies TLS *first*, so the matchers saw decrypted garbage. CaddyUI only ever replaces or removes the layer4 wrapper it wrote itself; a layer4 wrapper set any other way is left alone. - The Layer4 form's error for ports 80/443 now points at sharing. - `Client.Validate`'s documentation was wrong: Caddy ignores `validate_only`, so a config it accepts is also applied. The comment now says so; behaviour is unchanged. ### Notes - Terminating TLS for a shared SNI route needs a certificate Caddy already holds for that hostname (a proxy host or managed certificate for the same name). - The `caddy-l4` wrapper makes Caddy log `tls is enabled, but listener wrapper returns a connection that doesn't implement connectionStater` once per connection; HTTPS and HTTP/2 were unaffected in testing. - Profiles cannot yet be set through the REST API. --- ## [2.60.0] - 2026-10-07 - Middleware profiles (#124) and a configurable fallback status (#123) Two features requested by @kernaxis (#124, also filed as #125) and @tkkost (#123). ### Added - **Middleware profiles** — a new admin-only **Middleware Profiles** page. A profile bundles HTTP policy that many hosts share: - **Security headers** — the standard bundle (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy) plus X-Frame-Options, Referrer-Policy, Permissions-Policy and Content-Security-Policy values. - **Forward auth** — auth URL, method, copied response headers, header prefix and skip paths (works with the v2.59.5 generator). - **IP restrictions** — an allowlist and a blocklist (IP addresses or CIDR ranges, IPv6 included). - **Extra headers** — request and response header maps. Pick a profile on a proxy host (the **Middleware profile** selector; step 3 of the guided publish flow, always visible in the advanced editor). **A value set on the host itself wins** over the profile; empty host fields inherit it; forward auth is taken as one unit (a host with its own auth URL keeps only its own settings); the two header maps merge by name with the host winning. The profile is applied when the Caddy config is built — **the host row is never rewritten**, so detaching a profile restores exactly what the host had. - **Editing a profile re-syncs every server that has a host using it**, so a change reaches Caddy without re-saving each host. Profiles are global to the controller, so hosts on different fleet servers share one definition; a host deployed to a fleet target carries its profile with it, and changing or removing it on the source updates the copy. - A profile that is still attached to hosts **cannot be deleted** (the page says how many); deleting a host releases it. Cloning a host keeps its profile. Profile names are unique; entries are validated on save (URLs, CIDRs, header names and values). - **Configurable fallback status** — **Settings → General → Fallback status code** (`400`–`599`, default `404`, blank = default). It is answered for a request that matches no proxy host, redirection or Advanced Route, on the HTTP **and** HTTPS servers and on the managed wildcard-certificate route, so every kind of unknown host behaves the same. *Custom 404 page HTML* is renamed **Custom fallback page HTML** and is sent with the chosen status (the stored setting is unchanged, so existing pages keep working). ### Notes - Profiles are managed by administrators; any account that can edit a host can attach one. For a non-admin's host, the normal upstream guard still applies to the resulting route (for example a profile's forward-auth upstream may not point at a blocked address). - Not part of this first version: WAF / Coraza (needs a Caddy module that the published image does not include), and setting a profile through the REST API (UI only). - New tables `middleware_profiles` and `proxy_host_profiles` (created automatically on SQLite and MariaDB); nothing changes for a host until you attach a profile. --- ## [2.59.5] - 2026-10-07 - Forward Auth works now: `unknown module: http.handlers.forward_auth` (#124, #125) Reported by @kernaxis while asking for reusable middleware profiles (#124, duplicated as #125): saving a Proxy Host with a Forward Auth URL failed with `unknown module: http.handlers.forward_auth`. ### Fixed - **Forward Auth never worked.** `forward_auth` is a Caddyfile *directive*, not a JSON handler module, and CaddyUI has emitted `{"handler":"forward_auth"}` since v2.9.27, so Caddy rejected the whole config for any host that used it. CaddyUI now generates exactly what the directive expands to (checked against Caddy's own `/adapt` output): a `reverse_proxy` to the auth service that rewrites the method and URI, passes `X-Forwarded-Method` / `X-Forwarded-Uri` and the original `Host`, and — on a 2xx answer only — copies the configured response headers onto the request that continues to the real upstream. A non-2xx answer (401/403/redirect to a login page) is relayed to the client by reverse_proxy's own behaviour. - **Copied headers cannot be spoofed.** Each copied header is deleted from the incoming request before the auth service's value is applied, and is only set when the auth service actually sent it, so a client can never supply `X-Authentik-Username` (or any other copied header) itself. - The existing options now do what their labels say: *Forward Auth Method*, *Copy auth headers*, *Headers prefix* (applied to the header name placed on the upstream request) and *Skip paths*. - The Forward Auth URL is validated when you save: it must be `http://` or `https://` with a host (for example `http://authentik:9000/outpost.goauthentik.io/auth/caddy`). If a stored URL is ever unusable, the host answers `503` instead of being served without authentication (fail-closed). - Because the auth service is now a real upstream, a non-admin account can no longer point Forward Auth at an address the upstream guard blocks (loopback, link-local/metadata, the Caddy admin API). - Verified end to end on a real Caddy 2.11 with a fake auth service: no cookie → `401` with the auth service's body; valid cookie → `200` and the upstream receives the copied header; a client-supplied copy of the header without valid auth → `401`; a skipped path reaches the upstream without an auth call; a prefix renames the copied header. ### Notes - Upgrading fixes existing hosts on the next sync (restart with sync-on-start, or any save). - Reusable Forward Auth / middleware profiles (#124) are a feature request and are tracked for the next feature release. --- ## [2.59.4] - 2026-10-07 - Unknown hosts get a 404 instead of an empty 200 (#123) Reported by @tkkost: a request for a hostname that matches no Proxy Host, Redirection or Advanced Route got an empty `200 OK` over HTTP, while hosts under a managed wildcard certificate got a 404 over HTTPS. ### Fixed - **Requests that match no host now always get `404 Not Found`, on both the HTTP (`:80`) and HTTPS (`:443`) server.** CaddyUI only generated a fallback route when custom 404 page HTML had been saved in Settings; otherwise Caddy's default for "no route matched" — an empty 200 — applied (reproduced on a real Caddy 2.11: unknown host → `200`, `Content-Length: 0`, on both listeners; the HTTPS 404 reported came from the managed-certificate route, which exists only on `:443` for the wildcard's own hosts). The fallback is now generated whenever the server has routes: the custom HTML when configured (unchanged: status 404), otherwise Caddy's error handler, which shows CaddyUI's branded 404 page on HTTPS and a plain empty 404 on `:80`. - The fallback is always the **last** route. On `:80` it sits after the HTTPS-redirect route, so a Force-SSL host is still redirected rather than answered with the 404 — checked on a real Caddy together with an unknown host and a normal proxied host. - A server with no routes at all is left alone as before: no fallback is added and CaddyUI does not take over an empty listener. ### Notes - This is a visible behaviour change: anything that relied on `http:///` or an unknown hostname answering `200` (an uptime check pointed at the bare IP, for example) now sees `404`. Point such checks at a real host, or add a host for them. - A configurable fallback status code and a generalised "custom fallback page" are tracked separately for the next feature release. --- ## [2.59.3] - 2026-10-07 - HTTP-only Advanced routes stay off HTTPS, and failed delete calls fixed (#121) Reported by @tkkost: an Advanced Route written as `http://127.0.0.1 { respond /health "OK" 200 }` was also generated on the HTTPS listener, so Caddy served it over HTTPS and issued an internal certificate for `127.0.0.1`, which then showed up as a "Direct certificate" in the certificates view. ### Fixed - **A route written with explicit `http://` addresses is now generated only on the plain `:80` server.** v2.58 stopped such a route from asking for a certificate or forcing HTTPS on save, but CaddyUI still copied it onto `:443` (every non-forced Advanced Route goes on both listeners). The HTTPS side — its routes, certificate skip list and DNS-01 certificate policies — now leaves them out, so the route is not reachable over HTTPS and Caddy no longer issues a certificate for its host. This is applied when the config is built, so existing routes are corrected by the next sync (a restart with sync-on-start, or any save) with no migration. Verified on a real Caddy: the route appears on `caddyui_http` only and no TLS policy mentions the host. - An `http://` route that an older save left with **Force SSL** on is no longer turned into a redirect to HTTPS on `:80`; it is served as written. - **Three delete calls to Caddy's admin API were sent to `/config/config/...`**, which real Caddy answers with HTTP 500 ("invalid traversal path"). They were the removal of CaddyUI's `:80` server when no `:80` routes remain, the removal of a stale Advanced-route server that has its own port, and clearing the HTTP/3 restriction when it is switched off. Depending on the call that failed a sync or was silently skipped. Fixed once, in the client, which now accepts the path with or without the leading `/config`. ### Notes - If one Advanced Route's Caddyfile mixes `http://` and `https://` (or plain) site addresses it is not treated as HTTP-only and stays on both listeners; put the `http://` sites in their own route. - A certificate Caddy already issued for such a host stays in Caddy's storage (and in the certificates view) until it is removed or expires; no new ones are requested. --- ## [2.59.2] - 2026-10-06 - Code-scanning clean-up (no behaviour change) ### Security - Two CodeQL `go/clear-text-logging` alerts (high) are closed. Two log lines — the import path skipping a proxy host the non-admin upstream guard refuses, and the sync-time tenant route guard skipping a non-admin-owned route — included a reason string that is derived from a generated route, and CodeQL traced a proxy host's API-key value into it. The reasons only ever contained handler names, upstream addresses or placeholder text, never the stored key, so nothing was leaked; but they no longer appear in the log. The log now says the route was refused and names the resource; the full reason is still shown as a form error when the owner saves the resource. No behaviour change. --- ## [2.59.1] - 2026-10-06 - Deleting the last resource now clears the live Caddy (#120) Reported by @tkkost right after v2.59.0: when deletion propagation removed the **last** host from a target, the resource disappeared from CaddyUI but its route stayed live in that Caddy. ### Fixed - **The last proxy host, redirection or advanced route on a server was never removed from the live Caddy.** CaddyUI refuses to push a configuration when it holds no entries for a server (so a fresh or wiped database can never blank a live Caddy), and that guard also swallowed the sync after an explicit deletion — the database was empty, Caddy kept serving the old route. Reproduced on a real Caddy 2: after deleting the only host, a normal sync left both routes live. Deletions now sync with an explicit "empty is intended" switch, so the live config converges to the database. This applies to the target of a **propagated deletion** (v2.59.0) and equally to deleting the last resource by hand — the delete buttons, the bulk-delete actions and the REST API. - The guard itself is unchanged: ordinary syncs (startup sync, auto-sync, any save) still refuse to push when there are no entries, and a regression test keeps it that way. - The server form no longer says a target left empty keeps its last live routes. --- ## [2.59.0] - 2026-10-06 - Opt-in deletion propagation to automatic deployment targets (#120) Requested by @tkkost as a follow-up to v2.57.0: let the source server act as the desired state for its **Automatic deployment targets**, deletions included. ### Added - **Propagate deletions to automatic deployment targets** — a checkbox in the *Automatic deployment targets* section of a Caddy Fleet entry (**off by default**; nothing changes until an administrator ticks it). When on, deleting a **proxy host**, **redirection**, **advanced route** or **Layer4 proxy** on that server deletes the paired copies CaddyUI previously deployed from it to its automatic targets. This covers the single-delete button, the bulk-delete actions and the REST API. - **It deletes only what it deployed.** Target-local resources, rows created independently on a target (that were never paired), copies on one-off **Also deploy to** servers that are not automatic targets, and a target row that another source resource is still paired with are all left alone. The deletion removes the copy from the target the same way deleting it there by hand would (including its managed DNS record) and then re-syncs that target's Caddy. - **Unreachable targets converge later.** If a target's Caddy can't be reached (or its sync is held), the deletion is kept as a *pending deletion* — the pairing is not forgotten — and retried every minute in the background until the target has converged. Pending deletions are listed on the source server's edit page with the attempt count and last error, **Retry now**, and **Discard** (admin-only; for a target that is gone for good). They are also recorded in the Activity Log. - New table `fleet_pending_deletions` and column `caddy_servers.propagate_deletions` (created automatically on SQLite and MariaDB); deleting a server drops its pending entries. ### Changed - Deleting a source resource now always forgets that resource's pairings (previously they lingered, so a reused row ID could inherit one), whether or not propagation is on. The Layer4 delete confirmation only says "copies were not removed" when propagation is off. ### Notes - A copy that was *adopted* by an earlier deployment (a target row with the same identity that the deployment then overwrote) counts as paired and is deleted too. - Certificates keep their own per-certificate targets and are not part of this. - As with any CaddyUI sync, a target left with **no** hosts at all is not pushed an empty config, so its last live routes stay until something else is configured there. --- ## [2.58.0] - 2026-10-06 - Layer4 Proxies: a dedicated section for TCP/UDP proxies (#122) Suggested and thoroughly tested by @tkkost. Until now Layer4 meant pasting a raw `layer4 { ... }` block into a server's edit form. Layer4 proxies are now first-class resources, managed like Proxy Hosts. ### Added - **Layer4 Proxies** (new sidebar entry, admin-only). Each proxy has a **name**, **protocol** (TCP or UDP), **listen address** (optional) and **port**, **upstream host** and **port**, an **enabled** switch, a **node-local** switch and notes. CaddyUI generates the `caddy-l4` configuration (`apps.layer4.servers.caddyui_l4_`) and pushes it to the server selected in the top bar; per-proxy create, edit, enable/disable and delete. A Caddy that lacks the `caddy-l4` module rejects the config at validation time, so a proxy it cannot run is never saved — use `applegater/caddyui-caddy`, which includes the module. - **Fleet behaviour like Proxy Hosts.** A saved proxy (and a quick enable/disable) is mirrored to the source server's **Automatic deployment targets** plus any one-off **Also deploy to** picks. A **node-local** proxy never leaves its server. Copies are idempotent and tracked in the same mapping table as hosts; a target that already has a proxy on the same protocol and listen socket adopts it instead of duplicating. **Sync from current** now carries Layer4 proxies too. As with hosts, **deleting a proxy does not delete its copies** on other servers. - Safety checks: ports 80, 443 and 2019 are refused (they belong to Caddy), the same protocol + socket cannot be used twice on a server, hostnames are validated, and `{env.}` / `{file.}` style placeholders are not accepted as upstreams. TCP and UDP can share a port number. ### Compatibility - The raw **Layer4 app** Caddyfile editor is unchanged and stays under **Caddy Fleet → edit server** (the new page links to it as the Advanced / raw mode). Both feed the same `apps.layer4`: CaddyUI merges the managed `caddyui_l4_*` servers into whatever the raw block produces, so existing raw configurations keep working untouched. - Removing the last proxy (or disabling every one) on a server clears the generated servers from Caddy on the next sync. - New table `layer4_proxies` (created automatically on SQLite and MariaDB); deleting a server also deletes its proxies. ### Fixed - The test fixture for the Caddy admin API matched the layer4 `DELETE` on the wrong path, so clearing `apps.layer4` was never exercised; the real path is now covered. --- ## [2.57.1] - 2026-10-06 - Security hardening release, Caddy 2.11.7, and #121 A full security audit — independent reviews of authentication and authorization, of injection / SSRF / file access, and of secrets and data exposure, plus a manual review of the paths that turn user input into Caddy config — found a number of access-control and isolation gaps. **None was externally reported**; each was reproduced against the real router before being fixed, and every fix ships with a regression test that fails on the previous code. Most of them need a signed-in account below admin, so a single-operator install with no extra users is much less exposed than a multi-user one — but anyone running **user** or **view** accounts should upgrade. Admin behaviour is unchanged. ### Security **Access control** - **Any role could read the live Caddy config, which contains certificate private keys and DNS API tokens.** `/caddy-config`, Import from Caddy (`/import`) and the snapshot list / download sat in the read-for-any-role group, so a read-only viewer could read every uploaded certificate's private key and the DNS provider tokens. They, with **snapshot upload / restore / delete** and the auto-snapshot setting (a customer-level user could upload arbitrary JSON and restore it to take over Caddy) and the Porkbun certificate import (which fetched private keys for any domain on the admin's Porkbun account), are now **admin-only**. - **A customer-level `user` account could download the whole database** (`/backup`: password hashes, TOTP secrets, every stored credential). It sat in the `requireWrite` group, which blocks only read-only viewers. Now admin-only. - **`GET /api/v1/certificates` returned `key_pem` for every shared certificate to every role**, including read-only API tokens. Private keys and file paths are now returned only to a browser session or full-scope token of someone who can manage that certificate. - **Missing per-row ownership checks** on the single-host export (`export.json` / `export.caddyfile`, which carry upstream basic-auth and API-key secrets), maintenance toggle, health page and expectation runner, and on certificate inspect. Any account could read or change another tenant's host by ID. - **API token scope was enforced only inside one route group**, so a `proxy_write` token could create a full-scope token, create an admin account and change settings. Scope is now enforced for every route, and API tokens cannot manage API tokens. - **A customer's fleet copy could overwrite another tenant's (or the admin's) host on a target server** by saving a host with the same domain and ticking *Also deploy to* — it adopted the unrelated row and reset its owner. A copy of a tenant-owned row now only updates the row CaddyUI created for it. Certificate export to a directory, certificate source-push and copying certificates between servers are admin-only. - **REST create/update and the JSON import skipped the duplicate-domain check** the UI enforces (a duplicate sorts first and hijacks the existing host's traffic). The import also trusted uploaded DNS record / zone / certificate ids — a crafted record id made deleting the host delete that DNS record with the admin's credentials. A non-admin can no longer reference another tenant's private certificate id. - Low-privilege roles saw other tenants' data: live traffic and its stream, the activity log and its CSV, the dashboard's recently-edited widget, every node's admin URL (`/api/v1/servers`) and the upstream list. All are now scoped to what the user may see. Helper endpoints that fetch or adapt user-chosen input (`test-upstream`, `preview`, `validate`) and the AI chat (which spends the admin's LLM key) now need a write-capable account. **Config injection by non-admin users (the "tenant route guard")** - **`{env.NAME}` and `{file./path}` in any tenant-controlled string are expanded by Caddy at request time** (verified on a real Caddy 2.11): a custom header, redirect target or static-response body could serve the Caddy process's environment — the shipped compose puts the DNS API token there — or any file the Caddy container can read, including its certificate storage. Caddyfile `{$ENV}` does the same while adapting, and an `import /path` reads files (a parse error echoes its first token). - Existing guards covered individual fields on individual paths, and each audit found another miss (Advanced routes, additional upstream rules, forward-proxy / forward-auth URLs, the redirection advanced JSON). A single guard now inspects the **generated Caddy route** for every non-admin-owned resource, at save time (clear error) and again when the config is built (enforcement for rows already stored, or saved by any path — UI, REST, import, AI tool, clone): blocked upstreams, `file_server` / `templates` / `acme_server`, dynamic upstreams and process-reading placeholders are refused or neutralised. Caddyfile text from a non-admin is refused *before* it is sent to Caddy. The upstream guard also now understands `tcp/` prefixes, unix sockets, placeholders and integer / hex IP forms, blocks more cloud-metadata addresses, and treats **every registered node's** admin API as off limits, not just the primary's. - CaddyUI's own probes build URLs from a host's first domain and a custom health check: a customer could set the domain to `10.8.0.2:2019` with a custom `POST` check and have CaddyUI send it to that address every minute. Non-admin domains must now be plain hostnames, custom checks are GET / HEAD only, and the direct upstream probe no longer follows redirects. **Authentication** - **The login lockout never fired**: it counted a column the failed-login row doesn't write to. `max_login_attempts` now works. Wrong two-factor codes are limited (5 per login attempt, and an account with 10 recent failures cannot start a new second-factor step — keyed on the account, so a shared proxy address cannot lock everyone out). - A password change or reset now signs the account out everywhere (the profile change keeps only the current browser); disabling 2FA or regenerating backup codes needs the current password; first-run setup is serialized (two simultaneous requests could create two admins) and enforces the 8-character minimum server-side. - Password-reset and invitation links were built from the request's `Host` header, so an unauthenticated request with a forged Host got a working reset link mailed to the victim. Links now use **`CADDYUI_PUBLIC_URL`**, else your OIDC redirect URL's origin; set it to close this completely. **Data exposure, hygiene and stability** - The request log no longer records query strings (reset and invite tokens, a newly created API token, pending 2FA tokens and the OIDC code all travel there); webhook / ntfy URLs and `CADDY_ADMIN_URL` passwords are redacted from other log lines. CSV exports neutralise spreadsheet formulas. Server data written into `innerHTML` is escaped (certificate names and upstream addresses are tenant-controlled). LIKE wildcards in host matching are escaped (a host of `*.%` matched everything). New password hashes use bcrypt cost 12, as `SECURITY.md` always said (existing hashes keep working). - **`syncCaddy` swapped a shared client for its whole duration**, so concurrent syncs of different servers pushed each other's routes — certificate keys and basic-auth hashes included — to the wrong node (52 and 72 cross-pushes in 40 iterations in a reproduction). Syncs are now serialized. ### Fixed - **An explicit `http://` Advanced route no longer needs a certificate or an HTTPS wait** ([issue #121](https://github.com/X4Applegate/caddyui/issues/121)): a route written as `http://127.0.0.1 { … }` now drops its certificate and Force SSL on save, skips the "deploying" page that waited for an ACME certificate that never comes, and the form disables both controls with an explanation. Reported by @tkkost. ### Changed - Bundled **Caddy 2.11.7** (2.11.6 carries security fixes — a `forward_auth` / `reverse_proxy` upstream-connection mix-up (GHSA-6365-7ppr-5r92), path canonicalisation for `handle_path` / `uri strip_*`, `path_regexp` backslash normalisation — and 2.11.7 fixes regressions from 2.11.6). **Go toolchain 1.26.8**, `go-jose` 4.1.5 and other dependency updates; `govulncheck` finds no affected vulnerabilities. If you run your own Caddy, update it too — the image you pull for CaddyUI's Caddy is rebuilt with these. ### Upgrade notes - **Non-admin accounts:** routes that use `{env.…}` / `{file.…}` placeholders, `file_server`, or an internal upstream are no longer deployed (the save is refused with a reason; an already-stored route is skipped or neutralised at sync and logged as `route skipped`). Read-only accounts lose snapshots, the live config view and the AI chat; user accounts lose the pages listed above. Admins are unaffected. - Set `CADDYUI_PUBLIC_URL` if you use password reset or invitations. - Passwords are re-hashed at cost 12 only when next changed. ### Not addressed here (tracked) Admin-reachable and hardening items found by the same audit and deliberately left for a follow-up: the Tailwind CDN script on authenticated pages (and the `unsafe-inline` / `unsafe-eval` policy that follows from it); the unauthenticated log-ingest TCP listener on `:9019` (documented as relying on network isolation); DNS-rebinding of upstream host names (the guard is config-time); OIDC account linking by verified email only (no `iss`+`sub` binding or PKCE); upstream TLS verification being off by default for `https://` upstreams; the SMTP STARTTLS downgrade; rate limiting on `/forgot-password`; request-body size limits on JSON endpoints; an optional first-run setup token; per-tenant isolation of the post-sync "expectations" auto-rollback (one tenant's failing check can roll back the whole server's config); and AI-chat error bodies being read unbounded. --- ## [2.57.0] - 2026-10-06 - Automatic deployment targets for Caddy Fleet servers ### Added - **Persistent default deployment targets** ([issue #120](https://github.com/X4Applegate/caddyui/issues/120)): the per-save **Also deploy to** picker only applied to the save it was ticked on, so a fleet where one Caddy is the configuration source for a fixed set of edge nodes had to re-select the same servers on every create and edit. A Caddy Fleet server's edit form now has an **Automatic deployment targets** section: tick the servers it feeds, and every proxy host, redirection and advanced route created or edited on that server in the web UI — by any user who can save there — is deployed to them on save. It uses exactly the same per-resource deployment as **Also deploy to** — idempotent, hostname changes stay attached to the right copy through the durable source-to-target mapping, target DNS records and custom certificate selections remain server-specific, and a managed wildcard certificate definition that covers the host travels with it — so nothing new has to be learned about how a deployment behaves. On the resource forms the targets appear as always-on rows labelled *default*; the **Also deploy to** picker stays available for one-off extra targets, and marking a resource **node-local** is the per-resource opt-out. Independent of the Layer4 **Also copy to** picker on the same server form, which has its own field. **Off by default and fully additive:** every existing server starts with no targets, so nothing deploys anywhere new until an operator picks some. Deleting a server also removes it from every other server's target list, so a reused ID can never start receiving another server's resources. **Deliberately not included:** deleting a resource does not delete its copies on the targets (propagating deletions is destructive, so it is left for an explicit opt-in); quick enable/disable toggles, bulk actions, Caddyfile and JSON import, and the REST API do not deploy; certificates keep their own per-certificate targets; and resources that already exist are deployed the next time they are saved — for a one-time bulk copy use **Sync from current** on the target. Deployment still happens as part of the save, exactly like **Also deploy to**, so a target that is unreachable can delay a save by up to ~10 seconds (the failure is recorded in the Activity Log); targets run one way, from the source to its edge nodes — a server that already deploys to this one cannot be added as its target, so two servers can never overwrite each other. Regression tests drive the real create and edit handlers for proxy hosts, redirections and advanced routes. Requested by @tkkost. --- ## [2.56.4] - 2026-10-06 - Fix: proxy host Advanced config could not import CADDYFILE_PATH snippets ### Fixed - **`import ` in a proxy host's Advanced config was rejected with `File to import not found`** ([issue #119](https://github.com/X4Applegate/caddyui/issues/119)): snippet definitions — `(my_snippet) { ... }` — in the Caddyfile mounted at `CADDYFILE_PATH` were auto-loaded for Advanced routes and the Caddyfile paste importer, but a proxy host's own **Advanced config** was adapted without them, so a valid snippet in the mounted file could not be imported there and the save was refused with Caddy's error. All three paths now share a single snippet loader, so they can't drift apart again. As before, only `(name) { ... }` blocks are taken from the file — never its global options or site blocks — and a snippet the text defines itself wins over the mounted one. The `CADDYFILE_PATH` rows in the README and Docker Hub docs now describe this instead of calling it a paste-import reference only. Regression tests added (they fail without the fix, with the exact error from the report) and the fix was also confirmed against a real Caddy 2.11.4. Reported by @kernaxis. --- ## [2.56.3] - 2026-10-05 - Multi-arch Docker images (linux/amd64 + linux/arm64) ### Changed - **`applegater/caddyui` and `applegater/caddyui-caddy` are now published as multi-arch images** ([issue #115](https://github.com/X4Applegate/caddyui/issues/115) follow-up): every tag (`:vX.Y.Z`, `:stable`, `:latest`) is now a manifest list covering `linux/amd64` and `linux/arm64`, so `docker pull` on an arm64 host (Raspberry Pi, Ampere, Graviton, Apple Silicon) no longer fails with `no matching manifest for linux/arm64`. Previously only the GitHub release archives had an arm64 build; the Docker images were amd64-only, which blocked arm64 edge nodes in a fleet from running the container at all. The `Dockerfile` and `Dockerfile.caddy` now build on the builder's native platform and cross-compile for the target (`GOOS`/`GOARCH`), so no QEMU emulation is involved and one `docker buildx build --platform linux/amd64,linux/arm64` produces both. The CaddyUI `Dockerfile` now also runs `go test ./web/` before compiling, so a builder that drops embedded `web/static/` assets fails the image build instead of shipping a UI that 404s on its font and stylesheet (issue #37). The old note that multi-arch builds were blocked by a BuildKit embed bug no longer reproduces and has been removed. No change to application behavior; amd64 users are unaffected. --- ## [2.56.2] - 2026-10-05 - Security: closed 4 more SSRF-guard gaps and 2 clone-IDOR gaps ### Security - **Four more proxy-host creation paths skipped the non-admin upstream SSRF guard ([GHSA-r4wm-rgc5-q834](https://github.com/X4Applegate/caddyui/security/advisories/GHSA-r4wm-rgc5-q834) follow-up):** auditing every proxy-host creation path after v2.56.1's AI-exec-tool fix turned up four more that predate `validateProxyUpstreamsForUser` (added in v2.52.2) and were never updated to call it: uploading a JSON file via **Proxy Hosts → Import**, **Import from Caddy** (pulls the live config and classifies it), pasting a Caddyfile block via **Caddyfile Import**, and reclassifying an existing raw route into a proxy host when the classifier improves. All four are reachable by any non-admin write-capable account and could previously create a proxy host pointed at the Caddy admin API, loopback, or link-local/cloud-metadata addresses. Fixed by applying the same guard every REST and form path already enforces. - **Clone endpoints were missing the ownership check every other access to the same resource enforces:** a non-admin account could clone — and thereby read the full configuration of, including upstream and advanced config — any other tenant's or the admin's proxy host or redirection host, simply by guessing its ID in `/proxy-hosts/{id}/clone` or `/redirection-hosts/{id}/clone`. Fixed with the same `canManageOwned` ownership check used on every other access to these resources. Both found during an internal audit, not externally reported. Regression tests added for all six. --- ## [2.56.1] - 2026-10-05 - Security: AI tool-calling endpoint enforced write-role and SSRF guard ### Security - **Read-only viewers could create proxy hosts and redirections via the AI tool-calling endpoint ([GHSA-5h8j-xxm3-7ggr](https://github.com/X4Applegate/caddyui/security/advisories/GHSA-5h8j-xxm3-7ggr)):** `POST /api/ai/exec-tool` was registered outside the `requireWrite`-protected route group that every other mutating route lives in, so an authenticated `viewer`-role account could call it directly and persist a new proxy host or redirection despite being explicitly blocked from every normal write path. The same handler's `create_proxy_host` action also skipped `validateProxyUpstreamsForUser`, the non-admin upstream guard added for GHSA-r4wm-rgc5-q834 — a non-admin account could use the AI tool as a side channel to point a proxy at the Caddy admin API, loopback, or link-local/cloud-metadata addresses even though the normal creation paths already block exactly that. Both gaps are closed: the route now lives in the same `requireWrite` group as every other mutating endpoint, and `create_proxy_host` now runs the same SSRF validation the REST and form paths already enforce. **Anyone running multi-user CaddyUI with `viewer` accounts and the AI assistant enabled should upgrade.** Reported by Sanidi Peiris ([@sanidipeiris-alt](https://github.com/sanidipeiris-alt)) and Sithum Shihara ([@zaara2004](https://github.com/zaara2004)). --- ## [2.56.0] - 2026-10-05 - Layer4 app support for TCP/UDP routing ### Added - **Per-server layer4 app support** ([issue #113](https://github.com/X4Applegate/caddyui/issues/113)): Caddy servers running [`caddy-l4`](https://github.com/mholt/caddy-l4) — TCP/UDP-level routing such as SNI/protocol multiplexing and raw proxying — can now paste a complete `layer4 { ... }` Caddyfile block, including the wrapper, into a new field on the Caddy server edit form. CaddyUI sends it to that server's own admin API `/adapt` endpoint and merges only the resulting `apps.layer4` subtree into the config it pushes on every sync; any other key the paste happens to adapt to is discarded, so pasting something unrelated can't contaminate the rest of the config. The block is checked against the server's own admin API immediately when the form is saved — a typo, or a Caddy build without the `caddy-l4` module, is refused by the pre-save validation with Caddy's own error, instead of silently breaking the server's next scheduled sync. A server dedicated entirely to layer4 routing (no proxy hosts, redirects, raw routes, or certificates of its own) now syncs normally rather than being refused by the "nothing to push" guard. The same text can be copied to other fleet members via an **Also copy to** picker — the same UX family as the existing "Also deploy to" pickers — with each target validated against its own admin API independently, so a target whose Caddy build lacks the module is skipped rather than silently broken on its next sync. This pass intentionally covers layer4 specifically, not general Caddyfile global-options support (snippets, `trusted_proxies`, and the rest) — see the issue thread for why a fully general "paste any global option" box isn't safe on top of CaddyUI's generated server blocks. Empty by default on every existing server, so this is fully additive. Requires a Caddy build with `caddy-l4` compiled in — the published [`applegater/caddyui-caddy`](https://hub.docker.com/r/applegater/caddyui-caddy) image now includes it. Regression tests in `layer4_test.go`. Requested by @BenRLange. --- ## [2.55.0] - 2026-10-05 - Share one managed ACME certificate across fleet nodes ### Added - **Opt-in "source push" fleet distribution for Managed ACME certificates** ([issue #115](https://github.com/X4Applegate/caddyui/issues/115)): the "Also configure on" picker on a Managed ACME certificate previously only cloned the DNS-01 *definition* to each target server, so every target independently obtained and renewed the same certificate — multiplying ACME orders against the CA's rate limits and requiring DNS API credentials on every edge node, for fleets that just want one shared wildcard. A new **"Push this certificate instead of letting each server obtain its own"** toggle (shown once a Managed ACME certificate has at least one target checked) switches selected targets to receiving the source's actual certificate and private key instead: the source server still performs the ACME order, and CaddyUI pushes the resulting PEM to the targets through the existing PEM fleet-copy machinery, so they need no DNS credentials and get no `apps.tls.automation.policies` entry of their own. Renewals are picked up automatically by the certificate lifecycle reconciler (within about an hour) without re-saving the certificate form; a first push attempted before the source has completed its own ACME order is skipped and simply retried on the next pass rather than pushing an empty certificate. Requires the source server's **Data directory** to be set (the same prerequisite as the existing "Export to a directory" feature), since reading a private key needs direct access to Caddy's on-disk certificate storage — a live TLS probe can only ever reveal the public certificate, never the key. **Off by default and fully additive:** every certificate that doesn't explicitly opt in keeps today's independent-ACME behavior with no change. Requested by @tkkost. --- ## [2.54.0] - 2026-10-05 - Configurable ACME DNS-01 resolver ### Added - **Configurable DNS resolver for Caddy's own ACME DNS-01 challenge** ([issue #116](https://github.com/X4Applegate/caddyui/issues/116)): Caddy's DNS-01 solver checks TXT-record propagation using the container's default resolver, which breaks split-horizon DNS setups where the same zone is served privately inside the network and publicly (e.g. via Cloudflare) for ACME validation — the internal resolver returns the private view and the propagation check never sees the record it just created. **Settings → DNS → ACME DNS-01 resolvers** now points Caddy's propagation checks at public or otherwise-reachable resolvers (comma-separated `host:port`, `:53` assumed, IPv6 as `[::1]:53`), independent of the existing "Verification resolver" setting, which only affects CaddyUI's own deploy/readiness checks. Applies to every DNS-01 automation policy — Managed DNS proxy hosts, redirects, raw routes, and managed certificates alike. Leave blank to keep using Caddy's own default resolver. Requested by @tkkost. --- ## [2.53.0] - 2026-09-24 - Security: certificate file reads confined to allowlisted directories ### Security - **Certificate file reads are now confined to allowlisted directories (review finding #8, defence in depth):** `safeAbsolutePath` rejected `..` and required an absolute path, but confined nothing — so an admin-configured certificate or key path could name *any* absolute file the CaddyUI process could read (`/etc/shadow`, another tenant's key), and its contents could surface through Inspect or be inlined into the database by fleet sync. Reads now go through `confinedReadPath`, which additionally requires the file to sit inside an allowed root: the directory holding CaddyUI's database, every Caddy node's mounted **Data directory**, the conventional locations (`/etc/caddy`, `/etc/ssl`, `/etc/letsencrypt`, `/certs`, `/data`), and any directory added in the new setting below. Symlinks are resolved on **both** sides before comparing, and the resolved path is what gets opened, so a symlink planted inside an allowed root cannot step out of it and there is no gap between the check and the read. A root of `/` is dropped rather than honoured, and with no usable roots nothing is readable (fails closed). Requires admin access to exploit, so this is hardening rather than a privilege boundary. Regression tests in `paths_test.go`. **This does not affect certificate serving.** Caddy loads certificate files itself, inside the Caddy container — CaddyUI only reads them as a convenience for the expiry column, Inspect, the probe fallback, and fleet copy-by-value. A certificate outside the allowed roots still serves exactly as before; it simply reports as unreadable in the UI, the same way a path that was never mounted into CaddyUI already does, and fleet sync copies it by path reference instead of inlining it as a PEM. ### Added - **Settings → Security → "Extra certificate directories":** one absolute path per line, for deployments that mount certificates somewhere unconventional. Saved normalised (de-duplicated, unusable entries dropped), and the page lists the roots currently in effect so the confinement is never an invisible DB-only setting. Leave it empty unless CaddyUI reports a certificate it should be able to read. --- ## [2.52.6] - 2026-09-21 - Fix: per-host Basic Auth now enforced + configurable AI chat timeout ### Fixed - **Per-host HTTP Basic Auth was never enforced ([#110](https://github.com/X4Applegate/caddyui/issues/110)):** enabling "Require Login" on a proxy host stored the bcrypt-hashed users and showed them in the form, but `BuildProxyRoute` never emitted Caddy's `authentication`/`http_basic` handler into the route JSON that is pushed to the live server — so requests passed straight through with no login prompt. CaddyUI now renders the `authentication` handler (bcrypt accounts + realm) as the last gate before `reverse_proxy`, so Basic Auth is actually enforced. Half-filled credential rows (blank username or hash) are skipped. **Anyone relying on per-host Basic Auth should upgrade and re-verify their protected hosts prompt for a login.** Reported by @Bappy1988. (The Caddyfile *export* still lists Basic Auth as a note rather than a directive — it is a lossy, non-round-tripping export; the live config is JSON.) ### Added - **Configurable AI assistant request timeout ([#109](https://github.com/X4Applegate/caddyui/issues/109)):** the AI chat turn had a fixed 90-second deadline, which cut off slow local-inference rigs with `context deadline exceeded`. Settings → AI assistant now has a **Request timeout (seconds)** field (default 90, range 5–3600) so users running large local models with heavy CPU offload can give a turn the minutes it needs. Requested by @Bappy1988. --- ## [2.52.5] - 2026-09-21 - Session tokens hashed at rest + access-group collaborative management ### Security - **Session tokens are now stored hashed (defence in depth):** the `sessions` table previously held the raw cookie token, so a database disclosure (backup leak, snapshot, or SQLi elsewhere) yielded directly replayable session cookies. Tokens are now stored as their SHA-256 (`auth.HashSessionToken`), mirroring the already-hashed `api_tokens` table; the raw token lives only in the user's cookie. Regression test in `auth_test.go`. **Upgrade note:** existing sessions are invalidated once — everyone signs in again after updating. ### Changed - **Access-groups now grant collaborative management (review finding #7):** members of a shared access-group could already *see* each other's proxy hosts, redirection hosts, raw routes, and certificates in list views, but opening/editing/toggling/deleting one returned `403`. Group peers can now fully manage resources owned by users they share a group with (admins and direct owners unchanged; global/NULL-owner rows stay admin-only). All single-object REST and HTML handlers, the bulk handlers, and the list-view edit predicates now funnel through one authorization gate (`Server.canManageOwned`, backed by `models.UsersShareGroup`), so the "visible but not manageable" asymmetry is closed and future handlers cannot drift. Regression test in `group_management_test.go`. --- ## [2.52.4] - 2026-09-21 - Security: login rate-limit hardening + server package split ### Security - **Login brute-force limiter no longer bypassable via header spoofing (Medium):** the per-IP failed-login lockout keyed on an IP taken from `X-Real-IP` / `X-Forwarded-For` that was trusted unconditionally, so a directly connected attacker could rotate a forged header per request and never trip `max_login_attempts`. The rate-limit key now comes from `rateLimitClientIP`, which only honours forwarding headers when the immediate peer is a trusted proxy — the admin-configured `trusted_proxies` CIDRs, or (when that setting is empty) loopback/private/link-local peers, i.e. the usual "CaddyUI behind a local reverse proxy" topology. When trusted, `X-Real-IP` wins, otherwise the right-most `X-Forwarded-For` entry (the hop appended by the closest proxy, which a client cannot forge); untrusted peers key on the direct connection address. Backed by a regression test (`ratelimit_ip_test.go`). Activity-log display is unchanged. ### Changed - **`internal/server/server.go` split for maintainability:** the ~17k-line handler file was decomposed (no behavior change) into focused same-package files — `api_v1.go`, `proxy_hosts.go`, `certificates.go`, `raw_routes.go`, `upstream_health.go`, `upstream_health_notifier.go`, and `dns_handlers.go` — dropping `server.go` to ~7k lines. Purely a file reorganization; `go vet`, `gofmt`, and the full test suite pass unchanged. --- ## [2.52.3] - 2026-09-21 - Security: bulk proxy-host ownership + toolchain refresh ### Security - **Bulk proxy-host toggle/maintenance per-row ownership (IDOR, High):** the HTML bulk endpoints `POST /proxy-hosts/bulk-toggle` and `POST /proxy-hosts/bulk-maintenance` did not enforce the per-row `owner_id` check that the single-object handlers and every other bulk handler (delete, certificate, raw-route toggle, redirection toggle) already apply. A non-admin `user`-role account could enable/disable or flip maintenance mode on **any** proxy host — including hosts owned by other tenants or admins — by submitting arbitrary `ids[]`, an availability/tampering vector on other tenants' routing. Both handlers now skip rows the caller does not own (admins remain unrestricted), matching the ownership model introduced in v2.52.1/v2.52.2. Backed by a new regression test (`bulk_ownership_test.go`). **Multi-account deployments should upgrade.** Found during an internal code review. - **Go toolchain pinned to 1.26.6 (native binaries):** added a `toolchain go1.26.6` directive so the standalone release binaries build on Go 1.26.6, clearing 7 reachable Go standard-library advisories present in 1.26.5 (GO-2026-6218 `net/url`, GO-2026-6091 `html/template`, GO-2026-6090 `crypto/tls`, GO-2026-6089 / GO-2026-5026 `net/http`, GO-2026-6088 `encoding/xml`, GO-2026-5972 `encoding/asn1`). `govulncheck ./...` now reports no reachable vulnerabilities. The Docker image already builds on `golang:1.27-alpine` and was unaffected. --- ## [2.52.2] - 2026-09-20 - Security: proxy-host upstream SSRF guard ### Security - **Proxy-host upstream SSRF guard for non-admin accounts** ([GHSA-r4wm-rgc5-q834](https://github.com/X4Applegate/caddyui/security/advisories/GHSA-r4wm-rgc5-q834), **High**): the third, lower-confidence finding noted (but not verified or fixed) in the v2.52.1 advisory is now confirmed and closed. A proxy host's `forward_host`/`forward_port`, its extra upstreams, and its upstream Host-header override flowed unvalidated into Caddy's `reverse_proxy` dial address. A low-privilege `user`-role account (per-row ownership was added in v2.52.1) could therefore create or edit a proxy host whose upstream pointed at the CaddyUI host's own loopback services, cloud metadata (`169.254.169.254`), or the Caddy admin API (`http://localhost:2019`) and reach those internal-only endpoints through the proxy (SSRF). Combined with the user-settable upstream Host-header override — which defeats the admin API's origin check — this exposed `GET /config/` (full config disclosure) and `POST /load` (RCE-grade config rewrite), reproduced end-to-end against a real Caddy. CaddyUI now validates every upstream on create **and** update, in both the REST `/api/v1` handlers and the HTML-form handlers: non-admin accounts may not target loopback, unspecified, or link-local addresses, or the configured Caddy admin endpoint. Admins remain unrestricted and private-LAN upstreams stay allowed, so delegated users can still front their own internal apps and the common single-admin "proxy to a localhost app" setup is unaffected. Backed by unit and end-to-end handler regression tests. **Deployments that grant non-admin `user`-role accounts should upgrade.** Reported by [@kta1kri](https://github.com/kta1kri). --- ## [2.52.1] - 2026-09-20 - Security: REST API v1 authorization fix ### Security - **REST API v1 per-row ownership enforcement** ([GHSA-r4wm-rgc5-q834](https://github.com/X4Applegate/caddyui/security/advisories/GHSA-r4wm-rgc5-q834), **Critical**): the `/api/v1` JSON handlers for certificates, raw routes, redirection hosts, and the proxy-host toggle/maintenance endpoints omitted the per-row `owner_id` check that the HTML-form handlers already enforce. As a result, any authenticated account — including the read-only `view` role — could read any certificate's TLS private key via `GET /api/v1/certificates/{id}`, and any `user`-role account could read, overwrite, delete, or toggle other tenants' raw routes, redirection hosts, and proxy hosts. All per-row `/api/v1` handlers now pass through a single shared authorization gate (admins may access any row; other accounts only rows they validly own; NULL-owner rows are denied for non-admins), backed by a regression test. **Multi-account deployments should upgrade.** Reported by [@kta1kri](https://github.com/kta1kri). ### Added - **Scheduled off-host database backups** ([issue #104](https://github.com/X4Applegate/caddyui/issues/104)): under **Settings → Backup**, enable automatic SQLite snapshots (`VACUUM INTO`) written to a directory on an interval, keeping the newest N. Point the directory at a mounted volume, network share, or object-store gateway to keep copies off the host. A **Back up now** button runs one on demand. (MariaDB installs continue to use their platform's own backup tooling.) - **OIDC / SSO login** ([issue #106](https://github.com/X4Applegate/caddyui/issues/106)): sign in to CaddyUI through an external identity provider (Authelia, Authentik, Keycloak, Google, …) alongside local password + TOTP — configure it under **Settings → Security**. Uses the standard auth-code flow with `state` + `nonce`, and delegates ID-token verification (JWKS, signature, `iss`/`aud`/`exp`, nonce) to the vetted `go-oidc` library. A verified email is required; it matches an existing CaddyUI user by email, or — opt-in — provisions a read-only account on first sign-in. Local login always remains available. ## [2.51.0] - 2026-09-17 - Per-host rate limiting ### Added - **Per-host rate limiting** ([issue #102](https://github.com/X4Applegate/caddyui/issues/102)): a proxy host can now cap requests — *allow N requests per M seconds, per client IP* — returning **429** on excess via Caddy's sliding-window `rate_limit` handler (keyed by client IP, so it honours Trusted Proxies). Configured under a host's **IP Allowlist / Blocklist** section. A natural companion to the v2.50.0 blocklist: soft-limit noisy or probing clients without a hard ban. ### Changed - `Dockerfile.caddy` now builds the [`caddy-ratelimit`](https://github.com/mholt/caddy-ratelimit) module. To use rate limiting, run the batteries-included Caddy — enabling it on a Caddy build without the module is refused by the pre-save validation with Caddy's error. - **Published custom Caddy image** [`applegater/caddyui-caddy`](https://hub.docker.com/r/applegater/caddyui-caddy) (`:stable`/`:latest`/`:v2.51.0`) — the prebuilt `Dockerfile.caddy` with all DNS providers, the CrowdSec bouncer and the rate-limit module, so fleet nodes pull one image instead of each running xcaddy. The Compose file now references it; uncomment the `build:` block to build locally instead. ## [2.50.0] - 2026-09-17 - Block an IP straight from analytics ### Added - **Block a visitor from the analytics drill-down** ([issue #100](https://github.com/X4Applegate/caddyui/issues/100)): the per-visitor page now has admin-only **Block on ** and **Block everywhere (fleet-wide)** actions. "On host" appends the IP (as a `/32` or `/128`) to that host's existing IP blocklist; "everywhere" adds it to a new **global blocklist** applied as a top-level 403 route ahead of all host routing, on every non-external server. Both re-sync automatically. This closes the loop from the v2.47–2.48 drill-down: see who's probing, block them in one click. - **Global IP blocklist** under **Settings → Security** — view and edit (unblock) the fleet-wide list; one IP/CIDR per line. ## [2.49.0] - 2026-09-17 - Configurable verification resolver ### Added - **Configurable DNS resolver for verification lookups** ([issue #98](https://github.com/X4Applegate/caddyui/issues/98)): the deploy/readiness checks (proxy hosts and Advanced routes) resolve records over Cloudflare DNS-over-HTTPS by default, which stalls in environments where outbound DNS to Cloudflare is filtered. **Settings → DNS → Verification resolver** now overrides this: leave it blank for the Cloudflare DoH default, enter one or more plain DNS servers (`192.168.1.10:53`, comma-separated; `:53` assumed) to use trusted, internal, or split-horizon resolvers, or a DoH URL that supports the JSON API. Requested by @BenRLange. ## [2.48.0] - 2026-09-16 - Drill down by status class ### Added - **Status-class drill-down** ([issue #94](https://github.com/X4Applegate/caddyui/issues/94)): the **Status mix** card and the **HTTP status breakdown** legend on a host's Analytics page are now clickable. Selecting a class (2xx/3xx/4xx/5xx/other) opens a view of the exact status codes within it, the top paths that produced them and the top visitors that received them — with the paths and IPs linking on into the per-path and per-visitor views. Same window/server scoping and top-N bounds as the rest of the drill-down. Requested by @BenRLange. ## [2.47.0] - 2026-09-15 - Deeper analytics drill-down ### Added - **Per-visitor and per-path analytics drill-down** ([issue #94](https://github.com/X4Applegate/caddyui/issues/94)): on a host's Analytics page, client IPs and paths are now links. Clicking a **visitor** opens a view of that IP's top paths, status breakdown, exact error detail and user-agents — useful for telling a noisy/misconfigured client apart from someone probing or fuzzing. Clicking a **path** shows which visitors requested it and its status breakdown. Everything stays scoped to the selected window (1d/7d/30d) and server filter, and reuses the existing per-host access-event range scans. - **Exact error detail** on the host page and both drill-downs: instead of only a "4xx" total, an **Error detail** table breaks errors down by exact status code + path (e.g. `404 /wp-login.php ×840`), so you can see what is actually failing or being probed. --- ## [2.46.0] - 2026-09-14 - Internal CA for local services ### Added - **Serve a proxy host from Caddy's internal CA** ([discussion #91](https://github.com/X4Applegate/caddyui/discussions/91)): a new **Internal CA (self-signed)** option in a proxy host's TLS section issues that host's certificate from Caddy's built-in local CA instead of ACME/Let's Encrypt — the equivalent of `tls internal` — for services on a private network where a publicly trusted certificate isn't wanted or possible. On sync CaddyUI emits a `tls.automation` policy scoped to the host's domains with the `internal` issuer, and excludes the host from DNS-01 issuance. The `internal` issuer is a core Caddy module, so no special Caddy build is required; clients must trust Caddy's root CA. Applies only when the TLS certificate is left on **Auto** (a custom or DNS-01 managed certificate still takes precedence). --- ## [2.45.2] - 2026-09-10 - Security headers were never sent ### Fixed - **Response-header handlers that both deleted and set the same header shipped no header at all.** Caddy applies header operations in the order add → set → delete, so the v2.10.4/v2.10.5 "delete-then-set" pattern used by the Security Headers bundle, the standalone Permissions-Policy, X-Robots-Tag (both toggles), `add_x_xss_protection_disabled` and `add_content_type_nosniff` deleted our own value right after setting it. Every host with Security Headers enabled was serving no `Strict-Transport-Security`, `X-Frame-Options`, `X-Content-Type-Options` or `Referrer-Policy` (Nextcloud's setup check flagged all four). Verified against Caddy 2.11.2. The handlers now `set` only — `set` already replaces any upstream value, so nothing stacks. Existing hosts are fixed on the next sync; no form changes needed. - **Security Headers bundle now respects the per-host "Strip response headers" list**, the same way it already respected the global one. A host that strips `X-Frame-Options` with the bundle on used to get `SAMEORIGIN` back from the bundle's own set (masked until now by the delete bug above). Regression test added. --- ## [2.45.1] - 2026-09-10 - Gandi collision wording ### Fixed - The Gandi provider's "record set already exists" message, its docs section and the v2.45.0 release note told users to pick "Override" on the collision warning — an action removed in v2.5.6. They now say what is true: CaddyUI never overwrites records it did not create; remove or change the existing set in the Gandi console, then save again. Caught by the new Claude pull-request review. --- ## [2.45.0] - 2026-09-10 - Gandi managed DNS ### Added - **Gandi provider support** ([discussion #72](https://github.com/X4Applegate/caddyui/discussions/72)): CaddyUI drives Gandi's LiveDNS v5 API with a Personal Access Token — domain discovery for the zone picker, managed A-record creation, collision detection and cleanup for proxy hosts, redirections and Advanced routes, and the same token is handed to Caddy for DNS-01 certificate issuance (`github.com/caddy-dns/gandi`, now built into `Dockerfile.caddy`). LiveDNS keeps records as sets keyed by name and type, so CaddyUI references them as `name/type`, refuses to create over an existing set (CaddyUI never overwrites records it did not create — remove the set in the Gandi console first), and raises TTLs below the LiveDNS minimum of 300 seconds. Settings → DNS gets a Gandi card and the in-app guide a Gandi section. ### Changed - **CI**: pull requests are now reviewed by Claude (`anthropics/claude-code-action`) instead of the Codex GitHub App — an automatic review on every non-draft, non-Dependabot pull request, and `@claude` in an issue or PR comment for questions and follow-up changes. Runs on the maintainer's Claude subscription via a `CLAUDE_CODE_OAUTH_TOKEN` repository secret; the jobs skip themselves when the secret is absent. No effect on the shipped image. --- ## [2.44.0] - 2026-09-09 - Settings is now a set of pages ### Changed - **Settings is split into eight pages** — General, Notifications, DNS, Security, Analytics, Integrations, AI assistant, Backup — with a navigation column (a scrolling row on narrow screens) instead of one form with sixteen cards and a row of jump links. `/settings` opens General; each page lives at `/settings/`, and old in-page links such as `/settings#settings-smtp` are redirected to the page that now holds the card. - Each page saves only its own settings. The save handler now owns a table of which page holds which key and writes nothing else, so saving General cannot blank the SMTP host or the analytics target. DNS provider credentials and integration settings are only collected from their own pages. Saving Notifications, AI or Backup no longer triggers a Caddy sync, which they never affected. - Links across the app and the docs that pointed at Settings anchors now open the right page directly. --- ## [2.43.1] - 2026-09-09 - The first big prune no longer slows every page ### Fixed - The first analytics prune after upgrading to v2.43.0 — tens of millions of backlog rows on an install that had never pruned — made every page take several seconds for the whole run. CaddyUI's SQLite pool is a single connection, and the prune ran its 20,000-row batches back to back, so each request waited for the next batch to finish. Batches are now 5,000 rows with a 200 ms pause between them, so requests interleave; the run takes a little longer and the UI stays responsive. Progress is logged every million rows. --- ## [2.43.0] - 2026-09-09 - Analytics retention actually runs, is configurable, and space can be reclaimed ### Fixed - **Raw analytics events were never pruned.** The 30-day prune loop has existed since v2.7.0, but nothing ever started it, so `access_events` grew without bound — one production database had reached 22 GB with 63 million rows older than 30 days. The loop now starts with the other background workers, runs an hour after start-up and hourly after that, and deletes in batches of 20,000 so the write lock is never held for long even on a first run against months of backlog. ### Added - **Settings → Analytics → Keep raw events for N days** (default 30; 0 keeps forever). Per-day totals per host are kept in a separate rollup, so shortening the retention does not blank long-range charts. - A **Storage** panel on the same card: database file size, space already freed inside the file, oldest and newest event, the last prune (rows removed and kept) and the last reclaim, with **Prune now** and **Reclaim space** buttons. Reclaim runs SQLite's VACUUM in the background — the only thing that actually shrinks the file after rows are deleted — and is refused when the data volume lacks room for a full copy; on MariaDB the panel points at `OPTIMIZE TABLE` instead. Both actions are recorded in the Activity log. - The Docker image sets `SQLITE_TMPDIR=/data` so VACUUM has a working directory in the scratch-based image, which has no `/tmp`. --- ## [2.42.2] - 2026-09-08 - Advanced config: JSON-style transport names and misplaced transport options are repaired ### Fixed - Saving a proxy host whose Advanced config used Caddy's JSON field names inside a transport block — `read_buffer_size 16384` / `write_buffer_size 16384`, as reported for a Nextcloud host — failed with `unrecognized subdirective read_buffer_size`. In a Caddyfile those options are spelled `read_buffer`, `write_buffer` and `max_response_header`; CaddyUI now respells them, and moves transport-only options typed directly under `reverse_proxy` (timeouts, buffers, keepalive, TLS, versions…) into the block's `transport http { … }`, merging with an existing one. The repairs also apply when an already-saved Advanced config is adapted for a sync, so such hosts start working on the next sync without being re-saved. - When Caddy still rejects an unknown sub-directive, the message now says which name it is, how it is spelled in a Caddyfile, where transport options go, and that the same settings exist as dedicated form options (Upstream → timeouts and buffers, Streaming → Flush immediately). --- ## [2.42.1] - 2026-09-08 - Changes Caddy rejects are refused up front, failed syncs are visible ### Fixed - **A proxy host, redirection or certificate that Caddy would not load was saved anyway** ([#74](https://github.com/X4Applegate/caddyui/issues/74)). The form said "saved", the sync failed only in the log, the live config stayed as it was — and every later sync for that server failed the same way. The reported case: a file-path certificate whose files exist on the host but not inside the Caddy container. Proxy hosts, redirections and certificates are now validated against Caddy before they are saved, as Advanced routes already were; a file Caddy cannot open comes back as "Caddy cannot open /path for certificate X … the file must exist inside the Caddy container — mount the directory into Caddy or paste the PEM instead". - **A failed sync is now visible.** When a sync is rejected for any reason after a save, an amber banner on every page names the server and Caddy's error, with **Retry sync now** and **Dismiss**, until the next sync succeeds. - The Caddyfile view of a host with a custom certificate rendered only a comment; a file-path certificate now renders as a real `tls ` line, and stored PEM / managed certificates say how Caddy gets them. The Caddyfile export does the same. - The Caddyfile view and export printed an Advanced config `reverse_proxy { … }` block verbatim after the host's own `reverse_proxy` block, i.e. as a second one; its sub-directives now render inside the generated block, marked "from Advanced config". - A reverse_proxy sub-directive typed bare in a host's Advanced config — `flush_interval -1` on its own — is now moved into a `reverse_proxy { … }` block automatically (merged into an existing block when there is one, multi-line sub-directives included) instead of being explained back to you. ### Security - Two CodeQL findings from v2.38.0: the post-apply banner's "back" redirect no longer accepts a protocol-relative `//host` path, and expectation paths are normalised to exactly one leading slash. --- ## [2.42.0] - 2026-09-08 - Export a managed certificate to a directory ### Added - **Export to a directory** on managed (ACME) certificates. Other services on the same host — a mail server is the usual case — need the certificate Caddy obtains, and need it again after every renewal. Mount the node's Caddy data volume into the CaddyUI container (read-only is enough, e.g. `caddy_data:/caddy-data:ro`), enter that mount path as the server's new **Data directory** under Caddy Fleet → edit, then give the certificate an export directory and file names (defaults `fullchain.pem` and `privkey.pem`). CaddyUI locates the newest certificate Caddy stored for the definition's domains — across issuers, wildcard names included — and copies the chain and key out atomically, the key readable by owner only. Exports run when the node reports an issuance or renewal (the certificate-event stream CaddyUI already receives), every 10 minutes as a safety net, after the certificate is saved, and on **Export now**; nothing is rewritten while the stored serial is unchanged. The form shows the last export (serial, expiry, files, or the error) and every export is recorded in the Activity log. - With this, a wildcard that a certbot job maintained for a mail server can move to Managed ACME: Caddy renews it and CaddyUI drops the files where the mail server reads them. ### Changed - Certificate file paths and export directories typed into forms are vetted before any file access: absolute, cleaned, no `..`. ### Fixed - Editing a managed certificate showed the Paste PEM text areas above the managed settings; the PEM section is now hidden unless PEM is the selected source. --- ## [2.41.0] - 2026-09-08 - Fleet sync and "Also configure on" for PEM and file-path certificates ### Added - **Custom certificates travel with the fleet.** Caddy Fleet → Sync configuration and the certificate form's **Also configure on** picker previously copied managed (ACME) definitions only, so a wildcard uploaded as PEM or referenced by file path had to be re-uploaded on every node by hand. Both now copy every certificate: a PEM certificate is stored on the target with its certificate and private key (save it again after replacing the PEM to push a renewal), and a file-path certificate is copied as stored PEM when CaddyUI can read both files — the target host cannot see the source's disk — or by path reference otherwise, in which case the sync summary and Activity log say so and the files must exist on the target at the same paths. - Proxy hosts, redirections and Advanced routes that a sync or **Also deploy to** creates on a target now reference the copy of their certificate instead of silently falling back to Auto TLS; **Also deploy to** copies a host's custom certificate first so the reference resolves. Existing target hosts keep their own certificate choice, as before. ### Changed - The fleet sync summary reads "certificates: N added, N updated" (formerly "managed certificates") and appends how many were copied by file path only. The picker's help text describes what travels for each source, and the docs Certificates section too. --- ## [2.40.0] - 2026-09-08 - Advanced config accepts a reverse_proxy block ### Added - A proxy host's **Advanced config** can now carry a `reverse_proxy { … }` block with no upstream address. Its sub-directives — `flush_interval -1`, `header_up` / `header_down`, `transport http { … }`, `lb_policy`, active and passive health checks, `trusted_proxies`, and the rest — are adapted through Caddy and merged into the host's own generated `reverse_proxy` handler, so the Forward host / port and every option in the form still apply and nested settings (a `transport` with TLS already configured, upstream request headers) combine rather than replace. Blocks that name an upstream or sit behind a matcher are refused with a message saying why. ### Fixed - Typing a reverse_proxy sub-directive bare in Advanced config — the reported case was `flush_interval -1` — came back from Caddy as `Caddyfile:2: unrecognized directive: flush_interval`, which says nothing about what to do. CaddyUI now catches every reverse_proxy sub-directive at the top level before adapting and explains that it belongs inside a `reverse_proxy { … }` block (with the block spelled out), and points at the dedicated form option where one exists (Streaming → Flush immediately for `flush_interval`). - The form's help text listed `reverse_proxy` among the rejected directives; it now describes the block instead. --- ## [2.39.0] - 2026-09-08 - File-path certificates show expiry via a live TLS probe ### Added - **Live TLS probe for custom certificates.** A *file path* certificate names files inside the Caddy container, which CaddyUI usually cannot read from its own container — so the Certificates page showed no expiry at all for them, and Inspect gave up. CaddyUI now opens a TLS connection to the Caddy node for the certificate's domain (the same handshake it already uses to confirm managed certificates) and reads what is actually served: expiry, days left, issuer, SANs, key type, serial and fingerprint. Probes run every 30 minutes, after a certificate is saved, when **Refresh status** is pressed, and before Inspect renders when the last result is stale. Results are kept in the database so they survive a restart. - The Certificates list marks such rows *live TLS probe*, shows **Not served** when Caddy answered with a certificate for a different name, and **Unverified** when the node could not be reached (hover for the reason). Inspect gets a **Live check** card for every PEM and file-path certificate with a **Probe now** button. - When CaddyUI *can* read the file (or the PEM is stored), the Live check compares it with what Caddy serves and says **Different certificate** when the serial numbers differ — the usual sign of a file renewed on disk that Caddy has not reloaded yet. The list shows the same warning inline. - The Operations dashboard's "custom certificates expire soon / are expired" recommendations now count file-path certificates too, using the file when readable and the live probe otherwise. They previously counted stored PEMs only. ### Changed - **Zero-code alternative documented:** mount the certificate directory into the CaddyUI container read-only at the same path Caddy sees (for example `- /certs:/certs:ro`), and CaddyUI reads the file directly. The docs Certificates section, the file-path form hint and a commented line in `docker-compose.yml` now say so. --- ## [2.38.1] - 2026-09-08 - Dependency updates ### Changed - **Go modules**: `github.com/aws/aws-sdk-go-v2` 1.43.7 → 1.45.1 (with `credentials` 1.20.2 and `service/route53` 1.68.0), `github.com/go-sql-driver/mysql` 1.10.0 → 1.10.1, `golang.org/x/crypto` 0.55.0 → 0.56.0 and `modernc.org/sqlite` 1.57.0 → 1.58.0. The module's minimum Go version moved from 1.25 to 1.26; the Docker image already builds with Go 1.27, so nothing changes for the shipped binary beyond the newer libraries. No application code changed. (#71) --- ## [2.38.0] - 2026-09-04 - Expectations: post-apply checks with automatic rollback ### Added - **Expectations** on proxy hosts: a table of checks — method, scheme, path, expected status (or any 2xx/3xx), optional Location prefix for redirects, maximum latency, valid-TLS requirement — that CaddyUI runs against the host's domain right after **every** config sync. If any check fails and **Settings → Post-apply checks → Roll back automatically** is on (the default), the previous live config is loaded straight back into Caddy, automatic syncs for that server are paused, and a banner on every page offers **Re-apply now** or **Keep the rolled-back config**. Off means report-only. Results show on each host's health page (with a **Run now** button) and every run, failure, rollback and re-apply is recorded in the Activity log. Suggested by Matt Holt on the Caddy forum: define the behaviour your sites must keep, and let the tool refuse a change that breaks it. --- ## [2.37.1] - 2026-09-04 - Live Traffic feed shows the date, and Clear sticks ### Fixed - The Live Traffic feed stamped rows with the time only, like Server Logs did before v2.35.5; rows now show `YYYY-MM-DD HH:MM:SS` in CaddyUI's configured timezone, both for the rows rendered with the page and for those that stream in. (#60 follow-up) - **Clear** on the Live Traffic feed only emptied the table, and every row came straight back on the next load. It now remembers, per browser and per server filter, the newest request seen when it was pressed: those rows stay hidden after a reload and the stream resumes after them. Nothing in the Analytics database is touched. --- ## [2.37.0] - 2026-09-04 - Each Caddy node ships its logs to a target it can reach ### Fixed - Every node in the fleet was configured to send its access logs, certificate events and runtime logs to the one fleet-wide ingest target, which defaults to `caddyui:9019` — a Docker service name that only a Caddy on the same Docker network can resolve. Nodes on other hosts therefore shipped nothing, silently: an empty Analytics page for that server, no certificate issuance status, and no Server Logs capture. Each node now has its own **Log ingest target** on its Caddy Fleet entry (blank = the fleet-wide setting), applied to all three log streams. ### Added - The Caddy Fleet list shows where each managed node ships its logs, and warns when a node reached at another host was handed a bare Docker service name it cannot resolve; the Analytics page shows the same for the node in scope when it has no traffic, instead of an unexplained empty page. Settings → Analytics now says when the fleet-wide default is not enough. --- ## [2.36.2] - 2026-09-04 - Certificates page lists Advanced routes ### Fixed - The **Caddy-managed (ACME)** section of the Certificates page listed the domains of proxy hosts and redirects but never those of Advanced routes — even though Caddy issues certificates for their host matchers in exactly the same way, and CaddyUI's certificate lifecycle tracking already followed them. Enabled Advanced routes without a custom certificate now appear there too, with the same wildcard mapping and issuance status as everything else. (#65) ## [2.36.1] - 2026-09-04 - Advanced routes on custom ports get their own listener ### Fixed - An Advanced route pasted as `:7070 { … }` never listened on port 7070. The Caddyfile adapter puts each site address on its own server, but CaddyUI flattened every server's routes into one list — dropping the port — and nested them all under its :443/:80 servers. Advanced routes now remember their listen address(es), and every sync emits one `apps.http.servers.caddyui_listen_` entry per distinct port set: routes sharing a port share a server, stale entries are removed when a port changes or the route is deleted, and servers the operator added by hand under other names are never touched. The port is captured automatically from the Caddyfile block on paste, import and edit; JSON-only routes get a **Listen on** field. Routes on the standard ports keep exactly today's placement, and validate-before-save now includes the new servers, so a port conflict is refused at save time instead of breaking the next sync. (#64) - A block on a custom port that looks like a plain reverse proxy (`example.com:7070 { reverse_proxy … }`) now stays an Advanced route on paste and import instead of being auto-classified into a proxy host, which has no notion of a port and would have silently lost it. ## [2.36.0] - 2026-09-04 - Health checks can use any HTTP method ### Added - **Custom monitoring → Method** now offers POST, PUT, PATCH, DELETE and OPTIONS alongside GET and HEAD. A route that only accepts one method — a POST-only API endpoint, say — answers `405` to a GET or HEAD probe from Caddy itself, so the request never reaches the backend and "expected status 405" only ever proved that Caddy was up. Choosing the method the route accepts lets the probe get through to the service. Both HTTP probes (the App/Port check and the Public check) honour it. Methods other than GET/HEAD are sent with an empty body; the form warns that they can have side effects, so point them at an endpoint that tolerates an empty request. Blank or unrecognised values still fall back to GET; TRACE and CONNECT are deliberately not offered. (#59) ## [2.35.5] - 2026-09-04 - Server logs show the date, and Clear now clears ### Fixed - The Server Logs page stamped every entry with the time only. CaddyUI's runtime-log buffer keeps the last 1000 entries for as long as the process runs, so on a quiet node it spans days and the times read out of order. Entries now show `YYYY-MM-DD HH:MM:SS` in CaddyUI's configured timezone — the same layout the Activity log already uses. (#60) - **Clear screen** only emptied the table; every entry came straight back on the next page load because the buffer behind it was untouched. The button is now **Clear logs**: it discards the buffered entries for the selected server as well (nothing on disk is involved — runtime logs are never persisted) and records the purge in the Activity log. New endpoint `POST /api/server-logs/clear`. (#60) ## [2.35.4] - 2026-09-04 - One rendering per Advanced route ### Fixed - `/raw-routes` drew every Advanced route twice at tablet-to-laptop widths (768–1024 CSS px — which is also where a zoomed desktop browser lands): the desktop table became visible at the `md` breakpoint, but the mobile card list only hid from `lg`. Both now switch at `lg`, matching `/certificates` and `/snapshots`. The second copy was the same database row rendered again, not a duplicate route — which is why deleting "one" of them removed "both". - A template guard test now asserts the table and card-list breakpoints on `/raw-routes` stay paired, so the mismatch can't silently come back. ## [2.35.3] - 2026-09-02 - Go 1.27 build toolchain ### Changed - **Go toolchain**: the Docker build stage now compiles with Go 1.27 (`golang:1.27-alpine`, up from 1.25). No application code changed — the shipped binary is the same source built on the newer toolchain. (#56) - **CI**: GitHub Actions workflows moved to `actions/checkout@v7`. No effect on the shipped image. (#57) ## [2.35.2] - 2026-08-31 - SSL Verify Upstream docs fixed, HTTPS-only condition surfaced ### Fixed - The Caddyfile → form-field mapping table in the docs referred to the "Verify Upstream TLS Certificate" checkbox by its internal field name, "SSL Verify Upstream", instead of its actual UI label — making the field hard to find (#54). - The checkbox only affects the generated config when Forward Scheme is `https`; toggling it under the default `http` scheme was silently a no-op, and that precondition lived only in a small form hint, not the docs table. The docs table now states it, and the form itself shows an inline warning when the toggle is currently inert for the selected scheme. ## [2.35.1] - 2026-08-21 - An always-present button to the Public health page ### Fixed - The "Public" health dot on the proxy hosts list was the only way to reach `/proxy-hosts/{id}/health`, and it stops being a clickable link precisely when there's the least data to look at — monitoring off, or no check recorded yet. That was a dead end right where it mattered most. A small, always-present history icon now sits next to the dot regardless of its state, visible to any viewer (it's read-only status, not an edit action). ## [2.35.0] - 2026-08-21 - Monitoring "Off" now covers all three probes ### Fixed - The v2.28.0 monitoring section promised that setting a host to **Off** would stop "all outbound probes for this host." It didn't. A third probe — the persisted "Public" health check behind `/proxy-hosts/{id}/health` and the left-most dot on the proxy hosts list, which runs every 5 minutes and keeps 24 hours of history — was never wired to it, and kept probing every enabled host regardless of `MonitorMode`. - The same gap meant **Custom** mode's path, method, and expected status only ever applied to the App and Port dots. A host with a non-default health path or an unusual expected status could still show the Public dot red, because that check always requested `/` and only accepted 2xx/3xx/401/403. - Both are fixed: the Public checker now reads the same `MonitorSettings` as the other two, is skipped entirely when a host is Off, and paces a custom interval against its own persisted history so pacing survives a restart. - The proxy hosts list and the health detail page no longer show a stale "up" or "down" status left over from before a host was switched to Off — both check the current `MonitorMode` first and show a neutral "monitoring off" state instead. - The monitoring section's copy on the proxy host form now names all three probes it drives and links to the Public check's history page, instead of only mentioning the App and Port dots. `NodeLocal` is deliberately not consulted by the Public checker — it describes where a host's *upstream* resolves, not whether its *public domain* should be checked from the internet-facing side, which is what this probe does. ## [2.34.0] - 2026-08-21 - One place for the update notice ### Removed - The **"↑ vX.Y.Z available"** pill in the sidebar footer. The Operations dashboard already announces the same update in context and links straight to the release, so the pill was a second place to notice it and a second place to dismiss it. The running version stays in the sidebar, where it is useful on every page. - The `badge-pulse` animation and `#update-badge` rule that existed only for the pill. The dashboard banner is unchanged. ## [2.33.0] - 2026-08-21 - Node-local proxy hosts and advanced routes ### Added - Proxy hosts and advanced routes can be marked **node-local**, which excludes them from full fleet syncs and from **Also deploy to**. Use it when the upstream only means something on the node that owns it — a Docker service name like `immich_server`, a VPN address, a loopback port. Copying such a host to another Caddy produced a route pointing at nothing. - Fleet sync reports what it left behind: *"skipped as node-local: 3 proxies, 1 advanced routes"*. Skipped resources are counted rather than silently dropped, so a sync summary can be read at face value. - The proxy host form warns when the upstream looks node-local but the host isn't marked as such — a single-label hostname with no dot, matching the rule CaddyUI already uses to decide it can't resolve a backend itself. Marking the host node-local dismisses the warning and hides the deploy picker. - Node-local hosts carry a badge in the proxy host list, next to the upstream that pinned them. ### Notes - Fleet sync copies a host's upstream verbatim, which is correct for **edge replication** — several Caddy nodes fronting the same backends — and wrong for **federated nodes**, where each Caddy fronts its own local stack. `preserveProxyTargetPolicy` already treated certificates, DNS records and keys as node-specific; the upstream address was the field most likely to differ and was still being copied. This closes that gap without introducing per-node upstream overrides. - The flag is opt-in and defaults to off, so existing hosts sync exactly as before. ## [2.32.0] - 2026-08-20 - Fix bulk-action bar covering the last row ### Fixed - The floating "N selected" bulk-action bar no longer covers the last row of a long list. It is `position: fixed` to the bottom of the viewport and never reserved space for itself, so once a list needed scrolling — a shorter window, or simply enough proxy hosts / redirections / advanced routes / certificates — the last row's Edit and Delete buttons sat directly under it, overlapping into an unreadable jumble as soon as any rows were selected and you scrolled to the bottom. - The scroll container now reserves exactly the bar's own rendered height (which can grow if it wraps to two lines on a narrow window) as bottom padding whenever the bar is visible, and releases it when the selection is cleared. Fixed centrally, so it applies to every page using this bar without per-page changes. ## [2.31.0] - 2026-08-20 - Analytics performance at scale ### Fixed - The Analytics page no longer slows down as proxy hosts are added to a node. Measured on 84,000 access events across 21 hosts, it went from **1.18 s to 0.31 s** end-to-end; the query responsible went from 1068 ms to 109 ms. - The cause was the server-scoped "top hosts" aggregation (`WHERE ts >= ? AND server_id = ? GROUP BY host`). No index could supply group order for it, so SQLite built temporary B-trees for the grouping, the distinct-visitor count and the sort, and fetched every matching row from the table to read `host` and `client_ip`. A covering index on `(server_id, host, ts, client_ip)` supplies the group order directly and carries every column the query reads. - Only the server-scoped view was affected, which is why this appeared as "it got slow once I had about twenty hosts". The fleet-wide view already had an index giving it group order and was never slow. - The index is created automatically on upgrade. Building it takes well under a second on a database of this size, and adds roughly two microseconds per ingested event. ### Added - Regression tests asserting the query plan still uses the covering index and still avoids a temporary B-tree for `GROUP BY`, so this cannot silently regress. ## [2.30.0] - 2026-08-20 - Deterministic config, visible migrations, CSP ### Fixed - Generated Caddy config is now byte-identical for identical input. Three handlers built a JSON array by iterating a Go map — the security-headers `delete` list, and the request and response header `delete` lists — and Go randomises map iteration. A host with **Security Headers** enabled produced 5 distinct payloads across 300 identical calls. Because Caddy compares configuration structurally, every sync of an unchanged host looked like a real change: an avoidable config reload each time, and a spurious entry in every config snapshot diff. The emitted configuration is semantically unchanged; only its ordering is now stable. - Schema-upgrade steps report their failures. 324 of them were written as `_, _ = db.Exec(...)`, so a failed `ALTER TABLE` left the column missing and the application failed later at query time, far from the cause — the shape of [#30](https://github.com/X4Applegate/caddyui/issues/30). Failures are now logged individually and summarised once at the end of the upgrade. ### Security - CaddyUI's own responses now carry a `Content-Security-Policy` and a `Permissions-Policy`. `X-Frame-Options`, `X-Content-Type-Options` and `Referrer-Policy` have been sent since v2.10.3; CSP was the missing piece, and it matters because authenticated pages load the Tailwind runtime from a CDN. The policy allows exactly the origins the templates use and nothing else. `form-action 'self'` and `connect-src 'self'` are the directives doing the real work. - `CADDYUI_CSP` overrides the policy: unset uses the default, `off` disables the header, any other value is sent verbatim. A subtly wrong CSP breaks the UI for everyone at once, so the escape hatch does not require waiting for a patch release. ### Added - Golden-file coverage for `BuildProxyRoute`, which generates the route JSON pushed to Caddy. It is the highest-consequence code in the project and previously had no tests. Regenerate intentional changes with `go test ./internal/caddy -update-golden`. - A determinism test that fails if any handler is ever added that builds a JSON array from a map without sorting. - Tests for `internal/auth` — password hashing, session issue/expire/delete, and cookie flags. Coverage went from none of its own to 87.8%. - `.github/dependabot.yml`. Security updates already worked without it, which is why Dependabot PRs had been merged before; *version* updates need explicit configuration, so routine dependency bumps were never opened. - CI now fails on unformatted code. Four files had drifted out of `gofmt`; they are reformatted here (whitespace only). ### Changed - `parseProxyHostForm` and its helper moved verbatim into `internal/server/proxy_host_form.go`. At ~660 lines it was the largest function in the codebase, inside a 16,600-line file. No behaviour changed. ## [2.29.0] - 2026-08-20 - CSRF protection ### Security - State-changing requests authenticated by session cookie now require a CSRF token. Session cookies were already `SameSite=Lax`, which blocks the classic cross-site form POST in current browsers, but that was a single layer resting on a browser default; this adds a synchroniser token as defence in depth across all 94 authenticated `POST`/`DELETE` routes. - The token is `HMAC-SHA256(secret, sessionToken)`, so it needs no storage of its own and is bound to one session. Logging out or logging in again rotates it, and a token from a closed session is rejected. The secret is generated once and persisted, so tokens survive a restart rather than 403-ing every logged-in user on deploy. - Requests authenticated by API bearer token are exempt and unchanged. A browser does not attach an `Authorization` header to a cross-site request on its own, so those calls are not CSRF-reachable. ### Changed - Pages render into a buffer before being written. This is what lets the hidden token input be stamped into every POST form from a single place, and as a side effect a mid-template error now produces a clean `500` instead of a truncated page followed by an error string. - `window.fetch` is wrapped in `app.js` to attach the `X-CSRF-Token` header to same-origin requests using an unsafe method, so scripted callers are protected by default. Cross-origin requests are untouched — the token is never sent to a third-party host. ## [2.28.0] - 2026-08-20 - Configurable status monitoring ### Added - Proxy hosts now control how CaddyUI's own status probes behave, via a **CaddyUI status monitoring** section on the host form. Three modes: **Automatic** (unchanged behaviour), **Custom** (probe path, method, expected status, interval and timeout), and **Off**. ([#39](https://github.com/X4Applegate/caddyui/issues/39)) - Setting a host to **Off** stops CaddyUI issuing any probe for it — both the 60-second public-domain check behind the App dot and the direct upstream check behind the Port dot. Intended for backends that are deliberately unreachable from the CaddyUI container (split-horizon DNS, firewalled networks), where the probes reported a misleading "down" and added recurring noise to firewall logs. - Hosts with monitoring off show a neutral grey dot labelled "monitoring off for this host" rather than a red "down" — no probe ran, so no verdict is claimed. ### Changed - The host form now states which system each health setting belongs to. CaddyUI's own monitoring and Caddy's active upstream health checks are separate mechanisms that were easy to confuse; the existing `health_check_*` options configure Caddy and are unchanged. - Custom probe intervals are floored at 60 seconds and round up to a multiple of the poller's 60-second tick. Timeouts are clamped to 1–120 seconds and expected status to 100–599; an out-of-range entry is pulled to the nearest bound rather than failing the save. ### Fixed - `ListProxyHostSummaries` now projects `monitor_mode`, which `/api/upstream-health` reads. Without it the Port dot would have resolved every host to "automatic" and ignored the off switch. ## [2.27.0] - 2026-08-20 - Self-contained assets and advanced-route cross-deploy ### Fixed - Two-factor enrolment now renders its QR code server-side as an embedded PNG instead of loading a script from `cdn.jsdelivr.net`. That CDN path began returning 404 and was additionally blocked by CORB, so no QR code appeared during 2FA setup. ([#37](https://github.com/X4Applegate/caddyui/issues/37)) - The Inter variable font is now committed to the repository rather than downloaded during the Docker image build. Because the font was fetched only at image-build time, binaries built from a source checkout — including every published GitHub release archive — embedded no font file and served 404s for it. ([#37](https://github.com/X4Applegate/caddyui/issues/37)) - Turnstile and reCAPTCHA secret keys are no longer rendered into the settings form. `type="password"` masked them visually, but the plaintext remained readable through the DOM. They now follow the same keep-blank-to-preserve pattern already used for the SMTP password and AI provider keys. ### Added - Advanced routes now offer the **Also deploy to** picker that proxy hosts and redirections already had. Mirroring an advanced route onto another fleet node previously required a full sync of the destination server, which overwrote unrelated configuration there. ([#38](https://github.com/X4Applegate/caddyui/issues/38)) - Regression coverage asserting that every asset referenced by a template is committed and embedded, that no template loads a third-party script, and that captcha secrets never reach the DOM. ### Removed - htmx is no longer vendored or loaded. It was added in v2.12.39 and shipped on every authenticated page, but no template ever carried an `hx-*` attribute — every form is a plain `
`. This removes a ~48 KB script from every page load, one build-time network fetch, and the vestigial `HX-Trigger` response header on the Caddy reload endpoint. ## [2.26.0] - 2026-08-18 - Live route configuration preview ### Added - Proxy-host forms now provide an in-browser Caddyfile excerpt alongside the generated Caddy route JSON, both updated from unsaved form changes and copyable without downloading a file. ([#33](https://github.com/X4Applegate/caddyui/issues/33)) - A **Configured** form mode hides option sections with no configured values while keeping the live configuration preview available. ### Fixed - The existing route JSON preview now opens and refreshes correctly; its client-side lookup previously referenced an ID that was not present in the form. ## [2.25.4] - 2026-08-18 - Operations fleet-node scoping ### Fixed - Operations traffic totals and sparklines now filter by the selected fleet node as well as hostname, so shared domains no longer merge traffic from multiple Caddy servers. - The Operations Caddy-version lookup and upstream telemetry now use the selected node's Admin API and saved Basic Auth credentials instead of the bootstrap Caddy client or an unauthenticated request. - The System health panel now clearly separates CaddyUI-host uptime/load/memory from selected-node Caddy request/upstream telemetry, and the certificate tile is labeled as selected-node certificate definitions. ## [2.25.3] - 2026-08-15 - Certificate and log-stream review follow-up ### Fixed - The persistent certificate monitor now receives Caddy's DEBUG `events/cert_obtained` records through a dedicated named logger while filtering unrelated event and TLS debug traffic out of the in-memory Server Logs ring. - Startup certificate reconciliation probes and verifies every identifier independently. A valid certificate for one name in a multi-domain managed definition can no longer mark unverified sibling names as Issued. - Server Logs retention now evicts the oldest entry ID when the 500-row display limit is reached, regardless of the active visual sort order. ### Changed - Added regression coverage for all three late automated review findings on PR #31. ## [2.25.2] - 2026-08-14 - Observability reliability follow-up ### Fixed - Analytics and fleet access-log forwarding no longer select a CaddyUI-owned logger as an HTTP server's default. Existing console, journal, file, and per-host logger configuration is preserved, while legacy v2.25.0/v2.25.1 overrides are repaired automatically. - Certificate lifecycle success can no longer regress to **Obtaining** when Caddy emits its post-issuance `releasing lock` cleanup line. Structured certificate events are also recognized as authoritative lifecycle signals. - Managed certificates with missing or stale obtaining/renewing history are reconciled at startup and periodically from the certificate actually served by each managed Caddy node. Retry, error, and revoked states remain visible until Caddy reports a real recovery. - Server Logs now shows whether its live stream is connected or reconnecting and provides a one-click **Refresh / reconnect** action. ### Added - Sortable columns on the Server Logs, custom certificate, and Caddy-managed certificate tables. - A **Refresh status** action on the Certificates page. - Regression coverage for existing Caddy logging preservation, lifecycle cleanup ordering, startup certificate reconciliation, and the new operational UI controls. ([#26](https://github.com/X4Applegate/caddyui/issues/26)) ## [2.25.1] - 2026-08-14 - SQLite upgrade hotfix ### Fixed - Existing SQLite installations now add the new analytics `server_id` and `server_name` columns before creating the fleet-attribution index. This fixes the CaddyUI startup crash introduced by v2.25.0 while preserving existing analytics rows as legacy/unattributed data. - Added a regression test that opens and migrates the pre-v2.25.0 analytics schema, verifies the original event data, and confirms the new index is created. ## [2.25.0] - 2026-08-14 - Fleet observability and certificate lifecycle ### Added - **Certificate lifecycle visibility** projects Caddy's structured TLS logs into per-node states such as obtaining, renewing, retry scheduled, issued, revoked, and issuance error. Managed-certificate lists and live server probes now show the latest actionable message instead of an unconditional green status. Only the compact latest state is persisted; raw logs are not. ([#26](https://github.com/X4Applegate/caddyui/issues/26)) - **Ephemeral Server Logs** gives administrators an in-app, structured runtime stream for a selected managed Caddy node with level and text filters. Full capture leaves Caddy's normal logs unchanged, stays in memory, and automatically disables after 15 minutes. ([#27](https://github.com/X4Applegate/caddyui/issues/27)) - **Authoritative fleet attribution** stamps every analytics event with the Caddy node that handled the request. Live Traffic, Analytics, host drill-downs, and CSV exports can distinguish and filter nodes even when load-balanced servers share the same hostname. ([#28](https://github.com/X4Applegate/caddyui/issues/28)) ### Changed - CaddyUI installs additive named log streams for analytics, certificate events, and temporary runtime capture. Existing Caddy container, journal, and file log outputs remain unchanged. - Existing analytics rows migrate as legacy/unattributed data instead of guessing a source server from current route ownership. - SQLite and MariaDB schemas now store access-event server identity and the latest certificate lifecycle projection, with parity and migration coverage for both backends. ## [2.24.0] - 2026-08-13 - Detailed Prometheus metrics ### Added - **Fleet-scoped Prometheus metrics controls** in Settings enable Caddy HTTP metrics, per-host labels, and `observe_catchall_hosts` on explicitly selected managed servers. The latter is validated against each target and requires Caddy 2.11 or newer. ([#25](https://github.com/X4Applegate/caddyui/issues/25)) - The Metrics card shows the private `/metrics` scrape target derived from each selected server's protected Admin URL and warns against exposing Caddy's unauthenticated admin port publicly. - Metrics can be configured even on a fresh Caddy server with no CaddyUI-managed routes or certificates. ### Fixed - Routine CaddyUI route syncs now have an explicit regression guarantee that global/app-level metrics loaded from a Caddyfile or written through the admin API remain untouched unless the administrator selects that server for CaddyUI metrics management. Detailed `per_host`, `observe_catchall_hosts`, and externally managed fields such as `otlp` are preserved. ### Changed - The repository's custom Caddy image is pinned to Caddy 2.11.4 so the shipped Compose build supports catch-all host metrics deterministically. ## [2.23.2] - 2026-08-13 - Persistent guidance toast fix ### Fixed - Permanent Settings guidance for Ollama model sizing, Porkbun API access, Route 53 IAM permissions, and CrowdSec modules now remains inline instead of being recreated as warning toasts on every page load. - Toast auto-conversion now uses explicit `data-toast` markers rather than inferring notification intent from presentation colors. Transient success, warning, and error feedback still appears as toasts without swallowing informational cards, status pills, or documentation callouts. ## [2.23.1] - 2026-08-12 - Route 53 DNS-01 reliability ### Fixed - Route 53 DNS-01 automation now pins Caddy's provider configuration to the exact public hosted zone selected in CaddyUI. This prevents `_acme-challenge` TXT records from landing in a similarly named or split-horizon zone. ([#24](https://github.com/X4Applegate/caddyui/issues/24)) - Managed DNS automation policies now replace stale policies for the same certificate subjects while preserving unrelated and catch-all policies. - DNS-01 provider configuration is included in Caddy's preflight validation, so a missing provider module or invalid configuration is reported before route subtrees are changed. - Saving managed DNS with public A-record creation enabled now gives an actionable form error when the selected Caddy server has no public IP, instead of failing later at the provider. ### Added - Proxy hosts, redirections, and advanced routes can disable **Create public A records** while continuing to use the selected provider and zone for ACME DNS-01. This supports internal and split-DNS services without publishing a WAN address. - DNS-01-only mode is persisted across SQLite and MariaDB, preserved as target-specific policy during fleet sync, and shown explicitly in deployment status. ## [2.23.0] - 2026-08-12 - Amazon Route 53 managed DNS ### Added - **Amazon Route 53 provider support** for public hosted-zone discovery and managed A-record creation, collision detection, retargeting, and cleanup across proxy hosts, redirections, and advanced routes. Existing records are never overwritten automatically. ([#24](https://github.com/X4Applegate/caddyui/issues/24)) - Route 53 credentials in Settings support long-lived IAM access keys plus optional STS session tokens and partition-specific regions. The access-key pair is also emitted to Caddy for ordinary and wildcard DNS-01 certificate issuance. - The custom Caddy image now builds `github.com/caddy-dns/route53` v1.6.2, with regression tests covering provider configuration, pagination, aliases, and exact record-set deletion. - The in-app guide includes a scoped IAM policy template covering the CaddyUI record lifecycle and Caddy's DNS-01 challenge flow. ### Security - Private Route 53 hosted zones are intentionally excluded from the managed-DNS picker because public ACME resolvers cannot query them and split-horizon duplicate names are easy to select accidentally. - Record cleanup preserves the complete Route 53 record-set shape for the exact-delete semantics required by AWS. CaddyUI refuses to delete Traffic Flow-managed records so it cannot leave a billable traffic-policy instance orphaned. ### Changed - DNS credential descriptors can now mark region/session fields optional, and blank non-secret settings can be cleared without disturbing saved secret values. --- ## [2.22.0] - 2026-08-12 - Idempotent fleet configuration sync ### Added - **One-click full routing sync** on Administration → Caddy Fleet merges every proxy host, redirection, advanced route, and managed certificate definition from the environment selected in the application shell into another managed Caddy server. Target-only routes remain intact, while target-specific DNS records and custom certificate choices are preserved. ([#23](https://github.com/X4Applegate/caddyui/issues/23)) - Fleet deployments now persist source-to-target resource mappings so later source edits—including hostname, label, and managed-certificate subject changes—update the same target rows. ### Fixed - **“Also deploy to” is now idempotent.** Repeating a proxy, redirection, or managed-certificate deployment updates the existing target resource instead of inserting a duplicate. - Cross-deploy validates managed source/target environments and applies the same proxy/redirect domain-collision safeguards as ordinary resource creation. - Fleet copies no longer carry source DNS record IDs or custom certificate IDs into another server's configuration. ### Changed - Added regression coverage for repeated deployment, tracked hostname changes, target-policy preservation, domain collisions, full routing sync, and SQLite/MariaDB schema parity. --- ## [2.21.2] - 2026-08-10 - Resilient analytics log forwarding ### Added - **Analytics log forwarding can no longer make Caddy depend on CaddyUI at startup.** Settings → Analytics now exposes Caddy's network-writer `soft_start` option and enables it by default, so Caddy loads its config when the CaddyUI ingest listener is unavailable and sends logs to stderr until the connection recovers. ([#22](https://github.com/X4Applegate/caddyui/issues/22)) - **Configurable analytics connection timeout** limits network-writer connection attempts to 1–60 seconds and defaults to 5 seconds. ### Changed - Enabled analytics loggers are reconciled across managed fleet servers when CaddyUI starts, allowing upgrades to apply the safer writer defaults without requiring an administrator to re-save Settings. - Added regression tests for generated `soft_start` / `dial_timeout` JSON, upgrade defaults, explicit overrides, and startup reconciliation. --- ## [2.21.1] - 2026-08-10 - Proxy advanced config fleet validation ### Fixed - **Proxy-host Advanced Config now validates against the selected Caddy Fleet server** instead of always adapting snippets through the default bootstrap client. Hosts saved while a non-primary server is selected no longer fail Advanced Config validation just because that environment uses a different admin URL or module set. ([#21](https://github.com/X4Applegate/caddyui/issues/21)) - Added a regression test covering request-scoped Advanced Config validation so future fleet changes keep using the active server's admin API during proxy-host save operations. --- ## [2.21.0] - 2026-08-01 - Fleet access logging and CrowdSec protection ### Added - **Independent Caddy file access logs** in Settings, with fleet-server targeting, HTTP/HTTPS scope, native JSON or console encoding, absolute file path, maximum size, retained-file count, and retention-period controls. - **Analytics-safe logging composition** preserves CaddyUI's existing network analytics logger when file logging is enabled; disabling either destination leaves the other destination intact. - **CrowdSec bouncer integration** configures the `crowdsec` Caddy app and injects `http.handlers.crowdsec` before every protected CaddyUI-generated proxy, redirect, advanced, and plain-HTTP route. - **CrowdSec fleet controls** include selected servers, masked keep-existing API keys, streaming mode, refresh interval, hard-fail policy, exact-host exclusions, path-pattern exclusions, pre-save module validation, and a live LAPI connection test. - **Configurable client IP headers** complement trusted proxy ranges so CrowdSec and access logs can use Caddy's resolved client IP behind Cloudflare or another load balancer. - The repository's custom Caddy build now includes `github.com/hslatman/caddy-crowdsec-bouncer/http`, and the Compose stack persists `/var/log/caddy` in a named volume. ### Fixed - Trusted proxy and client-IP-header settings are now written to both CaddyUI-generated HTTP servers during sync and stale values are removed when cleared. --- ## [2.20.2] - 2026-07-30 - Fleet API and TLS/version reliability ### Fixed - **Version checks now follow every Docker Hub tag page** and select the highest semantic version across the complete repository. Installations no longer report an old `v2.9.x` release after newer tags move beyond the first page. - **Port-qualified proxy and redirection domains now use port-free Caddy host matchers and certificate identities**. A host such as `project.sub.example.com:8000` correctly reuses a managed `*.sub.example.com` wildcard instead of attempting an invalid exact certificate order containing `:8000`. - DNS-01 automation subjects, custom-certificate exclusions, wildcard discovery, and cross-server wildcard deployment now share the same normalized hostname behavior. ### Added - **Read-only fleet API**: authenticated clients can call `GET /api/v1/servers` with a session or API token to retrieve server IDs, names, admin URLs, management types, health, Caddy versions, tags, and last-contact timestamps without scraping the Caddy Fleet page. - The fleet response intentionally excludes admin credentials and public IP configuration. It is available to every authenticated role, matching the fleet switcher already rendered in the signed-in application shell. --- ## [2.20.1] - 2026-07-29 - Actionable unused-certificate cleanup ### Fixed - **Unused certificate recommendations now identify the certificates** instead of linking to an unfiltered list with only a count. - **Managed ACME definitions are no longer reported as unused**. Standalone managed and wildcard certificates are intentionally automated by Caddy and are not attached through a resource `certificate_id`. - Certificate usage is checked against every proxy host, redirection, and advanced route in the current environment, preventing tenant-scoped views from incorrectly labeling a shared certificate as unused. ### Added - **Filtered cleanup view**: the Operations recommendation opens `/certificates?usage=unused`, where only unassigned PEM/file-path certificates appear and every row carries an **Unused** marker. - **Named dashboard detail**: the recommendation includes the first affected certificate names before the operator leaves Operations. - **Persistent dismissal**: administrators can dismiss the cleanup recommendation for the selected Caddy environment. The exact unused-certificate set is remembered; the recommendation automatically returns if a different certificate becomes unused later. - **Cleanup guidance** explains that assigning or deleting the listed certificates clears the condition. --- ## [2.20.0] - 2026-07-29 - Enterprise workflows and guided operations ### Added - **First-run account wizard**: new installations begin with a clear four-stage operating journey—create the administrator, connect Caddy, secure the account, and publish the first service. - **Live readiness guide**: **Getting Started** derives progress from the actual selected Caddy environment, TOTP state, DNS profiles, and proxy inventory instead of storing a dismissible tutorial flag. Administrators can return to it whenever an environment changes. - **Guided service publishing**: new proxy hosts now use a focused Hostname → Upstream → Policy → Review workflow. The final review shows the effective hostname, upstream, HTTPS policy, DNS automation, and initial state before CaddyUI writes configuration. - **Advanced-editor escape hatch**: experienced operators can open the complete proxy-host editor with one click. Existing hosts continue to use the full editor, and guided creation preserves every established advanced default. - **Global create menu**: the persistent application bar provides direct entry points for publishing a service, creating a redirection, adding a certificate, or building an advanced route. - **Environment readiness on Operations**: incomplete environments receive a compact, actionable readiness summary that links back to the live guide. ### Changed - **Semantic color contract** is now explicit and consistent: blue means action/current context, green means healthy/enabled/completed, amber means pending/degraded/maintenance, red means failure/offline/destructive, violet means TLS/automation/managed policy, and gray means disabled/unknown/informational. - **Caddy fleet workflow** now presents registered environments as an operational inventory with environment, health, admin endpoint, management policy, last contact, and scoped actions. - **Caddy connection form** now follows an Identify → Connect → Authorize sequence and explains safe admin-API connectivity before configuration is saved. - **Proxy Hosts** has an enterprise page header, workflow language, and a primary **Publish service** action. - **Required readiness** uses four foundations. DNS automation remains visible as an optional capability and never prevents an environment from reaching 100% readiness. - **Navigation terminology** now uses **Caddy Fleet** and **Publish service** consistently where those labels better describe the operator's goal. ### Accessibility - Guided steps expose current-state semantics, native form validation, predictable Back/Continue controls, keyboard focus targets, and a review stage before submission. - Setup, onboarding, publishing, fleet, and connection workflows have dedicated compact layouts for narrow screens and retain the existing reduced-motion behavior. - The enterprise workflow system supports light, dark, Carbon Orange, Forest, Rose, and Indigo themes through shared semantic tokens. --- ## [2.19.0] - 2026-07-29 - MariaDB and large-installation performance ### Added - **Optional MariaDB backend**: set `CADDYUI_DB_DRIVER=mariadb` and `CADDYUI_DB_DSN` to run CaddyUI on MariaDB while SQLite remains the default. - **Automatic MariaDB schema and migrations**: fresh databases receive the complete CaddyUI schema, including all current proxy-host fields and performance indexes. - **Read-only SQLite → MariaDB migration command**: `caddyui migrate-db --from-sqlite /data/caddyui.db` preserves IDs and relationships, copies in bounded transactions, rejects non-empty destinations, and supports `--skip-analytics` for oversized traffic histories. - **MariaDB deployment overlay and CI**: `docker-compose.mariadb.yml` provides a health-checked MariaDB 11.4 service; CI validates schema parity, core resource queries, and cross-database migration. ### Performance - **Proxy Hosts traffic lookup** now queries only domains visible on the page. SQLite uses the existing `(host, ts)` covering index instead of scanning the complete analytics history. - **Post-render upstream checks** start after the browser's initial paint and pause while the tab is hidden. - **List-page indexes** cover proxy ordering, advanced routes, certificate options, and last-sync activity lookups. - **Portable daily rollups** use timestamp ranges instead of engine-specific date conversion, improving index use on both SQLite and MariaDB. ### Changed - Sessions, login throttling, activity retention, settings upserts, and analytics rollups now use portable SQL shared by both database backends. - Settings explains that MariaDB backups belong to the database platform; Caddy configuration snapshots continue to work normally. --- ## [2.18.0] - 2026-07-29 - Enterprise operations UI ### Added - **Enterprise operations center**: the dashboard now organizes active-server identity, route inventory, host health, traffic, system telemetry, recommendations, fleet status, recent changes, and live upstream reachability into one operational hierarchy. - **Semantic design system**: reusable surface, border, text, brand, status, spacing, radius, shadow, panel, button, metric, table, and empty-state primitives establish a consistent foundation for future page migrations. - **Persistent command search**: desktop users can open the existing resource search directly from the top bar; the button uses the same reset, fetch, focus, and keyboard navigation workflow as `Ctrl/⌘+K`. - **Workflow-first actions**: Create proxy host, Sync Caddy, and Manage servers are visible at the point of use. The create action remains hidden for read-only viewers. ### Changed - **Application navigation**: destinations are regrouped into Overview, Traffic, Observe, Configuration, Developer, and Administration sections without changing routes or role-based access. - **Environment context**: the active Caddy server and status are promoted in both the sidebar and persistent top bar, with faster switching in multi-server installations. - **Mobile shell**: the previous stacked mobile toolbars are consolidated into one compact bar with navigation, environment, search, theme, and account controls. - **Dashboard density**: redundant cards are consolidated into structured panels and responsive data grids that scale from a phone to a wide operations display. - **Dashboard documentation**: the README screenshot now reflects the enterprise operations layout. ### Accessibility - **Keyboard focus visibility** is standardized across links, buttons, inputs, selects, textareas, and disclosure controls. - **Reduced-motion support** disables nonessential application-shell transitions and animations when requested by the operating system. - **Responsive validation** covers 390 px mobile and 1440 px desktop layouts without horizontal page overflow. --- ## [2.17.3] - 2026-07-29 - Visible certificate selection for Auto TLS ### Added - **Certificate identity in the Caddy-managed table**: the Certificates page now shows which certificate each Auto TLS domain uses. Hosts covered by a managed wildcard display that certificate's CaddyUI name; hosts without wildcard coverage display **Direct certificate**. - **Responsive certificate identity**: the same wildcard/direct distinction appears in both the desktop table and mobile certificate cards. --- ## [2.17.2] - 2026-07-29 - Enforced wildcard reuse and live renewal visibility ### Fixed - **Auto TLS now reuses managed wildcards**: exact proxy, redirect, and advanced-route hostnames covered by a standalone managed wildcard are added to Caddy's `automatic_https.skip_certificates` list. HTTPS remains enabled, but Caddy no longer obtains an unnecessary exact-host certificate and instead serves the wildcard already managed in its cache. - **Safe wildcard boundaries**: only exact hostnames covered by a one-label wildcard are skipped. The wildcard subject itself remains eligible for DNS-01 issuance; apex and multi-level names still obtain their own certificate unless separately covered. ### Added - **Live per-server managed certificate status**: editing a managed certificate now shows every configured Caddy server, whether the definition is deployed there, the certificate actually served, issuer, expiration timestamp, days remaining, and renewal problems such as expired, mismatched, or unreachable certificates. - **Wildcard-safe certificate probing**: CaddyUI uses a synthetic wildcard-covered SNI name and connects directly to each Caddy server, so a standalone wildcard can be inspected without creating public DNS for a fake hostname. - **Managed wildcard documentation**: the GitHub README and Docker Hub overview now include the complete setup, multi-server deployment, Auto TLS reuse, provider-module requirement, and renewal-status workflow. --- ## [2.17.1] - 2026-07-29 - Multi-server wildcard certificate workflow ### Added - **Managed certificate cross-deploy**: Certificates → New/Edit can copy a managed ACME DNS-01 definition to selected Caddy servers. Each server obtains and renews its own certificate and private key; secrets and certificate material are not copied between Caddy instances. - **Automatic wildcard discovery during proxy cross-deploy**: when “Also deploy to” is selected on a proxy host, CaddyUI finds managed certificates on the source server that cover the hostname and ensures equivalent definitions exist on every target. This works for servers added later without permanent cross-server certificate links. - **Wildcard matching safeguards**: automatic reuse follows WebPKI semantics—`*.example.com` covers `app.example.com`, but not the apex or multi-level `deep.app.example.com`. ### Fixed - **Managed certificate expiry display**: standalone ACME rows now show **Auto-renewed** in the Expires column instead of an unexplained blank value. - **Multi-server form guidance**: certificate and proxy forms now explain which configuration is replicated, that each Caddy instance owns its own key/order, and how managed wildcards are reused. --- ## [2.17.0] - 2026-07-29 - Standalone managed wildcard certificates ### Added - **Managed ACME source on Certificates**: create `*.example.com`, `*.sub.example.com`, or ordinary SAN certificates using any saved DNS credential profile. Caddy obtains and renews them through DNS-01; CaddyUI never stores or exports the private key. - **Certificate-only wildcard routes**: managed entries emit a terminal 404 fallback host route, which instructs Caddy Automatic HTTPS to manage the requested subjects without requiring a fake proxy upstream. Real proxy and redirect routes retain priority and reuse the wildcard certificate. - **Guided setup**: the certificate form and built-in documentation explain the Settings → DNS prerequisite, provider-module requirement, expected 404 fallback, and how Caddy reuses the certificate. ### Changed - **Certificate selectors stay unambiguous**: managed ACME entries do not appear in the custom-certificate dropdown because they are automatically selected by SNI rather than attached as uploaded PEM material. --- ## [2.16.10] - 2026-07-28 - DNS automation sync compatibility ### Fixed - **Fresh Caddy DNS-01 sync**: Managed DNS automation policies now create the missing `apps.tls` configuration branch before writing `apps.tls.automation`. This prevents `sync_apply_automation_failed` with Caddy's `invalid traversal path at: config/apps/tls/automation` error on installations that do not already have a TLS app. - **Existing TLS settings preserved**: installations that already have `apps.tls` continue using the narrow automation-only update, preserving unrelated TLS configuration and avoiding unnecessary TLS module reprovisioning. --- ## [2.16.9] - 2026-07-26 - Caddy HTTP listener compatibility ### Fixed - **Default port-80 sync failure**: CaddyUI now owns one explicit HTTP server for both open-HTTP hosts and Force SSL redirects, while disabling Caddy's competing automatic redirect listener. Sync works with Caddy's default `http_port` and no longer requires moving that port as a workaround. - **Force SSL routing**: proxy hosts, redirections, and advanced routes with Force SSL enabled receive a CaddyUI-managed permanent HTTPS redirect; routes with Force SSL disabled continue serving normally over both HTTP and HTTPS. - **Safe listener transitions**: sync disables Caddy's automatic redirect listener before creating the CaddyUI listener, and reverses that order when removing it, so live subtree updates never attempt to bind port 80 twice. --- ## [2.16.8] - 2026-07-25 - Faster Proxy Hosts page ### Fixed - **Proxy Hosts page load time**: the list now selects only the 19 fields it renders instead of loading and scanning every optional proxy-host field, including large advanced-config, header, rule, and custom-page blobs. - **Duplicate live Caddy request**: the separate Live upstream status panel no longer starts a second Caddy admin request automatically when the page opens. Row health continues loading normally; the detailed panel refreshes on demand. - **Health endpoint query size**: live row-health checks reuse the lightweight proxy-host summary query. --- ## [2.16.7] - 2026-07-25 - DNS-01 certificate support and Caddy sync compatibility ### Added - **DNS-01 for every Managed DNS provider**: proxy hosts, redirections, and advanced routes with Managed DNS selected now reuse those credentials for Let's Encrypt DNS-01 issuance. Supported providers are Cloudflare, Porkbun, Namecheap, GoDaddy, DigitalOcean, and Hetzner; the connected Caddy build must include the matching `caddy-dns` module. - **All-provider Caddy build**: `Dockerfile.caddy` now includes the six DNS provider modules supported by CaddyUI. ### Fixed - **Caddy sync `skip_redirects` rejection**: CaddyUI no longer emits the unsupported `automatic_https.skip_redirects` field. Hosts with Force SSL disabled are served through an explicit CaddyUI-owned `:80` server, matching Caddy's native Caddyfile adaptation while retaining HTTPS on `:443`. - **Affected config cleanup**: sync removes any stale `skip_redirects` key before validation, so existing installations recover on their next sync. --- ## [2.16.6] - 2026-07-24 - MCP community integration and token-scope hardening ### Added - **Community MCP integration docs**: added a README link to the community-maintained [CaddyUI-MCP](https://github.com/loryanstrant/CaddyUI-MCP) project, with guidance to use dedicated users and narrow API token scopes for agent automation. - **API token scope documentation**: the REST API reference now explains `read_only`, `proxy_write`, and `full` token behavior, including the expectation that integrations managing redirections, raw routes, or certificates need a full-access token from an appropriately limited user. ### Fixed - **`proxy_write` API token enforcement**: `proxy_write` bearer tokens are now limited to writes under `/api/v1/proxy-hosts`. Other write routes remain read-only for that token scope instead of behaving like full access. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.16.6` published. --- ## [2.16.3] - 2026-07-21 - Dashboard recommendations and CI gate ### Added - **Dashboard recommended actions**: the dashboard now surfaces a compact advisor panel for actionable operational issues, including down upstreams, maintenance mode, SSL-disabled enabled hosts, incomplete managed DNS, missing DNS profile references, expired or soon-expiring custom PEM certificates, stale or missing syncs, missing snapshots, disabled 2FA enforcement, and an empty admin IP allowlist. - **Basic Go CI gate**: added a GitHub Actions workflow that runs `go test ./...`, `go vet ./...`, and `go build ./cmd/caddyui` on pushes and pull requests to `main`. - **Dashboard advisor tests**: added focused unit tests covering operational recommendations, admin hardening recommendations, certificate expiry detection, and recommendation noise limits. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.16.3` published. --- ## [2.16.2] - 2026-07-21 - Proxy Hosts loading speed hotfix ### Fixed - **Proxy Hosts page slowdown after v2.16.1**: the new bulk certificate selector now loads lightweight certificate options (`id`, `name`, `domains`) instead of full certificate PEM/key payloads. This avoids dragging private key blobs into the proxy-host list render path. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.16.2` published. --- ## [2.16.1] - 2026-07-21 - Bulk proxy certificate assignment ### Added - **Bulk apply certificates for proxy hosts**: the proxy-host bulk action bar now includes a certificate selector. Select multiple proxy hosts, choose a custom certificate or `Auto / ACME`, and apply once. ### Changed - **Single sync after bulk certificate changes**: CaddyUI updates the selected proxy hosts and syncs Caddy once with TLS refresh, avoiding the old one-host-at-a-time workflow when rotating renewed certificates across many containers. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.16.1` published. --- ## [2.16.0] - 2026-07-21 - Multiple DNS credential profiles ### Added - **Multiple Cloudflare DNS credential profiles**: Settings → DNS now supports additional named Cloudflare profiles, each with its own API token, proxy mode, and zone allow-list. This lets one CaddyUI install manage domains split across multiple Cloudflare accounts or tokens. - **Per-host DNS profile selection**: proxy hosts, redirection hosts, and raw routes can pick a specific DNS credential profile while the legacy provider-wide settings continue to work unchanged. ### Changed - **Managed DNS lifecycle**: zone loading, existing-record checks, record create/delete, public-IP retargeting, and wildcard DNS-01 automation now resolve credentials from the selected profile. - **Safety guard**: if a saved row references a deleted/missing DNS profile, provider calls no-op instead of falling back to the global token and risking changes in the wrong account. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.16.0` published. --- ## [2.15.3] - 2026-07-21 - Security dependency refresh ### Security - **Dependabot crypto alerts**: updated `golang.org/x/crypto` from `v0.45.0` to `v0.52.0`, resolving the open GitHub Dependabot alerts for the module. - **Go toolchain**: updated the module and Docker build stage to Go 1.25 so the patched crypto module can be used. - **Transitive modules**: refreshed `golang.org/x/sys` from `v0.38.0` to `v0.45.0` via `go mod tidy`. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.15.3` published. --- ## [2.15.2] - 2026-05-25 - Simple mode keeps DNS & TLS visible ### Fixed - **Proxy form Simple mode**: DNS and TLS sections (`ph-dns`, `ph-tls`) now stay visible in Simple mode — those are the two most essential sections for any proxy host. Previously Simple mode hid everything including those, making it unusable. All other advanced sections (Security, Headers, Path routing, Rewrites, etc.) remain hidden until Advanced mode is selected. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.15.2` published. --- ## [2.15.1] - 2026-05-25 - Cache-bust static assets so new themes load immediately ### Fixed - **Static asset cache-busting**: `app.css`, `app.js`, and `htmx.min.js` URLs now include `?v={{.AppVersion}}` so browsers always fetch the latest version after an upgrade instead of serving the 24h-cached old file. This was why the Forest / Rose / Indigo themes appeared as blue on first load — the new CSS never arrived. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.15.1` published. --- ## [2.15.0] - 2026-05-25 - Major UI overhaul: toasts, sparklines, themes, cert bars, quick-mode form ### Added - **Toast notifications**: all form success/error/warning banners now slide in as auto-dismissing toasts (bottom-right, spring animation). Errors stay until dismissed. Maintenance banners remain inline. `window.caddyuiToast(msg, type, duration)` exposed for programmatic use. - **Dashboard sparklines**: the three stat cards (Requests / Visitors / Bandwidth) now show a 7-day trend sparkline inline with the number. Fetched from new `/api/dashboard-sparklines` endpoint after first paint. - **Certificate expiry progress bars**: each certificate now shows a thin horizontal bar below the name — green (≥ 60 d), yellow (< 60 d), amber (< 30 d), red (< 15 d / expired). Visual urgency at a glance in both mobile cards and desktop table. - **3 new color themes**: Forest Green, Rose / Crimson, Deep Indigo — each with full light + dark palette. Theme picker in Settings now offers 5 options (+ existing Default Slate Blue and Carbon Orange). - **Proxy form Simple / Advanced mode**: a pill toggle above the proxy host form hides all `
` expansion sections in Simple mode, showing only Domain, Forward Host/Port, and SSL — perfect for new users. Preference persisted to `localStorage`. Defaults to Simple on `/new`, Advanced on edit. - **Toast container** (`#toast-container`) added to layout — `pointer-events: none` wrapper so it never blocks clicks. ### Fixed - **Dark mode inconsistency in Settings**: all `dark:*-gray-*` Tailwind utility classes in `settings.html` replaced with the canonical `ink-*` token system used everywhere else. Resolves card background mismatches in dark mode (e.g., `dark:bg-gray-900` vs the `.dark .bg-white` override). ### Backend - **`GET /api/dashboard-sparklines`**: new authenticated endpoint returning 7 days of daily `{ views, visitors, bandwidth }` totals scoped to the active server's hostnames. Used by JS sparkline renderer. - **Color theme validation**: `postMyColorTheme` now accepts `forest`, `rose`, `indigo` in addition to `default` and `orange`. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.15.0` published. --- ## [2.14.3] - 2026-05-15 - Fresh-install Caddy crash-loop fix ### Fixed - **Fresh install**: `docker-compose.yml` command now bootstraps an empty `{}` config on first boot so Caddy starts cleanly when `autosave.json` doesn't exist yet on a fresh volume. Previously Caddy would crash-loop with `open /config/caddy/autosave.json: no such file or directory`. Existing installs are unaffected — the one-liner is guarded by `[ -f ... ] ||` and skips entirely when the file already exists. ([#7](https://github.com/X4Applegate/caddyui/issues/7)) ### Docs - `README.md` Quick Start and Agent mode compose examples updated with the bootstrap command. - `DOCKERHUB.md` compose example updated with the bootstrap command and a fresh-install callout note. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.14.3` published. --- ## [2.14.2] - 2026-05-13 - Porkbun API spec full compatibility ### Fixed - **Porkbun domain struct**: added `securityLock`, `apiAccess`, and `notLocal` fields to the `Domain` struct - previously missing, silently dropped during decode. All three use the flexible `pbInt` type to handle string-or-int like the other integer fields. - **Porkbun SSL import**: `publickey` field from `/ssl/retrieve` is now captured and returned in the `SSLBundle`. - **Porkbun DNS create**: `id` field in the create-record response is now decoded as `string` (matching the v3 spec) instead of `json.Number`. > `:preview`, `:stable`, `:latest` updated. Pinned tag `:v2.14.2` published. --- ## [2.14.1] - 2026-05-13 - Porkbun whoisPrivacy / autoRenew type fix ### Fixed - **Porkbun JSON type mismatch**: Porkbun's `/domain/listAll` returns `whoisPrivacy` and `autoRenew` as quoted strings instead of bare integers. Added a private `pbInt` type with a custom `UnmarshalJSON` that accepts both forms. Resolves the "cannot unmarshal string into Go struct field Domain.domains.whoisPrivacy of type int" error on the Import from Porkbun and DNS pages. (Reported: issue #6) > `:preview` updated. Pinned tag `:v2.14.1` published. --- ## [2.14.0] — 2026-05-13 · Porkbun SSL certificate import ### Added - **Import from Porkbun**: new button on the Certificates page (visible when Porkbun API credentials are configured) that opens a domain picker and pulls the SSL bundle directly from your Porkbun account into CaddyUI as a stored PEM certificate. Uses the Porkbun API v3 endpoint. - method added to the internal Porkbun client (). - New page at with domain dropdown (lists all Porkbun domains via ). ## [2.13.0] — 2026-05-10 · REST API v1 — full coverage for raw routes and certificates > `:preview` updated. Pinned tag `:v2.13.0` published. ### Added - **v2.13.0 — REST API v1 for raw routes.** Full CRUD + toggle: `GET /api/v1/raw-routes`, `GET /api/v1/raw-routes/{id}`, `POST /api/v1/raw-routes`, `PUT /api/v1/raw-routes/{id}`, `DELETE /api/v1/raw-routes/{id}`, `POST /api/v1/raw-routes/{id}/toggle`. Create/update accept `label`, `json_data`, `caddyfile_src`, `enabled`, `certificate_id`, `force_ssl`, `block_common_exploits`. Responses include the same fields plus `owner_email`, `created_at`, `updated_at`. Caddy is synced after every write. Requires write-scoped token or admin/write role for mutations. - **v2.13.0 — REST API v1 for certificates.** Full CRUD: `GET /api/v1/certificates`, `GET /api/v1/certificates/{id}`, `POST /api/v1/certificates`, `PUT /api/v1/certificates/{id}`, `DELETE /api/v1/certificates/{id}`. Create/update accept `name`, `domains`, `source` (`pem` or `path`), `cert_pem`, `key_pem`, `cert_path`, `key_path`. Delete is blocked with HTTP 409 if the certificate is still referenced by any proxy host, redirection host, or raw route. Respects the existing per-user ownership model — non-admin users see only their own uploads plus global (admin-owned) certs. - **v2.13.0 — `models.ToggleRawRoute`.** Added `ToggleRawRoute` to the models package (mirrors `ToggleProxyHost` / `ToggleRedirectionHost`) to support the new API toggle endpoint cleanly. --- ## [2.12.54] — 2026-05-10 · REST API v1 — full proxy host field coverage + docs fixes > `:preview` updated. Pinned tag `:v2.12.54` published (multi-arch `linux/amd64` + `linux/arm64`). ### Added / Fixed - **v2.12.54 — PUT /api/v1/proxy-hosts/{id} now applies all 42 fields.** The update handler previously merged only 16 of the 42 fields accepted by the create endpoint — CORS settings, compression, security headers, TLS minimum version, health-check URI/interval/method, maintenance message, upstream timeout, keepalive connections, sticky sessions, load-balancer policy, strip-response headers, blocked agents, upstream SNI, HSTS preload, max connections per host, upstream retries, force-HTTP/1, proxy protocol, extra upstreams, basic-auth enabled, and certificate ID were all silently ignored on PUT. Every field in `apiProxyHostInput` is now applied on update (strings: merge if non-empty; booleans/ints: always applied). - **v2.12.54 — API docs corrected field names.** The `/api/docs` reference page used wrong field names in several examples: `domain_names` → `domains`, `forward_domain_name` → `forward_domain`, `http_code` → `forward_http_code`. Examples now show the actual JSON keys the API returns and include the full set of response fields. --- ## [2.12.53] — 2026-05-03 · Multi-provider AI + Lighthouse 99/100/100/100 + Caddyfile export + bug fixes (cumulative) > **Patch wave on top of v2.12.35.** Pinned tag `:v2.12.53` (multi-arch `linux/amd64` + `linux/arm64`). `:latest` + `:stable` retag from v2.12.35 → v2.12.53. `:preview` rolls on every dev push. Eighteen patch versions in one cumulative entry, covering the multi-provider AI assistant (v2.12.36), the long performance + accessibility wave that took PageSpeed from ~30 to **99 desktop / 76 mobile / 100 a11y / 100 SEO / 100 best-practices** on `/login`, the new Caddyfile export, and a critical SQL-INSERT column-count fix that affected fresh installs of v2.12.48–v2.12.52. ### Critical fix (v2.12.53) - **v2.12.53 — proxy host INSERT had 331 placeholders for 330 columns.** Introduced in v2.12.52 when `disable_upstream_compression` was added and an unrelated field was partly reverted, leaving one stray `?` in the `VALUES (...)` line. Reproducible on a fresh DB: any "New Proxy Host" submit failed with `SQL logic error: 331 values for 330 columns (1)`. Reported via [issue #5](https://github.com/X4Applegate/caddyui/issues/5). Fixed by regenerating the `VALUES` line via Python so the count is provably correct, plus an audit script in the commit message that catches this kind of regression across every INSERT in `models.go`. - **v2.12.53 — friendly bind-mount permission error.** Bind-mount users on docker-compose / podman-compose used to hit cryptic SQLite "unable to open database file (1)" errors when the host directory wasn't owned by uid 10001. The binary now probes `/data` writability at startup and prints an actionable multi-line error pointing at the three reasonable fixes (chown / `--user 0:0` / named volume). ### Caddyfile export (v2.12.49 + v2.12.50) - **v2.12.49 — Caddyfile export, full-server.** New `GET /proxy-hosts/export-all.caddyfile` endpoint + UI button next to "Export JSON". Exports every enabled proxy host, redirection, and raw route on the active server as a Caddyfile, round-trippable through the existing `/caddyfile-import` paste flow for the directives it contains. Same RBAC scoping as the JSON exports — admin gets the full server config, non-admin only their own resources. Stable sort by hostname for diffability. Per-host `# Notes:` block lists settings that don't fit cleanly into Caddyfile syntax (maintenance windows, basic-auth users, scheduled blocks, IP allow/deny, URL rewrites, path-based upstreams, blocked agents) so users see what was elided rather than silently losing it on round-trip. - **v2.12.50 — Caddyfile export, per-host.** Single-host download from the proxy-host edit page. Same renderer as the full-server export. ### Notification channels (v2.12.51) - **v2.12.51 — ntfy.sh notification channel + sendNotification fan-out wrapper.** Adds [ntfy.sh](https://ntfy.sh) alongside the existing webhook + email notification channels. Useful for self-hosted push notifications without running a Discord/Slack workspace. Configure in **Settings → Notifications**: ntfy server URL (default `https://ntfy.sh`), topic name, optional auth token. The new `sendNotification` wrapper fans out cert-expiry / upstream-down events to every enabled channel in one call. ### Performance + accessibility wave (v2.12.38 → v2.12.48) | Version | Change | Mobile | Desktop | |---|---|---|---| | ~v2.12.37 baseline | Pre-tuning | ~30–40 | ~50–60 | | v2.12.38 | `Cache-Control: max-age=86400` on `/static/*` + custom request/response header `+ Add` buttons (the buttons existed but had no JS click handler) | ~40 | ~70 | | v2.12.39 | Self-host Inter variable font + htmx (drop fonts.gstatic.com + unpkg.com from cold loads); service-worker rewrite (auto-purges stale caches per release) | ~50 | ~80 | | v2.12.40 | Preload Inter font, preconnect cdn.tailwindcss.com | ~55 | **91** | | v2.12.41 | Externalize 28 KB inline JS to `/static/app.js` (cached across navigations); TBT 2,230 ms → 690 ms | ~55 | ~95 | | v2.12.42 | Preload `app.js`, defer non-critical `/api/version-check` + `/api/ai/status` past Lighthouse's measurement window; TBT 690 ms → 0 ms | 55 | ~96 | | v2.12.43 | `auth.css` for `/login` (24 KB precompiled, drops 124 KB Tailwind CDN runtime on the page Lighthouse always tests) | **76** | ~99 | | v2.12.44 | Section anchor pills on the proxy host form, ordered by usage frequency | 76 | 99 | | v2.12.45 | Physical reorder of proxy host form sections to match the pill priority | 76 | 99 | | v2.12.46 | `aria-current="page"` on active nav link, `aria-label`s on icon-only buttons, skip-to-main-content link | 76 | 99 | | v2.12.47 | `