NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Sentinel for Laravel

What it detects

Sentinel detects; database grants and row-level security prevent. The two complement each other — Sentinel tells you that a sealed value, a seal or the history behind it was changed outside the application, and refuses to build on it until someone signs the change off.

Actors

IdActorCapabilitiesIn scope
A1DB writerINSERT/UPDATE/DELETE on any table incl. sentinel_*; no code execution; no access to env, config or APP_KEY.yes
A2DB writer + snapshot restoreA1 + replace tables or the whole database with an older snapshot.yes (needs anchors for full coverage)
A3Network/client attackerReplays, alters or duplicates HTTP requests.yes
A4Authorised insiderUses the app normally (e.g. acknowledges changes).audited (actor + reason + changed attributes in the MAC’d ledger); authorisation is the host’s job (Gate hook)
A5Holder of signing keys / APP_KEY (database-driver keys) / RCE / config writeCan forge anything.out of scope

Detection matrix

“Window” means the time since the last checkpoint — scheduled every minute by default.

#AttackDB only (no anchor)With ≥ 1 external anchorStatus / finding
1Change a sealed columnyesyesTampered (+ changed attributes via field tags)
2Change rows feeding a computed valueyesyesTampered
3Copy values + seal from another row / model / seal name / tenant scope / app contextyes (domain separation)yesTampered
4Re-point a seal row (sealable_id, seal)yesyesTampered
5Delete a seal rowyesyesMissing (seal_deleted when ledger history exists)
6Insert an unsealed row (strict seal)yesyesMissing (never_sealed); a lenient seal → Unsealed (by definition not a finding)
7Restore an older row + its older seal rowyes, while the newer ledger entries existyesStale (newer_version)
8#7 + delete the newer ledger entriesyes if those entries were already checkpointed (CheckpointMismatch); no inside the windowyes outside the windowledger findings
9Restore the whole database (incl. ledger + checkpoints) to an older snapshotnoyes (AnchorAhead) for any rollback past the last anchored checkpointledger finding
10Delete the checkpoint tail (+ entries)noyesAnchorAhead
11Rewrite a ledger entry or checkpointyes (entry MAC / checkpoint MAC + chain)yesEntryInvalid, CheckpointInvalid, ChainBroken
12Delete a sealable row without a tombstoneyes (sentinel:verify --ledger: live ledger head, row gone)yesEntityDeleted
13Rewrite the stored algorithm / key_id / ring on a seal rowyes (the algorithm comes from the key; ring allow-list; HKDF info binds ring + kid + algorithm)yesAlgorithmMismatch / UnknownKey / Tampered
14Database-driver key rows: swap ring/kid/algorithm, replace a public key, rewrite, clear or add a label, plant an envelope minted through an encrypted castyes (an AEAD envelope under its own APP_KEY-derived key binds every field — the label too, since 1.2 — and its row); a key still in the pre-1.2 envelope binds its label only once re-sealed (sentinel:key:reseal)yesKeyIntegrityException → UnknownKey (+ KeyIntegrityViolated)
15Database-driver key rows: restore an older envelope (un-revoke a key) — or, on a host upgraded from 1.1, a pre-1.2 envelope whose label is then rewrittenno (an authentic old ciphertext) — mitigation: list the kid in SENTINEL_REVOKED_KEYS, which always wins. The pre-1.2 case: yes once sentinel.keys.require_bound_label is onno; the pre-1.2 case as DB only— / with require_bound_label: KeyIntegrityException → UnknownKey
16Changes made and rolled back entirely inside the windownono— (shorten the checkpoint interval)
17Replay a signed HTTP requestyes (created/expires window + nonce de-duplication)—replayed, too_old, expired
18Alter the body/headers of a signed request, or add a method overrideyes (content-digest + signature; the executed method must be the signed one)—digest_mismatch, invalid_signature, malformed
19Duplicate a POST (retry storm, double click)yes (replay / 409)—idempotency
20Reuse an idempotency key with another payloadyes (422)—idempotency
21Reuse a single-use URLyes (atomic consume)—403

What it does not guarantee

  • Sentinel detects; it does not prevent. A row may be tampered with between two verifications.
  • Rows written through the query builder, raw SQL or withoutSealing() are unsealed or stale until re-sealed. Strict seals report them.
  • Database-driver keys are only as safe as APP_KEY. The config driver keeps keys out of the database — recommended for the default ring.
  • Without an external anchor, a full-database rollback (#9) and checkpoint truncation (#10) are undetectable. With one, a checkpoint never overwrites an anchor the database disagrees with, so after a rollback the anchor keeps the evidence until someone investigates and clears it.
  • Verification proves integrity relative to the last authorised seal, not that the authorised value was correct.
  • Computed values that read related rows are only as fresh as the last re-seal of the owning model.
  • A raw-secret MAC (verifyMac()) is symmetric: whoever can verify it holds the secret and can forge one — the receiving application and every sender holding the same key. Give every sender its own key, bound by label or owner, and use Ed25519 HTTP signatures where the verifier must not be able to sign.
  • A pre-1.2 key’s label is trusted from its first re-seal: whatever the label column holds when the key is first re-sealed — by sentinel:key:reseal or any key write — is what gets bound, so check the labels the command prints. Until sentinel.keys.require_bound_label is on, a restored pre-1.2 envelope still opens with whatever label its column holds.
  • MACs carry no freshness: verifyMac() proves who sent the bytes, not that they arrive once — put a sequence number or timestamp into the message and de-duplicate.

Out of scope by design: confidentiality (use Laravel’s encrypted casts), an attacker holding the signing keys, APP_KEY (for database-driver keys), the deployed code or the configuration, and intercepting query-builder or raw SQL writes — they are detected afterwards.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.