Skip to content

Security: puretechteam/nous

Security

docs/SECURITY.md

Security Policy — Nous

Nous is designed against the McCumber Cube: a three-dimensional risk model whose axes are security goals (Confidentiality, Integrity, Availability), information states (Storage / Transmission / Processing) and safeguard classes (Technology / Policy / People). Controls are placed at the intersection of these axes. This document is the Wave 4 McCumber Cube compliance record for the API surface (src/Nous.Api).

Related: ../README.md carries the living system notes; PRIVACY.md and CONTENT_POLICY.md cover the data and community dimensions.


1. McCumber Cube compliance matrix

Each cell lists the safeguard technology, policy and people/education control(s) that address a (goal × state) intersection. Cells marked n/a are not applicable because no asset in that state is exposed to that goal in a way that is mitigated by a distinct control.

Storage (at rest)

Security goal Technology Policy People / Education
Confidentiality Passwords hashed with Argon2id (P=1, m=19 MiB, t=2, 16-byte salt, 32-byte hash). TOTP shared secrets encrypted at rest with AES-256-GCM (12-byte nonce + 16-byte tag) keyed from Security:MasterKey. Refresh/reset tokens never stored — only a SHA-256 hash (TokenHash) is persisted. Jwt:Key, Security:MasterKey and DB credentials sourced from configuration/environment, never source control. Secrets rotated on a schedule; key material reviewed in code review; no plaintext credentials in source or logs. Engineers trained on secret management (OS/env-var/user-secrets); mandatory rotation cadence.
Integrity EF Core unique indexes (Users.Email, Posts.Slug, PostVotes.Save/(UserId,PostId)) prevent duplicate/corrupt writes. AuditLog is append-only. Column MaxLength attributes enforce field bounds at the model layer. Write-path code review requires integrity tests; migrations reviewed before deploy. Data-quality owner assigned per feature; incident triage runbook.
Availability SQLite EnsureCreated for local dev only; production uses PostgreSQL with volume + backup configuration. Database:AutoInitialize=false in tests. Backup/restore runbook; point-in-time recovery tested monthly. On-call rotation documented; RTO/RPO defined in incident playbook.

Transmission (in transit)

Security goal Technology Policy People / Education
Confidentiality app.UseHttpsRedirection() + app.UseHsts() outside Development enforces HTTPS. AddJwtBearer sets RequireHttpsMetadata = !IsDevelopment(). Security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, default-src 'self') set on every response. TLS 1.3 is enforced at the host/reverse-proxy layer. TLS 1.2 minimum enforced by infrastructure; TLS 1.3 preferred everywhere. Mixed-content blocked. TLS certificate lifecycle owned by infra; expiry alerts configured.
Integrity JWTs signed HS256 with Jwt:Key; TokenValidationParameters validate issuer, audience, lifetime and signing key; 30s clock skew tolerance; jti claim enables replay tracking. JWT lifetime ≤ 30 min; refresh token rotation; signing key ≥ 32 bytes. Token-validation review checklist in PR template.
Availability Rate limiting: auth endpoints capped at 20 req/min (shared); global per-IP limiter (anonymous 60/min, authenticated 120/min). Centralized UseExceptionHandler + ProblemDetails returns bounded error payloads. Abuse threshold escalation to IP/block; monitoring for 429 spikes. SRE on-call alerted on sustained 429 rate.

Processing (in use)

