The live record is now in the application: https://gp.veripath.co.uk/infrastructure/maintenance/
From 2026-10-13 the GP Booking App holds every maintenance run — patching, image refreshes, credential
rotations, schema migrations — reported automatically by the scripts that perform them, with filters, a
status strip and a CSV export for the evidence folder. Design and decisions:
Maintenance Records — In-App View.
This page therefore keeps what the app does not: the declared cadence, the pre-2026-10-13 history
(transcribed from the host logs before any reporting existed), the policy-facing summary an assessor
reads first, and the open items. Companion pages:
Software Patching — Assessment & Recommendations and
Standard 9b: Patching & Software Updates.
| Host | Job | Slot (UTC) | Definition |
|---|---|---|---|
| Primary | patch_monthly.sh --apply --reboot — OS updates, Trivy, DefectDojo push, reboot |
1st, 02:00 | cron; script now version-controlled in veripath-ops |
| App engine | patch-host.sh --apply --reboot — OS updates, reboot |
1st, 03:00 | patch-monthly.timer (Persistent=true) |
| Auxiliary (ERPNext) | patch-host.sh --apply --reboot |
1st, 04:30 | same unit file, per-host slot via a systemd drop-in |
| All stacks | refresh-image.sh <compose-dir> <service> — deliberate per-stack image refresh |
quarterly, first Sat of Jan/Apr/Jul/Oct, 03:30 | driven by the weekly Trivy result |
| Every host | unattended-upgrades (security repository) |
daily | /etc/apt/apt.conf.d/20auto-upgrades |
| Client VPSs (SIAAS) | client-side patch agent — in scope 2026-10-13 | not yet declared | Client VPS Patching — Managed Service; the slot is declared here once Matthew rules on the schedule |
The staggered slots are deliberate: the three hosts never patch at the same minute. Scans: weekly Trivy
(Sat 03:00, Sun 03:00), weekly nmap (Sun 03:30), weekly Lynis (Sun 06:30), monthly ZAP (first Sunday 04:00).
Transcribed from /var/log/patching/summary.log, which remains the underlying evidence.
| Run date | Applied | Outstanding after | Reboot | Notes |
|---|---|---|---|---|
| 2026-05-25 | 2 | 0 | none | manual run (patch_2026-05-25.log) |
| 2026-06-01 | 3 | 4 | scheduled | — |
| 2026-07-01 | 17 | 5 | scheduled | — |
| 2026-08-01 | 15 | 1 | scheduled | — |
| 2026-09-01 | 101 | 5 | scheduled | large backlog cleared |
| 2026-10-01 | 43 | 12 | scheduled | Trivy + DefectDojo push completed 02:17 |
Five consecutive monthly runs with a dated log is the evidence that the cadence is real rather than
declared — it is what prompted building the live view.
Both hosts had no scheduled run before this date and relied on unattended-upgrades alone, leaving
reboots outstanding (app engine since 2026-10-07 with 44 updates; auxiliary with 55). Their baseline rows
were written by dry runs, in the same shape as the primary's log:
| Host | Baseline row | Superseded by |
|---|---|---|
| App engine | 2026-10-13 \| applied=0 \| pending=44 \| reboot=required \| apply=false |
the 2026-10-14 03:00 bootstrap reboot |
| Auxiliary | 2026-10-13 \| applied=0 \| pending=55 \| reboot=required \| apply=false |
its first scheduled run, 2026-11-01 04:30 |
Restart-safety was checked before enabling a reboot on either host: all five ERPNext containers on the
auxiliary host are restart=unless-stopped (and the app engine keeps vps2-stack-guard.timer as a
backstop). Note there is no stack guard on the auxiliary host — recovery there rests on the restart
policy and the compose healthchecks, which is adequate for a controlled 04:30 reboot but is a difference
worth knowing.
Auxiliary host bring-forward — completed 2026-10-13. Rather than carry 55 outstanding packages and a
pending reboot until 2026-11-01, Matthew asked for it immediately. A weekday daytime slot was explicitly
acceptable (the test client's ERPNext is in development use, so no quiet window was needed), the app engine's
own bootstrap on 2026-10-14 03:00 was left as it was, and the monthly 04:30 drop-in slot was untouched.
| Run | Started | Applied | Upgradable after | Reboot | Record |
|---|---|---|---|---|---|
| vps3 one-off bootstrap | 2026-10-13 14:32:50 UTC | 53 (≈179 MB, incl. docker-ce / containerd.io, grub) |
55 → 2 | shutdown -r +1; host rebooted 14:36:49 UTC |
maintenance view id 2, reboot=performed |
The 2 left behind are python3-software-properties / software-properties-common 0.120.1, held by
apt phasing at 0% — normal staged rollout, cleared by a later run. 53 applied + 2 phased = 55, so nothing
was skipped.
Restart check after the reboot (the only real risk on that host, since it has no stack guard): all five
containers came back — erpnext_web / _worker / _scheduler healthy, _postgres / _redis up —
/var/run/reboot-required cleared, and both test-client ERPNext hostnames answering HTTP 200. Confirmed
independently from the primary the same afternoon: the tunnel to that host active and both hostnames 200.
Its monthly cadence is unchanged — next scheduled run 2026-11-01 04:30.
A reporting defect found and fixed in the same pass (veripath-ops 7f92d1d): the patch scripts
reported the reboot field only as required|none, captured before the reboot fired, so a run that did
reboot read as outstanding and the view could never show performed. Both scripts now report performed
when --reboot scheduled one, while summary.log keeps the required|none shape this page reads. The
already-written vps3 row was corrected by hand, with the reason recorded in its own notes field — a
legitimate correction, and disclosed in the record itself rather than silently, which is what an auditor
wants to see.
| Kind | Reporter | Notes |
|---|---|---|
os_patch |
patch_monthly.sh (primary), patch-host.sh (app engine, auxiliary) |
posts success and failure |
image_refresh |
refresh-image.sh |
failure posted via a trap on any early exit |
credential_rotation |
the rotation pack's finalize.sh |
so the 2026-10-18 rotation is a row from day one |
| manual / ad-hoc work | the app's staff-only form | source = the user, trigger forced to manual |
Ingest is POST /infrastructure/api/maintenance-record/ with a per-host token (X-Host-Token), idempotent
on (host, kind, started_at) so a re-post cannot double-log. Tokens live host-local in
/etc/veripath/maintenance.env (0600) and are never in git.
unattended-upgrades installed kernel 6.8.0-147 after that, so the running kernel is 6.8.0-142 against anpost-reboot-verify.service, recorded as item A6 in themanage.py migrate_client runs only from the pharmacyscan_trivy.sh / scan_trivy_weekly.sh run there andrefresh-image.{service,timer}OnCalendar = the 1st Saturday of Jan/Apr/Jul/Oct 03:30,Persistent=true, next fire 2027-01-02), running refresh-image-sweep.sh report-only against averipath-ops/image-refresh/vps{1,2,3}.conf), one row per stale service. Thestatus=0; then a full apply-then-rollbacknetdata (chosen for the smallest blast radius rather than minio) — applied, healthy, then rolledMaintenanceRecord.detail_json accepts the run's apt Upgrade: list (name, from → to, and a security/infrastructure/maintenance/export/packages/ carries the detail as its own CSV.{roles, carriers} and never a value.minio can no longer be pulled from any registry (decided 2026-10-13: replace it, later).minio/minio, and docker manifest inspect fails for bothminio/minio:latest and the quay path — MinIO has withdrawn the community image, which is a licensingsha256:14cea493…), data lives on bind mounts (/opt/minio/data) and the container is unless-stopped,dental-radiographs,dental-radiographs-test and the orphan patient-images; the sole app config naming MinIO is the dentaltest-client-dental carries minio_bucket. The access surface is one moduledental/services/minio_client.py), so the swap is contained.docker system prune -a-s3.port/-dir corrections and the correct index digest 4e61d15f…); the shared store copied andseaweedfs:9000 (WO-M3); and the coldminio service and its digest pin were then removed from /opt/infra (the minio/opt/minio/data are retained read-only and must not be pruned until the surplus image's removal is explicitly194.164.18.33 now serves dental-radiographs from SeaweedFS at portprimary-tunnel.service is unchanged); the mirror verified 123/123 objects byte-identical and theExited (0), minio-data volume) for rollback. The accepted risk isveripath-ops af46534.A DSPT check asks whether updates are downloaded and installed promptly, so the numbers below were taken live on
all three hosts rather than copied from the run history above.
| Host | Security updates available | unattended-upgrades |
Restart state | Next scheduled run |
|---|---|---|---|---|
| Primary | 0 | active | outstanding — 6.8.0-147 installed, 6.8.0-142 running | 1 Nov, 02:00 (cron) |
| App engine | 0 | active | none | 1 Nov, 03:00 (last run 13 Oct 13:13) |
| Auxiliary | 0 | active | none | 1 Nov, 04:30 (last run 13 Oct 14:00) |
Non-security upgrades outstanding at the same moment: 33 on the Primary, 9 on the app engine and 11 on the
Auxiliary — all phased or held by apt, none of them security fixes. The Primary's figure is the residue of the
October run's pending=12 plus the daily security stream; it does not become a security backlog, because the
security pocket is applied daily and currently stands at zero.
What this establishes for item 8.3.5. The estate has a declared cadence, staggered so no two hosts patch in the
same minute; a daily security pocket on every host; a run history going back to May 2026; and a live in-app record
from 2026-10-13. Today's measurement shows zero outstanding security updates on all three hosts. The one
weakness is the Primary's outstanding restart, which is tracked and now has a verifier attached — a restart gap, not
an unpatched-security gap.
The app is the record; this page is the summary. When the cadence or a slot changes, update both: the
seed row (MaintenanceCadence) or the drop-in on the host, and the table above. Never paste host
addresses, credentials or scan output here — the CSV export exists for the evidence folder, and findings
belong in DefectDojo.