Skip to content

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 20:41
· 82 commits to main since this release
f9ef7ee

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/Caddyfile if 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 run hotserve init from 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 baseline resets that commit, for example after main is force-pushed. hotserve box describe and hotserve box webhook read 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-apply says what it did.
  • The template: make signer makes your SSH key the first signer, make check checks each box's file, and the workflow checks every push and pull request, then sends main to each box. GitHub's merge button cannot land a change the box will apply: land with git merge --ff-only from 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 the max_header_size server option.
  • Request header names containing . are dropped, the same as names with _ since 2.11.4. Opt back in with expected_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_auth no longer carries over to more specific site blocks. Config validation is stricter, and method matcher values are uppercased.
  • hotserve CLI errors, from hotserve validate for example, are no longer a plain Error: ... 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 for Error: 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 as CF-Connecting-IP included. 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_keys is below 128 (#196). That leaves one slot per shard, so the store tracks 64 clients, not max_keys. It is a warning only, never a refusal.
  • The hint header is stripped on the empty-key path too (#199), when strip is 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_penaltybox goes first in a route block (#201). Outside a route, Caddy sorts it after handle and after cache, so cached hits are neither counted nor box-checked. The starter Caddyfile's commented example now wraps it, cache and reverse_proxy in a route. 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_file that every account on the box can read or write, at config load and at each launch (#191);
  • a deploy_trust block 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):
    • it refuses a dirty tree when VERSION defaults from git, and checks VERSION's characters before minting a token (#197);
    • it refuses a HOTSERVE_URL that is not https:// (#186).
  • bin/apply (box):
    • a push that Cloudflare could not get an answer for (520–524, 530) is asked after rather than reported red (#219);
    • a push's result is waited for up to 30 minutes (#222).

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_VERSION repository variable of just v installs 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