---
title: "Trust and security"
description: "How the service is built, what the server stores, and which security and compliance controls exist today."
canonical: https://keepitlocal.app/trust
last-updated: 2026-09-13
---

# Trust and security

How the service is built, what the server stores, and which security and compliance controls exist today.


Version 0.1 (draft). Last updated 7 September 2026.

This page describes how KeepItLocal Redact for the web is built, what the server holds, and
which controls exist today. It is written so that a security reviewer can check the claims
rather than take them on faith, and so that most of a security questionnaire is answered before
it is sent.

## How it works

You open a document in the browser. A WebAssembly build of our redaction engine, the same engine
as the desktop application (coming soon), loads into the tab, renders the pages, runs the
detectors, applies the marks you approve and writes the redacted file. All of that happens on
your machine.

The server's job is narrow: it holds your account, decides whether your plan allows the export,
and signs a certificate for it.

### How to check that for yourself

1. Open your browser's network panel before you start.
2. Load a document, redact it and export it.
3. Watch what the tab sends. You will see the page's own assets, the engine and its entitlement
   ticket, and one small certificate request carrying two checksums and a few counts. You will
   not see the document, the rendered pages or any text out of it.

The reason you cannot see it is not that we chose not to send it. The page is served with a
content security policy whose `connect-src` directive is `'self'`, so the browser refuses to
let the page or the engine open a connection to any other origin. That single directive is the
enforcement behind the whole architecture, and it is worth checking in the response headers.

## What the server stores

| The server holds | The server never holds |
| --- | --- |
| Your email address and display name | Your documents |
| Session records: a hash of the session token, timestamps, user agent, network address | Any page image, thumbnail or preview |
| An audit trail of security-relevant actions, including failures | Any text a detector matched |
| Export certificates: two SHA-256 checksums, a redaction count, a page count, an engine version and a timestamp | The names of your files, unless you publish a report |
| Export counters for plan metering | Your redaction marks or their coordinates |
| Shared report metadata, if you choose to share a report | Your password (only an Argon2id hash) |
| Early access requests, if you submitted the form | Card details (Paddle holds those as merchant of record) |

One caveat we state rather than hide: the certificate records the checksum of the original,
unredacted file. A checksum cannot be reversed into a document, but someone who already holds
a copy of a short or templated document can confirm that it is the same document. That is why
the checksum is a pointer, not an opaque value, and why we intend to remove the account identifiers from
certificates issued in future. That change is not made yet.

## The certificate model

Every certified export produces a certificate that says: a file with checksum A was processed
by engine version V, producing a file with checksum B, with N redactions across P pages, at
time T. It is signed with an Ed25519 key held by the server and stored alongside its signature.

A recipient checks it at the verification page for that certificate,
`/verify/<certificate id>`, which recomputes the signature over the
stored record. A record altered in the database fails that check and reports itself as unknown
rather than certifying something nobody signed. The recipient hashes the file they were sent
(`certutil -hashfile FILE SHA256` on Windows, `shasum -a 256 FILE` on macOS, `sha256sum FILE`
on Linux) and compares it with checksum B.

A certificate outlives the account that issued it, but not the workspace: deleting a workspace
destroys the certificates it issued, and `/verify/<certificate id>` then answers "unknown
certificate" for files that are genuine. If you are the recipient, keep the `certificate.json`
you were given rather than relying on the link alone.

What a certificate does not say: that the redaction was complete, that the document is safe to
disclose, or anything about what was in it. Those are professional judgements, and they stay
with you.

## Credentials and access

- Passwords are hashed with Argon2id. Session tokens are 32 random bytes, stored only as a
  SHA-256 digest, in cookies that are httpOnly, secure, sameSite and, in production, carry the
  `__Host-` prefix.
- Customer sessions time out after 1 day idle and 14 days absolute, enforced on the server on
  every request. Operator sessions are 8 hours idle and 24 hours absolute. You can list your own
  active sessions and revoke any of them.
- Two-factor authentication uses TOTP (RFC 6238) with a replay guard and single-use backup
  codes. The secret is encrypted at rest with AES-256-GCM; the backup codes are stored as
  keyed hashes.
- The administration console has its own identity, its own session and its own cookie, separate
  from customer accounts. Every protected page and every server action calls an authorisation
  guard, and a test that walks the source tree fails the build if one does not.
- Every action that matters, including failures such as a failed sign-in or a denied
  authorisation, is written to an audit trail. Metadata passes through an allowlist scrubber
  first, and a failure event records only the DOMAIN of a customer's email address, never the
  address itself. An operator's own address is recorded in full on a console action, so that
  "who did this" stays answerable after an operator account is deleted.
- Each audit row carries a SHA-256 hash over the row's non-erasable fields (its id, the
  action, the target and the timestamp) and the hash of the row before it, so a row that is
  deleted, reordered or has any of those fields edited breaks the chain, and the chain is
  verified when the trail is read rather than trusted. The actor, the workspace, the network
  address, the user agent and the metadata are deliberately outside the hash because a lawful
  erasure clears them. Revoking UPDATE and DELETE on the trail from the application role is
  the second layer that protects those, and it is not in place yet: see the open items.

## What the export strips out of a PDF

A PDF carries more than its pages: JavaScript, an action that runs when the file opens, page
actions, form field scripts and XFA, actions on annotations, embedded attachments, name trees,
optional content layers, thumbnails and several metadata blocks. Any of them can survive a
black rectangle drawn over a page.

