server: refuse unsigned releases and polluted reserved addresses

Self-update now verifies SHA256SUMS.sig (ed25519, relsign package) against
a public key baked into the binary; the private key exists only in the CI
secret store, so a compromised release host can withhold updates but not
inject one. CI signs on every server-v* tag and hard-fails without the
secret. Operators with their own pipeline override the key via
ECHOLOT_SELF_UPDATE_PUBKEY (mint a pair with release-sign -gen).

Startup also now proves 80/443 are actually free on the reserved
measurement addresses by asking the OS (throwaway bind), not the config -
CheckReserved could never see a stray process, and the adb-beacon receiver
on 0.0.0.0:443 was exactly that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-02 12:32:02 +02:00
co-authored by Claude Opus 5
parent 20cfecf566
commit a49bef5821
12 changed files with 472 additions and 29 deletions
+6
View File
@@ -70,6 +70,11 @@ type Config struct {
// e.g. https://git.example.net/api/v1/repos/owner/repo
SelfUpdateAPI string // ECHOLOT_SELF_UPDATE_API / --self-update-api
// SelfUpdatePubKey overrides the release-signing public key baked into the binary
// (selfupdate.DefaultPublicKeyB64) — for operators running their own release pipeline
// against their own Gitea. Empty = the built-in project key.
SelfUpdatePubKey string // ECHOLOT_SELF_UPDATE_PUBKEY / --self-update-pubkey
// Uploaded-run storage. The default is "anonymous": any enrolled device may upload,
// which is what a self-hosted server wants. Operators of shared servers turn it down.
UploadsMode string // ECHOLOT_UPLOADS / --uploads (off|anonymous|account)
@@ -185,6 +190,7 @@ func Load(args []string) (*Config, *Actions, error) {
fs.StringVar(&c.StateDir, "state-dir", envOr("STATE_DIR", defaultStateDir()), "state directory (device store, generated TLS)")
fs.StringVar(&c.Name, "name", envOr("NAME", "echolot"), "server profile name")
fs.StringVar(&c.SelfUpdateAPI, "self-update-api", envOr("SELF_UPDATE_API", ""), "Gitea repo API base for self-update; empty disables")
fs.StringVar(&c.SelfUpdatePubKey, "self-update-pubkey", envOr("SELF_UPDATE_PUBKEY", ""), "release-signing public key (base64 ed25519) self-update verifies against; empty uses the built-in project key")
fs.StringVar(&c.UploadsMode, "uploads", envOr("UPLOADS", "anonymous"), "who may upload measurement runs: off|anonymous|account")
fs.Int64Var(&c.UploadMaxBytes, "upload-max-bytes", int64(envInt("UPLOAD_MAX_BYTES", 4<<20)), "largest accepted uploaded run, bytes")
fs.IntVar(&c.UploadRetentionDays, "upload-retention-days", envInt("UPLOAD_RETENTION_DAYS", 90), "delete uploaded runs older than this; 0 disables")