Skip to Content
Security

Security

Security practices, controls, and measures applied across the Leadmetrics platform. Covers all layers: authentication, authorization, data, transport, application code, infrastructure, and supply chain.

Related: Auth | Multi-tenancy | Infrastructure | Governance


Principles

  1. Defence in depth — multiple independent controls at every layer; compromise of one does not collapse the system
  2. Least privilege — agents, users, and services receive the minimum permissions needed for their specific role
  3. Zero trust at API boundaries — every request re-validates identity and tenant context; no implicit trust between services
  4. Fail closed — when a permission check fails or a gate is uncertain, deny by default; never auto-proceed
  5. Auditability — every write action and every LLM call is logged with actor, timestamp, before/after state; logs are immutable
  6. No secrets in code — all credentials live in Doppler; no hardcoded keys, tokens, or passwords anywhere in the repository

Authentication Security

See Auth docs for the full authentication design. Security-relevant specifics:

Tokens

  • Access tokens are short-lived (15 min web, 30 min mobile) and unsigned in-memory only — never written to localStorage or sessionStorage
  • Refresh tokens use 256-bit random values — unguessable, opaque, stored server-side
  • Token rotation on every refresh — reuse of a superseded token triggers full session termination for all devices
  • RS256 asymmetric signing — private key never leaves the API service; public keys published via JWKS

Session Storage

ClientAccess tokenRefresh token
WebJavaScript memoryHttpOnly; Secure; SameSite=Strict cookie
MobileReact Native memoryiOS Keychain / Android Keystore (hardware-backed)

HttpOnly cookies prevent JavaScript from reading the refresh token — XSS cannot exfiltrate it. SameSite=Strict prevents CSRF from forging requests with the cookie.

Brute Force Protection

  • Login endpoint: 5 failed attempts per (email + IP) within 15 minutes → 15-minute lockout
  • 2FA verification: 3 failed attempts per challenge token → challenge invalidated, user must re-login
  • Password reset: rate-limited to 3 requests per hour per email address
  • All lockout events are written to the audit log

Password Security

  • Minimum 12 characters, character complexity required
  • Bcrypt hashing with cost factor 12 (≈250ms on current hardware — fast enough for UX, slow enough to resist offline cracking)
  • Breach check via haveibeenpwned.com  k-anonymity API at registration and password change
  • Last 5 passwords cannot be reused

Authorization & Tenant Isolation

Multi-Layer Tenant Isolation

Tenant data is isolated at five independent layers — compromise of one does not expose cross-tenant data:

LayerMechanismWhat it prevents
DatabasePrisma middleware appends WHERE tenant_id = :tenantId to every queryDirect DB query returning another tenant’s rows
ORM schematenantId is a non-nullable foreign key on every tenant-scoped tableSchema-level enforcement; inserts without tenantId fail at DB level
API middlewareJWT tenantId claim extracted and validated on every request; mismatches rejected before handler runsHandler receiving a request for a different tenant
QueueShared BullMQ queues agent__{agentRole}; worker verifies tenantId in job payload before executionAgent job processed with wrong tenant’s data
Skills/RAGMongoDB documents carry tenantId; all queries include { tenantId } filterSkills or knowledge base content leaking across tenants

Agent Permission Layers

Agents are further constrained beyond tenant isolation:

  1. Tool whitelistagent_configs.tool_names[]; the tool dispatcher rejects calls to any unlisted tool
  2. Write-action approval gate — any tool call that publishes, sends, or spends requires a resolved approvals record
  3. HITL mandatory — all deliverables pass through human approval; agents cannot bypass the approval gate
  4. Budget halt — activity, campaign, and tenant spend caps enforced before every agent dispatch; exceeded caps cancel the job
  5. Output validators — character limits, brand voice scoring, banned words filter run after agent output, before any write action

Role-Based Access Control

