carlos / platform Public

Scanner host: object-storage scanning on Carloku and Tito Feature Request open

On branch worktree-scanner-host

Tracking the rollout of object-storage virus scanning to carloku.com, then Tito.

Where things are. The scanner code has been live in both consoles since v0.18.0, but neither deployment has a clamd to talk to, read/tag permission on store objects, or a scan tag key. This branch adds what is missing:

  • infra/modules/scanner-host: a clamd box in the home region, reachable on 3310 from the console's security group only. The port is open only while a self-test has passed against the clamd process running now (spec §3.2).
  • region-host: store_scanning, the console's opt-in GetObject/GetObjectTagging/PutObjectTagging grant.
  • internal/clamd: Heuristics.Limits.Exceeded.* is recorded as unscanned (MaxScanTime as failed, so it is retried), never infected or clean.

Spec: docs/superpowers/specs/2026-10-04-scanner-host-rollout-design.md (draft 6, rulings D1-D8 in §2). Plan: docs/superpowers/plans/2026-10-05-scanner-host-rollout.md (13 tasks). Branch: worktree-scanner-host

Measured, not assumed: with ClamAV's packaged limits, a zip member that expands past MaxFileSize is trimmed and the trimmed copy is answered OK, with no limit raised (spec §3.4 has the table from a real clamd 1.4.3). The module sets MaxFileSize = MaxScanSize = 100M and AlertExceedsMax yes, and the box re-proves it at every clamd start.

Paul's decisions (2026-10-05): clamd on its own box, home region only; plain TCP inside the VPC fenced by security group; Carloku first; both pieces as platform modules; a partly scanned object with no signature match is unscanned, a real match stays infected. Tonight stops before the v0.23.0 release: nothing is applied to any deployment yet.

Follow-ups, not in this rollout:

  1. zip64 and encrypted archive members are not opened by clamd at all, so a clean verdict never covers them. Say so in the scan request disclosure (copy review), and consider AlertEncrypted yes mapped to unscanned.
  2. A configurable scan limit would need the self-test's fixtures derived from it.
  3. Raising limits later does not re-scan objects already recorded skipped.
  4. Log clamd's raw reply when a scan is recorded failed, so a MaxScanTime object can be told from a broken engine.

Progress and the remaining live steps (release, Carloku apply and box acceptance checks, console roll, key, wiring, enable, proof; then Tito) will be posted here.

2026-10-05, overnight progress. Code done and reviewed: clamd client change, store_scanning, the scanner-host module, and the docs (Paul approved 108 strings). Fable whole-branch review found two live-box blockers, both fixed and re-reviewed: Debian's AppArmor profile denied clamd /var/tmp; debconf would have turned StreamMaxLength 26214400 into 25 TiB on the first package upgrade (now written in MiB and debconf frozen). Astra's code review: accept with fixes; its two findings (self-test accepted a scan timeout as proof of the limits; a console write failure could exit without the FAILED marker) are fixed, re-review running.

Full gate (build, vet, every Go test, darwin/windows cross-compile) passes with main merged in. tofu tests: region-host 3/3, scanner-host 13/13.

Waiting on Paul: a three-sentence copy review of README text the fixes changed: http://docs-t1.tail5fd3ba.ts.net:46101 . The merge waits for it. Then: v0.23.0 (Paul's call), and everything live on Carloku.

Merged. main 562f3df, squash of worktree-scanner-host. CI green on the tip; GitHub mirror fast-forwarded to 562f3df.

Reviews behind it: a per-task review on every task, a Fable whole-branch review (fixed: AppArmor denied clamd /var/tmp; debconf would inflate StreamMaxLength to 25 TiB on the first configure, now written in MiB with debconf frozen), and an Astra code review (fixed: the self-test accepted a scan timeout as proof of the limits; a console write failure could exit without the FAILED marker). Copy: 111 strings, all approved by Paul.

Not done, by Paul's choice tonight: no release, nothing applied anywhere. Remaining, in order:

  1. Task 6: cut v0.23.0 (carries everything on main since v0.22.2). Paul's call.
  2. Task 7: Carloku's targeted apply (scanner box + storescanning on hostie only), then the box's readiness and four acceptance checks.
  3. Task 8-9: roll the flagship console to v0.23.0 (roll-console.sh, /proc exe check), key to SSM, wire, enable.
  4. Task 10: the throwaway-app proof. Needs Paul to name the account.
  5. Tasks 11-12: Tito, after #201 phase 2.

Parked for follow-up: log clamd's raw reply on a failed scan (objstore); a StreamMaxLength fixture in the self-test; the four spec §11 follow-ups in the first post.

v0.24.0 released (release), cut from main eef3e121. v0.23.0 had already been cut at 3ca43d33 before the scanner merged, so the scanner module first ships in v0.24.0: pin scanner-host and host_ie to ?ref=v0.24.0, not v0.23.0 as the spec and plan say (being corrected). Tap #23 merged, apt v0.24.0-1 live; verified from the public side. No boxes rolled.

Next: Task 7, Carloku's targeted apply, on Paul's go.

Scanning is live on carloku.com (2026-10-06), with no member store granted.

  • v0.24.0's scanner box failed its first boot, safely: Debian's AppArmor profile lets freshclam read only its config under /etc/clamav, and the boot script handed it a copy in /run. Port never opened. Fixed in 592b667, shipped as patch v0.24.1 (v0.24.0 + that fix only).
  • Carloku pins host_ie and the scanner to v0.24.1 (carloku-infrastructure #4, merged). Scanner box i-0a61f580df1acfa65 booted READY from the module alone; readiness and all four acceptance checks pass (kill, forced fail, EICAR via the console box, reboot; 0 ordering cycles, 0 AppArmor denials, debconf round trip keeps the keys).
  • Console rolled to v0.24.1, key in SSM /carlos/scan-tag-key, wired, antivirus enable.
  • Proof in throwaway account bew: clean 1 / infected 2 / over-limit archives skipped 2, signed and ETag-bound; rewrite re-scanned; enforce on, engine unwired, enforce off still works; purge left no records, no store user, no bucket, and the name was claimable at once.

Next: Tito (plan Tasks 11-12), after #201 phase 2. Follow-ups unchanged from the first post, plus: the status page says "enforce on blocks reads" even while enforcing.