Status: DONE — executed and verified 2026-10-15 under Matthew's authorisation. The dental Client VPS's MinIO is
replaced by SeaweedFS at port parity; MinIO is stopped and retained for rollback. The as-built record is the
plan page's WO-M4 section; the steps below are kept
as the executed procedure (and for a future VPS).
Read the correction history before trusting any earlier claim on this page — there were two wrong answers, and
both said "no access". First the page said the swap "cannot be done remotely" and needed a build visit; then it
said it was "blocked on a credential nobody holds". Both came from the same faulty comparison — the GP Client
VPS's revoked key read as if it belonged to this VPS — and the Access section keeps the record so neither is
re-derived. The test steps below never changed and are correct.
Run on 194.164.18.33 as root (/root/.ssh/vps_client), with the box's primary-tunnel.service
(-R 127.0.0.1:5438:127.0.0.1:9000) left unchanged:
chrislusf/seaweedfs:4.48@sha256:4e61d15f…; /opt/seaweedfs/data bind mount;0600 s3.json (uid/gid 1000) carrying the tenant's own credentials (minioadmin + the MinIO root secret,dental-radiographs MinIO→SeaweedFS (rclone, temp port 8333): 123/123 objects, 80,309,999 bytes76,392,599 transcription slip; the store confirms 80,309,999.)403 unauthenticated / bucket served authenticated; migrate_tenant_radiographs --verify → 6 present /placeholder.png); the app's own client did put → readback → presigned GET over HTTP →Exited (0), minio-data volume, 79 MB). Rollback = docker stop seaweedfs && docker start minio. Step 6 (agent image_refresh list) waits on an agent being installed on this VPS — there is none today.Not accepted on the record alone: re-measured from outside the build's tooling, read-only, with one deliberate
login.
| Check | Measured |
|---|---|
| Containers on the VPS | seaweedfs up (started 11:42), minio Exited (0), client_db and dental_orthanc untouched (Up 2 months) |
| The new service | pinned chrislusf/seaweedfs:4.48@sha256:4e61d15f…; -s3.port=9000, -s3.config /etc/seaweedfs/s3.json, -dir=/data; bind mounts /opt/seaweedfs/data and /opt/seaweedfs/s3.json; config 0600 |
| Objects served by the VPS store | 123 objects, 80,309,999 bytes — and all 123 carry a LastModified inside the mirror hour (11:xx), so nothing has been added since |
| Platform path through the tunnel | migrate_tenant_radiographs --tenant test-client-dental --verify → 6 present, 1 missing (the known placeholder.png) |
| Tenant row | unchanged — minio_host / minio_port (5438) / minio_bucket as before, keys still encrypted |
| Rollback material | MinIO container Exited (0); minio-data volume 79 MB retained; SeaweedFS data dir 77 MB |
| Other consumers on the VPS | none — no other container references port 9000 |
One discrepancy against the as-built record above, for the evidence trail. It says the mirror was "123/123
objects, 76,392,599 bytes both sides". The object count is confirmed, but the store reports 80,309,999
bytes for those 123 objects — a difference of 3,917,400 bytes — and it is not a subtree artefact (per-prefix
sums: raw/* 55,741,172, processed/* 24,568,827; neither matches the quoted figure). Because every object
was written inside the mirror run, this is not post-mirror drift: either the quoted byte total came from something
other than the object listing, or the two sides differ in size somewhere the sample-of-5 byte check did not cover.
Re-take or explain it — a total that cannot be reproduced from the store is the first thing an inspector pulls
on, and it is the only number in this migration's record that does not reconcile.
Under the SIAAS ruling (a), test-client-dental keeps its radiographs on its own Client VPS. The tenant row
routes that tenant to its own VPS's object store over the 5438 tunnel, and the tunnel unit
tun-vps1-dental-minio.service (app engine → primary loopback 5438 → the VPS) is the live path. So that VPS's
MinIO is the store that retires the real risk — replacing it at the same port changes neither the tunnel nor the
tenant row.
What is on the VPS (Phase 0, 2026-10-14): dental-radiographs — 123 objects, newest 2026-09-24; but the
tenant's database references only 7 keys (6 present, 1 missing) — the placeholder
dental/radiographs/2026/09/09/placeholder.png. Most of the 123 are orphans.
| Check | Result |
|---|---|
| Root SSH from the primary | Works. ssh -i /root/.ssh/vps_client root@<dental Client VPS> returns ubuntu, and whoami returns root — one deliberate attempt, 2026-10-15 |
| The key | /root/.ssh/vps_client on the primary (the host that holds it), present since 2026-07-27 |
| What the VPS runs | client_db (5436), dental_orthanc, minio — read over that login |
| Hosting-panel route | Still unknown, and it matters (below): the VPS's registry row records no hosting account, unlike the GP row |
| The GP Client VPS | A different VPS: its key was revoked 2026-08-11 and it is monitored by a TCP port-22 probe. That revocation is not this VPS's |
The mistake this section exists to prevent. I read the GP revocation as if it applied here, then read a
truncated ls of the primary's /root/.ssh — cut off before vps_client, which sorts last — as proof that no
key existed, while testing with the wrong keys from the wrong host (the app engine's, which never included this
one). Three independent errors, every one of them pointing the same wrong way, and none tested against the VPS
actually in scope. Reachability and authorisation are separate questions, and both must be tested on the host in
question with the credential that host would use. OpenCode caught this; their correction is the authoritative
record, and it is the reason the table above is written as measurements rather than as a conclusion.
Posture, and why holding this login is acceptable. These Client VPSes exist to develop the apps, so SSH is part
of the development model rather than a SIAAS breach — but the production posture is tunnel-only (no standing
SSH; maintenance by the WO-B agent over the outbound tunnel), and Matthew wants the switch between the two modes
documented and rehearsed on the test estate. Until that exists, treat this login as a development affordance:
use it, but never let it become the mechanism the platform depends on.
Also: do not probe a Client VPS's SSH repeatedly to test credentials — failed attempts can trip its own ban rules
against our host. The bad attempts made while establishing the paragraph above were stopped for that reason.
vps_client login (held on the primary) and roughly one hour, outside clinic hours — the VPS carrieschrislusf/seaweedfs:4.48@sha256:4e61d15fd35994cb1e43e1e553dff106794841fd9a99ade2fc8c8bfce4d7872d.mc (or rclone) on the VPS for the copy.docker inspect digest/ID), the port itcommand: server -s3 -s3.port=<MinIO's port> -s3.config <file> -dir /data; a durable bind mount for data; a0600 credential file (never MinIO's); a healthcheck on the master status path. Leave MinIO running.mc mirror the VPS's bucket(s) MinIO → SeaweedFS on the VPS; then verify object counts match.dcm and a processed/ PNG).up -d — never docker compose down.python manage.py migrate_tenant_radiographs --tenant test-client-dental --verify still reports 6/7 present.--images, inminio/minio:latest, so image_refresh covers the new service. (The list is per-VPS config, not a repoRevert step 4 — point the consumer/tunnel target back at MinIO and restart. MinIO's data was never written to, so
rollback is a config change and a restart.
image_refresh list names SeaweedFS rather than MinIO./root/.ssh/vps_client on the primary logs in as root to this VPS, and I reproduced itubuntu / root, plus client_db, dental_orthanc, minio from docker ps). The cause was conflating the twoTerminology: this page says "Client VPS" throughout (2026-10-15). "Box" was shorthand from the patching work
and caused confusion about which machine was meant — which is exactly how the two VPSes got conflated here.