---
title: "Configuration — Sentinel for Laravel | Roundly"
description: "Every config key with its default and env variable — keys and rings, sealing, verification, ledger and anchors, idempotency, nonces, signatures, schedule."
url: https://roundly-consulting.com/open-source/docs/sentinel-for-laravel/configuration
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Sentinel for Laravel](https://roundly-consulting.com/open-source/docs/sentinel-for-laravel.md)

# Configuration

The published config/sentinel.php documents every key, and the common knobs are env-driven, so you rarely need to publish it. The default ring’s key material, for example, comes entirely from the environment:

```bash
SENTINEL_KEY_ID=default-20261002-k3f9qa
SENTINEL_ALGORITHM=hmac-sha256
SENTINEL_KEY="base64:…"
SENTINEL_PUBLIC_KEY="base64:…"                  # asymmetric keys; alone = verify-only
SENTINEL_PREVIOUS_KEYS="default-20250901-a1b2c3|hmac-sha256|base64:…"   # verify-only, comma-separated
```

## Every key

| Key | Default | Env | Purpose |
| --- | --- | --- | --- |
| `context` | `''` | `SENTINEL_CONTEXT` | Application domain separator bound into every MAC (UTF-8, ≤ 255 bytes). Set it once: changing it invalidates every seal (re-seal with --acknowledge) and, for good, every ledger entry and checkpoint written before — a new context means a new ledger. |
| `key_type` | `bigint` | `SENTINEL_KEY_TYPE` | Id type of sealed models in the morph columns: bigint, uuid or ulid (set before migrating; any other value makes the migration throw). |
| `actor_key_type` | `bigint` | `SENTINEL_ACTOR_KEY_TYPE` | Id type of actors, key owners and nonce subjects — the same values and rule. |
| `models` | `[]` | — | Models sentinel:verify scans first when given none; every class that has seals is discovered after them. |
| `database.connection` | `null` | `SENTINEL_DB_CONNECTION` | Connection of sentinel\_keys, sentinel\_idempotency\_keys and sentinel\_nonces. |
| `keys.default_ring` | `default` | `SENTINEL_DEFAULT_RING` | Ring used by seals that name none. Never a ring HTTP message signatures use (refused). |
| `keys.revoked` | `''` | `SENTINEL_REVOKED_KEYS` | Revoked keys as ring:kid,… — beats every driver and survives a restored key row. |
| `keys.require_bound_label` | `false` | `SENTINEL_REQUIRE_BOUND_LABEL` | Refuse database keys still in the pre-1.2 envelope (sentinel.key/1), whose label is not bound — they fail their integrity check. Turn it on once sentinel:key:reseal has re-sealed every key and you have checked the labels it printed; a fresh 1.2 install can turn it on right away. |
| `keys.rings.default.driver` | `config` | `SENTINEL_KEY_DRIVER` | config, database, chain or a custom driver. |
| `keys.rings.default.algorithms` | all six | — | The ring’s algorithm allow-list. |
| `keys.rings.default.key_id` | `null` | `SENTINEL_KEY_ID` | Config driver: the current key id. |
| `keys.rings.default.algorithm` | `hmac-sha256` | `SENTINEL_ALGORITHM` | Config driver: the current key’s algorithm. |
| `keys.rings.default.key` | `null` | `SENTINEL_KEY` | Config driver: base64: secret or private key. |
| `keys.rings.default.public_key` | `null` | `SENTINEL_PUBLIC_KEY` | Config driver: base64: public key (verify-only nodes). |
| `keys.rings.default.previous` | `''` | `SENTINEL_PREVIOUS_KEYS` | Config driver: verify-only keys, kid\|algorithm\|base64:… comma-separated. |
| `keys.rings.default.drivers` | `['config', 'database']` | — | Chain driver order. |
| `keys.rings.http.driver` | `database` | `SENTINEL_HTTP_KEY_DRIVER` | Driver of the RFC 9421 ring. |
| `keys.rings.http.algorithms` | the four RFC 9421 algorithms | — | The http ring’s allow-list. |
| `keys.rings.http.key_id / algorithm / key / public_key / previous / drivers` | as above | `SENTINEL_HTTP_KEY_ID, SENTINEL_HTTP_ALGORITHM, SENTINEL_HTTP_KEY, SENTINEL_HTTP_PUBLIC_KEY, SENTINEL_HTTP_KEYS` | When the http ring uses the config driver. |
| `sealing.auto` | `true` | `SENTINEL_AUTO_SEAL` | Seal on Eloquent writes (turn off on verify-only nodes). |
| `sealing.on_tampered_write` | `refuse` | `SENTINEL_ON_TAMPERED_WRITE` | What an Eloquent write does to a model that is not intact: refuse, reseal or skip. |
| `sealing.allow_suspension` | `false` | `SENTINEL_ALLOW_SUSPENSION` | Permit Sentinel::withoutSealing(). Off unless you opt in: not set (absent, null or blank SENTINEL\_ALLOW\_SUSPENSION=) refuses it. |
| `sealing.field_tags` | `true` | `SENTINEL_FIELD_TAGS` | Keyed per-field tags that name changed attributes (HMAC keys). |
| `sealing.reason_max_length` | `1000` | — | Acknowledgement, unseal and baseline reasons (1–10000). |
| `sealing.transaction_attempts` | `3` | — | Retries of Sentinel’s own transactions, 1–10 (never the host’s save()). |
| `verification.check_ledger` | `true` | `SENTINEL_VERIFY_LEDGER` | Compare seals with the ledger (detects replayed seals). |
| `verification.outdated_is_intact` | `true` | `SENTINEL_OUTDATED_IS_INTACT` | A changed definition over intact data counts as intact. |
| `verification.log_channel` | `null` | `SENTINEL_LOG_CHANNEL` | Where findings are logged (null or blank = default channel; a non-string throws). |
| `verification.retrieve_reaction` | `throw` | `SENTINEL_RETRIEVE_REACTION` | Default reaction of verify-on-retrieve: throw or report (both report the finding; throw also refuses to load the model). |
| `verification.retrieve_checks_ledger` | `false` | `SENTINEL_RETRIEVE_CHECKS_LEDGER` | Ledger check on retrieve (one more query per model). |
| `acknowledgement.ability` | `null` | `SENTINEL_ACKNOWLEDGE_ABILITY` | Gate ability checked before an acknowledgement (null or blank = none; a non-string throws). |
| `ledger.enabled` | `true` | `SENTINEL_LEDGER` | Write ledger entries (off loses replay and rollback detection). |
| `ledger.ring` | `default` | `SENTINEL_LEDGER_RING` | Ring whose current key signs checkpoints; an unconfigured ring, or one HTTP message signatures use, is refused. It ships as default and does not follow keys.default\_ring: renaming the default ring (SENTINEL\_DEFAULT\_RING) means setting SENTINEL\_LEDGER\_RING too. |
| `ledger.connections` | `[null]` | — | Connections holding seals and the ledger; every scan, count and check covers each one — once. |
| `ledger.batch_size` | `1000` | — | Entries per checkpoint transaction (1–100000). |
| `ledger.backlog_warning_seconds` | `600` | — | Age of un-checkpointed entries reported as a backlog (60–86400). |
| `ledger.anchors` | `''` | `SENTINEL_ANCHORS` | Comma list of cache, filesystem, log or custom anchors. |
| `ledger.anchor_drivers.cache.store` | `null` | `SENTINEL_ANCHOR_CACHE_STORE` | Cache store of the cache anchor (not the app database); null or blank = the default store. |
| `ledger.anchor_drivers.cache.key` | `sentinel:ledger:anchor` | — | Cache key prefix; blank = the default, a non-string value throws. |
| `ledger.anchor_drivers.filesystem.disk` | `local` | `SENTINEL_ANCHOR_DISK` | Disk of the filesystem anchor (object lock recommended); blank = the default disk, a non-string throws. |
| `ledger.anchor_drivers.filesystem.path` | `sentinel/anchors` | `SENTINEL_ANCHOR_PATH` | Directory on that disk; blank = the default, a non-string value throws. |
| `ledger.anchor_drivers.log.channel` | `null` | `SENTINEL_ANCHOR_LOG_CHANNEL` | Log channel of the write-only log anchor (null or blank = default channel; a non-string throws). |
| `middleware.verified_status` | `409` | — | Status of sentinel.verified on a failed check (400–599). |
| `middleware.verified_reaction` | `abort` | `SENTINEL_VERIFIED_REACTION` | abort, or only report and continue. |
| `idempotency.store` | `database` | `SENTINEL_IDEMPOTENCY_STORE` | database, cache or a bound custom store. |
| `idempotency.cache_store` | `null` | `SENTINEL_IDEMPOTENCY_CACHE_STORE` | Lock-capable cache store for the cache store. |
| `idempotency.header` | `Idempotency-Key` | — | Request header. |
| `idempotency.replay_header` | `Idempotent-Replayed` | — | Header on replayed responses. |
| `idempotency.methods` | `['POST', 'PATCH']` | — | Methods the middleware applies to. |
| `idempotency.ttl` | `86400` | `SENTINEL_IDEMPOTENCY_TTL` | Seconds a key lives after it was first seen (60–2592000). |
| `idempotency.lock_seconds` | `60` | — | Lease of a request in progress (1–3600). |
| `idempotency.min_length / max_length` | `16 / 255` | — | Accepted key length (1–255); a min\_length above max\_length is refused — it would admit no key. |
| `idempotency.accept_unquoted` | `true` | `SENTINEL_IDEMPOTENCY_ACCEPT_UNQUOTED` | Accept bare (unquoted) keys. |
| `idempotency.store_client_errors` | `true` | — | Store and replay 4xx responses. |
| `idempotency.store_server_errors` | `false` | — | Store 5xx responses (default: release the key). |
| `idempotency.transactional` | `false` | `SENTINEL_IDEMPOTENCY_TRANSACTIONAL` | Handler and record in one transaction. Needs idempotency.store = database — refused otherwise. |
| `idempotency.encrypt` | `true` | `SENTINEL_IDEMPOTENCY_ENCRYPT` | Encrypt stored responses. |
| `idempotency.max_response_bytes` | `1048576` | — | Larger responses are not replayable. |
| `idempotency.replayed_headers` | `content-type, content-language, location, etag, last-modified, cache-control` | — | Headers stored and replayed (set-cookie never). |
| `nonces.store` | `database` | `SENTINEL_NONCE_STORE` | database, cache or a bound custom store. |
| `nonces.cache_store` | `null` | `SENTINEL_NONCE_CACHE_STORE` | Lock-capable cache store. |
| `nonces.ttl` | `900` | — | Seconds a nonce lives (1–2592000). |
| `nonces.length` | `43` | — | Nonce length in base64url characters, 32–128 (43 ≈ 256 bits). |
| `problems.type_base` | `null` | `SENTINEL_PROBLEM_TYPE_BASE` | RFC 9457 type = base + #code (null: omitted). |
| `signatures.default_profile` | `default` | — | Profile of sentinel.signed without a parameter. |
| `signatures.profiles.<name>.ring` | `http` | — | Ring the profile’s key ids resolve in; a ring that isn’t a name throws. |
| `signatures.profiles.<name>.label` | `null` | — | Signature label to verify (null: the only one, or the one with the profile tag). |
| `signatures.profiles.<name>.tag` | `null` | — | Required tag parameter. |
| `signatures.profiles.<name>.components` | `@method, @authority, @path` | — | Components that must be covered. Never @status (refused: a request has none, and a response signature always covers it). |
| `signatures.profiles.<name>.require_query` | `true` | — | Cover @query when the request has a query. |
| `signatures.profiles.<name>.require_content_digest` | `true` | — | Cover content-digest when there is a body. |
| `signatures.profiles.<name>.require_nonce` | `true` | — | Require (and de-duplicate) a nonce. |
| `signatures.profiles.<name>.max_age` | `300` | — | Seconds a signature is accepted after created (1–86400). |
| `signatures.profiles.<name>.clock_skew` | `30` | — | Allowed clock difference in seconds (0–3600). |
| `signatures.profiles.<name>.algorithms` | the four RFC 9421 algorithms | — | Allowed algorithms (required); an empty list is a configuration error (fail closed). |
| `signatures.profiles.<name>.accept_signing_keys` | `false` | — | Accept a key this application can sign with. Off: a request the app signed itself never passes as a partner’s. |
| `signatures.outbound.ring` | `http` | — | Ring of Http::withSignature() keys; a ring that isn’t a name throws. |
| `signatures.outbound.label` | `sig1` | — | Label of outgoing signatures. |
| `signatures.outbound.components` | `@method, @authority, @path, @query, content-digest, content-type` | — | Components signed (absent ones are dropped). Never @status (refused). |
| `signatures.outbound.digest` | `sha-256` | — | Content-Digest algorithm: sha-256 or sha-512. |
| `signatures.outbound.expires_in` | `null` | — | Seconds until the expires parameter, 1–86400 (null or blank: none). |
| `signatures.outbound.tag` | `null` | — | The tag parameter. |
| `signatures.outbound.include_alg` | `false` | — | Send the alg parameter. |
| `signatures.advertise` | `true` | `SENTINEL_ADVERTISE_SIGNATURE` | Accept-Signature on 401 responses. |
| `schedule.enabled` | `true` | `SENTINEL_SCHEDULE` | Register the upkeep tasks on the scheduler (off: schedule the commands yourself). They skip while none of Sentinel’s tables exists, so a host that never migrated Sentinel needs no false here. |
| `schedule.checkpoint` | `everyMinute` | `SENTINEL_SCHEDULE_CHECKPOINT` | sentinel:checkpoint (only while the ledger is on) — the rollback window. Blank is not set → everyMinute. |
| `schedule.verify` | `daily` | `SENTINEL_SCHEDULE_VERIFY` | sentinel:verify --allow-empty --ledger; hourly suits small tables. |
| `schedule.prune` | `daily` | `SENTINEL_SCHEDULE_PRUNE` | sentinel:prune. |

Frequencies are everyMinute, everyTwoMinutes, everyFiveMinutes, everyTenMinutes, everyFifteenMinutes, everyThirtyMinutes, hourly, everyTwoHours, everyThreeHours, everyFourHours, everySixHours, daily and weekly — or off to disable a task (a blank value is not set, so the task keeps its default). Each task runs without overlapping and on one server.

Ring, profile, anchor and store names follow \[a-z\]\[a-z0-9\_-\]{0,63}. Add rings under keys.rings.<ring> (driver and algorithms required; for a config-driver ring, sentinel:key:generate --ring=<ring> prints SENTINEL\_<RING>\_\* lines — read them in the ring’s section with env()) and inbound signature policies under signatures.profiles.<name>.

## How values are read

Every setting is validated at first use, and an invalid one throws InvalidSentinelConfigurationException naming the key — it never falls back to a default. A key that is not set — absent, null or blank ('' or whitespace, what SENTINEL\_LEDGER= in .env gives) — takes its default from the table above. Booleans accept true/false, on/off, yes/no, 1/0 (any case); any other value throws, and the message names the key and the value it got — SENTINEL\_ALLOW\_SUSPENSION=disabled is an error, never “allowed”. A value out of range, an unknown enum case, a malformed list or a non-string name (a log channel, an ability, a store, an anchor disk, a signature ring) throws the same way; a blank optional name is not set, so it reads as unset. A blank schedule.\* frequency keeps its default — only off (or null) switches a task off.

php artisan about shows a Sentinel section — rings, driver, signing key present or missing, flags, anchors, stores, sealable models and schedule, never key material — and sentinel:check reports every invalid setting at once.

## Translations

English and Slovak ship with the package; publish them with the sentinel-translations tag to reword them or add a locale:

| Key | Used by |
| --- | --- |
| `sentinel::messages.tampered` | The generic sentinel.verified failure message. |
| `sentinel::messages.problems.<code>.title / .detail` | RFC 9457 problem responses — codes idempotency\_key\_missing, invalid\_idempotency\_key, idempotency\_key\_reused, idempotency\_request\_in\_progress, idempotent\_response\_unavailable, nonce\_rejected, signature\_rejected. |
| `sentinel::validation.intact_seal` | The IntactSeal rule (:attribute). |
| `sentinel::statuses.<status>` | Status labels in sentinel:verify output, one per VerificationStatus value. |

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/sentinel-for-laravel.md)
