Security

How we protect your secrets.

Exactly how your values are encrypted, isolated, and logged. Every claim on this page points at a mechanism you can ask us to demonstrate.

The server can decrypt your values.

penv-cloud uses a server-side envelope. The server can decrypt your values to serve them to your machines and to show them in an audited reveal. That is what makes penv pull work. This is not zero-knowledge encryption, and we do not describe it as such.

Your key material is wrapped by AWS KMS and every access is least-privilege and audited. On Enterprise the wrapping key can be one you own — in your own AWS, Google Cloud or Azure account, or your own HashiCorp Vault. Disabling it ends our access to your secrets immediately — we cache no copy that would keep working.

The specification

Cipher
AES-256-GCM
Data key
Per value, zeroized after use
Key wrapping
AWS KMS, key-id per version
AAD
org / project / env / path / name
Isolation
Postgres RLS, forced, per transaction
Audit log
Insert-only at the database
01

Encryption

Every value is encrypted with AES-256-GCM under a data key minted for that value alone. That data key is wrapped by an AWS KMS key, which is classic envelope encryption, and the plaintext data key is zeroized the moment the write completes. Decryption zeroizes it again on the way out, including when the decrypt throws.

The wrapping key-id travels on every version rather than living in one global setting, so the key that opens a value is a property of the row.

dek
fresh per value, never persisted in the clear
wrapped_dek + key_id
stored on every value_version

Encrypted, each value under its own key, on every plan. Encryption, address binding and tenant isolation are identical on every plan. They are properties of the system rather than of what you pay.

02

Address binding

The additional authenticated data for each value is its full parameter address. Ciphertext is sealed to the exact place it was written, so a value lifted into another environment, another project, or another organization fails its authentication tag and will not open. Moving the row moves nothing.

aad
org / project / environment / path / name
same address
decrypts
any field changed
UndecryptableError

A value that fails to open renders in its own state, present but undecryptable. It stays visibly distinct from an empty value, so a key problem never reads as a missing secret and the ciphertext is kept intact.

Integration credentials go one step further: their address is recomputed from the row being read rather than taken from the stored AAD column, so a rewritten column changes nothing about what opens.

03

Tenant isolation

Every tenant row is guarded by Postgres row-level security keyed on your organization id, set per transaction and enforced with FORCE ROW LEVEL SECURITY. The app connects as a non-superuser role, so RLS binds and fails closed: a query that arrives without an org context returns zero rows.

app.current_org = <your org>
your rows
app.current_org unset
0 rows

Isolation is the same on every plan, because the database enforces it rather than a tier flag.

04

Machine identity

Machines authenticate as machine identities bound to a single project and environment, holding a role that says what they may do there. A token is shown once at creation and a SHA-256 hash of it is the only thing we keep. Revocation takes effect immediately and is terminal.

bound to one environmentstored as sha-256revocablefree on every plan

CI authenticates without a stored key

A CI job presents its OIDC token and receives a credential with a fixed expiry. There are two exchanges and they have different lifetimes, so here are both:

POST /auth/oidc
1 hour · machine credential
POST /auth/oidc/delivery
5 minutes · carries values

Both TTLs are fixed in the route and neither is sold as an upgrade. The end-of-job revoke hook shortens the window further when it runs, and we treat it as best effort rather than a guarantee, because a cancelled runner may never call it. The expiry is the control that always holds.

Hosts that can attest nothing

A plain VPS has no vTPM and serves unsigned metadata, so there is nothing to verify against hardware. Those hosts enroll an Ed25519 keypair and every login is a challenge-response that advances a generation counter both sides remember. The generation is inside the signed bytes and the advance is a compare-and-set, so only a key holder can move it.

Copy that key and the copy authenticates once. It also moves the counter, so the real host arrives stale on its next login and both are locked out, along with every credential already minted from that keypair. An audit entry names both generations.

This is detection, not prevention.

A cloned key does get one successful authentication before the desynchronization is visible. That is the most an unattested host can offer, and we would rather state the window than imply there is none.
05

Dynamic secrets

A dynamic parameter mints a credential from the upstream provider when it is read, and we store none of it. This is structural rather than a policy we promise to keep: the lease table has no credential column to write one into.

