Security Policy
Truthful, controls-mapped, audit-ready posture for the WECO SPV-disclosure platform.
Effective: 2026-05-11 · Last revised: 2026-05-11 · Applies to release ≥ v1.0.0. Detailed traceability: see dist repo + docs/AUDITOR_TRACEABILITY.md in the release tarball.
1. Threat model
WECO publishes regulatory disclosures for SPVs / SFOs / fund vehicles. The threats we explicitly defend against, in priority order:
- Public-site tamper — an attacker modifies a published disclosure (PDF replacement, content edit, defacement). Mitigations: append-only audit log, atomic deploy with rollback dir, per-slug advisory lock, signed releases, optional FS-WORM target for backups.
- Admin-VM compromise — credential theft, account takeover, lateral movement. Mitigations: bcrypt cost 12, rate-limited login + account lockout, email 2FA (TOTP — Phase 2), per-host SSH-key encryption, append-only audit, optional KMS for `MASTER_SECRET`.
- Supply-chain — backdoored dependency or release. Mitigations: minisign-signed release tarball, SHA-256 chain, public dist repo, SBOM published per release, npm package-lock pinned, install.sh self-checks SHA before running, FF_ALLOW_UNVERIFIED rejected on tagged releases.
- License/JWT abuse — pirated or replayed license. Mitigations: Ed25519 signing key in offline laptop (production) / file-mode-600 on issuance server (trial), per-customer JWT with expiry, revocation list (Phase 2), tier-cap enforcement in DB layer.
Threats we explicitly do not defend against — see §6 "Out of scope".
2. Controls — what's actually shipped today
2.1 Authentication
| Control | Status | Evidence |
|---|---|---|
| Email + bcrypt password (cost 12, min 12 chars) | Shipped | admin/scripts/seed-admin.ts, admin/src/lib/auth.ts |
| Account lockout: 5 attempts → 15-minute lock | Shipped | admin/src/app/api/auth/password-step/route.ts |
| Per-IP rate limit: 10 attempts / 15 min | Shipped | admin/src/lib/rate-limit.ts (in-memory LRU; Redis backend = Phase 2) |
| Email 2FA (one-time code via SMTP) | Shipped | admin/src/lib/twofa.ts |
| TOTP 2FA (RFC 6238 — authenticator app) | Shipped v0.0.19 | via otplib + AES-GCM-encrypted seeds, recovery codes. FF_TOTP_ONLY=1 (v0.0.30) refuses email-2FA enrollment for AAL2. |
| WebAuthn / FIDO2 passkeys (AAL3) | Shipped v0.0.37 | via @simplewebauthn/server. userVerification=required for AAL3 phishing-resistance. Roaming security keys + platform authenticators. |
| Sealed-mode MFA enforcement | Shipped v0.0.32 | FF_SEALED_MODE=1 + FF_TOTP_ONLY=1 = no privileged user logs in without TOTP. Grace window via FF_SEALED_MODE_GRACE_DAYS. |
| Step-up auth on destructive routes | Shipped v0.0.36 | FF_REQUIRE_STEP_UP=1. Cookie minted from fresh TOTP, 5-min TTL, scope-aware. |
| WebAuthn / FIDO2 / hardware key | Planned 2027 | Driven by AAL3 customer demand. |
| SSO — SAML 2.0 SP | Shipped v0.0.35 | via @node-saml/node-saml. SP metadata at /api/auth/saml/metadata; ACS, login, JIT user provisioning, group → role map. |
| SSO — OIDC RP (PKCE S256) | Shipped v0.0.35 | via openid-client. JWKS auto-rotation. State + code-verifier cookie binding. |
| SCIM 2.0 provisioning | Shipped v0.0.37 | RFC 7644 Users + ServiceProviderConfig + ResourceTypes. Joiner/mover/leaver pushed by customer's IdP. |
FF_SSO_ONLY sealed mode | Shipped v0.0.35 | Local password login refused; only SAML / OIDC / WebAuthn paths mint sessions. |
2.2 Authorization
| Control | Status | Evidence |
|---|---|---|
Two roles: superadmin / admin | Shipped | admin/prisma/schema.prisma — role String @default("admin") |
| License-tier caps enforced at DB-write time | Shipped | admin/src/lib/license.ts → requireCapacity() |
| Per-site / per-host RBAC grants | Shipped v0.0.21 | UserHostGrant + UserSiteGrant + requireSiteAccess() / requireHostAccess() guards in lib/grant-guard.ts. CI-enforced: every protected route opens with the guard. |
| 4-eyes JIT for destructive actions | Shipped v0.0.38 | FF_JIT_REQUIRED=1. Different superadmin must approve site.delete / host.rotate_credentials / self-update. Single-use, time-boxed. |
| Break-glass account | Shipped v0.0.36 | npm run break-glass:create. Random password + TOTP secret + 10 recovery codes printed ONCE for sealed-envelope storage. |
2.3 Encryption at rest
| Control | Status | Evidence |
|---|---|---|
| AES-256-GCM with AAD binding on all sensitive columns (host SSH keys, host bearers, 2FA seeds) | Shipped | admin/src/lib/crypto.ts (encrypt/decrypt API). CI lint npm run check:security rejects encrypt-calls without AAD. |
Master key (MASTER_SECRET) — pluggable provider chain | Shipped v0.0.23 | 5 providers: file:, env-cmd:, vault: (HashiCorp Vault Transit), aws-kms:, gcp-kms:. Default file: for dev. FF_KEY_LOCAL_FORBID=1 (v0.0.34) refuses file:/env-cmd: in production. Master-secret rotation CLI: npm run rotate:master-secret (v0.0.34). |
| Audit chain HMAC + Ed25519 witness | Shipped v0.0.30 / v0.0.31 / v0.0.32 | Every AuditEvent row HMAC-chained to its predecessor + Ed25519-signed by a witness key whose private half lives DISJOINT from FF_AUDIT_SECRET. Verifier: npm run audit:verify. Independent off-system witness via FF_AUDIT_SIEM_URL with back-pressure queue (v0.0.36). |
| Backup encryption with separate KEK | Shipped v0.0.28 / v0.0.34 | AES-256-GCM, scrypt-derived. Refuses BACKUP_PASSPHRASE == MASTER_SECRET (v0.0.34) so a single-secret leak cannot decrypt both DB and backup. |
| Full-disk encryption (LUKS / BitLocker) | Customer responsibility | Set at VM creation; install.sh does not touch disk encryption. Documented in §3 of the Customer Deployment Guide. |
2.4 Encryption in transit
| Control | Status | Evidence |
|---|---|---|
| TLS 1.2+ on admin nginx (Mozilla intermediate config) | Shipped | admin/nginx/admin.conf + Let's Encrypt |
Admin → host: SSH local port-forward; host-api binds 127.0.0.1 only | Shipped | admin/src/lib/tunnel.ts |
| HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy | Shipped | nginx vhost templates |
| Full-disk encryption gate (refuse install if LUKS not detected) | Shipped v0.0.34 | install.sh:check_disk_encryption blocks install on bare VMs without LUKS. Override: FF_ALLOW_UNENCRYPTED=1 (dev only) or FF_DISK_ENCRYPTION=cloud-managed. |
| Mandatory HTTPS gate | Shipped v0.0.20 | Stage 2: HTTPS is mandatory. --https-mode=letsencrypt|selfsigned|provided|none. none is dev-only. |
| Cloudflare WAF integration (customer-side) | Shipped v0.0.33 | install.sh --cloudflare writes nginx real_ip_header CF-Connecting-IP + Cloudflare IP allowlist. Customer signs the Cloudflare contract. |
| Egress allowlist (iptables) | Shipped v0.0.36 | scripts/install-egress-allowlist.sh: outbound restricted to KMS / SIEM / GitHub / DNS / NTP / apt mirror. |
2.5 Audit and integrity
| Control | Status | Evidence |
|---|---|---|
| Append-only audit log: every mutating action recorded with actor, target, IP, timestamp, metadata | Shipped | admin/src/lib/audit.ts · table AuditEvent |
| Audit-export — HMAC chain + Ed25519 witness | Shipped v0.0.30-v0.0.32 | Every row HMAC-chained + Ed25519-signed. Verifier: npm run audit:verify --witness-pub PATH. Tamper detection: any DELETE / UPDATE breaks the chain at first failure. |
| Anomaly detection rules | Shipped v0.0.37 | Built-in: failed-login burst (10/60s), bearer rotation storm (5/5min), off-hours deploy. Forwarded to SIEM as anomaly.* events. |
| Evidence pack (compliance binder export) | Shipped v0.0.34 | npm run evidence:pack produces a tar.gz with audit-verify-report, audit-export-30d, SBOM, env-redacted, controls.md, threat-model. SOC-2-style ready for auditor handoff. |
| ClamAV upload scan + Ghostscript flatten | Shipped v0.0.38 | Every PDF scanned + re-rendered through gs -dSAFER on upload. Drops embedded JS / forms / file attachments. FF_REQUIRE_AV=1 (sealed mode) makes a virus hit a hard 422. |
| WORM / immutable backend for audit storage | Planned 2027 | Customer can mount a WORM target (S3 Object Lock, Azure Immutable Blob) for the audit table dumps; out-of-the-box integration is on the 2027 roadmap. |
Append-only host-api audit at /var/log/ff-host-audit.jsonl | Shipped | vms/host-api/src/audit.ts — never logs tokens, request bodies, or file bytes. |
2.6 Supply chain
| Control | Status | Evidence |
|---|---|---|
| Minisign-signed release tarball | Shipped | Every release has .tar.gz.minisig. Public key baked into install.sh. |
| SHA-256 of tarball + install.sh self-check | Shipped | admin/install.sh:self_check() — refuses to run on hash mismatch. FF_ALLOW_UNVERIFIED=1 is rejected on tagged releases. |
| SBOM (CycloneDX) per release | Shipped from v0.0.18 | ff-admin-vX.Y.Z.sbom.json as a release asset; also at SBOM.cdx.json inside the tarball. |
| CI workflows shipped inside release tarball | Shipped from v0.0.18 | .github/workflows/ included so licensee can verify our security-invariants lint, schema-drift check, and release pipeline. |
| Dependabot / Snyk / npm-audit gating | Partial | npm-audit runs in CI from v0.0.18. Dependabot enabled on antonorlov888/ff_webpage_constuctor_admin. Snyk integration = Phase 2. |
| Source snapshot pushed to public dist repo per release | Shipped | github.com/antonorlov888/orox_siteconstuctor_admin-dist — orphan branch snapshot-vX.Y.Z per release. |
2.7 Install-time outbound network calls — full disclosure
The installer makes the following outbound HTTPS calls during initial install only (zero at runtime). Each is replaceable in the air-gap variant (Phase 2 Q3 2026) or via operator allowlisting:
| Endpoint | Purpose | Mitigation |
|---|---|---|
github.com/antonorlov888/orox_siteconstuctor_admin-dist/releases/download/<tag>/... | Fetch signed release tarball + install.sh | Replace with internal mirror; verify SHA-256 + minisign before extract. |
deb.nodesource.com/setup_20.x + deb.nodesource.com/node_20.x/... | Install Node 20 from NodeSource apt repo | Pin to a customer-controlled apt mirror via FF_NODE_APT_REPO=...; or air-gap mode bundles Node binary directly. |
acme-v02.api.letsencrypt.org + acme-staging-v02.api.letsencrypt.org | ACME challenge for HTTPS cert | Replace with internal ACME server (Smallstep CA, Boulder, step-ca) via FF_ACME_SERVER=...; or use --no-tls for air-gap with operator-supplied certs. |
api.ipify.org | Discover public IP for ACME challenge | Removed in v0.0.18 — replaced with VPS metadata (cloud-init / DigitalOcean / Hetzner / Yandex) or operator-supplied FF_PUBLIC_IP=.... |
archive.ubuntu.com (apt deps: postgres, nginx, certbot, etc.) | OS package install | Internal apt mirror; air-gap mode bundles deb packages directly. |
No outbound calls at runtime. No telemetry, no auto-update, no analytics, no error reporting. Verifiable by inspecting the running VM with tcpdump -i any -nn after install completes.
3. Vulnerability disclosure
Send a description + reproduction steps + (if you have one) PoC to anton.orlov@nexaxt.com with subject [WECO-SECURITY]. Encrypt sensitive details with our PGP key (fingerprint published at /.well-known/pgp-key.asc from v0.0.18).
Our commitments to a reporter:
- Acknowledge within 2 business days.
- Confirm or deny the issue within 5 business days.
- Patched release for any critical/high CVSS finding within 14 calendar days.
- Public credit (or anonymity if requested) in the GitHub Security Advisory.
Bounty programme is in scope for Phase 2 (Q4 2026).
4. Audit trail of security claims
Every claim on this page maps to a file or table in the release tarball. To verify independently:
# Download
curl -LO https://github.com/antonorlov888/orox_siteconstuctor_admin-dist/releases/download/v0.0.18/ff-admin-v0.0.18.tar.gz
curl -LO https://github.com/antonorlov888/orox_siteconstuctor_admin-dist/releases/download/v0.0.18/ff-admin-v0.0.18.tar.gz.minisig
curl -LO https://github.com/antonorlov888/orox_siteconstuctor_admin-dist/releases/download/v0.0.18/ff-admin-v0.0.18.sbom.json
# Verify provenance
minisign -V -P 'RWQHQhnHIdSeftw6PLpDHYqFQRzDvTslvKw5mluIjFjyYfwumLT+9siX' \
-m ff-admin-v0.0.18.tar.gz
# Verify SBOM (CycloneDX)
cat ff-admin-v0.0.18.sbom.json | jq '.components | length'
# Inspect security posture and CI
tar -xzf ff-admin-v0.0.18.tar.gz
cd ff-admin-v0.0.18
cat SECURITY.md # this document, source of truth
cat LICENSE # FSL 1.1 / Apache 2.0 future
cat EULA.md # operational terms
ls .github/workflows/ # CI configs
cat admin/scripts/check-aad-binding.ts # the AAD-binding lint that gates encrypt() calls
# Run the AAD-binding lint yourself
cd admin && npm ci && npm run check:security
5. Compliance posture
WECO is not independently certified at SOC 2, ISO 27001, or PCI-DSS as of 2026-04-25. We are realistic about this — a sole-developer, source-available product cannot ship pre-certified, and we do not claim what we do not have.
We are a viable building block for customers who hold their own SOC 2 / ISO 27001 / regulator certifications and need a compliance-publishing layer they can install inside their certified perimeter. The audit-export, append-only audit log, signed releases, and on-prem-only architecture are designed to map onto the customer's existing control framework.
For customers who require a vendor-side certification before procurement: SOC 2 Type II is on our 2027 roadmap, conditional on revenue. Until then, the alternative is the standard Vendor Security Questionnaire — we respond within 5 business days with citations into the source tree.
6. Out of scope (boundary statements)
The following are customer responsibilities, documented in §3 of the Customer Deployment Guide. We mark them out of scope here for honesty, not deflection — every WECO contract makes these explicit.
- Volumetric L3/L4 DDoS. Front the admin with a CDN/WAF (Cloudflare, Akamai, Imperva) or use the hosting provider's anti-DDoS service. WECO terminates at nginx; we do not absorb network-layer floods.
- Full-disk encryption (LUKS / dm-crypt / BitLocker). Set at VM creation. Every cloud provider supports this.
- Backup target separation. The pg_dump (containing encrypted host credentials) and the admin VM `.env` (containing
MASTER_SECRET) must live on different backup media with different access controls. Together they re-create the single point of failure we encrypted against. - Network-layer SIEM / IDS / EDR. WECO emits structured audit logs at the application layer; integrate them into your existing SIEM (Splunk, Elastic, Sentinel) via the audit-export endpoint.
- Personnel security clearance for operators with admin access. WECO provides RBAC primitives; identity vetting is the customer's process.
7. What this product does NOT do
For procurement-side honesty:
- WECO does not generate iXBRL / XBRL / ESEF / EMT-EPT / FIRDS / KID / PRIIPs filings. It is a CMS for the human-readable disclosure website that accompanies those filings; the structured filings are produced upstream (Workiva, Confluence, Donnelley Venue, fund administrator's tooling).
- WECO does not ingest live NAV / AUM / market data. The
documentsblock accepts PDFs only. Customers that need live data display can render the data into an HTML block via the admin's API, but live feed connectors are out of scope. - WECO does not provide HA / multi-AZ / hot-standby on Phase 1. Single Ubuntu admin VM. RTO/RPO targets are negotiated per Order Form for Premium / Enterprise tiers, with the customer's chosen backup cadence.
- WECO does not provide a migration path FROM other CMSes. New SPVs are configured fresh through the admin UI.
8. Contact
Security disclosures: anton.orlov@nexaxt.com · subject [WECO-SECURITY].
This document tracks tag-aligned changes in the dev repo at SECURITY.md. The web copy is updated within 7 calendar days of each release.