Security overview
A pantry’s client list is a list of people who needed help. Leaking it does real harm to people who are already having a hard year. This page describes what we actually do about that — and, at the end, what we do not yet claim.
Contents
1. Isolation between organizations
PantryPOS is multi-tenant: many pantries share one system. The usual way to keep them apart is to
remember to add WHERE org_id = ? to every query. That works right up until the one
query where somebody forgets.
We do it in the database instead. Every table holding tenant data has PostgreSQL row-level
security switched on, with FORCE ROW LEVEL SECURITY, and a policy that compares
each row’s organization against the organization pinned to the current connection. Each API
request pins exactly one connection to the signed-in user’s organization for the life of that
request, and releases it when the response finishes.
The practical effect: a query that forgets its filter returns nothing, rather than returning another pantry’s clients. The database refuses, not the application.
Isolation is verified by an explicit test suite that attempts cross-tenant reads and writes and requires every one of them to fail.
2. Authentication
- PINs are hashed with scrypt, a deliberately memory-hard algorithm, with a per-user random salt. We never store a PIN in recoverable form. Nobody at PantryPOS can tell you what your PIN is, and neither could anyone who stole a copy of the database.
- PIN comparison is constant-time, and the length of the stored value is checked before comparison, so an unknown username and a wrong PIN are indistinguishable from the outside — both return the same error, and neither returns a server error that would give the attacker a signal.
- Two-factor authentication (TOTP) is available, works with any standard authenticator app, and includes a replay guard so a code cannot be used twice.
- Per-account lockout: after a configurable number of failed attempts (5 by default) the account locks for a configurable period (15 minutes by default).
- Per-IP rate limiting on every sign-in surface — staff login, the client portal and the volunteer portal — plus a tighter limit on password-reset requests.
- Sessions are signed with HMAC-SHA256 and verified in constant time, with domain separation so a token minted for one surface cannot be replayed against another. Session cookies are
HttpOnlyand are markedSecureover HTTPS. - Sessions are revocable: deactivating a user, or changing their role, takes effect on their next request rather than whenever their session happens to expire.
Why a PIN and not a password? Because a check-in desk with a queue of people will write a password on a sticky note, and share it. A short PIN that each volunteer can actually type between guests, backed by lockout and rate limiting, produces a more honest audit trail than a strong password nobody uses individually. With a 5-attempt lockout and a 15-minute cooldown, guessing a 6-digit PIN takes on the order of weeks per account — before the per-IP limit is counted.
3. Access control inside your pantry
Staff hold one of two roles. Administrative surfaces — user management, settings, API keys, bulk client export, network export, custom reports — are gated server-side on the administrator role, not merely hidden in the interface. A volunteer at the check-in desk cannot bulk-export your client list by guessing a URL.
In a multi-site organization, staff can be scoped to their own location while directors see the whole network.
4. Encryption and transport
- All traffic is served over HTTPS; plain HTTP is redirected permanently to HTTPS.
- HSTS is enforced with a one-year max-age, so a browser that has visited once will refuse to connect insecurely afterwards.
- A strict Content-Security-Policy is applied to every response, along with
X-Content-Type-Options: nosniff,X-Frame-Options: DENYand a same-origin referrer policy. Framing the application is blocked outright. - Backups are encrypted before they leave our infrastructure; the off-site provider stores ciphertext it cannot read.
5. Logging without leaking
An audit log is only worth having if it does not become a second, less protected copy of the data it is supposed to protect.
- Every change is recorded with the acting user, the action and the time.
- The payload of each entry is passed through a recursive redactor before it is written, which strips PINs, tokens, API keys, signatures, embedded data URLs and personal-data fields.
- Server error records are separately scrubbed of email addresses, phone numbers and long numeric identifiers before storage — a database error message will happily quote a client’s email address otherwise.
- A committed checking script walks the audit log structurally and reports whether redaction is demonstrably active. It reports “inconclusive” rather than “clean” when there is nothing to check, so an empty log can never be mistaken for a verified one.
6. Backups and recovery
- Nightly logical database backups, retained for 30 days.
- Nightly full-system snapshots of the application server.
- Both are encrypted and copied off-site every night.
- A staleness monitor runs separately and alerts if a night’s backup does not appear — because a backup job that silently stops looks exactly like one that is working.
- A written restore procedure is maintained alongside the code.
- An automated restore drill runs every week. It restores the most recent nightly backup into a scratch database, compares it against production, and checks that the database-level isolation between organizations came back with it — then discards the copy. It alerts us if it fails.
Why the drill checks isolation, not just row counts. A restore that brings all the records back but loses the row-level security policies would look like a complete success and would be a breach: every organization would be able to read every other one. Counting rows would not catch it. The drill fails if the policies do not come back, and it has been tested against a deliberately corrupted backup to confirm it actually fails rather than only ever passing.
Recovery position, in plain numbers. Backups run nightly, so the most we would lose is up to 24 hours of changes. Restoring the database takes minutes. Rebuilding a whole server from an off-site image would take an hour or more, and we will say plainly that the image path has never been rehearsed — the database path, which is the one that holds your records, is the one we test.
7. Application security
- No string-built SQL. Every query is parameterised; the few places that build a query dynamically select from a fixed allow-list of column names.
- Transactional integrity on the operations where a race would corrupt data — checkout, FIFO lot allocation, client merges, appointment booking and slot capacity all take explicit row locks.
- A minimal dependency tree — five runtime dependencies. Most of what a framework would pull in, this does not have, which is a large class of supply-chain risk simply not present.
- Scoped API keys, stored hashed, individually revocable, with a visible prefix so you can tell which key is which without storing the key.
- The application runs behind a reverse proxy and does not expose the database to the network.
8. Operational practice
- Database schema changes go through a tracked migration runner, so a rebuild produces the same schema as production rather than something close to it.
- Application secrets are held in a file readable only by the service account.
- The service restarts automatically on failure and has a memory ceiling that recycles it rather than letting the box degrade.
- A health endpoint reports database connectivity and recent error volume, and is polled continuously.
- The system has been through a structured security audit across authentication, access control, data integrity, front-end, operations and compliance, and again specifically for tenant isolation. Findings were fixed and re-verified against the running system rather than against the notes.
9. Reporting a vulnerability
Email dale.weaver@0xpi.com with enough detail to reproduce the issue. We will acknowledge within two business days.
We will not pursue legal action for good-faith research that stops at demonstrating the problem, does not access or modify other organizations’ data, does not degrade the service for pantries, and gives us a reasonable chance to fix it before publication. Please do not test against a live pantry account that is not yours — ask us and we will provide one.
10. What we do not claim
Security pages are usually a list of everything that sounds reassuring. Here is the other half.
- We are not SOC 2 or ISO 27001 certified. No third party has audited these controls. Everything above is our own account of our own system.
- We do not offer a contractual uptime guarantee yet. We monitor continuously and the software keeps working offline, but we will not put a number in a contract that we cannot yet evidence.
- Data is not encrypted at the individual field level. It is encrypted in transit and in backups; the live database relies on host and access controls.
- We have not yet run a live distribution day at a partner pantry. The product is fully built and tested end to end, but it has not been proven under a real queue on a real Tuesday morning. We are looking for the first sites to pilot it, and we would rather you knew that going in.
- Whole-server images have never been test-restored. The database backups are restored and verified weekly and automatically; the full-machine images are known to be written and encrypted, which is not the same as known to restore. We would rather draw that distinction than let “we have backups” cover both.
If any of these is a blocker for your organization, say so — several are on our roadmap, and knowing which one you need moves it up.
PantryPOS