Skip to content

Security Controls

This page answers the security section of a vendor questionnaire. Each control is stated as the route, header or setting a reviewer can check against a running install with a super_admin token, and each section ends with how to check it. Where a control does not exist, the page says so, and the last section lists what is not built. Configuration lives on each feature's page.

ControlRequires
Security report, API keys, rate limit protections, sign-in, TOTP and backup codes, passkey and cross-device sign-in, the super admin MFA reset, one OpenID Connect provider, setup token, password recoveryIncluded free
Monthly request limits per API keyA license with apikey-pro
Custom rate limit rules, and changes to a sign-in protectionA license with rate-limit-pro
Passkey enrollment, and marking a passkey as the backup keyA license with mfa-pro
More OpenID Connect providers, the Okta and Azure AD templates, role mapping and a backup providerA license with oauth-pro
More than three admin seatsAny license that is active or in grace
Audit trailA license with audit
Streaming the audit trail to a SIEMA license with audit and audit-pro
Web application firewallA license with waf
SAML, SCIMA license with saml or scim

See pricing. Data subject requests, holds, retention, residency and masking are on Data Protection.

GET /api/admin/security/controls, super_admin. The server evaluates each security control at startup and logs the result. This route returns the same report evaluated live, so a control turned on by a license change after startup shows without a restart.

Terminal window
curl https://cms.example.com/api/admin/security/controls \
-H "Authorization: Bearer $TOKEN"
{
"checked_at": "2026-10-02T09:10:06Z",
"controls": [
{"control": "pii-mask", "status": "SKIP", "detail": "pii-mask plugin not active", "remedy": "Enable a plugin that masks personal data in responses and logs."},
{"control": "waf", "status": "PASS", "detail": "..."}
],
"failures": 0
}
StatusMeaning
PASSIncluded, configured and enforcing
WARNIncluded but not configured
FAILConfigured but not enforcing
SKIPIncluded but not running, for example without a license

A row other than PASS carries remedy, the sentence that turns the control on. A control the server does not include has no row. The controls reported are pii-mask, data-residency, quota/rate-limit, quota (usage quota enforcement), waf, mfa and session-anomaly (login history and brute-force detection from Trusted devices). FAIL is the row that matters: a control that is configured and not enforcing. Start the questionnaire from this response.

The data-residency row reads WARN whenever the feature runs, even on an engine started with INSTANCE_REGION, so it cannot show that residency is enforcing. Check residency with a write instead, as Data Protection describes.

Requires a license with the audit feature. See pricing.

The Audit log records each entry with sequence, tenant_id, user_id, action, resource_type, resource_id, ip, user_agent, the timestamp, the before and after state of the record, and chain_hash.

chain_hash is an HMAC-SHA256 over the previous entry's hash and every field above. Altering an entry breaks that entry's own hash, and removing one breaks the link from the entry after it. The first entry links to a fixed starting value. The key is LYEVE_AUDIT_HMAC_KEY (64 hex characters). An install without one hashes with a zero key and logs a warning at startup, and production refuses to start without it. The chain is one sequence for the whole install, not one per tenant.

What is recorded includes:

  • content create, update, delete, rollback and schedule
  • schema and schema preset changes
  • API key create, revoke, delete and monthly limit changes
  • admin token changes
  • user, tenant and domain changes
  • sign-in factor changes: TOTP and passkey enrollment and removal, backup codes, an administrator's MFA reset (mfa.admin.reset)
  • password reset requests and completions
  • rate-limit and WAF rule changes
  • data export jobs
  • subject export and erasure

How entries change or leave, so a reviewer knows what the chain can and cannot show:

  • No route edits an entry's content.
  • Subject erasure redacts the person in place: it clears user_id, sets ip and user_agent to [redacted] and empties the before and after state. Nothing rehashes the chain afterwards.
  • Entries are deleted by POST /api/admin/audit-log/prune (super admin), by a retention policy, or by tenant deletion. A legal hold stops a retention policy from deleting what it covers. It does not stop a prune.
Audit routes
MethodPathRole
GET/api/admin/audit-logadmin. Anyone but a super admin sees only their tenant, with ip and user_agent masked
POST/api/admin/audit-log/prunesuper_admin. Body {"older_than": "<RFC3339>"}, answers {"deleted": n}
POST/api/admin/audit-log/exportadmin
POST/api/admin/audit-log/export-allsuper_admin

Check it: the list route returns chain_hash per entry, but not the before and after state the hash covers, so the chain cannot be recomputed from the API. See the last section.

Included free on every install. Monthly request limits per key need apikey-pro.

A key is ly_ followed by 64 hex characters from 32 random bytes. The plaintext is returned once, in the raw_key field of the create response, and never stored. The server keeps an HMAC-SHA256 of the key under its pepper (plain SHA-256 when no pepper is configured), and never returns it. See API keys.

