Documentation

Security and privacy

Version 1.14 ·Updated 2026-09-16

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 keeps.

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

Where the data lives

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

The host admits SSH, HTTP and HTTPS from the internet and refuses everything else, so the database and the application are reachable only through the site. An intrusion-prevention agent reads the host's authentication log and bans an address that keeps failing to sign in to it. Sign-in to the host itself is by key alone.

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 all products and tells a reader nothing about which attacks it stops.

In transit, browser to Kestrel

The site offers TLS 1.3, and still accepts TLS 1.2 for the programs that call the API. It offers no older version. The 1.2 cipher suites are ECDHE with AEAD only, so both versions are forward-secret. Certificates are 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 1.3. Before it sends anything, it checks the certificate against the one authority allowed to have issued it. Both ends of this hop are ours, so the hop requires the current version: the application will not open a connection below 1.3, and the database will not accept one. The database also refuses a connection that is not encrypted at all. 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

The application itself encrypts the fields classified restricted with AES-256-GCM, under a separate key for each school, before they reach the database. The key that unwraps a school's key is kept outside the database and is not in any backup of it. Destroying one school's key makes that school's records unreadable and leaves the records of all other schools 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

Secrets are 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

The Content-Security-Policy 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 has a nonce this server minted for that single response, so a script injected into a page does not run. Framing is refused.

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

Access control

One Postgres schema per school
Each school's record tables are in a schema of their own, 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, keeps a connection open, writes raw SQL or names a schema. That check stops a single handler from stepping around the isolation while all the isolation tests still pass.
Passwordless sign-in
Sign-in is by passkey, with a one-time email code as the fallback. No password is stored, so there is no password to leak, reuse or phish. A school that has finished enrolling passkeys can turn the emailed code off. Kestrel refuses to turn it off while somebody at that school still has no key.
Sign-in through the school's own directory
A school can point Kestrel at Microsoft Entra ID, Google Workspace or any other provider that speaks OpenID Connect or SAML 2.0. Staff then sign in with the credentials they already hold, and leaving the directory closes the door here too. The directory's groups decide standing at each sign-in, so a person taken out of a group has their standing narrowed the next time they sign in. Kestrel checks the token's signature against the keys the directory publishes and ties the token to the browser that began the sign-in. It refuses an account outside the school's own domain. The client secret is stored as ciphertext under the school's own key and is never sent back to a screen.
Server-side authorisation on each route that serves student data
Each guarded route checks the session on the server, and a build check fails if a handler omits the check. The browser-side route guard turns away a visitor with no session as a convenience, and the server relies on its own check.
Configuration is an administrator's act
A person holds standing at one school, as an administrator or as staff, and that standing does not extend to another school they also work at. Only an administrator of that school can change what the whole school sees, such as its name, its palette, who else works here and whether an emailed code is a way in. The server refuses the change from anyone else, and a build check fails if a configuration route omits the check. The browser also hides the section from anyone else. That is a courtesy, and the server's refusal is the control.
Classification declared per field
Each 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. A field without a classification is returned to no-one, so a new column reaches no extension until somebody classifies it. 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 both restricted, and an ordinary read capability reaches neither.

The audit trail

Kestrel records each read and each change of a record. The entry names who did it, which records it reached, when, the highest confidentiality tier in the answer, and whether it was allowed or refused. Refusals are recorded alongside successes, because a caller repeatedly asking for something they may not have is the signal worth keeping. A copy taken out of Kestrel needs a stated reason, and that reason is recorded with it.

The request pipeline writes the trail from a declaration on each endpoint, so an endpoint added later is covered by the same rule. The trail is in the school's own database schema, behind the same isolation as the records it describes, and it moves with the school's records.

Nothing in Kestrel can edit or delete an entry once it exists, and Kestrel checks that no entry has been changed. Each entry is fingerprinted over the fingerprint before it, so an entry that was altered, removed or reordered breaks all the fingerprints after it. An administrator can check a date range from the screen. The screen tells them what happened, which entry it happened at, and what to do about it. Disposal removes an aged stretch as a whole and records what went and where the chain resumes, so a lawful disposal can be told apart from an alteration. The chain proves the trail is internally consistent. It is not anchored anywhere outside this host, and no copy of the fingerprints is published elsewhere.

An entry names a record by reference and never reproduces it, and any count it records is a number with no content. A trail entry outlives the record it names, so removing a person does not remove the evidence of who read them. Entries are kept as their own retention class, listed below.

Third-party plugins are recorded under their own identity, so a scheduled run at two in the morning is attributed to the plugin that ran it.

Retention and disposal

Each record has a retention class. Removing a record from the working set and disposing of it are separate acts. A registrar removing a duplicate has made no 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, 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.
  • The audit trail: 7 years, counted from the access, because a privacy incident is often found years after the access that caused it. A trail entry outlives the record it names.
  • 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. Keeping them indefinitely is what 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, because a hold that applied only to records past their date would protect nothing.

Where a record has more than one class, the longest wins. A person's file and the names and affiliations on it are one record for this purpose, so nothing on it is disposed of while any part of it is still owed.

Disposal runs each night at half past two in your school's own time zone. Each run records how many records went, under which class, and how many stayed and why. It records counts and none of the records. A list of what was disposed of would be a copy of what we had just accepted an obligation to destroy.

