Documentation

Security and privacy

Version 1.5 ·Updated 2026-08-31

Next reviewed by 2027-02-28.

Written for IT managers, business managers, and anyone filling in a vendor questionnaire.

What this covers

Kestrel is a student information system: enrolment, attendance, assessment, wellbeing and medical records on one student record. That is children's personal information, some of it health information. Some of it, such as parenting orders and child-safety records, is among the most sensitive material a school holds.

This document describes the posture as it stands, so that a school can see what protects its records without asking for it a school at a time.

Where the data lives

One virtual machine runs the application container and one PostgreSQL instance. Each school's records are held in their own Postgres schema inside that one database. There is no document store and no file upload: everything a school holds here is a database record. No data is sent to any third party for processing, analytics or model training.

Encryption, and what each layer protects against

The second column is the one worth reading. "Encrypted in transit and at rest" is true of almost every product and tells a reader nothing about which attacks it stops.

In transit, browser to Kestrel

TLS 1.2+, certificates issued by Let's Encrypt and renewed automatically by the reverse proxy. HSTS is set for two years, including subdomains.

Protects against Anyone on the same network as the school office reading or altering a session.

In transit, inside the host

The application reaches its database over TLS, and checks the certificate against the one authority allowed to have issued it before sending anything. The database itself refuses a connection that is not encrypted, so a client that skipped TLS is turned away rather than served. Neither the application nor the database is published to the internet.

Protects against Another program on the same host reading the database traffic, or answering in the database's place.

At rest

Full-disk encryption on the host.

Protects against A disk leaving the building.

At rest, the most sensitive fields again

The fields classified restricted are encrypted a second time by the application itself, with a separate key for each school, before they reach the database. The key that unwraps a school's key is held outside the database and is not in any backup of it. Destroying one school's key makes that school's records unreadable and leaves every other school's untouched.

Protects against A stolen database dump, a backup that ends up somewhere it should not, and anybody whose access to the database is broader than their access to the application.

Secrets

Resolved at deploy time and age-encrypted to a key generated on the server, which never leaves it. No secret is in git, in the container image, or in any environment file on a developer's machine.

Protects against A leaked repository, a leaked image, or a stolen laptop.

In the browser

A Content-Security-Policy that permits no external origin: fonts are self-hosted, icons are bundled server-side, and there is no analytics, tag manager or third-party script of any kind. Inline script is refused unless it carries a nonce this server minted for that single response, so a script injected into a page does not run. Framing is refused outright.

Protects against Cross-site scripting, clickjacking, and data leaving to a third party by accident.

Access control

One Postgres schema per school
Every record table for a school is in its own schema, reached through a database role that has privileges on that schema and no other. The role is assumed for the length of one transaction and dropped at commit. Postgres itself refuses the crossing, so the refusal does not depend on our query builder getting it right.
Proved by attempting the crossing
The test suite connects as the least-privilege application role against a real Postgres and tries to read another school's rows directly. It asserts the database refuses. A separate test asserts the suite is not running as a superuser, because that assertion would otherwise pass for the wrong reason.
Nothing outside the data layer speaks SQL
A build check fails if any handler imports the database driver, holds a connection, writes raw SQL or names a schema. That is what stops a single handler quietly stepping around the isolation while every isolation test keeps passing.
Passwordless sign-in
Passkeys, with a one-time email code as the fallback. No password is stored, so there is no password to leak, reuse or phish.
Server-side authorisation on every route that serves student data
Every guarded route checks the session on the server, and a build check fails if a handler omits it. The browser-side route guard closes the anonymous case for convenience; the server relies on its own check.
Classification declared per field
Every field an extension can read has one classification: internal, sensitive or restricted. A capability granted at one tier reads fields at that tier and below, and a field nobody has classified is returned to nobody, so a new column reaches no extension until somebody decides what it is. The same declaration decides what is encrypted: a field classified restricted is stored as ciphertext, and a build check fails if one is not. A death recorded against a person, and the reason someone left the school, are restricted, and an ordinary read capability does not reach either.

Retention and disposal

Every record has a retention class rather than a delete button. Removing a record from the working set and disposing of it are separate acts, because a registrar removing a duplicate has not made a decision about a forty-five year obligation and should not be able to.

  • Child-safety records: 45 years, counted from the child's date of birth rather than from when the file closed, because the obligation is owed to the child.
  • Attendance registers and NCCD evidence: 7 years past the year they record.
  • Immunisation status: 3 years past the enrolment it was collected for.
  • An enquiry that never became an application: 2 years. A person is created at first contact, so most person records belong to people with no ongoing relationship to the school. Holding them indefinitely is the thing the Children's Online Privacy Code exists to stop.
  • Legal hold overrides all of it. A hold is checked before a record's age and before whether it was deleted at all, because a hold that only applied to records past their date would protect nothing.

Where a record has more than one class, the longest wins: a floor another class could bring forward is not a floor.

Backup and recovery

Production is backed up three times a day, at 03:15, 11:15 and 19:15 Melbourne time. Each run dumps the whole database in one transaction-consistent snapshot, which is every school at the same instant rather than a set of files taken minutes apart, compresses it and encrypts it. The job refuses to run rather than write an unencrypted copy. Eight days are kept.

The restore is tested, not assumed. On the first of each month the most recent backup is decrypted into a scratch database beside the live one, and the schema count and the school registry are checked against what was recorded when the dump was taken. A backup job that has been silently failing for six weeks looks exactly like one that works, and so does a bundle that restores into an empty database. Both the backup and the verification report to monitoring, so a run that stops happening raises an alert rather than being discovered when a restore is needed.

The backups are held on the host that runs the database, encrypted, for eight days. They cover a dropped volume, a bad migration, a deleted container and an operator mistake.

The recovery point is up to eight hours, being the gap between scheduled backups. Recovery lands on whichever backup you choose.

The test environment is not backed up, deliberately. It holds invented data and exists to be rebuilt.

Incident response

If we become aware of unauthorised access to a school's data, we contact that school's nominated privacy contact directly, in writing, with what we know and what we do not. Australia's Notifiable Data Breaches scheme requires an assessment within thirty days and notification to affected individuals and the OAIC where serious harm is likely; a school is the entity with the relationship to those families, so our job is to give them what they need to make that assessment quickly.

Reach us at security@seraco.io.

Sub-processors

The complete list. Nothing else receives any data. The Privacy Policy shows the same list, because both pages read it from one place.

The hosting provider

Runs the virtual machine that hosts the application and its database.

Receives All of it, as the operator of the host.

In Australia

Let's Encrypt

Issues the TLS certificate that secures the connection to the site.

Receives The domain name. No personal information.

In United States

Resend

Delivers the one-time code that signs a member of staff in by email.

Receives The email address the code goes to, and the code. No student information.

In United States

There is no analytics provider, no error-reporting service, no customer-messaging widget and no advertising network. The content policy blocks them, which is deliberate.

Children's Online Privacy Code

The OAIC's Children's Online Privacy Code binds vendors from 10 December 2026. Two design decisions are made against it rather than retrofitted: a person exists from first contact, which means the enquiry retention class above is load-bearing rather than housekeeping; and no data leaves for analytics or model training, so there is no secondary use to disclose.

Questions this does not answer

Send them to security@seraco.io and we answer in writing, then add the answer here. A question a school had to ask is a question the next school will ask too.