Skip to content

Security Architecture

Breeze is an RMM platform — it has privileged access to every device it manages. Security is not a feature bolted on after the fact; it is foundational to every layer of the architecture. This document describes the security controls, practices, and design decisions in Breeze. It is intended for MSPs evaluating Breeze, security teams conducting assessments, and contributors building on the platform.


Every request passes through multiple security layers before reaching application logic. No single layer is relied upon in isolation.

LayerControl
TransportTLS 1.2+ with HSTS preload
OriginCORS strict allowlist (no wildcards in production)
ContentContent Security Policy (CSP)
CSRFHeader-based validation on state-changing requests
Rate LimitingRedis sliding window with in-memory fallback (100K entry cap)
AuthenticationJWT + MFA + session tokens
AuthorizationRBAC with permission middleware
Tenant IsolationPostgreSQL row-level security (enabled + forced) under an unprivileged DB role, plus app-layer site-scope enforcement
AuditStructured event logging on all security-relevant actions
Encryption at RestAES-256-GCM for secrets, Argon2id for passwords

Breeze implements multi-factor authentication with defense-in-depth:

ControlImplementation
Password hashingArgon2id — 64 MB memory, 3 iterations, 4 threads
Password policy8–128 chars, mixed case, numeric required
Access tokensJWT (HS256), 15-minute lifetime, audience/issuer-scoped
Refresh tokensJWT, 7-day lifetime, unique JTI, revocable
Session tokensCryptographically random (nanoid 48), SHA-256 hashed in DB
MFATOTP (RFC 6238), 10 recovery codes (XXXX-XXXX format)
SMS MFAOptional Twilio integration for SMS-based codes
Passkey MFAWebAuthn/FIDO2 (@simplewebauthn/server) — phishing-resistant platform authenticators and security keys; per-credential signature counter for clone detection. Enrolment requires a current-password step-up.
Token revocationExplicit session invalidation, bulk logout per user; refresh-token family reuse detection (a replayed rotated token revokes the entire family)

Plaintext tokens are never stored. All token storage uses SHA-256 hashes.

API keys follow the same security model as agent tokens:

  • Format: brz_ prefix for identification
  • Storage: SHA-256 hash only — the plaintext key is shown once at creation, never again
  • Scoping: JSONB scope array with wildcard support (* for full access)
  • Lifecycle: Configurable expiration, revocable, status tracking (active/revoked/expired)
  • Rate limiting: Per-key configurable request limits
  • Audit trail: lastUsedAt timestamp and usageCount updated on every use

Agents authenticate using brz_-prefixed tokens issued during enrollment. The token is SHA-256 hashed and stored in devices.agentTokenHash — the plaintext is never persisted server-side. Every REST request and WebSocket connection validates the bearer token against the stored hash. Decommissioned and quarantined devices are rejected with 403.

For organizations requiring proof-of-possession at the TLS layer, optional Cloudflare mTLS adds certificate-based mutual authentication.


Partner (MSP) → Organization (Customer) → Site (Location) → Device Group → Device

Every entity is scoped to this hierarchy. A user at one organization can never access another organization’s data — this is enforced at the database layer, not just the application layer.

The API connects to PostgreSQL as an unprivileged role (breeze_app) — never as the database owner or a superuser. Every tenant-scoped table has row-level security enabled and FORCED, with policies that constrain visibility to the caller’s tenant. Because RLS is forced, the policies apply even to the table owner; a SQL-injection foothold or a logic bug in a single query cannot read or write across tenants, because the database itself rejects out-of-tenant rows.

Tenant context is supplied per request through PostgreSQL session variables:

breeze.scope = 'system' | 'partner' | 'organization'
breeze.org_id = UUID of current organization
breeze.accessible_org_ids = comma-separated list or '*'

These variables are set via set_config() within the request transaction context using Node.js AsyncLocalStorage. Queries that don’t have proper context set will fail — there is no default permissive state, and the bare connection pool is forbidden in request code.

Coverage is enforced mechanically: a contract test asserts that every tenant-scoped table carries the correct RLS policy shape, so a new table cannot ship without isolation. Cross-tenant writes are additionally validated by forging an out-of-tenant insert as breeze_app and confirming PostgreSQL rejects it with a row-level-security violation.

ComponentDescription
RolesNamed definitions scoped to system, partner, or organization level
PermissionsAtomic resource:action pairs (e.g., devices:read, scripts:execute)
Wildcards*:* grants all permissions (system admin only)
MiddlewarerequirePermission(resource, action) enforced on every protected route
Caching5-minute in-memory permission cache to reduce DB lookups

