Pentest Platform Security Architecture The Page We Would Ask a Vendor For.

Not a trust badge and not a list of adjectives. The mechanisms Pental is built on, described precisely enough that you could go and verify every one of them.

121RLS policies
99tables with RLS
107freshness checks
2independent signatures
9encrypted secret columns
0copies of your data
If We Were Breached

The Question Worth Asking Any Platform in This Space.

Every vendor will tell you they take security seriously. The useful question is narrower: when they are compromised, what does the attacker actually get of yours? Here is ours, answered precisely.

If Pental Were Compromised
What Is Exposed
What runs your account: who you are, your subscription and the billing record held with our payment processor, how you sign in to pental.io, your portal's address and where your database is, two counts of your users and client records, and what you have sent us. That is the entire inventory on our side, the one the privacy policy sets out, and it is the same list whether one firm is affected or all of them.
Mail on Its Way Out
Until your portal sends from your own mailbox, its email leaves through Pental's sending mailbox, which keeps no copy of it.
What Is Not
Every client record, engagement, phase, finding, piece of evidence, screenshot, report, retest and stored credential you hold. None of it is on our systems to take.
Why Not
Your data lives in a Postgres database you created, in your own account, in a region you chose. We keep no copy, no replica and no backup of it, and there is no general-purpose read path from our infrastructure into yours.
What We Would Owe You
Notice, and an account of what was reached. Not a data breach notification on your behalf, because your clients' personal data would not be involved.
Why a Pental Employee Cannot Read Your Data
Wrong Credentials
Reading a finding needs a session your database will accept. It signs its own tokens with its own keys, so nothing we hold validates against it. There is no support account, no break-glass login and no key of ours in your project.
The Database Decides
Access is enforced by row-level security policies Postgres evaluates on every query, and those policies also require that the session recently proved a second factor. A request that fails them receives zero rows, whoever made it.
Only You Can Let Anyone In
Adding a user, changing a role or resetting a factor happens in your own portal, by your own administrators. We cannot do any of it for you, so there is no internal process that ends with one of us holding a valid session.
Not a Promise, a Position
This is not a policy saying our staff will not look. It is that the project is in your hosting account, under your credentials, and we were never given a way in.
What Protects the Code We Serve
Why It Matters
Your portal's code comes from Pental, and it is what a signed-in person in your firm acts through. Whoever could change it could reach what that person can, so who can change it matters as much as what we hold.
Who Can Deploy It
Only Pental's technical director, from Pental's own computer, through a Vercel account behind two-factor sign-in.
Signed on That Computer
Updates to your database are signed there with Pental's release key, which never leaves it. A gateway and the self-hosting kit apply none without that signature, so a compromise of our servers cannot make one. On Supabase Cloud you run each update yourself, and can check it against its SHA-256 first.
A user list showing each team member’s name, email address and role.
Thornbury CyberTheir teamDemonstration portals. The four firms are invented; every screen is the real software.
Isolation

One Firm, One Database. The Separation Is Physical.

Every firm runs on its own Postgres database (PostgreSQL in its own cloud, a self-hosted stack or a Supabase project), in its own account, in a region it picks. No shared table holds several firms’ rows next to each other, so there is no tenant filter that could be written wrong.

The usual arrangement is one database with a tenant column, where the entire separation rests on every query remembering to filter. Here there is nothing else in the database to reach.

Separation
physical, not logical
Shared tables
none
Region
yours, at creation
Authorisation

Access Is Decided by Postgres, Not by Application Code.

121 row-level security policies, and row-level security enabled on 99 tables, evaluated on every query. Application code cannot wave them through and neither can a bug in it. A request that fails a policy gets zero rows, not an error describing what it was not allowed to see.

Roles sit in the same place: internal roles by seniority, external testers who see only the work handed to them, and client accounts locked to their own organisation. A client reads its own record, only its own assessments, and only the findings you release. None of that is enforced by hiding a button.

Policies
121
Tables
99
Enforced by
Postgres, per query
Freshness

A Stolen Token, on Its Own, Reads Nothing.

Most platforms treat MFA as a property of the account: you enrolled once, so you are trusted. The right question is whether this session has recently proved possession of a second factor.

Every data-access policy calls mfa_is_fresh(), 107 policies in all, so lifting a token off a machine is not enough to read a finding. Enrolment is a gate on first use rather than a setting, and binding a new factor needs control of an existing one, so a hijacked session cannot enrol a device of its own.

Freshness checks
107 policies
Enrolment
before the portal opens
New factor
needs an existing one
Server Calls

Two Signatures, Held by Two Different Parties.

When our back end calls into your database it cannot simply present a secret. The call carries an envelope signed twice with Ed25519, once by the application and once by an independent cosigner under separate administration. Your database holds both public keys and verifies both.

A call signed with only one of the two keys does not validate, so there is no single machine an attacker can own that produces a valid instruction to your database.

The envelope is bound to one operation and one tenant. Your database learns which firm it belongs to on its own, while it is set up or from Pental’s first call after an update, with no step for you; from then on it refuses an envelope made for any other firm, so a call captured at one firm’s database is worth nothing at yours. It carries a nonce checked against a store with a unique constraint, and expires. Expiry is bounded in both directions, rejecting anything stale and anything dated more than ten minutes ahead, so a forged clock buys nothing.

Signatures
two, held separately
Bound to
one operation, one tenant
Replay
blocked by a nonce store
Clock skew
bounded both ways
Least Privilege

The Most Sensitive Routines Refuse the Server Entirely.

Reads of your data and administrative mutations are gated behind a real user session, so they cannot be invoked with server credentials at all. Even with full control of our infrastructure those routines still refuse, because what they require is a signed-in person in your portal. That person acts through the code Pental serves, so who can change that code is set out under If We Were Breached.

Grants are explicit rather than left at Postgres defaults: every privileged function is revoked from PUBLIC, anonymous and authenticated callers, then granted back only where needed. All 609 definer functions pin their search path, so an object planted in an earlier schema cannot redirect them.

Explicit revokes
723
Definer functions
609
Pinned search paths
609
MFA reset
needs your own admin
Secrets

Encrypted on Write, Inside Your Database, with a Key We Never See.

SMTP passwords, AI keys, Stripe keys and integration tokens are encrypted by a trigger the moment they are written, using a key generated inside that same database. The plaintext column is nulled, so only ciphertext is at rest, and decryption happens in memory only to carry out the action you switched on. AI requests and calls to a client’s tracker are made from your own database, or from your Pental gateway from version 1.5.0, so the key and what is sent do not pass through Pental. Stripe is called the same way: your database makes the calls with your own Stripe key, on Supabase or behind a Pental gateway from version 1.9.0, and once it has, Pental’s servers are no longer given that key. The email the portal writes leaves the same way: your database sends it through a connected Microsoft 365, Google or Zoho mailbox, and a gateway from version 1.6.0 through your own mail server as well. Pental’s servers only renew the mailbox’s sending key, which can send mail and cannot read it, and where your side cannot send, the email Pental sends leaves out a finding’s title and score and a comment’s words.

Nine columns are covered, and the install verifies every one of them on each run rather than assuming.

Encrypted columns
9
Key location
your database
Plaintext after write
nulled
Credentials

Sign-In Built the Way the Guidance Actually Says to Build It.

Email and password by default, then a mandatory second factor. Hashes are bcrypt at cost 12 with a SHA-256 pre-hash, because bcrypt truncates at 72 bytes and a long passphrase should not quietly become a short one. Policy follows NCSC: a 12-character floor, no composition rules, no forced expiry, and rejection of the padded common roots people actually type.

One switch moves the sign-in page to emailed one-time codes instead. Setup, reset and code links travel in the URL fragment, which browsers never transmit to a server, so a mail scanner that pre-opens links cannot consume one and cost the recipient their sign-in. Fragments stay out of server logs and referrer headers too.

Setup and reset tokens are stored only as a SHA-256 hash, single use and expiring, so reading the database cannot be replayed into an account takeover.

Where your own database sends the portal’s email, it makes sign-in as well. A code or a setup link is made in your database and sent from your own mail, a code kept only as a salted hash bound to the browser that asked for it, and single sign-on is checked by your database against Google’s and Microsoft’s published keys (behind a Pental gateway, from version 1.9.0). Once your side makes all of it and has signed someone in, Pental’s servers are refused if they try to sign anyone in to your portal.

Default
email and password, then MFA
Hashing
bcrypt 12, SHA-256 pre-hash
Code mode
one switch away
Reset tokens
hashed, single use
Enumeration

A Wrong Address and a Real One Behave Identically.

Throttling is keyed on a hash of the address submitted, not on the account. If only real accounts could be throttled, an attacker learns which addresses exist from which ones eventually answer "too many attempts". Hashing also stops the table becoming a readable list of who has been typing at your login form.

Rejections are undifferentiated across unknown, inactive, no-password and wrong-password, with constant hashing work on every one so timing gives nothing away. It throttles rather than locking out, because a hard lockout hands an attacker a denial of service against a named user.

Throttle key
hash of the address
Rejection
one generic answer
Timing
constant work
Lockout
throttle instead
Destruction

The Dangerous Operations Require a Hardware Key over the Payload.

Where an operation is genuinely destructive it can require a physical security key, and the detail that matters is what the key signs: a hash of the actual operation and its parameters, not a bare approval prompt. What was approved cannot be swapped afterwards.

Challenge
hash of the operation
Approval
bound to one payload
Swap after approval
not possible
Boundaries

The Edges Nobody Puts on a Marketing Page.

Storage buckets holding avatars and assessment files are private, not public with an unguessable path. Sessions are per-device rows you can list and revoke, with a configurable timeout and stale sweep. Self-hosted AI endpoints are filtered against private address ranges, so a URL field cannot become a request-forgery primitive against your own network.

The marketing site sends no framework banner and ships no browser source maps. The contact form is behind reCAPTCHA. Rate limits are counted in your own database, so they keep working independently of us.

File storage
private buckets
Sessions
revocable per device
AI endpoints
private ranges blocked
Rate limits
in your database
Questions

Questions About Security.

Yes, for everyone who signs in, your team and your clients alike, using an authenticator app or a passkey. Changes need a recent second factor, and active sessions can be listed and signed out, so a session left open on another device can be closed.

Through the portal rather than by email. The client fills in a logins grid and can attach files, and each tester views the credentials once and acknowledges them, which keeps passwords out of inboxes and chat threads.

Yes. AI can be switched off entirely, or run on a local model through Ollama on hardware you control, so drafting never involves an outside provider. If you do use a hosted provider, it is your own key for OpenAI, Anthropic or Groq, stored encrypted in your database.

No. Row-level security policies in your own database decide who sees what, and each client sees only what you have released to them. Your own team is governed by the same policies, so access is decided by the database rather than by what the interface chooses to show.

Questions This Page Did Not Cover.

Send them. Technical questions get a technical answer from someone who wrote the code.