The leak check in the [studio](/studio) lists what your file carries and says, beside each
item, whether the export removes it. Both exports do: a redaction writes a new document whose
pages are images, so nothing from the original is carried over. Sanitizing keeps the document
while removing the containers, after which the check is run again on the result so that you
see a comparison rather than a claim. The same list is written into the report that
travels with the file. The one thing neither export removes on its own is text you can see on
the page, which is what redaction is for.

## What the export strips out of an Office file

A Word, Excel or PowerPoint file is a zip of parts. Comments, tracked changes, the custom XML
store, macros, embedded objects, vanished text, field instructions, the package thumbnail and
the authoring properties are dropped on sanitize. Speaker notes in a deck stay: they are
slides, not leftovers. Visible text stays too, which is what redaction is for. The leak check
in the studio lists each of those against the file you opened, and the export is read back
before a certificate is issued so a leftover comments part cannot be signed as clean.

## Compliance mapping

Read this as a description of engineering controls, not as a certification. Nothing here has
been audited by a third party.

| Requirement | What exists today | Where |
| --- | --- | --- |
| GDPR Art. 5(1)(c), data minimisation | Document content never reaches the server; the server receives two checksums and a few counts per export | Content security policy, `connect-src 'self'` |
| GDPR Art. 5(1)(e), storage limitation | A retention period per data category, applied by a purge that records every run; the sweep is triggered by hand until a scheduler is provisioned, and every run is recorded either way | Retention schedule, available on request |
| GDPR Art. 13 and 14, transparency | This page and the privacy notice | [Privacy notice](/privacy) |
| GDPR Art. 15 and 20, access and portability | A download in Settings that produces a zip of the account record the server holds: profile, sessions, memberships, certificates, published reports and the audit rows where the account is the actor | [Privacy notice](/privacy) |
| GDPR Art. 17, erasure | Self-service account deletion with password re-authentication, or a typed confirmation checked on the server for accounts that signed in with Google and have no password; the exceptions are written down | [Privacy notice](/privacy) |
| GDPR Art. 28, processors | Published data processing agreement and sub-processor list | [DPA](/dpa), [sub-processors](/subprocessors) |
| GDPR Art. 32, security of processing | The measures in Annex II of the DPA | [DPA](/dpa) |
| GDPR Art. 33, breach notification | Documented runbook with a 72 hour clock | Incident response runbook |
| HIPAA 164.312(a)(1), access control | Unique accounts, no shared logins, role-based access, per-page guards | Access control |
| HIPAA 164.312(a)(2)(iii), automatic logoff | Idle and absolute session caps enforced server side. A shorter, configurable idle window for regulated workspaces is an open item | Session policy |
| HIPAA 164.312(b), audit controls | Broad write coverage including failures, in a hash-chained trail. Logging of operator reads is still open | Audit trail |
| HIPAA 164.312(c)(1), integrity | Certificates signed and re-verified on read; the audit trail carries a hash chain, checked from the operator console, that shows whether any row was deleted, reordered, or had its action, target or timestamp rewritten | Certificate model |
| HIPAA 164.312(d), authentication | Argon2id, TOTP, replay guard, backup codes, and a separate operator two-factor requirement that the console's own toggles cannot switch off | Credentials |
| HIPAA 164.312(e)(1), transmission security | TLS with HSTS, and a production boot check that refuses a non-https origin | Encryption in transit |

The full mapping, with the gaps named, is in our HIPAA technical safeguards checklist and our
SOC 2 readiness checklist. Both are maintained in the repository and are available on request.

## What we do not claim

- We have no SOC 2 report. The controls are being built first; the audit is deferred until a
  specific need justifies it.
- We hold no certification of any kind, and no badge on this site represents one.
- We publish no customer list and no customer logos.
- We do not claim the service works offline. The engine is delivered from the server behind a
  session and a short-lived ticket, so a live connection is needed to start a session.
- We do not claim that detection finds every piece of personal data in a document. It finds
  candidates; you decide.

## Reporting a security problem

Email hello@keepitlocal.app with what you found and how to reproduce it. We will acknowledge
within 2 working days, tell you what we are doing, and credit you if you want it. Please do not
run automated scanning against the production service without asking first, and please do not
access other people's data to prove a point.

## Documents

- [Privacy notice](/privacy)
- [Terms of service](/terms)
- [Data processing agreement](/dpa)
- [Sub-processors](/subprocessors)
- Verify a certificate: open the `/verify/<certificate id>` link printed on the certificate

## Open items

Stated here rather than left for a reviewer to discover:

- The site is hosted on Hetzner in the EU, live at keepitlocal.app and gated. Nightly
  backups and the outside uptime pinger are still open items.
- Logging of operator reads of pages that show customer data is still open, and so is a
  shorter, configurable session idle window for regulated workspaces.
- The audit trail's second layer is not in place: the database grant that revokes UPDATE and
  DELETE on it from the application role belongs with the hosting decision. Until it is, the
  hash chain catches a deleted, reordered or rewritten row, and the fields outside the hash
  (the actor, the workspace, the address, the user agent, the metadata) could be rewritten
  without the chain noticing.
- Nothing schedules the retention sweep yet. The purge itself is built and records every run;
  the scheduler that calls it nightly is provisioned with the hosting decision.
- Nothing on this page has been reviewed by a third party.