RoleCan readCan writeCan approveCan configure
memberOwn tenant dataSubmit campaigns, content requests
adminOwn tenant dataAll tenant actionsAgent configs, team
reviewerAssigned tenantsApprovals, interventions
super_adminAll tenantsAll platform actionsGlobal platform config

Transport Security

  • TLS 1.2+ enforced everywhere — HTTP requests are rejected (Traefik redirects to HTTPS)
  • HSTS with max-age=31536000; includeSubDomains; preload — clients remember to use HTTPS for 1 year
  • TLS 1.0 and 1.1 disabled on all endpoints
  • Certificate management — Traefik (bundled with Coolify) handles Let’s Encrypt certificates automatically, with auto-renewal
  • Internal service communication — service-to-service calls within the Docker network use a shared API secret; not directly internet-exposed

HTTP Security Headers

Applied by Traefik globally and by Next.js per-app:

HeaderValuePurpose
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadEnforce HTTPS
Content-Security-PolicySee belowPrevent XSS and data injection
X-Content-Type-OptionsnosniffPrevent MIME-type sniffing
X-Frame-OptionsDENYPrevent clickjacking
Referrer-Policystrict-origin-when-cross-originLimit referrer leakage
Permissions-Policycamera=(), microphone=(), geolocation=()Disable unused browser features
Cross-Origin-Opener-Policysame-originIsolate browsing context
Cross-Origin-Resource-Policysame-originPrevent cross-origin reads

Content Security Policy (Dashboard/DM Portal):

default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.leadmetrics.io; font-src 'self'; frame-ancestors 'none';

unsafe-inline on style-src is required for Tailwind CSS. No unsafe-eval anywhere.


Input Validation & Injection Prevention

SQL Injection

  • Prisma ORM uses parameterised queries exclusively — no string concatenation into SQL
  • Raw SQL is banned in application code (enforced by ESLint rule no-raw-sql)
  • Database users have minimum required privileges — the app DB user has no DROP, CREATE, or TRUNCATE permissions

NoSQL Injection (MongoDB)

  • Mongoose schemas enforce strict type validation — $where, $regex, and operator injection in user-supplied fields are blocked by schema coercion
  • User-supplied filter values are validated with Zod before reaching Mongoose queries

XSS

  • All user-supplied content rendered in React is escaped by default (JSX auto-escaping)
  • dangerouslySetInnerHTML is forbidden (ESLint rule) except in one location: the deliverable preview pane, which sanitises HTML with DOMPurify before rendering
  • TipTap rich text editor output is sanitised with DOMPurify on both write (before storage) and read (before render)
  • CSP script-src 'self' blocks inline scripts injected via XSS

Command Injection

  • The ClaudeAdapter passes task prompts as the -p flag argument — prompts are never interpolated into shell strings using template literals; they are passed as discrete argv elements to child_process.spawn
  • Playwright (browser automation) runs in a sandboxed context with no shell access

Path Traversal

  • File upload paths (skill files, report PDFs) are resolved through path.resolve + allowlist validation — paths outside the designated upload directory are rejected
  • Skill file names are sanitised (alphanumeric + hyphens only) before temp directory creation

Prompt Injection (LLM-specific)

Adversarial content in user-supplied inputs (brand guides, campaign briefs, scraped content) could attempt to hijack agent behaviour:

  • System prompt boundary — agent instructions are placed in the system prompt; user content is in the user turn with explicit framing (---BEGIN USER CONTENT---)
  • Input sanitisation — HTML and Markdown formatting stripped from scraped external content before injection into agent context
  • Output validation — agent output is validated against expected format before any write action; unexpected instructions in output (e.g. “ignore previous instructions”) do not trigger tool calls
  • Tool call schema validation — all tool call inputs are validated against strict Zod schemas; malformed inputs from the LLM are rejected, not executed

Data Security

Encryption at Rest

