Skip to content

Security: ahmedkobbi/Posidon_Licensing_Server

Security

docs/SECURITY.md

Aegis — Security Model

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.


Authentication

Password storage

  • 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.

Passkeys (WebAuthn / FIDO2) — primary, encouraged sign-in for every account type

  • 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.

TOTP — fallback

  • otpauth library, 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.

AAL2 requirement

  • 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.

OAuth 2.0 Device Authorization Grant (RFC 8628)

  • 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.

Token lifecycle

  • 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 family column on RefreshToken (see docs/schema.prisma).

Authorization

RBAC via NestJS Guards + CASL

  • 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/ability for fine-grained ABAC on top (e.g., "a Support agent can view a customer but cannot refund a wallet").

API key auth (server-to-server)

  • 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.

Licensing & signing

Ed25519 license certificate signing

  • alg: EdDSA, crv: Ed25519 — Node's native crypto module (or @noble/curves in 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.

Hardware fingerprint

  • 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.

Replay protection

  • Every certificate carries a nonce. The SDK tracks the last-seen nonce per license and rejects a replayed cert within its lifetime.
  • iat / exp claims bound to a reasonable window.

Fraud heuristics

  • Flag one license activating from an implausible number of distinct fingerprints (e.g., 12 activations on a 5-seat license in 24 hours → fingerprint_fanout tamper 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.

Kill switch (Remote Lock / Wipe) — Section 8.12

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.

What "destroy" means here — and what it must never mean

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.

Scope enforcement in code

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.

Escalation ladder

Don't jump straight to the end state:

  1. Flag — a tamper signal is reported and risk-scored; no user-visible effect, just logged for review (TamperSignal).
  2. 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.
  3. 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.
  4. 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. LOCK can be policy-automated; WIPE generally shouldn't be, because it's the hardest action to walk back with a customer who turns out to be legitimate.

Signals that feed the risk score (each on its own is a flag, not a verdict)

  • 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.

Required safeguards

  • Every LOCK/WIPE command 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.

Transport & infrastructure

  • TLS 1.3 everywhere, HSTS enabled.
  • helmet middleware, 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 /validate and /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 .env files.

Data at rest

  • 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.

Wallet & payment integrity

  • Ledger invariant: Wallet.balanceCents only ever changes inside the same DB transaction that inserts the corresponding WalletTransaction row — no code path updates the balance without a matching ledger entry. Enforced via a Postgres trigger in production; the trigger fires RAISE if UPDATE Wallet SET balanceCents = ... runs without an accompanying INSERT INTO WalletTransaction in 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-Signature header and the endpoint's signing secret before any side effect.
  • Idempotency: webhooks will be retried and will arrive out of order. The unique providerRef constraint on WalletTransaction makes duplicate inserts a no-op.

Audit trail

  • Immutable, append-only: no UPDATE or DELETE permitted on AuditLog from the app layer. Enforced via Postgres GRANT — the app role has INSERT + SELECT only.
  • 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].

Supply chain

  • 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 will npm install.
  • CodeQL (or equivalent SAST) in CI.
  • Compile → lint → test after every module, not at the end.

Definition of Done (security)

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_LICENSE commands require explicit admin action (dual control by default) — never triggered directly by an automated heuristic with no human in the loop.
  • DeviceCommand scope is enforced in code (the WipeTargets interface), 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/DELETE on AuditLog from 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.balanceCents only ever changes inside the same DB transaction that inserts the corresponding WalletTransaction row.
  • 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.

There aren't any published security advisories