Repository navigation
v0.3.0
hotserve 0.3.0 puts the box's config in git. The box's Caddyfile can live in a private repository of its own. A change reaches the box only as a commit signed by a key the box lists, sent by that repository's workflow with no stored secret, and checked by the box's own binary before it is applied. Your first deploy now sets this up on day 0, from the new hotserve-example-box template.
Upgrading from 0.2.x
Upgrading has the steps. Two things to know:
- dpkg asks about
/etc/hotserve/Caddyfileif you have edited it, because the starter file's text changed. Answer N, the default, to keep yours. Answering Y would replace your config with the starter. - Nothing else changes until you choose to. A box without a box repository keeps its hand-edited
/etc/hotserve/Caddyfile, and a reload still applies it. The box channel starts only when you runhotserve initfrom a box repository: the box README's Set it up covers a box that already serves its own file.
New: the box's config in git
hotserve init <commit>, run as root in the box's directory from your repository, installs that file and records the commit the box starts from.hotserve box baselineresets that commit, for example after main is force-pushed.hotserve box describeandhotserve box webhookread a box's file the way the box does.box_webhook, on the deploy host's site, is where the repository's workflow sends each push. The box checks the run's token and every commit's signature since the commit it runs, checks that the file is the one in the commit, validates it, and then applies it. A refused push changes nothing, and a reload that fails puts the previous file back. The workflow's run prints the box's verdict and the installed-versus-pushed diff. On the box,journalctl -u hotserve-box-applysays what it did.- The template:
make signermakes your SSH key the first signer,make checkchecks each box's file, and the workflow checks every push and pull request, then sendsmainto each box. GitHub's merge button cannot land a change the box will apply: land withgit merge --ff-onlyfrom a signer's laptop. - Docs: the tutorial's step 3 is the box repository. The second page runs the box first and hardens it second: SSH by key only, port 22 shut to everyone but you, and an optional administrator. Root keeps key-only SSH as the way back at 3am.
Behaviour changes
Caddy v2.11.6 (#163). Apps behind hotserve will see these upstream changes:
- Request headers are capped at 16 KiB by default (was 1 MB); larger ones get
431. The fix is themax_header_sizeserver option. - Request header names containing
.are dropped, the same as names with_since 2.11.4. Opt back in withexpected_dot_headers. - New 1-minute idle read/write timeouts: a body read or response write that makes no progress for a minute is aborted. Pauses between SSE writes don't count.
- A wildcard site's
client_authno longer carries over to more specific site blocks. Config validation is stricter, andmethodmatcher values are uppercased. hotserveCLI errors, fromhotserve validatefor example, are no longer a plainError: ...line. They go through Caddy's logger: coloured on a terminal, and a JSON log line when piped. The message text is unchanged, but scripts that grep forError:need adjusting.
penaltybox:
- IPv6 clients are counted by /64 (#194). Addresses in one /64 share a budget and a box, so a client can no longer step to a fresh address to avoid one. IPv4-mapped (
::ffff:…) and well-known-prefix NAT64 (64:ff9b::…) clients are counted as their IPv4 address. This applies to any key whose whole value is one IP address, a header key such asCF-Connecting-IPincluded. Clients sharing a /64 now share a budget: a SLAAC office or home network, customers given /128s from a shared /64, and IPv4 clients behind NAT64 with a network-specific prefix or behind Teredo. The hash that spreads keys across the store's shards is also seeded at random each time the store is built, so a client can't pick keys that fill one shard. No config change is needed. - A warning when
max_keysis below 128 (#196). That leaves one slot per shard, so the store tracks 64 clients, notmax_keys. It is a warning only, never a refusal. - The hint header is stripped on the empty-key path too (#199), when
stripis on. Before, a client could leave a header key out and see the origin's hint. A hint sent as an HTTP trailer or on a 1xx response is still not stripped: send it as a header. hint_penaltyboxgoes first in arouteblock (#201). Outside aroute, Caddy sorts it afterhandleand aftercache, so cached hits are neither counted nor box-checked. The starter Caddyfile's commented example now wraps it,cacheandreverse_proxyin aroute. If you uncommented the old example, wrap those lines the same way.
Recovery (#203). A state.json that is not hotserve's own regular file now stops recovery for that app, with a log line saying why. This covers a link (it is no longer followed), a dangling link, a FIFO, or a file over 64 KiB. To fix it, redeploy the app, or remove the file and restart hotserve.
New warnings (warnings only, never refusals):
- an
env_filethat every account on the box can read or write, at config load and at each launch (#191); - a
deploy_trustblock wider than it reads, at config load (#189).
The deploy record names the sha256 a deploy was pinned to, and a rollback carries the pin forward (#185).
The example templates. Repositories made from a template are copies that do not update; copy the new file in to get each change.
scripts/deploy.sh(Node, Deno):bin/apply(box):
Known issues
These are tracked in #223:
- The tutorial's steps 1 and 2 say step 2 detects a host that cannot run the sandbox. It is first detected at step 3's
hotserve init. Use Debian 13: an older Debian installs and runs, then fails at step 3. - In the box template, a
HOTSERVE_VERSIONrepository variable of justvinstalls the latest release instead of being refused.
Not in this release
Backups. v0.3.0-rc1 was cut from the backup branch, which is on hold (#179); 0.3.0 is cut from main and does not include them.
What's Changed
- docs: the artifact byte cap is for the whole archive, not for each file in it by @earthlingdavey in #126
- build(deps): bump go.opentelemetry.io/otel/sdk from 1.44.0 to 1.45.0 in /penaltybox by @dependabot[bot] in #127
- build(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc from 1.43.0 to 1.45.0 in /penaltybox by @dependabot[bot] in #130
- build(deps): bump go.opentelemetry.io/otel/sdk from 1.44.0 to 1.45.0 in /liveswap by @dependabot[bot] in #132
- build(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.43.0 to 1.45.0 by @dependabot[bot] in #134
- build(deps): bump github.com/darkweak/storages/otter/caddy from 0.0.19 to 0.0.20 in the go-deps group across 1 directory by @dependabot[bot] in #142
- liveswap: an archive's symlinks cannot take extraction outside the release by @earthlingdavey in #148
- liveswap: a failed artifact fetch does not print the hop's query string by @earthlingdavey in #149
- liveswap: review follow-ups whose fix needed no decision (F-4, F-5, F-9, F-12, F-16 + three low) by @earthlingdavey in #150
- build(deps): bump the go-deps group across 2 directories with 2 updates by @dependabot[bot] in #157
- ci(vulncheck): accept unreachable GO-2026-6508 via pin-watch alert_dismissals by @earthlingdavey in #158
- build(deps): bump caddy to v2.11.6 by @earthlingdavey in #163
- build(deps): bump golangci/golangci-lint from v2.13.2 to v2.14.0 in the compose-images group by @dependabot[bot] in #164
- build(deps): bump the dockerfiles group across 2 directories with 1 update by @dependabot[bot] in #156
- ci(pin-watch): drop the GHSA-w34q and GHSA-gcjh alert dismissals by @earthlingdavey in #165
- fix(liveswap): a failed deploy's log tail keeps the lines journald could not name a unit on (PR 7, liveswap half) by @earthlingdavey in #167
- build(deps): bump golang.org/x/net to v0.60.0 (fixes govulncheck gate) by @earthlingdavey in #181
- docs: introduce hotserve as a standalone product by @earthlingdavey in #180
- build(make): package clears the stale .deb before building (#151) by @earthlingdavey in #184
- ci(fuzz): fuzztime reaches go test as data, not shell (#151) by @earthlingdavey in #183
- fix(packaging): prerm deconfigure no longer tears down the box (#151) by @earthlingdavey in #182
- feat(liveswap): the deploy record names the sha256 a deploy was pinned to (#145) by @earthlingdavey in #185
- fix(examples): deploy.sh refuses a non-https HOTSERVE_URL before minting (#151) by @earthlingdavey in #186
- fix(packaging): postinstall says when the upgrade restart failed (#151) by @earthlingdavey in #187
- docs(box): the config channel's design, and its place in the threat model (PR 0) by @earthlingdavey in #178
- feat(liveswap): an env_file open to every account is warned about, at load and at launch (#106) by @earthlingdavey in #191
- docs(threat-model): env_file values are unit state in the hotserve user's manager (#106) by @earthlingdavey in #192
- feat(liveswap): config load warns about a deploy_trust block wider than it reads (#106) by @earthlingdavey in #189
- feat(box): the proof core and the reading of the signed file (PR 1) by @earthlingdavey in #190
- refactor(liveswap): deploy auth becomes the leaf package deploytrust, for both webhooks (box PR 2a) by @earthlingdavey in #188
- build(deps): bump the go-deps group across 4 directories with 1 update by @dependabot[bot] in #193
- test(sandbox): the cgroupfs probe tells read-only from absent and denied (#151) by @earthlingdavey in #195
- fix(penaltybox): key IPv6 clients by /64 and seed the shard hash (#151) by @earthlingdavey in #194
- fix(examples): deploy.sh refuses a dirty tree's default version and validates VERSION (#151) by @earthlingdavey in #197
- fix(penaltybox): warn when max_keys leaves one slot per shard, and document the per-shard split and empty-key fail-open (#151) by @earthlingdavey in #196
- ci(lint): lint and format-check integration-tagged files too by @earthlingdavey in #198
- fix(penaltybox): strip the hint header on the empty-key path too (#151) by @earthlingdavey in #199
- feat(box): the box option, box_webhook's GET / and
hotserve box webhook(box PR 2b) by @earthlingdavey in #200 - docs(penaltybox): the design doc describes the shipped module, counting is stated correctly, and the directive goes first in a route (#151) by @earthlingdavey in #201
- ci: a go mod tidy -diff gate per module, grouped gomod security updates (#151) by @earthlingdavey in #202
- fix(liveswap): state.json is written and read without following links, like the deploy records (#151) by @earthlingdavey in #203
- feat(box): the applier core (box PR 3a) by @earthlingdavey in #205
- feat(box): the applier's units and packaging (box PR 3b) by @earthlingdavey in #204
- feat(box): box_webhook admits pushes and answers result polls (box PR 3c) by @earthlingdavey in #206
- test(liveswap): the deploy-record test reads version fields, not the whole body by @earthlingdavey in #207
- fix(box): a refusal names its commits unmasked by @earthlingdavey in #208
- test(e2e): the box channel end to end (box PR 3d) by @earthlingdavey in #209
- fix(box): the applier's follow-ups — left-on-disk line, own rollback words, SIGTERM by @earthlingdavey in #210
- feat(box): hotserve init and hotserve box baseline (box PR 4) by @earthlingdavey in #216
- test(e2e): compose names the e2e images per project by @earthlingdavey in #215
- fix(box): the fast path asks every step that needs no signature by @earthlingdavey in #214
- feat(box): step 6 reads the adaptation for reachability (box PR 3c-r) by @earthlingdavey in #211
- fix(liveswap): a retired JWKS client closes its idle connections by @earthlingdavey in #217
- feat(box): the template applies by signed push (box PR 5) by @earthlingdavey in #213
- docs(box): "Why 202 and a poll" gives the reasons that hold by @earthlingdavey in #218
- fix(examples): bin/apply asks after a push a CDN could not answer (Cloudflare 520-524, 530) by @earthlingdavey in #219
- feat(box): a push is waited for 30 minutes, past root's worst case by @earthlingdavey in #222
- docs(box): the box's config in its own repository from day 0 (box PR 6) by @earthlingdavey in #220
Full Changelog: v0.2.1...v0.3.0