Files
notely/SECURITY.md
domrichardson df40cc57e1 first commit
2026-03-24 16:03:04 +00:00

7.7 KiB

Security Implementation Guide

This document outlines the security measures implemented in Notely.

🔐 Authentication Security

Password Hashing

  • Algorithm: Argon2id (memory-hard, resistant to GPU attacks)
  • Configuration:
    • Memory: 64 MB
    • Time: 1 iteration
    • Parallelism: 4 threads
    • Salt: 16 random bytes (cryptographically secure)
// Generated hash format:
$argon2id$v=19$m=65536,t=1,p=4$salt_hex$hash_hex

JWT Tokens

  • Algorithm: HS256 (HMAC-SHA256)
  • Access Token TTL: 1 hour
  • Refresh Token TTL: 7 days (HTTP-only secure cookie)
  • Claims:
    • user_id: User's MongoDB ObjectID
    • email: User's email address
    • username: User's username
    • iat: Issued at timestamp
    • exp: Expiration timestamp
    • iss: Issuer (verified against hardcoded value)

Brute-Force Protection

  • Track failed login attempts in login_attempts collection
  • Rate limit: Max 5 failed attempts per IP per 15 minutes
  • Account lockout: 15 minutes after 5 consecutive failures
  • Cleanup: Expired records auto-deleted via TTL index

🛡️ Authorization Security

Role-Based Access Control (RBAC)

Space Roles:
├── Owner (all permissions)
├── Editor (create/edit/delete notes)
└── Viewer (read-only)

Space-Level Data Isolation

ALL queries include mandatory space_id filter

// Correct query pattern:
db.notes.find({ space_id: spaceID, ... })

// Never allow:
db.notes.find({ user_id: userID })  // ❌ Cross-space leak possible

Middleware Authorization Flow

1. Extract JWT token → Verify signature & expiration
2. Load user credentials → Verify user is active
3. Check space membership → Verify user_id + space_id + role
4. Execute request → With space_id context

🔑 Data Encryption

At Rest

  • OAuth client secrets encrypted with AES-256-GCM
  • Stored in MongoDB with encryption key in environment variables
  • Decryption happens only when reading from database
plaintext, err := encryptor.Encrypt(clientSecret)  // Stores encrypted blob
recovered, err := encryptor.Decrypt(plaintext)     // Decrypts on retrieval

In Transit

  • HTTPS/TLS required in production (enforced via Nginx)
  • Secure cookies: Secure, HttpOnly, SameSite=Lax flags
  • All sensitive data transmitted over encrypted channels

🚨 Input Validation

Backend Validation (MANDATORY)

Every endpoint validates:

  1. Type validation - JSON schema validation
  2. Length limits - min/max string lengths
  3. Format validation - email, ObjectID, URL formats
  4. Range validation - pagination limits
type CreateNoteRequest struct {
    Title   string `validate:"required,min=1,max=255"`
    Content string `validate:"max=50000"`
    Tags    []string `validate:"max=100,dive,max=50"`
}

Frontend Validation

  • Input sanitization - trim whitespace
  • Format validation - regex patterns
  • Debounced searches - prevent query spam
  • Client-side feedback - improve UX

Output Sanitization

Markdown → HTML conversion sanitized with DOMPurify:

// XSS prevention
const dirty = marked.parse(userMarkdown);
const clean = DOMPurify.sanitize(dirty);

// Blocks: scripts, event handlers, dangerous attributes

🌐 Web Security Headers

Implemented via Nginx and Go middleware:

Header Value Purpose
Strict-Transport-Security max-age=31536000 Force HTTPS
X-Content-Type-Options nosniff Prevent MIME sniffing
X-Frame-Options DENY Prevent clickjacking
X-XSS-Protection 1; mode=block XSS protection (older browsers)
Content-Security-Policy Restrictive policy Prevent XSS attacks
Referrer-Policy strict-origin-when-cross-origin Referrer control

CSP Policy:

default-src 'self'
script-src 'self' 'unsafe-inline' (for development only)
style-src 'self' 'unsafe-inline'
img-src 'self' data: https:
font-src 'self'
connect-src 'self'
frame-ancestors 'none'

Access Token (via Authorization header)

  • Stored in memory (not localStorage)
  • Passed via Authorization: Bearer {token}
http.SetCookie(w, &http.Cookie{
    Name:     "refresh_token",
    Value:    token,
    Path:     "/",
    MaxAge:   7 * 24 * 60 * 60,  // 7 days
    HttpOnly: true,               // ✅ Cannot access from JavaScript
    Secure:   true,               // ✅ HTTPS only
    SameSite: http.SameSiteLaxMode, // ✅ CSRF protection
})

🔄 Rate Limiting

API Rate Limiting

  • General: 50 requests / second per IP
  • Login: 10 requests / second per IP
  • Burst allowance: 20 additional requests
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req zone=api_limit burst=20 nodelay;

Login Attempt Tracking

  • Track per email + IP combination
  • Maximum 5 attempts per 15 minutes
  • Exponential backoff on repeated failures

🔒 Database Security

MongoDB

  • Authentication: Username/password with role-based access
  • Network: Runs in secure Docker network (not exposed)
  • Admin credentials: Stored in Kubernetes Secrets (not in code)
  • Backups: TBD - use MongoDB Atlas or encrypted backups

Connection String

mongodb://admin:password@mongodb:27017/dbname?authSource=admin

🚨 Logging & Monitoring

Security Events Logged

  • User registration attempts
  • Login attempts (success/failure)
  • Authorization failures
  • Permission denied events
  • Sensitive data access

Data NOT logged

  • Passwords/hashes
  • JWT tokens
  • Encryption keys
  • OAuth secrets

🧪 Security Testing

What to Test

  1. Authentication: Register, login, token refresh, logout
  2. Authorization: RBAC enforcement, space isolation
  3. Input validation: Invalid data rejection
  4. XSS prevention: Markdown sanitization
  5. CSRF protection: Token validation
  6. Rate limiting: Too many requests blocked
  7. SQL Injection: MongoDB-specific (parameterized queries safe)

Manual Testing Commands

# Test invalid input
curl -X POST http://localhost:8080/api/v1/auth/login \
  -d '{"email":"not-an-email","password":""}'

# Test expired token
curl -H "Authorization: Bearer expired.token.here" \
  http://localhost:8080/api/v1/spaces

# Test rate limiting
for i in {1..100}; do
  curl http://localhost:8080/api/v1/auth/login &
done

🛠️ Production Checklist

  • Change default JWT_SECRET (min 32 characters)
  • Change default ENCRYPTION_KEY (32 bytes)
  • Generate TLS certificates (Let's Encrypt recommended)
  • Configure Nginx SSL/TLS
  • Enable HTTPS redirect
  • Set up database backups
  • Configure logging & monitoring
  • Implement CORS whitelist (specific domains)
  • Set up rate limiting (tuned to your traffic)
  • Enable database authentication
  • Use Kubernetes Network Policies
  • Set up Pod Security Policies
  • Enable audit logging
  • Configure Secrets encryption at rest

📚 References


Last Updated: March 2026 Security Level: Production-Grade