Platform Agents Security Pricing Docs
Read as
Book a walkthrough

Security

Security you can check.

Every request to Lyhnis passes four guards. Each one says no by default, and our tests fail if anyone takes one away. Send a request and watch where it stops.

RequestmeridianAllowed

  1. AWhose data is this?—
  2. BAre you allowed?—
  3. CWhere is it going?—
  4. DWho did it?—

Response
Fig. 1  One request through the four guardsIllustrative requests

01Guards

Four guards that say no first.

Each guard answers one question: whose data is this, are you allowed, where is it going, and who did it. They are built differently because the questions are different, and every one of them refuses by default.

One default-deny chokepoint per axis, each of the right type for its job: a query listener, a pure permission check, a URL validator and a single write path for the audit trail.

Live

Guard A

Whose data is this?

Every read from the database is filtered to your company before it runs. If our own code ever forgets to say which company, the read fails instead of returning anything.

A single session-level do_orm_execute listener adds tenant criteria to every tenant-bearing table (24 today, found by mapper introspection, so new tables are covered automatically). An unset scope raises TenantScopeViolation. Fail-closed is the default.

Held in place by
Automatic coverage of every table with a tenant column
Not yet
A second, database-level layer (Postgres row-level security) under raw SQL
Live

Guard B

Are you allowed?

Every action checks the person’s role before it runs. A viewer can look but can’t approve or change anything, and another company’s records don’t exist for you at all.

can(principal, permission, resource) is a pure decision. It denies with no principal, with a permission outside the role, or when the resource belongs to another tenant. 104 route declarations use it, and it also gates agent tool calls and the inbound MCP bridge.

Held in place by
A test that classifies every route. A route with no declared check fails the suite.
Not yet
Nothing structural outstanding
Live

Guard C

Where is it going?

When a workflow calls a web address you configured, Lyhnis checks where that address really points, and checks again after every redirect. Internal networks and the cloud’s metadata service are refused.

validate_egress_url(): scheme allowlist, an explicit block on the cloud metadata address, and a denylist of loopback, private, link-local and reserved ranges on every resolved IP. It fails closed on resolution errors and re-runs on each redirect hop.

Held in place by
Wired into every step that takes a user-supplied URL, including webhooks, MCP servers and OAuth
Not yet
Built-in connectors to fixed hosts (Stripe, Google, Microsoft and others) don’t pass through it yet. They can’t be pointed elsewhere, and one universal egress layer is planned.
Live

Guard D

Who did it?

Every change goes into the audit trail through one front door. It records who acted (a person, an API key or the system) and from where, and strips secrets out before anything is stored.

audit_emit() owns attribution, redaction and de-duplication. Mutating requests are recorded by default, the API key that acted is recorded, and 97 older call sites were moved onto it.

Held in place by
A test that pins the audit posture and every reviewed exemption
Not yet
For five sensitive action types, the action and its record can be saved as one transaction. That mode is built and is being switched on type by type.

02Origin

How we got here.

In June we audited our own platform and logged 62 findings. Nearly all of them had the same cause: each protection was written by hand, wherever someone remembered to. So instead of 62 patches, we built four guards that refuse by default, and tests that fail if anyone goes around them.

Findings SEC-001 to SEC-062 traced to one root cause: no enforced default-deny primitive on any axis. They were closed as four chokepoints (Guards A to D, built C → B → A → D), each locked by tests, on 24 June 2026.

SEC-001 to SEC-062Closed as four guards, 24 June 2026

62Findings logged
4Guards built
104Routes checked
24Tables scoped automatically

03Sign-in

Sign-in that holds up.

Multi-factor sign-in, careful recovery and short sessions. What isn’t built yet is labelled below, not implied.

A factor-abstraction schema, so passkeys arrive as new rows rather than a migration.

Authenticator-app codes

Any standard authenticator app. The secret behind it is encrypted with its own key, kept apart from the vault that holds your credentials.

TOTP via pyotp; secrets encrypted with a separate FERNET_MFA_KEY to contain the blast radius.

Ten recovery codes

Shown once, stored only as hashes, and each works one time. Using one is written to the audit trail.

SHA-256 at rest; consumed with an atomic update, so two uses can’t race.

Lockout

Five wrong attempts in fifteen minutes locks the account for fifteen minutes.

Redis-backed; HTTP 423 with Retry-After.

Password check before MFA changes

Removing a factor, making new recovery codes or turning MFA off asks for the password again.

Step-up re-confirmation on every destructive MFA operation.

Admin reset for a lost phone

An admin can reset a member’s MFA after re-entering their own password. The record names both people.

