Security model
Password hashing, encrypted settings, CSRF and signatures, audit logging, lockouts, and the data-privacy commands — the platform's defensive posture in one place.
Security model
This page collects the security-relevant mechanics that are spread across the codebase. Each item cites the real class or command; nothing here is aspirational.
Who this is for
Developers doing a security review, and hosting admins hardening a production install.
Difficulty: Advanced · Time: about 15 minutes.
Credentials and secrets at rest
| Data | Protection |
|---|---|
| User passwords | Laravel's password hashing (Hash), never stored reversibly |
| Meta app secret (platform) | App\Settings\MetaSettings with #[ShouldBeEncrypted]; never rendered back in the SA form — MetaDeveloperSettings::mount() fills a masked field |
| Billing provider keys | App\Settings\BillingSettings — provider key fields #[ShouldBeEncrypted] (SA → Billing → Providers & Settings) |
| WhatsApp tokens | WhatsAppConnection casts access_token, app_secret, verify_token as encrypted |
| Calendar OAuth tokens | CalendarConnection casts access_token, refresh_token as encrypted |
| Outbound webhook signing secrets | OutboundWebhook casts secret as encrypted |
| 2FA secrets | User casts two_factor_secret as encrypted |
| Mobile device tokens | Only device_tokens.token_hash is stored — the plaintext token never lands in the database |
| OTP codes | Stored HMAC-SHA256-hashed with a random salt; 5 verify attempts; issuance rate-limited per identifier and IP |
Encryption uses Laravel's APP_KEY — losing/rotating it without re-encrypting makes these values unreadable. Back it up with the database.
Transport and request integrity
- CSRF is on by default for web routes. The explicit exemptions (registered in
bootstrap/app.php) are exactly the token/signature-authenticated inbound surfaces:webhook/*,billing/webhook/*,billing/commercial/*,widget/*/submit. - Inbound webhook verification:
- Meta:
X-Hub-Signature-256HMAC (connection credential or platformMetaSettings::appSecret()). - WhatsApp:
X-Hub-Signature-256when the connection has anapp_secret; token mismatches return the same anonymous200 {"ok":true}. - Billing: provider signature verified against the raw body; invalid → 400; idempotent
billing_eventskeyed(provider, event_id)+ payload SHA-256. See Webhooks (inbound).
- Meta:
- Trusted proxies are opt-in (
TRUSTED_PROXIES; default trusts nothing) so lockout/audit IPs cannot be spoofed by header injection. - Security headers (
SecurityHeadersmiddleware) add X-Frame-Options, X-Content-Type-Options and Referrer-Policy to every web response; the embeddable widget opts out per-route deliberately. - Signed links: guided-onboarding client links are signed URLs valid for 72 hours (
/guided-onboarding/{tenant}), so they cannot be guessed or enumerated.
Access control
- Login lockout: 5 failed attempts → 15 minutes, recorded in
login_attempts(LoginAttempt::recordFailureAndMaybeLock). Panels enforce it insideApp\Filament\Auth\Pages\Login(Filament 4 posts via Livewire, not the login URL); thelogin.lockoutmiddleware alias exists for custom routes. The mobile login endpoint applies the same rule. - Super Admin IP allowlist: optional, enforced by
EnforceSuperAdminIpAllowlist(aliassa.ip.allowlist). - Tenant isolation: fail-closed
BelongsToTenantscope — an unresolved tenant returns empty sets, never cross-tenant data. See Multi-tenancy & isolation. - API keys: per-key scopes (
api.key:read:leads…), per-key and per-tenant hourly rate limits, and a subscription-active gate before any CRUD runs. See Authentication. - RBAC: Spatie permissions via
HasRolePermissions;NavTieris display-only and must never be treated as authorization. See Roles & permissions (RBAC). - Recording playback is tenant-scoped at the model level (foreign ids 404) and streamed through an authenticated route — there is no public recording URL. See Storage & file handling.
Audit logs
- Security-relevant changes are written through
AuditLog::recordintoaudit_logs. - Retention: 180 days for
audit_logs, 30 days forlogin_attempts, 90 days for processedbilling_events— enforced byphp artisan leadhub:purge-audit-logs(daily 03:00). Override windows per table viaLH_AUDIT_RETENTION_DAYS/LH_LOGIN_ATTEMPT_RETENTION_DAYS/LH_BILLING_EVENT_RETENTION_DAYS.
Data privacy (GDPR)
| Command | Purpose |
|---|---|
php artisan leadhub:execute-erasure |
GDPR Art. 17 hard-delete of tenants whose deletion cool-off elapsed (TenantErasureService::COOL_OFF_DAYS = 30); --dry-run previews |
php artisan leadhub:purge-recordings |
30-day recording retention (files deleted, metadata kept); --dry previews |
php artisan leadhub:purge-audit-logs |
The audit/login/billing retention windows above |
Deletion flow: a tenant requests deletion → the tenant is flagged (deletion_requested_at) → after the 30-day cool-off the daily sweeper hard-deletes. The full privacy story is in Data retention & privacy (GDPR).
Documentation rules
Never put real secrets in docs These pages use your-… placeholders only. No tokens, keys, passwords, webhook tokens or real customer PII belong in documentation, screenshots, tickets or logs. Redacted diagnostics (for example the Android support report) exist precisely so support never needs raw credentials.
Related guides
Was this page helpful?
Thanks — noted. ✦