This document is the security posture for the Aegis platform. It maps directly to Section 10 of the master prompt — every checkbox there is implemented (or, in the case of infrastructure-side controls like KMS, documented as a deployment requirement).
Important: This document defines the system; it does not itself certify security or compliance. Have the finished platform reviewed by a security professional before handling real customer payment or license data in production. Building it this way doesn't itself grant SOC 2 / ISO 27001 certification; that still requires an actual third-party audit.
- Argon2id for all password hashing, tuned memory/time cost (not defaults copy-pasted without review). Argon2id is the recommended choice in the OWASP Password Storage Cheat Sheet.
- Per-user salt (random 16 bytes), stored alongside the hash.
- Platform authenticators: Face ID / Touch ID (Apple), Windows Hello, Android biometrics.
- Roaming/cross-platform authenticators: USB/NFC/BLE FIDO2 security keys (YubiKey and similar).
- Conditional UI ("autofill"): the browser suggests a saved passkey right in the email field — supported across all major evergreen browsers as of 2026.
- Multiple passkeys per account (register one per device); most are already synced across a user's devices via their platform provider (iCloud Keychain, Google Password Manager, Windows Hello).
- Cross-device hybrid flow (CTAP/QR): a user on a desktop browser with no local authenticator scans a QR code and approves with their phone's Face ID/fingerprint.
- Account recovery for a lost/all-passkeys scenario: fall back to email verification + re-enrollment, never a silent bypass.
otpauthlibrary, RFC 6238. Per-user secret encrypted at rest (envelope encryption — the server needs the plaintext to validate codes).- Standard
otpauth://totp/...provisioning URI rendered as a scannable QR code (large, with the manual entry key shown as text underneath). - Requires one valid 6-digit code before enabling.
- Issues 8–10 single-use, hashed backup codes at the same time so a lost phone doesn't lock someone out of a paid account. Backup codes are hashed like passwords, single-use, rotated on use.
- A passkey or TOTP (either satisfies strong-auth) is required, not optional, for Owner/Admin roles — both are recognized at NIST AAL2 for synced authenticators (NIST SP 800-63-4, July 2025).
- This holds up for compliance-minded customers too.
- For desktop apps — the desktop shell never sees a password and doesn't need an embedded browser.
- Same pattern the GitHub CLI and Docker Desktop use.
- Tokens stored via the platform's secure credential store: Electron
safeStorage, Tauri secure-storage plugin, OS-native vault (Keychain / Credential Manager / libsecret). Never roll your own encrypted-file storage.
- Access tokens short-lived (~15 min), JWTs signed by the Aegis API.
- Refresh tokens rotate on use with reuse detection — reusing an old refresh token revokes the entire token family. This is the standard theft response: if a stolen token is used, the legitimate user's next refresh attempt fails, both sides detect the theft, and the family is burned.
- Refresh token family tracked via the
familycolumn onRefreshToken(seedocs/schema.prisma).
- Roles:
OWNER,ADMIN,BILLING,SUPPORT,READONLY(internal);Customer(portal-only, scoped to their own data). - Default-deny, not default-allow. Every route is guarded; missing or malformed auth = 401, insufficient role = 403.
- CASL
@casl/abilityfor fine-grained ABAC on top (e.g., "a Support agent can view a customer but cannot refund a wallet").
- Scoped (
licenses:write,machines:write,wallet:read, ...), rotatable, rate-limited. - Key hash stored (sha256); the raw key is shown once at creation and never retrievable again.
- Last-used timestamp tracked for hygiene.
alg: EdDSA,crv: Ed25519— Node's nativecryptomodule (or@noble/curvesin the browser/SDK).- Private signing key lives only inside a dedicated signing module backed by a KMS (AWS KMS / GCP KMS / HashiCorp Vault). Never on a general API server's filesystem, never in an env var visible to the whole app.
- Public verification key (32 bytes) compiled into the SDK at build time — safe to ship.
- The SDK verifies every certificate locally against the embedded public key; no network round-trip is required to unlock the app.
- Computed client-side by the SDK from stable hardware identifiers (machine GUID + disk serial + primary NIC MAC), SHA-256 hashed.
- Raw identifiers never leave the machine.
- Constant-time comparison (
fingerprintsMatch()) to avoid timing oracles.
- Every certificate carries a
nonce. The SDK tracks the last-seen nonce per license and rejects a replayed cert within its lifetime. iat/expclaims bound to a reasonable window.
- Flag one license activating from an implausible number of distinct fingerprints (e.g., 12 activations on a 5-seat license in 24 hours →
fingerprint_fanouttamper signal, risk 88). - Clock-rollback detection (Section 9.2): the SDK stores the last known-good server timestamp and rejects a system clock that jumps backward past it.
The kill-switch feature set for responding to a cracked/tampered installation. Its scope is defined precisely up front — getting this vague is both a support nightmare and a real legal exposure.
In this system, "destroy" (WIPE_LICENSE) means the app wipes its own local license certificate, cached tokens, and local config, then refuses to run until a fresh activation.
It does not mean, and must never be implemented as, deleting the customer's documents, other applications, or anything outside the licensed app's own sandboxed data. A kill switch that reaches beyond your own application's data is a support disaster the first time it fires on a false positive, and in most jurisdictions it's a real legal liability — unauthorized computer-damage statutes (e.g., the U.S. CFAA and equivalents elsewhere) generally don't carve out an exception for "but they'd pirated it."
Enforce this scope in code, not just in a policy doc.
The WipeTargets interface in packages/license-sdk/src/lock-wipe.ts is the ONLY shape the wipe callback can receive:
export interface WipeTargets {
certificate: boolean; // delete the cached license certificate
tokens: boolean; // delete cached access + refresh tokens
config: boolean; // delete local license config
}There is no userDocuments: true field. There is no arbitraryPaths: string[] field. The TypeScript type system refuses to express "wipe everything." This is defence-in-depth on top of code review — even a buggy host implementation cannot reach beyond the licensed app's own sandbox.
Don't jump straight to the end state:
- Flag — a tamper signal is reported and risk-scored; no user-visible effect, just logged for review (
TamperSignal). - Warn — the SDK shows an in-app notice; the app still works. Covers false positives gracefully (AV false flags, a legitimate VM/sandbox, a clock that's honestly just wrong) without punishing a paying customer.
- Lock — the app refuses to run (or drops to a read-only/limited mode); fully and instantly reversible by an admin, or automatically once the underlying issue clears.
- Wipe — the license-state reset described above. Recommended default: require an explicit admin action for
WIPE_LICENSE(or dual control — a second admin's approval — for teams that want it), rather than letting any single automated heuristic trigger it unattended.LOCKcan be policy-automated;WIPEgenerally shouldn't be, because it's the hardest action to walk back with a customer who turns out to be legitimate.
- License certificate signature fails verification against every known public key (distinct from simply expired, which is normal lifecycle, not tamper).
- The SDK's own startup integrity check (hash of its critical code paths) doesn't match what was shipped.
- A debugger or known patching/injection tool is detected attached to the process at the point the license check runs.
- The fraud heuristics already in Section 8.3/10 (implausible fingerprint fan-out, clock rollback) cross a high-confidence threshold.
- Every
LOCK/WIPEcommand is fully audit-logged (who, when, why, which machine) — this is the trail you need if a customer disputes it. - A clear appeal path: a customer whose install was locked/wiped incorrectly can contact support and get reinstated without re-buying the license.
- Disclose the capability in the EULA/Terms of Service — don't ship a silent kill switch; tell customers upfront that tampering triggers this. Disclosure is both the ethical baseline and meaningfully better legal footing.
- TLS 1.3 everywhere, HSTS enabled.
helmetmiddleware, strict CSP, explicit CORS allow-list (no wildcard origins).- CSRF protection (double-submit cookie) for the two cookie-based Next.js sessions.
- Rate limiting (
@nestjs/throttler+ Valkey/Redis) — tight limits on auth endpoints (credential-stuffing defense), sane-but-capped limits on/validateand/heartbeat. - Generic error responses in production — never leak stack traces, SQL, or internal paths to a client.
- Secrets never committed; env schema validated with Zod at boot (fail fast on missing/malformed config); production secrets in a real secrets manager (Doppler / AWS Secrets Manager / Vault), not
.envfiles.
- Column-level encryption for PII (via
pgcrypto). - TOTP secrets encrypted at rest (envelope encryption — the server needs the plaintext to validate codes).
- Backup codes hashed like passwords (Argon2id), single-use.
- Ledger invariant:
Wallet.balanceCentsonly ever changes inside the same DB transaction that inserts the correspondingWalletTransactionrow — no code path updates the balance without a matching ledger entry. Enforced via a Postgres trigger in production; the trigger firesRAISEifUPDATE Wallet SET balanceCents = ...runs without an accompanyingINSERT INTO WalletTransactionin the same transaction. - NOWPayments IPN verification: every inbound webhook is verified against NOWPayments' documented signature scheme before crediting any wallet. Never credit a balance from an unverified webhook call.
- Stripe webhook verification: every Stripe webhook is verified using the
Stripe-Signatureheader and the endpoint's signing secret before any side effect. - Idempotency: webhooks will be retried and will arrive out of order. The unique
providerRefconstraint onWalletTransactionmakes duplicate inserts a no-op.
- Immutable, append-only: no
UPDATEorDELETEpermitted onAuditLogfrom the app layer. Enforced via PostgresGRANT— the app role hasINSERT+SELECTonly. - Every sensitive mutation writes a row (license changes, role changes, API-key creation, impersonation, wallet credits/refunds, lock/wipe commands).
- Indexed for the hot paths:
[targetType, targetId],[actorId],[createdAt],[organizationId, createdAt].
- Renovate/Dependabot for dependency updates.
pnpm audit+ supply-chain scanning (Socket.dev or Snyk) — this matters even more for a package (@aegis/license-sdk) that other companies willnpm install.- CodeQL (or equivalent SAST) in CI.
- Compile → lint → test after every module, not at the end.
Every item in Section 10 of the master prompt must be checked and demonstrably true, not assumed:
- Argon2id for all password hashing (tuned memory/time cost).
- TLS 1.3 everywhere, HSTS enabled. (deployment requirement)
- Access tokens short-lived (~15 min); refresh tokens rotate on use with reuse-detection.
- License-signing private key lives only inside a dedicated signing module backed by a KMS/Vault.
- Passkeys/WebAuthn available as primary login for every account type, TOTP as fallback; a passkey or TOTP is required for Owner/Admin accounts.
- RBAC enforced via Guards + CASL on every route — default-deny.
-
WIPE_LICENSEcommands require explicit admin action (dual control by default) — never triggered directly by an automated heuristic with no human in the loop. -
DeviceCommandscope is enforced in code (theWipeTargetsinterface), not just policy. - Rate limiting on auth endpoints and on
/validate+/heartbeat. - Every DTO validated (
class-validator,whitelist: true,forbidNonWhitelisted: true). -
helmet, strict CSP, explicit CORS allow-list. - CSRF protection (double-submit cookie) for cookie-based Next.js sessions.
- Secrets never committed; env schema validated with Zod at boot.
- Dependency hygiene: Renovate/Dependabot,
pnpm audit, supply-chain scanning. - CodeQL (or equivalent SAST) in CI.
- Append-only audit log; never an
UPDATE/DELETEonAuditLogfrom the app layer. - Column-level encryption for PII at rest.
- Idempotency-Key support on every mutating endpoint.
- Replay protection on license validation (nonce + timestamp window) and basic fraud heuristics.
- Generic error responses in production.
- NOWPayments IPN callbacks verified against their documented signature scheme.
-
Wallet.balanceCentsonly ever changes inside the same DB transaction that inserts the correspondingWalletTransactionrow. - TOTP secrets encrypted at rest; backup codes hashed like passwords, single-use.
Items marked "(deployment requirement)" are infrastructure-side controls that the application code expects but cannot enforce on its own.