Three scope levels control data visibility:

  • System: Full access to all organizations (super-admin only)
  • Partner: MSP access to their portfolio, configurable per-org (all, selected, none)
  • Organization: Single-tenant access, no cross-org visibility

Scope is computed once per request via resolveOrgAccess() and applied to all downstream queries.

Organization users can be restricted to specific sites (via organization_users.site_ids). Site is a sub-organization authorization axis that is not covered by PostgreSQL RLS — it is enforced in the application layer, layered above org-level row-level security. The caller’s allowed site set is resolved once during auth middleware into a canAccessSite() closure that every device-acting path consults before doing work.

A site-restricted technician is gated on every path that acts on a device:

  • Device mutations — device PATCH, including moving a device to a new site (the target site is checked).
  • Script execution — the device’s site is verified before scripts are listed or run.
  • Automations — create, update, and manual trigger reject any automation whose resolved target set escapes the caller’s allowed sites (no unbounded org-wide automations for site-restricted users).
  • Playbooks — execution and execution updates verify the target device’s site.
  • Configuration-policy patch jobs — target devices outside the caller’s sites are rejected.
  • AI tools — read/enumeration tools that lack an explicit device filter narrow their results to the caller’s in-scope devices; a technician with no in-scope devices gets empty results.
  • Security threat actions — quarantine/remove/restore verify the threat’s device site before queueing.

A device outside the technician’s site allowlist is unreachable even though it belongs to the same organization.


The agent runs on customer endpoints with elevated privileges. Its security is paramount.

ControlDetail
Token formatbrz_ prefix tokens generated during enrollment
Token storageSHA-256 hash in devices.agentTokenHash — plaintext never persisted
Request validationEvery REST and WebSocket request validates bearer token against stored hash
Config directory0750 (rwxr-x---) — agent owner + group read for Helper
Config file0640 (rw-r-----) for agent.yaml, 0600 (rw-------) for secrets.yaml — auth token isolated in root-only secrets file
Message validationAll incoming WebSocket messages validated against Zod discriminated union schema

When a device is provisioned, the API does not return the long-lived agent secrets inline. Instead it returns a short-TTL, single-use fetch URL. The credential bundle (agent auth token, watchdog and helper tokens, mTLS private key, manifest trust keys) is retrievable exactly once: the fetch is consumed with an atomic UPDATE ... WHERE consumed_at IS NULL, and the stored plaintext is hard-deleted immediately after the first successful read. The handle expires after PROVISION_HANDLE_TTL_MINUTES (default 5 minutes); a replay returns 404, and the fetch is additionally org-access-checked as defense-in-depth on top of the token. This keeps agent secrets out of logs, command history, and any persistent at-rest store.

For zero-trust authentication where both server and agent verify each other’s identity, Breeze integrates with Cloudflare Client Certificates API. Certificates are issued during enrollment, renewed automatically at 2/3 lifetime, and expired certificates trigger device quarantine pending admin review.

See Cloudflare mTLS for the full setup guide.

Mutating commands sent to agents are logged to the audit trail:

  • Registry modifications (REGISTRY_DELETE, REGISTRY_KEY_DELETE)
  • File operations (FILE_DELETE)
  • Patch operations (PATCH_SCAN, INSTALL_PATCHES, ROLLBACK_PATCHES)

Each audit entry captures: command type, target device, exit code, stderr output, and the actor who initiated the command.


ControlImplementation
TLS terminationCaddy reverse proxy with automatic Let’s Encrypt certificates
HSTSmax-age=31536000; includeSubDomains; preload
HTTP redirectOptional FORCE_HTTPS environment variable
WebSocketWSS (encrypted WebSocket) for all agent communication
Internal trafficAPI listens on localhost only — no unencrypted external exposure
DataAlgorithmDetails
PasswordsArgon2id64 MB memory, 3 iterations, 4 threads, 32-byte hash
Auth tokensSHA-256One-way hash — tokens, API keys, session tokens, enrollment keys
SecretsAES-256-GCMAuthenticated encryption with per-operation random IV
MFA secretsAES-256-GCMEncrypted before storage, decrypted only during verification

Secrets encrypted at rest use the format: enc:v1:{base64url(iv)}.{base64url(authTag)}.{base64url(ciphertext)} — 12-byte random IV generated per encryption (never reused), GCM authentication tag prevents tampering, and isEncryptedSecret() prevents double-encryption.


Breeze uses Redis-backed sliding window rate limiting. The implementation is fail-closed — if Redis is unavailable, requests are denied.

