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

Joulely

Your home bills, checked on your own numbers — £0 commission on any switch.

Check your home · Check your energy bill (free) · Council tax check · Guides

About · For business · Who we support · Privacy & your data · Terms · Tariff data & supplier corrections