A key carries roles, schemas, scopes (resource:action), expires_at and an optional monthly_limit. A key that holds the admin or super_admin role must expire, at most 90 days ahead. A key's roles cannot exceed its creator's, unless the creator is a super admin. Revoke disables the key in place. Delete removes it with its usage history. A disabled or expired key authenticates nothing. Clients send the key in the X-API-Key header, and every use is recorded in the key's history.

There is no rotate route. Rotation is: create the replacement, deploy it, revoke the old key. SCIM provisioning tokens do rotate in place (POST /api/admin/scim/providers/{id}/rotate).

API key routes
MethodPathRole
GET, POST/api/admin/api-keysadmin
POST/api/admin/api-keys/{id}/revokeadmin
DELETE/api/admin/api-keys/{id}admin
GET/api/admin/api-keys/{id}/auditadmin
PATCH/api/admin/api-keys/{id}/monthly-limitadmin

Check it: create a key, use it once, and read GET /api/admin/api-keys/{id}/audit.

Included free on every install. Custom rules and changes to a sign-in protection need the rate-limit-pro feature.

Every limit answers 429 with Retry-After, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, RateLimit-Policy and the X-RateLimit-* equivalents.

The built-in public limiter is always on. It covers the public routes of features, POST /api/v1/auth/token, POST /api/admin/auth/login, /refresh and /mfa-verify, and the two subject request routes. First-run setup (/api/admin/setup) is outside it. Every client address shares one cap of 50 requests per second with a burst of 100 across all of these routes, and these routes also have their own budget per address:

RouteRate per secondBurst
Login, token refresh, MFA step and Content API token510
Password reset request35
Password reset confirm510
Magic link request510
Magic link verify1020
Subject export1020
Subject erasure510

PUBLIC_RATE_LIMITS (METHOD:/pattern=rate:burst,...) and PUBLIC_RATE_LIMIT_GLOBAL (rate:burst) override them, and a malformed entry stops the server at startup. The Admin API and the Content API each keep their own limiter, each tracking at most 10,000 address and route pairs and dropping the least recently used.

The global limiter is RATE_LIMIT_RPS, a per-address limit on every request. Production refuses to start without it. Counts are in memory per instance, or shared across replicas with RATE_LIMIT_BACKEND=redis, which is free.

The sign-in protections, managed under Rate limiting: POST /api/v1/auth/token and POST /api/admin/auth/login each allow 5 requests per 15 minutes with a burst of 5, plus the password reset, magic link and MFA limits. A protection can never be set to zero requests. Custom rules per endpoint pattern, tenant or role, and changes to a protection, need rate-limit-pro. A rule keeps enforcing after a license lapses. Only editing it is refused.

The client address comes from the connection unless TRUSTED_PROXIES names the proxy in front.

Check it: send six logins in a row for a test account and read the 429 and its Retry-After. The protection counts per address, so use an address and an account you can afford to lock for 15 minutes.

Requires a license with the waf feature. See pricing.

The web application firewall inspects the path and query (URL-decoded repeatedly), the body, the headers and the cookies of each request against rules in the categories sqli, xss, path_traversal, tenant_injection, command_injection, ssrf and custom. A rule's action is block or log, with a severity. A request is refused when a block rule matches or the summed anomaly score exceeds anomaly_threshold (default 5). The answer is 403 with {"error":"request blocked by WAF","code":"waf_blocked", ...} and the header X-WAF-Block: true, and the violation is stored.