DataEncryption
PostgreSQL databaseAES-256 at volume level (managed by cloud provider / disk encryption)
MongoDB databaseAES-256 at volume level
S3 / DO Spaces blobsAES-256 server-side encryption (SSE-S3 or SSE-KMS) — enabled at bucket level
Channel OAuth tokensAES-256-GCM at application level (in channels.access_token, channels.refresh_token) — double-encrypted
Agent API keysBcrypt-hashed before storage
LLM API keys (per LLM provider config)AES-256-GCM at application level
Tenant secrets (per-agent env vars)AES-256-GCM, key derived per tenant from master encryption key
User passwordsBcrypt (cost 12)

Application-level encryption uses keys injected via Doppler (ENCRYPTION_KEY). The master encryption key is never stored in the database.

Object Storage (S3) Security

Bucket configuration:

  • All buckets are private — no public access, no public ACLs. BlockPublicAcls, BlockPublicPolicy, IgnorePublicAcls, RestrictPublicBuckets all enabled.
  • Server-side encryption enabled at bucket level (SSE-S3 minimum; SSE-KMS for production)
  • Versioning enabled on the {tenantId}/contracts/ prefix to support Object Lock
  • Object Lock (WORM) on the {tenantId}/contracts/ prefix — legal records cannot be overwritten or deleted

Access control:

  • The API service uses a dedicated IAM role/user with minimum permissions: s3:PutObject, s3:GetObject, s3:DeleteObject on the application bucket only
  • No s3:ListBucket on the root bucket — the API constructs keys deterministically and never enumerates
  • Admin operations (bucket policy changes, enabling Object Lock) require a separate privileged IAM role not held by the application

Pre-signed URLs:

  • All content is served via pre-signed URLs with a 15-minute expiry — the API never proxies S3 content
  • Pre-signed URLs are generated server-side using the tenant’s session identity — a user from tenant A cannot obtain a URL for tenant B’s content
  • URL contains the full S3 key including {tenantId}/ prefix — cross-tenant access is impossible without a valid pre-signed URL for that tenant

Tenant isolation in S3:

  • All S3 keys are prefixed with {tenantId}/ — tenant data is logically isolated within the bucket
  • For Enterprise on-prem, each tenant can be assigned their own MinIO bucket for complete storage isolation

Local dev / on-prem (MinIO):

  • MinIO is configured with anonymous: none — no unauthenticated access
  • MinIO credentials are in .env.local (local only, never committed) and in Doppler for staging/production
  • On-prem MinIO deployments follow the same bucket policy and encryption standards as production S3

Encryption in Transit

  • All external traffic: TLS 1.2+
  • Internal service-to-service (Docker network): unencrypted on the private network but protected by network-level isolation; services are not internet-accessible
  • Database connections: TLS enabled for production PostgreSQL and MongoDB; certificate validation enforced

Data Minimisation

  • LLM call logs store promptHash and responseHash (SHA-256) — not the full prompt or response text
  • Tool call logs store sanitised payloads — credentials and PII fields are redacted before storage
  • Analytics data is aggregated; raw user behaviour events are not retained beyond 90 days

PII Handling

  • Email addresses and user names are stored in the users table only — never duplicated into logs or audit trails
  • Lead data (CRM) is tenant-scoped and never included in cross-tenant analytics
  • Audit log before/after diffs redact fields annotated @pii in the Prisma schema (custom extension)

Secrets Management

All secrets are managed by Doppler. No secrets exist as environment variables in CI/CD pipelines, Dockerfiles, or .env files in the repository.

Secret rotation policy

SecretRotation frequencyAutomated
JWT signing keys (RSA)Every 90 daysManual (operator-triggered via Admin app)
API keys (Anthropic, OpenAI, SendGrid, etc.)On compromise or every 180 daysManual
Channel OAuth tokens (per tenant)Automatic re-auth when expiredAutomatic via OAuth refresh
Database passwordsEvery 90 daysVia Doppler secret rotation
Tenant agent env secretsOn operator requestManual via Manage app

Repository secret scanning

  • GitHub Advanced Security — secret scanning enabled; any committed secret triggers an immediate alert and CI failure
  • truffleHog — runs in CI on every PR to catch secrets in diff

