Zodiac Tech CRM Docs

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.

developer Advanced ⏱ 15 min Updated 2026-10-05

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-256 HMAC (connection credential or platform MetaSettings::appSecret()).
    • WhatsApp: X-Hub-Signature-256 when the connection has an app_secret; token mismatches return the same anonymous 200 {"ok":true}.
    • Billing: provider signature verified against the raw body; invalid → 400; idempotent billing_events keyed (provider, event_id) + payload SHA-256. See Webhooks (inbound).
  • Trusted proxies are opt-in (TRUSTED_PROXIES; default trusts nothing) so lockout/audit IPs cannot be spoofed by header injection.
  • Security headers (SecurityHeaders middleware) 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 inside App\Filament\Auth\Pages\Login (Filament 4 posts via Livewire, not the login URL); the login.lockout middleware alias exists for custom routes. The mobile login endpoint applies the same rule.
  • Super Admin IP allowlist: optional, enforced by EnforceSuperAdminIpAllowlist (alias sa.ip.allowlist).
  • Tenant isolation: fail-closed BelongsToTenant scope — 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; NavTier is 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::record into audit_logs.
  • Retention: 180 days for audit_logs, 30 days for login_attempts, 90 days for processed billing_events — enforced by php artisan leadhub:purge-audit-logs (daily 03:00). Override windows per table via LH_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.

Was this page helpful?