Security goal Technology Policy People / Education
Confidentiality Role-based authorization — RequireAuthorization() on posts/media/reports; RequireAuthorization("Admin") on /api/v1/admin/*; health check AllowAnonymous. JWT sub claim identifies the user on protected endpoints. Least-privilege: every protected endpoint verifies identity; admin endpoints double-checked in review. Admin access granted via documented onboarding; 2FA required for admin roles.
Integrity Server-side validation on every DTO: ContentService.CreatePostAsync enforces Title ≤ 300 chars and non-empty Body; AuthService.RegisterAsync validates email format and password length 12–128; ResetPasswordAsync validates the new password length; entity MaxLength constraints backstop EF writes. Every input validated server-side regardless of client; no DTO trusted blindly. Secure coding training covers input validation; QA runs fuzz tests.
Availability Health endpoint (/api/v1/health) for liveness; scoped DbContext per operation; in-memory failed-login lockout (5 attempts → 15 min). Rate-limit and lockout response recorded to AuditLog. Abuse patterns reviewed weekly by SRE.

2. Cryptographic inventory

Asset Algorithm / scheme Where At-rest form
Passwords Argon2id (P=1, m=19456 KiB, t=2, 16B salt → 32B hash) PasswordHasher.cs argon2id$... string
TOTP shared secret AES-256-GCM (12B nonce + 16B tag) CryptoHelper.EncryptAtRest base64(nonce‖ciphertext‖tag) in User.TotpSecretEnc; key = Security:MasterKey
TOTP codes HMAC-SHA1, 30s step, 6 digits (RFC 6238) TotpService.cs (Otp.NET) n/a (ephemeral)
TOTP recovery codes 8 codes × 8 random bytes → 16 hex chars AuthService.EnrollTotpAsync each stored as CodeHash = SHA-256 of the plaintext code
Access token JWT, HS256, 30 min, issuer/audience/lifetime validated JwtTokenService.cs opaque to client; never persisted by the API
Refresh token 64 random bytes, 14-day expiry, rotated on use, reuse chain tracked JwtTokenService + AuthService.RefreshAsync stored as TokenHash = SHA-256 of the raw token
Password-reset token 32 random bytes hex, 1-hour expiry, single-use, all pending invalidated AuthService.ForgotPasswordAsync / ResetPasswordAsync stored as TokenHash = SHA-256 of the raw token

3. Transport & host security

  • HTTPS everywhere outside Development. app.UseHsts() runs before app.UseHttpsRedirection() in production so the HSTS header is emitted on the redirect response. JWT bearer validation also requires HTTPS (RequireHttpsMetadata = !IsDevelopment()).
  • TLS 1.3 at the host. The API is deployed behind a TLS-terminating reverse proxy that enforces TLS 1.3 (TLS 1.2 as a minimum fallback). The proxy must set X-Forwarded-Proto and the app enabled UseForwardedHeaders in production so URL generation and redirection are scheme-correct.
  • Security response headers (set on all responses, including errors): X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, Content-Security-Policy: default-src 'self'.
  • CORS. Production deployments must configure App:CorsOrigins as an explicit allow-list. The AllowAnyOrigin fallback is Development-only (see Program.cs).

4. Rate limiting & abuse controls

Surface Limiter Limit Key
/api/v1/auth/* auth named policy 20 req/min shared bucket (per-host)
All other routes GlobalLimiter (Program.cs) 60 req/min anonymous / 120 req/min authed per-IP (anonymous vs authenticated buckets)
Failed logins in-memory lockout 5 attempts → 15 min throttle per normalized email

Rejected requests return HTTP 429 (StatusCodes.Status429TooManyRequests).


5. CSRF

CSRF is not a material risk for the current threat model: bearer JWTs are delivered in the Authorization header (chosen at registration in Program.csJwtBearerDefaults), not in cookies, and the API does not read state-changing intent from ambient credentials. Cookie-based auth would require CSRF tokens; that path is not used today.


6. Secret management

  • Jwt:Key (≥ 32 bytes, HS256 signing key), Security:MasterKey (base64 32-byte AES key) and Database:Password are supplied via environment variables / user-secrets — never committed (.gitignore excludes appsettings.Local.json, .env*, *.key, *.pem).
  • Outside Development the app fails fast if Jwt:Key is blank.
  • In Development, an ephemeral dev-only signing key is generated so the app still boots; it must never reach production.

7. Incident response

  1. Detect — 429 spikes, failed-login lockouts, AuditLog anomalies (auth.login.failure, auth.refresh.reuse_detected, auth.password_reset.*) are streamed to the observability sink.
  2. Triage — the on-call engineer triages within 15 min (SRE paging).
  3. Contain — compromised signing keys are rotated via environment variable + app restart; Security:MasterKey rotation re-encrypts TOTP secrets on next user TOTP re-enrollment.
  4. Eradicate — leaked refresh/reset tokens are rejected because only SHA-256 hashes are stored; the raw token is shown to the client exactly once.
  5. Recover — rollback the offending deployment, restore DB from the last known-good backup (PostgreSQL point-in-time), and re-validate integrity via the AuditLog append-only trail.
  6. Post-incident — blameless 5-whys; action items tracked and re-tested within one sprint.

Contact: security@puretech.team — report suspected vulnerabilities via the private security channel (GPG preferred) per the project's Responsible Disclosure Policy.

There aren't any published security advisories