ADMIN-only, same tenant; the audit row carries the admin and the target user.

Short sessions

Access tokens last 30 minutes. Changing your password or your MFA ends every older session.

Tokens issued before password_changed_at or mfa_changed_at are rejected.

Not live yet

  • Require MFA for everyone in a companyIn development
  • Single sign-on with Microsoft Entra IDRoadmap
  • PasskeysRoadmap

04Secrets

Secrets stay secret.

The API keys, passwords and tokens you store in Lyhnis are encrypted, never sent back by the API, and decrypted only inside the step that uses them. Every decryption is recorded.

Fernet-encrypted at rest. Never serialised into an API response. Decrypted at dispatch into the step’s credential context, and the decrypt itself is an audited event.

Reading a stored credential200
GET /api/v1/credentials/stripe_live

{
  "name": "stripe_live",
  "type": "API_KEY"
}
# no secret field, in any response, ever
Fig. 2  What the API returns for a secretIllustrative response
  1. 01Stored encryptedFernet, with its key held outside the database
  2. 02Never returnedReads show the name and type only
  3. 03Decrypted in the stepFor one run of one step, then gone
  4. 04RecordedEach decryption lands in the audit trail

05Money

Money can’t move twice.

Security for work that moves money includes the mistakes nobody intended: a shop that sends the same claim twice, an operator who clicks retry, a network that drops a reply.

Idempotent triggers, replay-safety declared per activity, and provider idempotency keys derived from the claim.

Response200 · duplicate
01

The same claim arrives twice

Lyhnis recognises it and answers the shop, but no second job starts and no second refund is issued.

The trigger de-duplicates on the claim reference and returns duplicate: true.

Response409 · not safe to replay
02

Someone retries the refund step

Steps that move money are marked as unsafe to repeat. A retry is refused instead of paying out again.

Replay of a step whose activity is not replay_safe returns 409 REPLAY_NOT_SAFE.

Sent withidempotency key
03

The refund call is sent again

Each refund carries a key built from the claim, so the payment provider pays once however many times the call is made.

A deterministic key (lyh_ plus a SHA-256 of the claim) travels with the Stripe call.

Try these on the home page

06Audit

The record auditors ask for.

Every change carries a name, a time and a place. Approvals also record how the decision arrived, and the history exports for your auditors.

Actor typing (human, API key, system, worker), client IP on every row, and a word-aware redaction gate that scrubs secret values while keeping counts such as token usage.

Audit trailmeridian
TimeWhat happenedWhoHow
Approved the €240 refundapproval.approveddanielpersonEmail link, confirmed in two steps
Used the Stripe credentialcredential.decryptedworkersystemRefund step, claim CLM-2291
Created an API keyapi_key.createdadminpersonSettings, from 203.0.113.24
Started a claim jobjob.startedstorefrontAPI keyWebhook trigger
Fig. 3  Four rows from one morning. Engine view shows the product’s audit action names.Illustrative rows

07Status

Every claim, and its status.

Live means in the product today. Anything still being built is labelled as such, and anything we don’t offer is listed here plainly.

Live14In development2Roadmap5Not offered6

As of September 2026

Live14
  • Company data isolated on every read, failing closed
  • A permission check behind every route
  • Outbound calls checked, including every redirect
  • One audited write path, with secrets scrubbed
  • Authenticator-app sign-in with recovery codes
  • Lockout after repeated failed sign-ins
  • Password check before MFA changes
  • Admin MFA reset, recorded under both names
  • Short sessions that end on password or MFA change
  • Encrypted credentials, never returned, decryption recorded
  • Duplicate claims recognised, no second job
  • Money steps refuse unsafe retries
  • Two-step approval links, tokens kept out of logs
  • Approval history exports, never truncated
In development2
  • Require MFA for everyone in a company
  • Action and audit record saved as one transaction, for sensitive actions
Roadmap5
  • Single sign-on with Microsoft Entra ID
  • Passkeys
  • Database-level row security as a second layer
  • One egress layer for built-in connectors too
  • SOC 2 examination

Nothing on the roadmap is claimed anywhere on this site.

Not offered today6
  • Choosing the data region per company
  • SCIM user provisioning
  • IP allowlists
  • Streaming the audit trail to a SIEM
  • A penetration-test report
  • ISO 27001 certification

If you need one of these now, ask us before you buy.

08Report

Found something?

security@lyhnis.com

A person reads every report.

Tell us what you found, how to reproduce it and what it could reach. Please don’t test against other companies’ data, and give us a chance to fix it before you publish.

Report a vulnerability

30-minute call with an engineer

Bring your hardest
security question.

Your security team sees the four guards in the running product. Not in slides.