Deliberate exemptions a reviewer should know:

  • Body inspection stops at max_inspect_body_bytes (default 128 KiB), so a large upload cannot stall the server.
  • The bodies of content writes (/api/admin/content, /api/v1/content/* on POST, PUT and PATCH) are not inspected, because a CMS has to store an article about SQL injection. Their path, query and headers still are.
  • Everything under /api/admin/auth/, the setup routes and the firewall's own /api/admin/waf/ routes is skipped, so a false positive cannot lock every administrator out.

False positives are recorded per rule, path pattern and parameter, for one tenant or the whole install.

Firewall routes
MethodPathRole
GET, POST/api/admin/waf/rulesadmin
GET, PUT/api/admin/waf/rules/{id}admin
GET, DELETE/api/admin/waf/violationsadmin
GET, POST/api/admin/waf/false-positivesadmin
DELETE/api/admin/waf/false-positives/{id}admin
GET/api/admin/waf/configadmin
PUT/api/admin/waf/configsuper_admin
GET/api/admin/waf/statsadmin

Check it: request /api/v1/content/post?q=1%27%20OR%201=1-- and read the 403 and its X-WAF-Block header.

Included free on every install. Enrolling a passkey needs mfa-pro, more than one OpenID Connect provider needs oauth-pro, and SAML and SCIM need the saml and scim features.

Passwords are hashed with bcrypt by default, or argon2id with PASSWORD_HASH_ALGO=argon2id. Sessions are tokens signed with Ed25519, key at JWT_KEY_PATH, lifetime JWT_EXPIRY_SECS (default 900, at most 3600 in production), with refresh tokens lasting REFRESH_TOKEN_TTL_SECS (default 30 days). When the key file cannot be written, production refuses to start and development signs with HMAC-SHA256 over JWT_SECRET instead.

After five failed logins in 15 minutes the account is locked for the rest of the window. MFA codes have their own limits: five failed codes per user in 15 minutes, five attempts per challenge, and a challenge that verifies once cannot be used again. With Caching on CACHE_DRIVER=redis and REDIS_URL set, this state is shared: replicas enforce one limit between them, a restart does not lift a lock, and a used challenge is refused on every replica. Without it the state is per instance, and the server says which at startup. If Redis cannot be read, a login is refused and an MFA challenge is not accepted.

First-run setup. POST /api/admin/setup is public because no account exists yet, so creating the first super_admin demands a setup token. It is LYEVE_SETUP_TOKEN when set (16 characters or more, or the server does not start), otherwise a random token the server prints to its log once at startup while no account exists, so reading it proves access to the host. Only a digest is kept, and a presented token is compared in constant time. A missing or wrong token answers 401. When two claims arrive together exactly one wins, and creating the account retires the token. Once an account exists, setup answers 409.

MFA. Multi-factor authentication offers TOTP and single-use backup codes on every install, and WebAuthn passkeys (resident keys and user verification preferred) with passwordless and cross-device login. Enrolling a passkey needs mfa-pro. A passkey enrolled under it keeps signing in after a license lapses. Routes are under /api/admin/mfa/ for enrolled users and /api/admin/auth/webauthn/ for login. TOTP secrets are encrypted with a per-tenant key derived from ENCRYPTION_KEY (HKDF-SHA256, AES-256-GCM). The server does not start without that key, and it must differ from JWT_SECRET. Backup codes are stored as SHA-256 hashes. POST /api/admin/mfa/admin-reset disables a user's MFA, takes an optional reason, needs a super admin not scoped to a tenant, and is audited as mfa.admin.reset. A login answers mfa_required: true when the user has a factor enrolled. There is no setting that makes MFA mandatory for a role or a tenant. Enrollment is per user.

Single sign-on. OAuth sign-in does OpenID Connect by discovery with nonce and JWKS validation of the id token. One provider is free, from the Google, GitHub and Apple templates or by hand. More providers, the Okta and Azure AD templates, a custom roles claim or default roles, and a backup provider need oauth-pro. SAML SSO is an SP-initiated SAML 2.0 service provider with templates for Okta, Azure AD and OneLogin, just-in-time user creation, a role mapping from the assertion (default editor) and POST /api/admin/saml-providers/{id}/rollover-cert for certificate rotation. SCIM provisioning serves SCIM 2.0 Users and Groups at /api/v1/scim/v2/ behind a bearer token per provider, with PATCH and filtering.

Recovering an account. Two paths, both free. Password reset answers 200 whether or not the address exists, sets the new password from a single-use token that lasts one hour, requires twelve characters or more, and ends the user's refresh tokens. Or a super admin sets the password, with the Set password action on the Users page or PUT /api/admin/users/{id}/password and {"password": "..."}. The new password is checked against the install's password policy, and on success every session the account held ends. Magic link sign-in is the passwordless email login beside these.

There is no telemetry channel to LyEve. Given a signed license token, verification is an offline signature check and the server makes no call to LyEve. Verify Offline shows how to confirm that with a network namespace, strace and a firewall.

The one call to LyEve happens only for an opaque license key (lyeve_live_...), set in LYEVE_LICENSE_KEY or posted to POST /api/admin/license/renew. The server posts the key and its random install id over https to LYEVE_LICENSE_SERVER_URL, verifies the token it gets back against the public key built into the server, caches it, and exchanges again only when the cached token is within seven days of expiry or no token is active. A key the server refuses grants nothing. When the server cannot be reached, a cached token keeps working. See Licensing and tiers.

Content leaves the install only through features an administrator configures, each to a destination that administrator chose:

  • Webhooks send the record's data, and its previous data on an update, to the URLs you register.
  • Email and the outbound nodes of flows send what you put in them.
  • The AI feature sends the request to the model provider you configured: the system prompt, the conversation (which includes the record being edited and its title), and for an image operation the picture. The request carries no tenant identifier, and nothing is redacted before it is sent. A local endpoint on your own network sends nothing to a third party. Transcripts are stored in your database under the retention window you set, and are covered by tenant deletion and by subject export and erasure.
  • SOC 2. LyEve has no SOC 2 report.
  • Verifying the audit chain over the API. No route walks the chain, and the list route does not return the before and after state the hash covers.
  • Rehashing after erasure. An erased entry is redacted in place, and the chain is not recomputed.
  • API key rotation in place. Create, deploy, revoke.
  • Mandatory MFA per role or tenant. Enrollment is per user.