EndpointLimitWindowKey
Login attempts55 minutesPer email
Password reset31 hourPer email
MFA verification55 minutesPer user
SMS verification31 hourPer phone
SMS login35 minutesPer email
Agent requests12060 secondsPer device
API key requestsConfigurable1 hourPer key

The implementation uses Redis sorted set (ZSET) sliding windows with MULTI pipelines for race-condition-free counting. Standard X-RateLimit-* headers and 429 Too Many Requests with Retry-After are returned when limits are exceeded.


All external input is validated using Zod schemas before processing:

Input TypeValidation
Emailz.string().email()
UUIDsz.string().uuid()
Phone numbersE.164 regex (^\+[1-9]\d{6,14}$)
MFA codesExact 6-character length
Passwords8–128 chars with complexity requirements
Paginationmin: 1, max: 100 limit enforcement
Agent messagesZod discriminated union for WebSocket payloads
API request bodies@hono/zod-validator middleware on every route

Validation errors return structured error objects with field paths. Sensitive values are never echoed in error responses.


Every response includes the following security headers:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self';
connect-src 'self' ws: wss:;
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
  • Production: Only explicitly configured origins allowed via CORS_ALLOWED_ORIGINS
  • No wildcards: Wildcard (*) origin is explicitly rejected in production
  • Development: localhost origins only, excluded from production builds unless opted in

State-changing operations (POST, PUT, DELETE) on sensitive endpoints require a x-breeze-csrf header. Requests without the header return 403.


Every security-relevant operation is recorded in the audit_logs table:

FieldDescription
actorTypeuser, api_key, agent, or system
actorIdUUID of the actor
actionSpecific operation (e.g., device.command.execute)
resourceTypeTarget entity type
resourceIdTarget entity UUID
resultsuccess, failure, or denied
ipAddressSource IP (IPv4/IPv6)
userAgentClient identifier
detailsJSONB metadata (command type, exit codes, etc.)
errorMessageFailure reason (if applicable)
  • Default: 365 days per organization
  • Configurable: Per-org retention policies via audit_retention_policies
  • Archival: Optional S3 archival before deletion
  • Synchronous: createAuditLog() — blocks until written (critical operations)
  • Asynchronous: createAuditLogAsync() — fire-and-forget (non-critical operations)

The AI system has access to powerful tools. Every AI-initiated action passes through a risk classification engine enforced by the RMM, not the AI.

Risk LevelBehaviorExamples
LowAuto-execute, loggedQuery devices, read logs, generate reports
MediumExecute + notify technicianRead-only scripts, pre-approved patch deployments
HighRequires human approvalState-changing scripts, patches outside maintenance windows
CriticalBlocked entirelyDevice wipe, bulk destructive operations
  • Risk policies are configurable per partner, organization, site, or device group
  • The AI cannot bypass the risk engine — it is enforced at the tool execution layer
  • BYOK mode: your API key, your data, your infrastructure — nothing sent to LanternOps unless you opt in

ControlImplementation
Base imagenode:24-alpine (current LTS, minimal attack surface)
Multi-stage builddeps → builder → runner (no build tools in production)
Non-root executionDedicated hono user (UID 1001), nodejs group (GID 1001)
File ownership--chown=hono:nodejs on all copied assets
Minimal exposureSingle port (3001) exposed

