1967aae42cc39493f97fe182ffb090dbf20da816
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1967aae42c
|
Report discovered printers to the cloud, off by default
The gateway could already find printers; only a human reading a terminal could see the result. So adding a printer in the web console still began with walking to a machine to read its IP address off the screen. Now a background task scans on a timer and sends a `discovered_printers` frame. It cannot finish the job - the credential is on that screen and no probe can read it - but it removes the address, the serial and the guesswork about which vendor the thing is. OFF by default, which is the load-bearing part of this commit. A /24 sweep is 254 TLS handshakes and up to three HTTP requests per host, and nothing watching can tell that from a port scan; on a corporate network, being indistinguishable from a port scan is itself the problem. A gateway that did this by default would scan networks its operator never meant to point it at, and the first they would hear of it is from their own security team. So: - `discovery.enabled` is false unless set, per gateway, in that gateway's own config. The cloud cannot switch it on. A setting whose whole purpose is to bound what this process does to the local network has to be decided where that network is understood, which is not in a data centre. - `subnets` is explicit, never derived from this host's interfaces, so a second NIC, a VPN or a container bridge cannot widen the sweep. Anything wider than a /24 is still refused, now at config-import time rather than when the timer first fires. - A flag that is neither `true` nor `false` is an error, not a shrug. The two readings of a typo are not equally safe. - Naming a subnet without setting `enabled` scans nothing - the safe reading of an incomplete instruction - and `enabled` with nowhere to look is refused as the mistake it is. - The env var can switch it *off* over a file that enables it, which is the direction that has to work: a Proxy Box's config is signed in the cloud, so an operator who finds their gateway probing cannot just edit it. What a scheduled scan will not do, unlike the example CLI: send `M115`. Opening a serial port toggles DTR, which reboots an Arduino-based board and ruins a print in progress. The example can do it behind `--yes-reset-the-printer` because a human is standing there; a timer has nobody to ask, so it does not get the option and reads sysfs instead. Two things that took more care than expected: `Scan::looked()`. An empty report means "this gateway looked and its network has no unconfigured printers", so sending one after every probe failed would state something untrue about the farm. A partial failure is still evidence, though - withholding a good sweep because a container can never bind SSDP would suppress it forever - so the test is "did anything succeed", not "did everything". `fleet::configured()` reads the fleet file at parse level rather than through `load`, because the entry it most needs to recognise is the one `load` refuses: a printer discovery suggested, pasted in, with the access code not yet typed in. Re-offering that printer as a fresh find is noise at the one moment the operator is mid-way through adding it. Verified against the real LAN: a sweep found both printers by TLS certificate with correct serials and no credentials, and the frame it produced validates against the backend's EdgeClientMessageSchema. The `wire` subcommand on the example prints exactly that frame, and the capture is pinned as a backend fixture. SSDP remains untestable here - this container is a router hop from the printers and multicast does not cross one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
062686b1a7
|
Authenticate config bundles: Ed25519, per-box identity, no plaintext
The hash in a bundle only proved it was undamaged. Anyone who could write the stick could write both files, so a Proxy Box would install a config handed to it by whoever got physical access - including a config pointing the box at a different cloud. Encryption alone does not fix that either. While plaintext was accepted an attacker simply did not encrypt, and the box took it. So a Box now refuses plaintext outright, and the bundle carries a signature. A third entry, `config.toml.sig`: strict TOML with `box_id`, `config_version` and a raw Ed25519 signature. What is signed is `signed_message()` - a domain tag, the length-prefixed box id, the version big-endian, then the raw `config.toml` bytes. Raw bytes rather than the archive, so the signature is independent of ZIP framing, compression, entry order and encryption. The box id and version are inside the signed message rather than beside it, so neither can be edited without breaking it, and the length prefix stops a byte-shifting collision between the id and the config. Past decryption the order is hash, signature, box id, version, then parse and validate. The claims in the `.sig` are attacker-controlled text until the signature over them verifies, so nothing is concluded from them before then - and nothing about this box's identity is revealed before then either, which is why unsigned junk cannot be used to probe which box it is talking to. Which error wins when a box cannot even open a bundle: a signature over the plaintext is only checkable after decryption, so a box with the wrong key cannot distinguish "meant for another box" from "forged". Wrong key wins. Neither bundle installs either way, it is nearly always the true cause, and calling something forged that we could not open would be an accusation we cannot support. Replay is enforced rather than documented away: `config_version` must not go backwards, recorded in a sidecar beside the installed config. An equal version is accepted on purpose, because the version is written before the config, so a power cut between the two leaves "new version, old config" and re-inserting the same stick repairs it. The other write order would leave a replay window instead. Both key sets are lists, so the cloud's signing key and a box's own key can be rotated. `load_provisioning()` is the single seam where a box's identity and keys come from; a TPM or secure element replaces that function and nothing else. A missing file means unprovisioned, and the box then says loudly what it cannot check - but an empty or malformed one is an error, because quietly reading a broken key file as absent would turn a provisioning mistake into a box that accepts unsigned bundles. Not verified: interop with the real cloud signer. These tests prove the box accepts what this code defines as correct, not that the cloud will produce the same bytes - the message layout, the pure-Ed25519-no-prehash choice and the `.sig` shape all need a cross-check against the configurator's own crypto library once it exists. A compromised cloud signing key still breaks every box trusting it; revocation is not built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
47c48ee02d
|
Load proxy settings from a file, and import them from a pendrive
Everything came from CONTINUUM_* environment variables, which suits neither shape the product ships in. A Proxy Box is an appliance whose settings arrive on a USB stick; a proxy on someone's PC is a config file they edit. Precedence is stated once, in config.rs: the file is the base, env overrides it, defaults fill the rest. `Config::from_env` keeps its behaviour and the struct is unchanged, so existing callers and tests are untouched. Layering is `Config::layered(file, env_lookup)` with the environment injected as a closure, so tests do not have to mutate process env to cover it. The bootstrap is read from $CONTINUUM_BOOTSTRAP, then /etc/continuum/bootstrap.toml, then nowhere - and nowhere means PC mode with ./continuum.toml, so an env-only deployment behaves exactly as before. The working directory is deliberately not searched, and the bootstrap itself must never come from a stick: it is what says whether sticks are trusted at all. Bundle format is continuum-config.zip holding config.toml and config.toml.sha256. The hash is checked before the bytes are parsed - the TOML parser should not be the first thing to read possibly-corrupt input - and **the comments say plainly what that buys: corruption and change, not forgery.** Anyone who can write the stick writes both files. It is an integrity check under a recorded trust assumption, that physical access to the box is trusted; a signature with the key in the box image is named as the follow-up that would give authenticity. sha256 via openssl, already a direct dependency, so no new crate and `cargo deny check licenses` still passes. The hazards of ingesting a file off removable media, all tested: Zip-slip is refused on absolute paths, `..` components, backslash separators, drive prefixes, NUL and non-UTF-8 names, and one bad entry rejects the whole bundle rather than being skipped. The stronger guarantee is structural though: entries are only ever read **by index, matched against two literal names**, and no write path is ever derived from an archive name, so a hole in the name check still writes nothing. A test confirms a bundle carrying `../evil.toml` alongside a valid config writes nothing outside the target and leaves the installed config alone. Zip bombs are capped at 16 entries, 256 KiB per entry and a 1 MiB file on the stick. Declared sizes are refused in a first pass that decompresses nothing, and a bounded reader then caps actual output so a lying header gains nothing. Installing is atomic: temp file in the destination directory - not /tmp, since rename is only atomic within a filesystem - created 0600 from the first byte because the config holds the gateway token, fsync, rename, then a best-effort directory fsync. A write that fails halfway leaves the previous config intact and no temp file behind. A bad stick never costs the box its config: a bad hash, bad TOML, an unsafe entry or missing settings all return Rejected and are logged as keeping the existing config. A correctly-hashed config is also validated **with no environment at all**, so a stick cannot install something that parses now and fails to start on the next boot - which would be a bricked box caused by exactly the stick we were being careful about. PC mode ignores pendrives twice over, by mode and by an emptied directory list, because on a general-purpose machine importing settings from a USB stick would be a way to change them behind the user's back. A [network] block is parsed, validated and exposed, and stops there. main.rs logs a warning that this build does not apply it, so a box given network settings cannot look like it acted on them. Applying them needs root, is specific to the appliance image, and a wrong write strands the box - it wants its own decision, not a side effect of reading a file. continuum.toml is gitignored, since it holds the gateway token. Not done, and not claimed: no signature, so no authenticity. There is no real pendrive here, so the removable-media path was tested with ordinary directories standing in for mount points - real udisks/udev mounts, FAT behaviour, a genuinely yanked stick and how promptly the 5s poll notices a mount are all unproven. The watcher's install-then-exit flow, which relies on a supervisor restarting the process, has no test and no supervisor exists in this environment; a stick left permanently inserted reports Unchanged rather than looping, because the hash matches. Env still overrides on a box, so a box is only pendrive-only to the extent its environment is trusted - bootstrap.rs says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |