Security
Security is central to VerifyWise's open-source AI governance platform. We're committed to secure development throughout our entire lifecycle with a transparent, community-driven approach.
Comprehensive security practices built into every aspect of our platform
Automated vulnerability scanning, license violation checks, and commit reviews to detect credential leakage using GitHub for secure source code hosting.
Values community feedback through open-source model allowing security experts to identify vulnerabilities. Security issues reported to security@verifywise.ai.
Committed to GDPR and privacy laws. Works with Data Protection Officer and maintains compliance with security standards.
Incorporates automated testing, provides deployment security guidelines, and continuous vulnerability monitoring.
Data encryption in rest and transit, role-based access controls (RBAC), and restricted system access.
Open-source security philosophy that welcomes professional contributions for faster vulnerability addressing.
Comprehensive security practices across all aspects of development
Secure coding practices required with detailed pull request review process.
GitHub tools scan third-party dependencies with routine dependency checks and updates.
Internal and community code reviews with automated and manual security checks.
Trusted contributor selection and monitoring of maintainer and contributor integrity.
Our open-source approach enables security experts worldwide to identify vulnerabilities and contribute to improvements. This transparent model allows for faster vulnerability addressing and professional community contributions.
Found a security issue? We value responsible disclosure and community feedback.
Security overview
An evidence-based account of security controls in the VerifyWise codebase. Every claim references specific source files. Where controls are partial or absent, this document says so.
Version 1.0 ยท February 2026 ยท Public
VerifyWise is an open-source AI governance platform for managing compliance frameworks, model inventories, risk assessments, vendor due diligence, and policy lifecycles. Deployed self-hosted, organizations retain full control over their data and infrastructure.
Schema-per-tenant architecture using SHA-256 derived identifiers, ensuring complete data separation between organizations.
JWT-based access tokens (1-hour lifetime) with separate refresh tokens (30-day lifetime, httpOnly cookies), verified through a 7-layer middleware chain.
Role-based access control with four roles (Admin, Reviewer, Editor, Auditor), enforced at the middleware layer with real-time database cross-checks.
AES-256-CBC with per-operation random initialization vectors for sensitive fields; bcrypt for password hashing.
Custom validation framework combined with parameterized SQL queries throughout.
Tiered rate limits across authentication, general API, file operations, and AI scan endpoints.
Weekly Dependabot scans across npm, pip, GitHub Actions, and Docker ecosystems with CI-enforced audit checks.
Docker Compose for development and small-scale deployments; Kubernetes with Kustomize for production environments.
Technology stack, multi-tenancy model, and deployment patterns
| Component | Technology | Version |
|---|---|---|
| Backend API | Node.js / Express / TypeScript | Node 24 (Alpine) |
| Database | PostgreSQL | 16.8 |
| Cache / Queue | Redis | 7 |
| Frontend | React 18 / TypeScript | - |
| Evaluation Server | Python (FastAPI) | - |
| Container Runtime | Docker / Kubernetes | - |
VerifyWise implements schema-per-tenant isolation at the PostgreSQL level. Each organization receives a dedicated database schema derived from its organization ID:
This produces a deterministic, collision-resistant schema identifier. All tenant data resides within its dedicated schema, and every query targets the tenant-specific schema qualified by the authenticated user's tenant hash.
Application layer
Internal network only โ no direct internet access
All database and cache services communicate over an internal Docker bridge network and are not exposed to external traffic. The backend API is the sole gateway to data services.
Defined in docker-compose.yml for development and small-scale deployments, with a production overlay in docker-compose.prod.yml. Secrets via .env file.
Kustomize-based manifests with a dedicated verifywise namespace, PersistentVolumeClaims for PostgreSQL, and optional TLS Ingress via cert-manager. Secrets via Kubernetes Secrets.
JWT-based authentication, role-based authorization, and rate limiting
VerifyWise uses a dual-token pattern. An access token is signed with JWT_SECRET using HMAC-SHA256 (HS256) and expires after 1 hour. A refresh token is signed with a separate REFRESH_TOKEN_SECRET and expires after 30 days, stored in an httpOnly cookie.
| Attribute | Value |
|---|---|
| httpOnly | true (always) |
| path | /api/users |
| expires | 30 days from issuance |
| secure | true in production, false otherwise |
| sameSite | none in production, lax otherwise |
Passwords are hashed using bcrypt v6 with a cost factor of 10. The bcrypt library handles salt generation automatically. Password verification uses bcrypt.compare(), which performs constant-time comparison to mitigate timing attacks.
Every authenticated request passes through the following sequential checks. If any layer fails, the request is rejected before reaching the controller.
Extracts Bearer token from the Authorization header. Returns 400 if absent.
Verifies the token signature against JWT_SECRET using jsonwebtoken.verify(). Returns 401 if invalid.
Compares the expire timestamp in the payload against Date.now(). Returns 406 if expired.
Verifies id is a positive number and roleName is a non-empty string. Returns 400 if malformed.
Queries the database to confirm the user belongs to the claimed organization. Returns 403 if not a member.
Fetches the user's current role from the database and compares it to the token's roleName. Returns 403 if the role has changed since token issuance.
Validates the tenant hash format using a regex, then recomputes the hash from the organization ID and compares. Returns 400 if mismatched.
| Role ID | Role name | Description |
|---|---|---|
| 1 | Admin | Full system access |
| 2 | Reviewer | Review and approval permissions |
| 3 | Editor | Content editing permissions |
| 4 | Auditor | Read-only audit access |
Four rate limiter tiers are implemented using express-rate-limit. Rate limit headers follow the IETF RateLimit-* standard.
| Limiter | Window | Max requests | Applied to |
|---|---|---|---|
authLimiter | 15 minutes | 5 | Authentication endpoints |
generalApiLimiter | 15 minutes | 100 | Standard API endpoints |
fileOperationsLimiter | 15 minutes | 50 | File upload/download/delete |
aiDetectionScanLimiter | 60 minutes | 10 | AI detection scan operations |
Encryption, tenant isolation, and SQL injection prevention
Sensitive fields (API keys, OAuth tokens, integration credentials) are encrypted using AES-256-CBC:
| Parameter | Value |
|---|---|
| Algorithm | aes-256-cbc (128-bit block cipher with 256-bit key) |
| Key | Derived from ENCRYPTION_KEY env var, padded or truncated to exactly 32 bytes |
| Initialization vector | 16 random bytes generated via crypto.randomBytes(16) per encryption operation |
| Output format | {iv_hex}:{ciphertext_hex} โ IV and ciphertext stored together, colon-delimited, both hex-encoded |
Tenant isolation is enforced at multiple layers:
Each tenant's data resides in a dedicated PostgreSQL schema created by createNewTenant(), which executes CREATE SCHEMA.
The tenant hash is embedded in the JWT payload at login and verified on every request.
Every authenticated request verifies the user belongs to the claimed organization via a database query.
A regex (/^[a-zA-Z0-9]{10}$/) rejects any tenant identifier that does not match the expected format.
SQL queries that interpolate schema names use escapePgIdentifier(), which validates the identifier and double-quotes it.
All database queries use parameterized statements via Sequelize's replacements option. Tenant schema names, which cannot be parameterized in PostgreSQL, are validated against the strict alphanumeric regex and escaped via escapePgIdentifier() before interpolation.
Share links use 64-character hex tokens generated by crypto.randomBytes(32).toString("hex"). Token format is validated server-side using /^[a-f0-9]{64}$/. Share links support configurable field visibility and optional expiration dates.
Input validation, XSS prevention, file upload controls, and security headers
VerifyWise implements a custom validation framework with type-specific validators, composed into schema objects and applied as Express middleware:
validateString()Required, minLength, maxLength, regex pattern, empty string, whitespace trimming
validateNumber()Required, min/max range, integer constraint, positive constraint
validateEnum()Validates against allowed set, supports arrays
validateDate()Valid date parsing, min/max date, future-only and past-only constraints
validateForeignKey()Validates positive integer IDs for database foreign key references
Rich text content is sanitized with explicit allowlists:
HTML stripping is performed using the striptags library for content validation, such as checking that policy content contains meaningful text after tag removal.
| Control | Implementation |
|---|---|
| Size limit | 30 MB maximum, enforced by multer |
| MIME whitelist | Documents (PDF, DOC, DOCX, XLS, XLSX, CSV, MD), images (JPEG, PNG, GIF, WEBP, SVG, BMP, TIFF), videos (MP4, MPEG, MOV, AVI, WMV, WEBM, MKV) |
| Extension validation | File extension must match the declared MIME type |
| Filename sanitization | Special characters stripped, spaces replaced with underscores, length limited to 200 characters |
| Storage | Memory storage (multer memoryStorage()), files stored as database BLOBs โ no temp files on disk |
| Access control | Upload and delete restricted to Admin, Reviewer, Editor via authorize() middleware |
| Rate limiting | 50 requests per 15 minutes via fileOperationsLimiter |
HOST env var, localhost, 127.0.0.1, or ::1credentials: true for cookie transmissionDocker configuration, Kubernetes security, and secrets management
node:24-alpine3.20 (pinned version, not :latest)npm ci for reproducible buildspg_isready) and Redis (redis-cli ping)All resources deployed to the verifywise namespace.
PersistentVolumeClaims for PostgreSQL data.
Optional cert-manager integration for automatic Let's Encrypt certificates, forced HTTPS redirect, and security headers.
Kubernetes Secrets for database credentials, JWT secrets, and encryption keys (base64-encoded, template provided).
Runs as an internal service with password authentication. Database port not exposed to host network โ only the backend accesses it via Docker's internal network.
Handles rate limiting and background job queues. Uses expose (internal only) rather than ports (host-mapped).
Structured logging, audit trails, and access tracking
VerifyWise uses Winston with DailyRotateFile for structured, tenant-specific application logging:
| Configuration | Value |
|---|---|
| File pattern | app-YYYY-MM-DD.log |
| Max file size | 10 MB per file |
| Retention | 14 days (auto-rotated) |
| Timezone | UTC (forced) |
| Directory | Tenant-specific subdirectories |
| Production mode | File logging only (no console) |
| Development mode | File + colorized console output |
{logId}, {timestamp (ISO 8601 UTC)}, {state}, {description}, {functionName}, {fileName}Three logging functions enforce consistent usage: logProcessing() records the start of an operation, logSuccess() records successful completion, and logFailure() records errors.
Non-read operations are automatically logged to the tenant-specific event_logs table:
| Column | Description |
|---|---|
event_type | Create, Update, Delete, or Error |
description | Human-readable description of the event |
user_id | ID of the user who performed the action |
Dependency scanning, CI/CD security checks, and base image pinning
Dependabot is configured for automated dependency scanning across all ecosystems:
| Ecosystem | Directory | Frequency | PR limit |
|---|---|---|---|
| npm (frontend) | /Clients | Weekly (Monday) | 10 |
| npm (backend) | /Servers | Weekly (Monday) | 10 |
| pip (evaluation) | /EvaluationModule | Weekly (Monday) | 5 |
| GitHub Actions | / | Weekly (Monday) | 5 |
| Docker (frontend) | /Clients | Weekly | - |
| Docker (backend) | /Servers | Weekly | - |
npm audit --audit-level=high โ flags known vulnerabilitiesnpm run build)dependency-review-action โ blocks PRs with high-severity CVEs or restrictive licensesnpm audit and dependency reviewBoth workflows use minimal GitHub token permissions: contents: read and security-events: write.
Docker base images use pinned versions rather than floating tags:
node:24-alpine3.20postgres:16.8redis:7Code review, CI verification, secrets prevention, and vulnerability disclosure
All code changes are submitted via pull requests targeting master or develop, triggering automated CI checks. Changes require review before merging.
On every PR, the CI pipeline verifies TypeScript compiles without errors, npm audit reports no high-severity vulnerabilities, new dependencies don't introduce high-severity CVEs or restrictive licenses, and backend tests pass.
The .gitignore file excludes .env files from version control. The Kubernetes secrets example file documents the pattern for managing secrets without committing actual values.
A SECURITY.md file exists at the repository root, directing reporters to create security advisories via GitHub's security advisory feature.
VerifyWise provides evidence-based security controls for AI risk management. Deploy on-premises or in your cloud with full control over your data and infrastructure.