Caddy reverse proxy handles TLS termination with automatic Let’s Encrypt certificate provisioning (ACME), HSTS with preload, zstd and gzip compression, and separate routing for /api/*, /metrics/*, and frontend assets.

  • API server listens on localhost — never directly exposed
  • Database and Redis accessible only within the Docker network
  • Metrics endpoint (/metrics/*) separated from public routes

ScannerWhat It ChecksTrigger
CodeQLStatic analysis (SAST) for JS/TS vulnerabilitiesEvery push and PR to main
GitleaksHardcoded secrets in source codeEvery push and PR to main
npm auditNode.js dependency vulnerabilities (high+)Every push and PR to main + weekly
govulncheckGo dependency vulnerabilitiesEvery push and PR to main + weekly
TrivyFilesystem CVE scan (high + critical)Every push and PR to main + weekly

All scanners run in CI and block merges on failure.

  • Lock file: pnpm-lock.yaml committed for reproducible builds
  • Package manager: pnpm with strict dependency resolution
  • Version pinning: All dependencies pinned to exact versions via lock file

SecretPurposeMinimum Strength
JWT_SECRETToken signing32+ characters
APP_ENCRYPTION_KEYAES-256-GCM encryption32-byte hex
MFA_ENCRYPTION_KEYMFA secret encryption32-byte hex
AGENT_ENROLLMENT_SECRETAgent enrollment32-byte hex
REDIS_PASSWORDRedis authentication (must appear in REDIS_URL)32-byte hex
RELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYSVerifies signed release manifests in GitHub-mode binary distributionBase64 SPKI
IS_HOSTEDDeployment mode flag (true/false); gates signup, billing, and email-verification policyExplicit boolean

Breeze validates environment configuration on startup:

  • Rejects 24 known placeholder/default values
  • Requires explicit CORS_ALLOWED_ORIGINS (no wildcards)
  • Enforces minimum secret strength
  • Logs warnings for non-critical misconfigurations
SecretProtection
User passwordsArgon2id
Session tokensSHA-256
API keysSHA-256
Agent auth tokensSHA-256
Enrollment keysSHA-256 with pepper
MFA secretsAES-256-GCM

For rotation procedures and schedules, see Secret Rotation.


  • RTO: < 1 hour
  • RPO: < 15 minutes (with WAL archiving) or last backup interval
  • Components: PostgreSQL, object storage (MinIO/S3), encrypted configuration

For full procedures, see Backup and Restore.

  • Generic error messages returned to clients — internal details never exposed
  • No stack traces in production responses
  • Structured JSON logging (LOG_JSON=true) for log aggregation
  • Optional Sentry integration for error tracking (SENTRY_DSN)
  • Sensitive data (tokens, passwords) never logged

Breeze’s security controls align with SOC 2 Trust Service Criteria.

CC6 — Logical and Physical Access Controls

Section titled “CC6 — Logical and Physical Access Controls”
CriteriaImplementation
CC6.1 — Logical access securityJWT + MFA + RBAC + API key scoping
CC6.2 — Credentials managementArgon2id passwords, SHA-256 token hashing, AES-256-GCM secrets
CC6.3 — Access authorizationRole-based permissions, scope enforcement, requirePermission() middleware
CC6.6 — External access restrictionsCORS allowlist, CSP, rate limiting, CSRF protection
CC6.7 — Data transmission securityTLS 1.2+, HSTS preload, WSS for agent communication
CC6.8 — Unauthorized access preventionFail-closed rate limiting, device quarantine, session invalidation
CriteriaImplementation
CC7.1 — Infrastructure monitoringAgent health checks, heartbeat monitoring, configurable alerting
CC7.2 — Anomaly detectionRate limit violation tracking, audit log analysis
CC7.3 — Vulnerability managementCodeQL SAST, Trivy CVE scanning, npm audit, govulncheck
CC7.4 — Incident responseDisaster recovery runbook, security incident procedures
CriteriaImplementation
CC8.1 — Change authorizationPR-based workflow, CI gate enforcement, code review requirements
CriteriaImplementation
CC9.1 — Risk identificationAutomated security scanning (5 scanners), AI risk classification engine
CC9.2 — Vendor risk managementDependency lock files, supply chain scanning, known vulnerability databases
CriteriaImplementation
A1.1 — Processing capacityRedis-backed rate limiting, BullMQ queue management
A1.2 — Recovery objectivesRTO < 1 hour, RPO < 15 minutes
A1.3 — Recovery testingDocumented procedures for 5 failure scenarios
CriteriaImplementation
C1.1 — Confidential data identificationMulti-tenant isolation, encryption key hierarchy
C1.2 — Confidential data disposalAudit log retention policies, S3 archival, configurable retention

We follow coordinated disclosure:


DomainControlsStatus
AuthenticationJWT + MFA (TOTP/SMS/Passkey) + Sessions + API KeysImplemented
AuthorizationRBAC + scope-based multi-tenancy + app-layer site-scopeImplemented
Encryption (at rest)AES-256-GCM, Argon2id, SHA-256Implemented
Encryption (in transit)TLS 1.2+ / HSTS / WSSImplemented
Rate limitingRedis sliding window (fail-closed)Implemented
Audit loggingStructured, org-scoped, async-capableImplemented
Input validationZod schemas on all external inputImplemented
Security headersCSP, HSTS, X-Frame-Options, Permissions-PolicyImplemented
CORSStrict allowlist, no production wildcardsImplemented
CSRF protectionHeader-based validation on state changesImplemented
Agent securityToken hashing + optional mTLS + file permissionsImplemented
AI safetyRisk classification engine with human approval gatesImplemented
Supply chain5 automated scanners blocking on failureImplemented
Docker hardeningMulti-stage, non-root, Alpine baseImplemented
Secret managementRotation procedures, production validation, no plaintextImplemented
Disaster recoveryDocumented runbooks, defined RTO/RPOImplemented