lease_key
the provider's revocation handle
remote_identity
a role session name, not a secret
credential
no column exists

Because a dynamic address holds no value, reading it is neither a hit nor a miss. All three verbs answer 409 with an explicit dynamic reason rather than guessing:

GET
409 · a 404 would invite an overwrite
PUT
409 · a stored version would open for nothing
DELETE
409 · would cascade away live leases
06

Audit

Every read, write, token, and member action is written to the audit log. Insert-only is enforced by the database: row-level security is forced on the table and only SELECT and INSERT policies exist, so UPDATE and DELETE are denied by default, for you and for us alike.

Token issue and revoke are recorded, and lastUsedAt is stamped each time a machine authenticates.

Free
7 days
Pro
90 days
Enterprise
Custom

Every write appends a new immutable version of the value, so history shows who set each version and when, a rotation is reversible, and a mistake is recoverable.

On Enterprise the log streams to your own SIEM. Batches are NDJSON, signed to the Standard Webhooks spec so an off-the-shelf library verifies them, and the cursor advances only on a 2xx — so a receiver that breaks stalls the stream rather than losing events, and the console tells you it has stalled. The signing secret is shown once.

07

Outbound requests

Syncing your secrets somewhere means our servers make requests to an address you chose. That is a server-side request forgery surface, and it is treated as one: every outbound request resolves and range-checks its host inside the socket's own DNS lookup, not before it. Checking first and connecting second leaves a window a DNS rebind fits through.

Private ranges, loopback, link-local and carrier NAT are refused — which includes 169.254.169.254, the cloud metadata address that turns an SSRF into a credential. Refusals are typed and terminal: your run says the address is not one we will connect to, and does not retry into the same answer.

Redirects are never followed. A 3xx is data, not an instruction — following one would re-send the request, with whatever authenticates it attached, to an address nothing validated.

08

What we have not built

Security pages tend to describe a roadmap in the present tense. These are the things a buyer asks for that we do not have today, stated plainly so an evaluation reaches the answer here rather than in month three.

Dedicated isolation
Shared infrastructure. No VPC option, no residency choice.
Private networks
We call your services from the public internet. A vault only reachable inside your VPC is out of reach, by design — see Outbound requests.
Hash-chained audit
Insert-only at the database. The chain is reserved, not live.

If one of these decides your evaluation, tell us. We will say where it sits rather than quote you a date we invented.

09

Compliance posture

Secret values are wrapped in a type whose serializers refuse to print them. Converting one to a string, to JSON, or logging it to a console yields [redacted], so redaction survives the paths where a developer forgets. Reading the value is an explicit call, and it is the only way through.

Traffic is TLS in transit and envelope-encrypted at rest, with least-privilege roles between the app and the control planes. SOC 2 is in progress, and compliance commitments are negotiated per contract on Enterprise.

A report is evidence that somebody independent checked. Until ours exists, here is what you can check yourself — each of these is a property of how the product is built, not a policy document, and several are enforced by the database or the test suite rather than by us remembering:

Logical access
Row-level security is FORCED on tenant tables and bound per transaction, so a query with no org context returns nothing.
Integrity of the log
Only SELECT and INSERT policies exist on the audit table. UPDATE and DELETE are denied to us as well as to you.
Key management
One data key per value, wrapped by KMS, with the key id recorded per version — so a rotation is provable rather than asserted.
Boundary protection
Outbound requests are range-checked inside the socket's own DNS lookup and never follow a redirect.
Vendor management
The subprocessor register is public and names which one holds plaintext in memory.
Secret handling
Values are wrapped in a type whose serializers print [redacted]; revealing one is an explicit call.

None of that is a substitute for an audit, and we are not going to present it as one. It is what an evaluation can stand on while the report is outstanding — and if your procurement gate is the report itself, the honest answer is that we do not have it yet.

The subprocessor register lists every third party in the path and exactly what each one can access, including the one that holds plaintext in memory because it runs the decrypt path.

Our security team answers email.

Talk to us →

Get your keys out of the chat.

Three people free. Machines never take a seat.