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.mdcarries the living system notes;PRIVACY.mdandCONTENT_POLICY.mdcover the data and community dimensions.
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.
| 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. |
| 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. |
| 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. |
| 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 |
- HTTPS everywhere outside Development.
app.UseHsts()runs beforeapp.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-Protoand the app enabledUseForwardedHeadersin 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:CorsOriginsas an explicit allow-list. TheAllowAnyOriginfallback is Development-only (seeProgram.cs).
| 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).
CSRF is not a material risk for the current threat model: bearer JWTs are delivered in the
Authorization header (chosen at registration in Program.cs → JwtBearerDefaults), 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.
Jwt:Key(≥ 32 bytes, HS256 signing key),Security:MasterKey(base64 32-byte AES key) andDatabase:Passwordare supplied via environment variables / user-secrets — never committed (.gitignoreexcludesappsettings.Local.json,.env*,*.key,*.pem).- Outside Development the app fails fast if
Jwt:Keyis blank. - In Development, an ephemeral dev-only signing key is generated so the app still boots; it must never reach production.
- Detect — 429 spikes, failed-login lockouts,
AuditLoganomalies (auth.login.failure,auth.refresh.reuse_detected,auth.password_reset.*) are streamed to the observability sink. - Triage — the on-call engineer triages within 15 min (SRE paging).
- Contain — compromised signing keys are rotated via environment variable + app restart;
Security:MasterKeyrotation re-encrypts TOTP secrets on next user TOTP re-enrollment. - Eradicate — leaked refresh/reset tokens are rejected because only SHA-256 hashes are stored; the raw token is shown to the client exactly once.
- Recover — rollback the offending deployment, restore DB from the last known-good backup
(PostgreSQL point-in-time), and re-validate integrity via the
AuditLogappend-only trail. - 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.