¶ DSPT Standard 9: IT Protection — Compliance Summary
Category: DSPT Evidence Checklist
Platform: gp_booking_app — Multi-tenant GP Booking Platform
Date: 14 May 2026 · Re-verified: 18 October 2026 — see the verdict section below; where the two disagree, the measurements win
Five months and a topology change separate this page from its original assessment: the estate is now three hosts
(gateway and data, booking apps, auxiliary), the connection pooler and identity provider were reworked, MinIO became
SeaweedFS, and the dental stack moved to the app engine. Every claim below was therefore measured again on
18 October 2026, on all three hosts, rather than inherited from May.
| Item |
What the standard asks |
Verdict on 18 Oct 2026 |
| 6.2.1 anti-malware |
Up-to-date AV on all computers and devices |
Partial — server gap closed 18 Oct 2026; staff endpoints still unrecorded (§A) |
| 9.5.2 encryption |
Devices that hold or give access to personal data are encrypted |
Partial — no disk encryption anywhere, TLS and tunnels sound, backups unencrypted (§B) |
| 9.6.1 boundary firewalls |
Firewalls on the network boundary (optional at Category 3) |
Present — but the statement needed two corrections (§C) |
| Control |
Primary |
App engine |
Auxiliary |
| ClamAV (1.5.4) weekly scan |
Sat 05:00 UTC — installed 18 Oct |
Sat 05:40 UTC — installed 18 Oct |
Sat 06:20 UTC — installed 18 Oct |
| rkhunter / chkrootkit |
absent |
absent |
absent |
| Host hardening audit (Lynis) |
weekly — last run 18 Oct 06:33 |
— |
— |
| Container image scanning (Trivy 0.70.0) |
weekly — 62 images, 38 actionable |
not scheduled |
not scheduled |
| Brute-force protection (fail2ban) |
5 jails (sshd, forgejo-sshd, three nginx) |
absent |
absent |
A ClamAV engine now runs on all three hosts — measured 18 October 2026. What was a gap in May is installed:
ClamAV 1.5.4 with a weekly scan (clamav-weekly-scan.timer) over the paths where content we did not write ourselves
lands — wiki uploads, dental DICOM in Orthanc, ERPNext client site files, spool space and the host backup bundles.
The slots are staggered so the three never scan in the same hour (Primary 05:00, app engine 05:40, auxiliary 06:20
UTC) and all fall on Saturday — deliberately a day clear of the Sunday security window (Trivy 03:00, nmap 03:30,
Lynis 06:30), so a scan never competes with an audit for the same cores.
It is deliberately not a full-disk scan. Database binaries and gzip dumps are not executable content, and a full
232 GB sweep on a 4-core production box would be a denial of service against our own clinics. The scan runs at
nice 15 / ionice idle under a 1.5 GB memory cap, so if it ever needs more, that unit dies and reports rather than
the clinic-facing containers. Reports are kept 120 days in /var/log/clamav, with the last result in
/var/lib/clamav-scan-last.json; clamav-freshclam keeps signatures current on every host. Worth recording, because
it is how a control quietly rots: the package installs clamav-freshclam disabled, so a reboot without enabling it
leaves a current signature database that never updates again. It is enabled on all three hosts and verified as such.
First measured runs, 18 October 2026 — all clean. Primary: 50,859 files / 1.74 GiB / 6 m 49 s, run through the
installed unit exactly as the timer will (1.0 GB peak RSS, inside the 1.5 GB cap; ConditionResult=yes). A manual run
minutes earlier scanned 50,863 files — the four-file difference is /tmp churn, and that is the honest resolution of
the two numbers. App engine: 107 files / 37 MiB / 14 s. Auxiliary: 610 files / 3.09 GiB / 206 s. Zero infections on
all three.
Which of the two options was taken. The first — install an engine plus a weekly scan — was implemented on
18 October 2026, on all three hosts. The compensating controls described above still hold, but they are no longer
the answer to 6.2.1. Open for the SIRO: this page is the evidence, not the decision; the choice to implement
rather than record a risk acceptance should be entered in the SIRO decision log with that date, or 6.2.1 is
evidenced but not governed.
What is still not met. The item asks for AV on all computers and devices, and there is still no register of
staff workstations, so the laptop and tablet end of 6.2.1 cannot be evidenced — the same missing device register that
limits 9.5.2 (§B). rkhunter and chkrootkit are also still absent: ClamAV is the only engine on the estate.
- At rest: no disk encryption on any host. All three are single ext4 roots on virtual disks with no LUKS mapping
active. Whether the provider encrypts underneath is unconfirmed — a question for the provider, not something
we can assert either way.
- In transit: sound. Every public hostname serves TLS; database traffic between hosts travels over SSH tunnels;
the app hosts expose no application port publicly (verified — only 22/tcp answers on the app engine and the
auxiliary host).
- Config hygiene: the global
nginx.conf still declares TLSv1 TLSv1.1, though handshakes for TLS 1.1 return
no cipher, so they are not actually offered. Delete the stale declaration so the configuration and the behaviour
agree.
- Backups are unencrypted. The nightly dumps and service bundles are gzip only and sit on the same disk as the
data they protect. This is the most concrete at-rest exposure on the estate, and it is fixable with one change to
the backup script.
- Devices: still unrecorded. The item asks about laptops, tablets and removable media. There is no register of
staff endpoints, so that half of 9.5.2 cannot be answered from evidence yet.
¶ C. 9.6.1 Firewalls and boundary — measured, with two corrections
- Primary: UFW active, default deny incoming, only 22/80/443 allowed; fail2ban with 5 jails; from outside, only
22/80/443 and the deliberate Forgejo SSH port answer.
- Correction 1 — UFW does not govern Docker-published ports. Publishing a container port inserts DNAT rules that
bypass UFW's filter, so a container port can be reachable while ufw status says otherwise. Verified from another
host: the Forgejo SSH port is publicly reachable (intentional — now stated rather than implied), while three
local-only node ports and the application ports 8000/8010/8005 were not.
- Correction 2 — the two app hosts have no host firewall. UFW is inactive on both the app engine and the
auxiliary host. Exposure is limited by binding services to internal addresses (only 22/tcp answers publicly), so the
boundary is the Primary's nginx. That is the design — but the earlier evidence implied a firewall on every host.
- The primary's docker rule chain carries explicit per-container egress rules (Forgejo's containers are limited to
their database, cache and proxy paths). Keep that in the evidence statement as the egress half of the control.
Two of the three items still answer "yes", with a statement that needs updating (§C). 6.2.1 moved on 18 October
2026: the server estate now runs an engine and a weekly scan on all three hosts, so only its staff-device half is
open. Neither the device half of 9.5.2 nor the backup-encryption gap is closed by this pass — both need a
decision, not more measurement.
Kept for history. Where it disagrees with the measurements above, the measurements win.
This page documents all work carried out to satisfy DSPT Standard 9 (IT Protection) requirements. It covers two key evidence requirements:
- Firewalls and boundary protection — network perimeter and application-level multi-tenant isolation
- Software patching — vulnerability scanning, patch automation, and DefectDojo tracking
Firewalls and boundary protection are in place
Evidence: IT supplier statement confirming firewall configuration, or network diagram
| Action |
Detail |
Status |
| Default inbound policy |
Deny all |
Active |
| Allowed ports |
22 (SSH), 80 (HTTP), 443 (HTTPS) |
Active |
| Allowed subnets |
172.16.0.0/12, 172.17.0.0/16, 172.18.0.0/16, 172.20.0.0/16 (Docker) |
Active |
| Verification |
ufw status verbose confirmed active |
✅ |
| Action |
Detail |
Status |
| Installation |
apt-get install fail2ban |
Installed |
| SSH jail |
Active, monitoring auth logs |
Active |
| Verification |
fail2ban-client status shows sshd jail |
✅ |
| Action |
Detail |
Status |
| Setting |
DEBUG=True → DEBUG=False in config/secrets/env.production |
Applied |
| Verification |
settings.DEBUG confirmed False after container restart |
✅ |
| Action |
Files Modified |
Detail |
| Added scoping helper |
appointments/views.py |
get_practice_scoped_appointments() function filters by StaffRoleRecord → Practice → Clinic chain |
| Updated 4 views |
appointments/views.py |
AppointmentListView, AppointmentDetailView, AppointmentCancelView, AppointmentResendNotificationView |
| Audit logging |
appointments/views.py |
_log_practice_boundary_check() logs PRACTICE_BOUNDARY events to AuditLog |
| Action |
Files Modified |
Detail |
| Permission class |
dental/api/views.py |
BelongsToDentalSector — rejects non-dental roles |
| 6 ViewSets scoped |
dental/api/views.py |
DentalPractice, DentalProvider, DentalAppointment, DentalClinicalNote, DentalTransaction, DentalRecallRule |
| User-to-practice lookup |
dental/api/views.py |
get_user_dental_practice() via DentalProvider.user |
| Action |
Files Modified |
Detail |
| Context manager |
tenancy/utils.py |
with_tenant() — sets thread-local tenant for Celery/non-HTTP code |
| Action |
Files Modified |
Detail |
| Reusable mixin |
core/mixins.py (extended) |
PracticeScopedQuerySetMixin — get_practice_scoped_queryset() with practice_fk_path support |
| Action |
Detail |
Status |
| Review |
Compose files only use expose (not ports); external port mappings from other containers blocked by UFW |
Complete |
| Action |
Files Modified |
Detail |
| RBAC violation logging |
core/middleware.py |
_log_boundary_violation() creates AuditLog with BOUNDARY_VIOLATION event type |
| Practice boundary logging |
appointments/views.py |
PRACTICE_BOUNDARY events with user, practice count, clinic count |
- Firewall assessment:
/tasks/dspt-firewall-boundary-assessment
- IT supplier statement:
/tasks/dspt-evidence-statement (includes network diagram)
Software is kept up to date with security patches
Evidence: Patching schedule or IT supplier confirmation of automatic patching
| Component |
Status |
| OS |
Ubuntu 24.04.4 LTS (Noble) — active LTS support until 2029 |
| unattended-upgrades |
Active — daily security patch auto-install |
| Kernel |
6.8.0-107 running, 6.8.0-111 installed (reboot pending) |
| Pending updates |
14 packages (docker-ce, snapd, rsyslog, etc.) |
| Docker images |
Varying ages — nginx 1.25 (2yr), Directus 11.6.1 (13mo) |
| Vulnerability scanning |
Trivy (weekly), Nmap (weekly), pushed to DefectDojo |
| Patching schedule |
❌ None documented |
| Patching log |
❌ None |
| Detail |
Value |
| Packages updated |
14 (including docker-ce, docker-buildx, snapd, rsyslog) |
| Docker service |
Restarted cleanly |
| Kernel |
6.8.0-111 available — reboot still required |
| Issue |
Fix |
| Engagements not created |
JSON date values missing quotes in defectdojo_push.sh |
| Push script response parsing |
Updated to handle JSON response properly |
| Verification |
459 findings imported for nginx:1.25 scan |
| Component |
Detail |
| Script |
/opt/security-scanner-scripts/patch_monthly.sh |
| Location |
/etc/cron.monthly/patch_monthly |
| Dry-run default |
Use --apply to install updates, --reboot to schedule reboot |
| Scans |
Trivy filesystem + Docker image scans |
| Reporting |
Results pushed to DefectDojo (Product ID 5: VPS Infrastructure) |
| Logging |
/var/log/patching/patch_YYYY-MM-DD.log + summary at /var/log/patching/summary.log |
| Image |
Action |
ghcr.io/requarks/wiki:latest |
Pulled latest |
redis:7-alpine |
Pulled latest |
postgres:15 |
Pulled latest |
- Patching assessment:
/tasks/dspt-patching-assessment
- Patching logs:
/var/log/patching/
- DefectDojo:
https://security.veripath.co.uk (VPS Infrastructure product)
| Type |
Schedule |
Location |
Retention |
| PostgreSQL dump |
Daily 02:00 |
/backups/wiki/wikijs_db_YYYY-MM-DD.sql.gz |
30 days |
| Git sync (Forgejo) |
Daily 02:00 |
ssh://git@172.20.0.2:2222/matthew/wiki_backup.git |
Full history |
| Script |
— |
/opt/security-scanner-scripts/backup_wiki.sh |
— |
| Cron |
— |
/etc/cron.d/wiki-backup |
— |
| Cadence |
Scope |
Method |
Evidence |
| Daily |
OS security patches |
unattended-upgrades (automatic) |
/var/log/unattended-upgrades/ |
| Weekly |
Vulnerability scan |
Trivy + Nmap → DefectDojo |
DefectDojo dashboard |
| Monthly |
Full OS update |
patch_monthly.sh --apply |
/var/log/patching/ + DefectDojo |
| Monthly |
Reboot check |
ls /var/run/reboot-required |
Script log |
| Quarterly |
Docker image refresh |
Pull latest + recreate containers |
Audit trail |
| Quarterly |
Full review |
Document all changes |
Wiki page update |
| File |
Change |
appointments/views.py |
Practice-scoped querysets + boundary audit logging |
dental/api/views.py |
Tenant-scoped DRF views + custom permission |
core/middleware.py |
RBAC boundary violation audit logging |
core/mixins.py |
PracticeScopedQuerySetMixin |
tenancy/utils.py |
with_tenant() Celery context manager |
config/secrets/env.production |
DEBUG=False |
/opt/security-scanner-scripts/defectdojo_push.sh |
Fixed JSON date quoting |
/opt/security-scanner-scripts/patch_monthly.sh |
Monthly patching automation |
/opt/security-scanner-scripts/backup_wiki.sh |
Wiki.js daily backup |
/etc/cron.monthly/patch_monthly |
Monthly cron job |
/etc/cron.d/wiki-backup |
Daily backup cron job |
This document was prepared on 14 May 2026 as part of the DSPT compliance evidence package. All modifications have been tested and verified. Links to supporting wiki pages provide further detail on each area.