Infrastructure Security

Network

  • All services run in a private Docker network — only Traefik (reverse proxy) is internet-facing on ports 80/443
  • Database ports (5432, 27017, 6379, 6333) are not exposed outside the Docker network
  • Ollama (11434) is not exposed outside the Docker network

Container Security

  • All Docker images use pinned tags (not latest in production) — prevents silent upstream changes
  • Non-root users inside all containers — services do not run as root
  • Read-only root filesystems where possible
  • No privileged containers

Dependency Security

  • Dependabot — automated PRs for dependency updates with CVE annotations
  • pnpm audit — runs in CI; high and critical CVEs fail the build
  • License compliancelicense-checker enforces no GPL-licensed packages in production code

Audit Logging

Every state change is recorded in the audit_logs MongoDB collection:

interface AuditLogEntry { tenantId: string; actorType: 'human' | 'agent' | 'system'; actorId: string; action: string; // e.g. 'approval.resolved', 'campaign.created' resourceType: string; resourceId: string; before?: unknown; // previous state (PII fields redacted) after?: unknown; // new state ipAddress?: string; // human requests only userAgent?: string; createdOn: Date; }

Properties of the audit log:

  • Append-only — no updates or deletes (MongoDB collection with { validationLevel: 'strict' })
  • Immutable documents — application DB user has insert only on audit_logs; no update or delete
  • Retained for 2 years minimum
  • Queryable by tenants (their own log) via Dashboard, and by super-admins for all tenants via Manage app

Vulnerability Management

Responsible Disclosure

  • Security issues: reported to security@leadmetrics.io
  • Response SLA: acknowledgement within 48 hours, initial assessment within 7 days
  • CVSSv3 scoring used to prioritise: Critical/High fixed within 7 days, Medium within 30 days, Low within 90 days

Penetration Testing

  • Annual third-party penetration test covering OWASP Top 10
  • Scope: all three web apps, Fastify API, agent callback endpoints, OAuth server
  • Results and remediation tracked in a private security repository

OWASP Top 10 Coverage

RiskControl
A01 Broken Access ControlTenant isolation at 5 layers; role checks on every endpoint; fail-closed default
A02 Cryptographic FailuresTLS 1.2+ everywhere; AES-256 at rest; RS256 JWTs; bcrypt passwords
A03 InjectionPrisma ORM parameterised queries; Zod input validation; DOMPurify; no shell interpolation
A04 Insecure DesignThreat-modelled approval gates; no auto-proceed on expired approvals; agent budget halts
A05 Security MisconfigurationSecurity headers; no default credentials; Dependabot; container hardening
A06 Vulnerable ComponentsDependabot; pnpm audit in CI; license scanning
A07 Authentication FailuresShort-lived JWTs; token rotation; brute-force lockouts; 2FA enforcement for admin/reviewer
A08 Software and Data IntegritytruffleHog; pinned Docker image tags; signed commits enforced on main branch
A09 Logging & Monitoring FailuresImmutable audit log; OpenTelemetry tracing; Grafana alerting; Loki log aggregation
A10 SSRFAgent tool URL allowlists; no user-controlled URLs in backend HTTP clients; Playwright runs sandboxed

On-Prem Security Considerations

Enterprise on-prem deployments have additional responsibilities:

AreaResponsibility
TLS certificatesCustomer provides certificates or uses Let’s Encrypt via Coolify Traefik
Network isolationCustomer controls firewall rules; Leadmetrics recommends private subnet for DB services
Backup encryptionCustomer responsible for encrypting database backups at rest
RSA key pairsCustomer generates and rotates JWT signing keys (see Sessions)
Secret managementCustomer may use Doppler, Vault, AWS Secrets Manager, or any compatible secrets injector
OS patchingCustomer responsible for host OS security updates
Air-gapped LLMsOllama-only mode available for full data sovereignty; no data sent to Anthropic/OpenAI

© 2026 Leadmetrics — Internal use only