★ Security
Security statement
This describes what we do today. It is not a roadmap. Where a control is absent,
the list near the bottom says so, and everything above it was checked against the running
production system rather than read out of our own notes.
The shape of the estate
We have no security operations centre, no ISO 27001 certificate and no SOC 2
report. What we have instead is a small estate you can enumerate: one production application,
one database, two people-facing surfaces, and a short complete list of third parties. Fewer
attestations, more specifics. Every measure below is checkable.
The data that matters
- Half-hourly household consumption. It is not special-category data under
Article 9 of the UK GDPR. At that granularity it still reveals when a home is occupied, and
when people wake, sleep, cook and go away.
- So we govern it as high-sensitivity personal data, and we ask our partners to
do the same. That decision drives most of the design below.
Encryption
- At rest, in the application rather than only on the disk. The sensitive core
is encrypted before it reaches the database, using Fernet, which is AES-128-CBC with
HMAC-SHA256. A database dump, a leaked backup or read access to Postgres yields ciphertext,
not readings. The encrypted columns are the half-hourly electricity, gas and export readings,
derived insight, the meter access token, household bill figures, saved bill-check results, and
the email address and IP address on a terms-acceptance audit row.
- What is not application-encrypted, stated because you would ask. Email
address, postcode, display name, MPAN and password hashes sit in the database protected by the
access controls below. Password hashes are one-way in any case.
- The key. It reaches the process from the server environment. It has never been
in source control. The production application refuses to start without it, which is an
explicit guard rather than a convention, so the system cannot quietly fall back to writing
plaintext. There are two keys with different values, one for the public application and one
for the administrative application, so a compromise of one does not decrypt the other's data.
Off-server copies exist only in the operator's password manager.
- There is no key-rotation mechanism. One key per application, no key version
column, and no re-encrypt-under-a-new-key path. Rotating would mean a scripted re-encryption
of the affected columns. It is on the list of gaps below.
- In transit. TLS on every public endpoint, terminated by Caddy with
automatically renewed certificates. HTTP is permanently redirected to HTTPS.
Backups
- Nightly at 03:30 UTC, on a systemd timer, using restic, encrypted and
deduplicated at the repository level. The snapshot holds a logical dump of the production and
staging databases, the administrative database, the server environment files and the
web-server configuration.
- Two copies, two data centres. The primary repository is on a dedicated block
volume in Nuremberg, Germany. Every snapshot is copied to a storage box in Falkenstein,
Germany, which is SFTP-only with no compute on it. Retention is 7 daily, 4 weekly and 6
monthly, then pruned.
- The encryption keys are excluded from the backups on purpose. Until
2026-08-12 a snapshot contained both the encrypted database and the key that opens it, so a
snapshot plus the repository password yielded plaintext. The key lines are now stripped from
the environment files before they enter the snapshot, and a note is placed in the snapshot so
a future restore is not mistaken for corruption. Every other secret stays, because everything
else is regenerable and a real restore needs it.
- What that protects, and what it does not. A leaked snapshot, a stolen backup
volume or the offsite copy each yield nothing readable on their own. It does not protect
against a live compromise of the running server, where the key is readable from the process
environment. That is the limit of the measure. The accepted cost is that a genuinely lost key
makes the data under it unrecoverable: meter tokens, which users reconnect, and saved bill
figures.
- Restore was tested once, on 2026-07-15, into a throwaway database. It has not
been re-tested since.
Infrastructure and network exposure
- One virtual server at Hetzner in Nuremberg, Germany, running Ubuntu 24.04 LTS,
with PostgreSQL 16 on the same host. Backups are in Falkenstein, Germany. Not the UK. That is
lawful without extra safeguards because the UK treats the EEA as adequate, and any contract
clause has to say Germany rather than the UK.
- Four ports accept traffic from the internet. 22 for SSH, public-key
authentication only. 25 for inbound mail. 80, which permanently redirects. 443 for TLS.
Nothing else.
- Everything worth attacking is bound to loopback. PostgreSQL, the public
application, the staging application and the administrative application are not reachable from
the network at all. PostgreSQL also requires SCRAM-SHA-256 authentication for any TCP
connection, including from loopback.
- There is no host firewall. The packet-filter policy is accept-all. That is a
real gap and it is in the list below. What stands in its place is the line above: the exposed
surface is SSH, inbound mail and the web server, so a firewall would be defence in depth over
that rather than the only thing between an attacker and the database.
- Automatic security patching is on and running, with daily package-list updates
and daily unattended upgrades.
Access control
- Who can reach production. SSH public-key only for the administrative account.
Root password authentication is disabled at the daemon and one public key is authorised. Two
accounts on the host have a login shell. Access is limited to the operator: there is no
bastion, no per-person key set and no multi-factor authentication on SSH. In a company this
size that is a description of reality rather than a policy.
- Secrets live in server environment files with owner-only permissions, are
never in source control, and are never written to logs.
- How end users sign in. Sign-in is passwordless by default. A six-digit code is
emailed and never stored: we keep an HMAC of it, keyed to the server secret and bound to the
email address, so a stolen hash is useless for any other account. Codes last 15 minutes and
allow 5 attempts. Magic links are signed tokens with the same 15-minute life. Passwords are
optional and stored as a one-way scrypt hash, so we cannot replay one. Mobile app sessions are
signed bearer tokens lasting 90 days. Session cookies are HttpOnly, SameSite=Lax and
Secure.
- Connecting a meter stores a token, never a credential. The user authenticates
at the meter-data provider. We receive a revocable token, encrypt it at rest, and the password
never passes through us.
- The administrative console is a separate service. Separate port, separate
database, separate encryption key, separate secret key, and a password plus TOTP two-factor
with an 8-hour session. A compromise of the public application yields neither administrative
access nor administrative data.
- API keys are 160-bit random values, shown once and stored only as a hash, with
a short prefix kept so a key can be identified. A database compromise yields no working key.
Keys are scoped, quota-metered and individually revocable. The hash is unsalted SHA-256, which
is correct for a high-entropy random value: there is no dictionary to attack and no rainbow
table to build. We state the reasoning so a reviewer who finds the call site does not have to
guess at it.
Minimisation, retention and deletion
- Minimisation by architecture, on the partner platform. We are built to receive
the partner's own opaque customer reference and an outward postcode. No name, no email address,
no telephone number, no full postcode, no account number. The partner authenticates its own
customers and we never run a login for them.
- We do not call that anonymous, and neither should you. Data the partner can
re-identify is still personal data under UK GDPR. Pseudonymisation is an Article 32 security
measure that materially reduces risk. It does not take either party outside the
regulation.
- Bill images have no third party at all. Optical character recognition for bill
photographs runs on infrastructure we operate: a local model on the application host and a
second server of ours at the same provider. No bill image is sent to any third-party API. The
image bytes are held in memory, never written to disk and never logged. That was a design
choice with a cost attached, not an accident.
- Retention automation enforces one lifetime today. A purge job runs every 15
minutes on a systemd timer. It deletes OCR job records, which hold extracted bill figures and
a notification email address, 60 minutes after creation. We state that narrowly on purpose: an
earlier version of our own documentation claimed this automation existed while the purge
function had never been wired to a timer, and production held records a week old. A purge
function is not retention. A purge function on a running timer is.
- There is no automated retention limit on stored readings or derived insight.
How long readings are kept after a household disconnects is an open policy question and
the first item in the list of gaps below.
- What is settled on the consumer side. Meter consent runs for 18 months and can
be withdrawn at any time from the account. Disconnecting keeps the readings already held, so a
reconnect is not an empty app. Deleting the account hard-deletes the record and the meter
connection immediately with no retained copy in the live system, and queues a release of the
meter point so the household stops being counted. Cached supplier tariff rows are purged after
30 days. Deleted data persists in encrypted backups until those snapshots expire on the ladder
above, so at most six months for a monthly snapshot.
- Erasure is implemented and works. It removes the user record, the meter
connection and the stored readings, and there is a self-service deletion route in the product.
Two records survive it, pseudonymised, and both are disclosed here rather than found later:
the terms-acceptance audit row, with the email address and IP address encrypted and the user
reference nulled, and the meter-release record, which holds the provider's revocation
identifier and is the evidence that we stopped being billed for that household. Neither
reconstructs the person. Both are defensible under Article 17(3)(e).
Monitoring, incidents and change control
- What is watched. A site watchdog every 30 minutes covers the health endpoint,
the key pages, the application error log, failed jobs and stale data. A deep health monitor
every 3 hours makes live requests and renders signed-in pages for a real user and for a user
with no meter. A nightly audit at 03:20 UTC runs ten checks including truth, links, stale
data, page weight, rendering and endpoints, and keeps its findings for 90 runs. An operational
digest goes out daily at 07:13 UTC. A mail monitor runs hourly. Failures alert by email, with
a cooldown and an explicit recovery notice.
- There is no security monitoring. Nothing watches failed authentication,
privilege use, file integrity or intrusion. No SIEM, no log shipping, no intrusion detection.
Everything above is availability and correctness monitoring: it would tell us the site was
down, not that someone was inside it. This is the most significant gap on the page.
- Incident response. Our commitment to a partner is contractual and specific:
notification of a personal data breach affecting partner data without undue delay and in any
event within 24 hours of becoming aware, updates at least every 48 hours while the incident is
open, and a written post-incident report including root cause within 20 working days of
closure. The internal runbook behind that commitment is not yet written. It is in the list of
gaps below.
- Change control. Database migrations that touch stored readings are wrapped in
a snapshot-and-diff guard. Releases run through an automated test suite and an end-to-end
crash gate before deployment. Staging is a separate service against an isolated database with
a mock meter source, so no test touches real household data.
What we do not have
In one place, so a reviewer does not have to hunt for it and so nothing above can
be read as implying more than it says.
- 1. No retention limit on consumption readings. The policy question is open.
The consumer position above is published and settled. Highest-priority open item.
- 2. No security monitoring, SIEM or intrusion detection. Availability
monitoring only. Small estate, four exposed ports, loopback-bound services.
- 3. No written breach-response runbook. The contractual deadlines are
committed. The internal procedure is not yet written down.
- 4. No host firewall and no fail2ban. Services worth attacking bind to loopback
only, four ports are exposed, and automatic patching is on.
- 5. No ISO 27001, no SOC 2, no Cyber Essentials. None held, none in progress.
We answer security questionnaires directly and accept a contractual audit right.
- 6. No penetration test on record. None has been carried out.
- 7. No key-rotation mechanism. One key per application, no rotation
path.
- 8. No multi-factor authentication on server access. Public-key-only SSH, one
authorised key, one operator. The administrative application does have TOTP.
- 9. No 24/7 staffed operations. Monitoring is automated and alerts by
email.
- 10. Restore tested once, in July 2026, and not since. Backups are verified
running with 30 snapshots present. The restore path has not been re-exercised.
- 11. A single host. Application, database, mail and administration run on one
server. There is no failover. It is also the reason no availability figure is committed on the
service levels page.
- 12. Sign-in rate limiting is per-process. The limiter runs in-process across
four workers, so the effective limit is roughly four times the intended one. Adequate against
casual abuse, not against a determined attacker.
- 13. No statutory Data Protection Officer. Not believed to be required on size.
The systematic-monitoring trigger is being checked.
Third parties
Every third party that receives data is named on
the sub-processor list, with what it sees and where it processes.
That page is maintained as the authoritative one, so this page does not duplicate it. The point
worth repeating: bill images have no third party, and no AI or large language model provider
processes partner or customer data in the live service.
Reporting a vulnerability
Report anything you find to
security@joulely.co.uk. We will acknowledge,
investigate and tell you what we found. We will not threaten legal action against anyone who
reports a genuine issue in good faith, does not access or modify data beyond what is needed to
demonstrate it, and gives us a reasonable opportunity to fix it before publishing.
Version 2026-08-13. Reviewed when the architecture changes and at
least every six months. Questions to
business@joulely.co.uk. The contracting entity is on
the trust pack.
Trust pack · Sub-processors · Service levels · Data processing agreement