Reaching, correcting and deleting a person's record

A person can ask to see what a school keeps about them, ask for it to be corrected, and ask for it to be deleted. Each request is recorded in the school's own schema from the day it arrives, with a reference the school quotes back. Two dates apply to it: acknowledged within 5 business days, answered within 30 days. Both are fixed when the request arrives, so a later change to either period does not move a date somebody is already waiting on.

The register is at /app/settings/privacy-requests, behind two capabilities of its own. The settings page shows how many requests are outstanding and how many are past a date. Reading the register, recording a request and recording what was done about it are each in the audit trail. The row contains references, dates and what was decided. It never contains the name, address or phone number of whoever asked, and the correspondence stays in the mailbox it arrived in.

A deletion request runs into the retention periods above. The answer says which parts of a record are being kept, which obligation keeps them and when that period ends. The Privacy Policy tells a parent, a student or a member of staff how to make the request.

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 captures all schools at the same instant, then compresses and encrypts it. If the job cannot encrypt the copy, it refuses to run. Eight days are kept.

The restore is tested. On the first of each month the most recent backup is decrypted into a scratch database beside the live one. The schema count and the school registry are then checked against what was recorded when the dump was taken. A backup job that has failed silently for six weeks looks like one that works, and so does a bundle that restores into an empty database. Each run of both jobs records whether it succeeded and the time it finished, as a metric the host collects. The last good backup and the last good verification are therefore on record.

The backups are kept 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, which is the gap between scheduled backups. A recovery restores whichever backup you choose. Shortening that gap means archiving the write-ahead log, which would put the archive on the same host as the database it protects. That would improve recovery from the faults the backups already cover and do nothing for the loss of the host. Eight hours is a decision taken on that basis, and the reasoning behind it is written down.

The recovery time is measured on a real restore. The monthly verification is the only full restore of the production database anyone performs on a schedule, so the number comes from it. The verification records how long the bundle took to decrypt and load, and an alert reaches us when a restore runs longer than thirty minutes. That is the figure we commit to, and it covers the database coming back. Rebuilding the host underneath it is a separate job. That job has not been timed, and its duration is outside the number.

The backups meet the recovery obligation and none of the retention ones. Eight days of bundles is how far back a mistake can be undone. The forty-five year child-safety retention and the shorter classes above are met by the live database, where the records are, and by the nightly disposal that applies each class. The same eight days make a disposal real: a record destroyed tonight is gone from all backups within eight days. A longer archive would keep a copy of what a school had just accepted an obligation to destroy.

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

Availability

The host asks the application every minute whether it can serve, and records the answer. It asks twice. The first question goes to the internal port, which answers only when the database is reachable and the schema is at the version this build expects. The second goes to the public address, through the reverse proxy and its certificate. The two are asked separately because they fail apart. A healthy application behind a certificate that stopped renewing is an outage for a school, and it cannot be seen from inside the host.

An alert reaches us when either answer has been refused for five minutes, which is long enough that an ordinary release does not raise one. The probe runs on the host it is asking about, so it reports a network fault between that host and a school as healthy.

The same alert sends your school's nominated contacts a message saying Kestrel is not available, and a second message on the recovery saying how long the outage lasted. The addresses are held on the host that runs the deployment, outside the application, so the message still goes out while Kestrel itself cannot serve. Tell us who should be on that list and we will set it.

No availability figure is published, because none has been measured across a full term. The Terms of Service publishes none either. What is in place is the measurement and the alert described above.

Capacity

The deployment serves forty people using Kestrel at once, across the schools it serves, and it has been measured serving half as many again. That is the 50 per cent headroom above the figure. A person here is a member of staff working in the console with a pause of about five seconds between one request and the next, which is faster than somebody reading a screen before they act on it.

The figure comes from a measurement. The production build of the application runs in a container limited to the memory the production container is given, with one processor, beside a database limited to its production memory and one processor, with the same number of database connections production uses. The school it serves is generated: a thousand students with their families, staff, timetable and a term of attendance. Each simulated person is a teacher on the roll screen who opens the roll, saves a mark, reads the roll again and opens a student's record. People are added in steps, and a step counts only when no request fails and nineteen requests in twenty are answered within one second. The published figure is at most two thirds of the largest step the run sustained.

It was last measured on 16 September 2026, on our own hardware under those limits and away from the production host. A change to either memory limit or to the number of database connections stops our build until the measurement has been taken again.

One application container serves the deployment. The rate limits count requests inside that container, so adding a second container is a change to the application and cannot be made by configuration alone.

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. The school has the relationship with those families, so our job is to give it what it needs to make that assessment quickly.

Reach us at security@seraco.io.

Sub-processors

This is the complete list, and 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.

Children's Online Privacy Code

The OAIC's Children's Online Privacy Code binds vendors from 10 December 2026. Two design decisions are made with it in mind. A person exists from first contact, so the enquiry retention class above decides how long the record of someone who only enquired is kept. No data leaves for analytics or model training, so there is no secondary use to disclose.

Seraco assesses the privacy impact of Kestrel on children and publishes the register of those assessments, with the finding of each, at Privacy impact assessments.

Questions this does not answer

Send them to security@seraco.io. We answer in writing and add the answer to this page.