We take security seriously. If you discover a security vulnerability in Client St0r, please report it responsibly.
Preferred Method:
- Use GitHub's Security Advisories: Report a vulnerability
Alternative Contact:
- Email: Create an issue on GitHub with the label "security" (do not include sensitive details in public issues)
Please provide as much information as possible:
- Type of vulnerability (e.g., XSS, SQL injection, authentication bypass)
- Step-by-step instructions to reproduce the issue
- Affected versions
- Potential impact and severity assessment
- Proof-of-concept or exploit code (if applicable)
- Suggested fix (if you have one)
- Initial Response: Within 48 hours
- Status Update: Within 7 days
- Fix Timeline: Critical issues within 30 days, others within 90 days
- Public Disclosure: After patch is released and users have reasonable time to update (typically 30 days)
| Version | Supported |
|---|---|
| 2.24.x | ✅ |
| 2.23.x | ✅ |
| < 2.23 | ❌ |
We provide security updates for the current major version and one previous major version. Please update to the latest version to receive security patches.
- Responsible Disclosure: We request 90 days before public disclosure
- Credit: We will credit researchers in release notes (unless you prefer to remain anonymous)
- CVE Assignment: For critical vulnerabilities, we will request CVE assignment
- Security Advisories: Published on GitHub Security Advisories
Version: 2.24.148 Last Updated: 2026-01-18 Architecture: Django 6.0.1 + DRF + Gunicorn + MariaDB + Anthropic AI
Client St0r implements defense-in-depth security with multiple layers of protection based on OWASP best practices, Django security guidelines, and enterprise SaaS security requirements.
- Reporting Security Vulnerabilities
- Security Features
- Production Security Checklist
- Environment Configuration
- Tenant Isolation
- API Security
- AI Endpoint Protection
- Secrets Management
- Security Headers
- Rate Limiting & Throttling
- Authentication & Authorization
- Monitoring & Auditing
- Incident Response
- Phase 2: Advanced Security
- Password Hashing: Argon2 (OWASP recommended, resists GPU attacks)
- 2FA: Mandatory TOTP via django-otp (Google Authenticator, Authy, etc.)
- SSO: Azure AD / Microsoft Entra ID OAuth integration
- Brute Force Protection: django-axes (5 attempts, 1-hour lockout)
- Multi-Tenancy: Organization-based with automated isolation tests
- Session Management: Secure cookies with HttpOnly, Secure, SameSite=Lax
- Encryption: AES-256-GCM via Fernet for all secrets
- Key Derivation: PBKDF2-SHA256 with 100,000 iterations
- Encrypted Fields: Passwords, API keys, OAuth tokens, credentials
- Key Rotation: Automated utilities for re-encrypting all secrets
- Separate Keys: Different keys per environment (dev/staging/prod)
- Production Mode: Browsable API disabled, JSON-only renderers
- Throttling: 6 granular scopes (anon, user, login, password_reset, token, AI)
- Authentication: API key + session-based
- Permissions: IsAuthenticated by default
- Schema/Docs: Require authentication
- CSP: Strict Content Security Policy with 10+ directives
- HSTS: 1-year max-age (31536000 seconds) in production
- Clickjacking: X-Frame-Options: DENY
- MIME Sniffing: X-Content-Type-Options: nosniff
- Referrer Policy: strict-origin-when-cross-origin
- Permissions-Policy: Disables geolocation, camera, microphone, payment, USB, FLoC
- CSRF: Multi-domain support with trusted origins
- XSS Protection: CSP + secure cookies + template escaping
- Request Limits: Per-user (100/day) and per-org (1000/day)
- Spend Caps: Per-user ($10/day) and per-org ($100/day)
- Burst Protection: 10 requests/minute
- Size Limits: 10,000 characters per prompt
- PII Redaction: Emails, phones, SSNs, credit cards, API keys
- Usage Tracking: Redis-backed counters with 24-hour TTL
- Audit Logging: All AI requests logged
- Password Breach Detection: HaveIBeenPwned k-anonymity integration
- Vulnerability Scanning: Snyk for Python/JS dependencies
- Tenant Isolation Tests: Automated test suite (11 test cases)
- Audit Trail: Comprehensive logging of security events
# ===== CRITICAL SECRETS =====
# Django secret key (unique per environment, 50+ characters)
SECRET_KEY=<generate-with-django-command>
# Encryption master key (for vault passwords, credentials)
APP_MASTER_KEY=<generate-with-secrets-command>
# API key signing secret (MUST differ from SECRET_KEY)
API_KEY_SECRET=<generate-separate-secret>
# Database password (strong, unique)
DB_PASSWORD=<strong-password>
# Anthropic API key (if using AI features)
ANTHROPIC_API_KEY=sk-ant-...
# ===== SECURITY SETTINGS =====
DEBUG=False
SECURE_SSL_REDIRECT=True
SECURE_HSTS_SECONDS=31536000
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=True
REQUIRE_2FA=True
# ===== ALLOWED HOSTS =====
ALLOWED_HOSTS=clientst0r.example.com,www.clientst0r.example.com
CSRF_TRUSTED_ORIGINS=https://clientst0r.example.com,https://www.clientst0r.example.com
# ===== AI LIMITS =====
AI_MAX_DAILY_REQUESTS_PER_USER=100
AI_MAX_DAILY_REQUESTS_PER_ORG=1000
AI_MAX_DAILY_SPEND_PER_USER=10.00
AI_MAX_DAILY_SPEND_PER_ORG=100.00
# ===== BRUTE FORCE PROTECTION =====
AXES_FAILURE_LIMIT=5
AXES_COOLOFF_TIME=1# SECRET_KEY (Django)
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
# APP_MASTER_KEY (encryption)
python manage.py secrets generate-key
# API_KEY_SECRET (must differ from SECRET_KEY)
python -c "import secrets; print(secrets.token_urlsafe(32))"# Check all secrets are configured correctly
python manage.py secrets validate
# Expected output: "All secrets configured correctly"# Tenant isolation tests (CRITICAL - must pass)
python manage.py test core.tests.test_tenant_isolation -v 2
# All 11 tests must pass before production deployment- Reverse proxy (nginx/caddy) terminates SSL
- Valid SSL certificate (Let's Encrypt or commercial)
- HTTPS-only (HTTP redirects to HTTPS)
- Database requires TLS (if remote)
- Database backups enabled and tested (daily + retention)
- Firewall rules limit access (SSH, database, application ports only)
- Fail2ban or similar for SSH brute force protection
- Log rotation configured (logrotate)
- Monitoring/alerting configured (Prometheus, Grafana, etc.)
- Admin username changed from default
- All users have 2FA enabled (enforce via REQUIRE_2FA=True)
- Unnecessary endpoints disabled
- Rate limits appropriate for traffic patterns
- AI spend limits appropriate for budget
- Snyk scans running and reviewed
- Audit logs reviewed for anomalies
# Week 1: Test with short duration
SECURE_HSTS_SECONDS=300 # 5 minutes
# Week 2: Increase
SECURE_HSTS_SECONDS=86400 # 1 day
# Month 1: Increase
SECURE_HSTS_SECONDS=2592000 # 30 days
# Month 6+: Final (1 year)
SECURE_HSTS_SECONDS=31536000 # 1 year
# Optional: HSTS Preload (IRREVERSIBLE - be 100% sure)
SECURE_HSTS_PRELOAD=True
# Submit to: https://hstspreload.org/# Verify all headers present
curl -I https://your-domain.com
# Online scanner (should score A or A+)
https://securityheaders.com/?q=your-domain.comExpected headers:
Strict-Transport-Security: max-age=31536000; includeSubDomainsContent-Security-Policy: default-src 'self'; ...X-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: geolocation=(), microphone=(), camera=(), ...
- Open browser DevTools → Console
- No CSP violations should appear
- If violations occur, adjust CSP in settings.py
# Test SSL configuration (should be A or A+)
https://www.ssllabs.com/ssltest/analyze.html?d=your-domain.com# Run automated tests
python manage.py test core.tests.test_tenant_isolation -v 2
# Manual verification:
# 1. Create two test organizations
# 2. Create user in each org
# 3. Create data (passwords, assets) for each org
# 4. Login as User A, try to access User B's data via URL manipulation
# 5. Verify 403/404 (never 200 with wrong org's data)# Check AI usage stats in Django admin or via API
from core.ai_abuse_control import get_ai_usage_stats
stats = get_ai_usage_stats(user=user, organization=org)
print(stats)# ========================================
# DJANGO CORE
# ========================================
SECRET_KEY=<unique-50+-character-string>
DEBUG=False
ALLOWED_HOSTS=clientst0r.example.com
CSRF_TRUSTED_ORIGINS=https://clientst0r.example.com
# ========================================
# DATABASE (MariaDB)
# ========================================
DB_ENGINE=mysql
DB_NAME=clientst0r
DB_USER=clientst0r_user
DB_PASSWORD=<strong-password>
DB_HOST=localhost
DB_PORT=3306
# Optional: Enable TLS for remote database
DB_TLS_ENABLED=True
DB_TLS_CA=/path/to/ca-cert.pem
# ========================================
# ENCRYPTION & SECRETS
# ========================================
# Master key for encrypting vault passwords, credentials, tokens
APP_MASTER_KEY=<unique-key-for-encryption>
# API key signing secret (MUST differ from SECRET_KEY)
API_KEY_SECRET=<unique-key-different-from-SECRET_KEY>
# ========================================
# SECURITY
# ========================================
SECURE_SSL_REDIRECT=True
SECURE_HSTS_SECONDS=31536000
SECURE_HSTS_PRELOAD=False # Set to True only after 1+ year of HSTS
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=True
REQUIRE_2FA=True
# ========================================
# BRUTE FORCE PROTECTION (django-axes)
# ========================================
AXES_FAILURE_LIMIT=5
AXES_COOLOFF_TIME=1 # hours
# ========================================
# AI (Anthropic)
# ========================================
ANTHROPIC_API_KEY=sk-ant-...
CLAUDE_MODEL=claude-sonnet-4-5-20250929
# AI Abuse Controls
AI_MAX_PROMPT_LENGTH=10000
AI_MAX_DAILY_REQUESTS_PER_USER=100
AI_MAX_DAILY_REQUESTS_PER_ORG=1000
AI_MAX_DAILY_SPEND_PER_USER=10.00
AI_MAX_DAILY_SPEND_PER_ORG=100.00
AI_PII_REDACTION_ENABLED=True
# ========================================
# PASSWORD BREACH CHECKING (HaveIBeenPwned)
# ========================================
HIBP_ENABLED=True
HIBP_API_KEY= # Optional, increases rate limit
HIBP_CHECK_ON_SAVE=True
HIBP_BLOCK_BREACHED=False # Set to True to block breached passwords
# ========================================
# EXTERNAL APIS (Optional)
# ========================================
GOOGLE_MAPS_API_KEY=
MAPBOX_ACCESS_TOKEN=
BING_MAPS_API_KEY=# Development (.env.dev)
DEBUG=True
SECRET_KEY=dev-key-insecure
SECURE_SSL_REDIRECT=False
SECURE_HSTS_SECONDS=0
SESSION_COOKIE_SECURE=False
CSRF_COOKIE_SECURE=False
REQUIRE_2FA=False
# Production (.env.prod)
DEBUG=False
SECRET_KEY=<actual-secret-50+-chars>
SECURE_SSL_REDIRECT=True
SECURE_HSTS_SECONDS=31536000
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=True
REQUIRE_2FA=TrueClient St0r uses single-database multi-tenancy with organization_id filtering:
┌─────────────────────────────────────────────┐
│ Single Database (MariaDB) │
├─────────────────────────────────────────────┤
│ All Models: │
│ - organization_id (ForeignKey) │
│ - Filtered by OrganizationManager │
├─────────────────────────────────────────────┤
│ Org 1 Data │ Org 2 Data │ Org 3 Data │
│ (filtered) │ (filtered) │ (filtered) │
└─────────────────────────────────────────────┘
↑ ↑ ↑
│ │ │
User A User B User C
(Org 1) (Org 2) (Org 3)
Key Components:
- BaseModel: All models inherit, adds
organizationForeignKey - OrganizationManager: Custom manager that auto-filters by
organization_id - CurrentOrganizationMiddleware: Sets organization context from authenticated user
- Object-Level Permissions: All views check
organizationbefore allowing access
✅ Users CANNOT:
- Access passwords from other organizations
- Access assets from other organizations
- Access documents, tickets, contacts from other organizations
- See audit logs from other organizations
- Make API requests for other organizations' data
✅ Enforced By:
- Database-level filtering (OrganizationManager)
- View-level permissions (organization check)
- API-level filtering (DRF permissions)
- Automated test suite (11 test cases)
# Run full test suite
python manage.py test core.tests.test_tenant_isolation -v 2
# Tests include:
# 1. Password isolation (can't query other org's passwords)
# 2. Asset isolation (can't query other org's assets)
# 3. Document isolation
# 4. Audit log isolation
# 5. Cross-tenant API access (403/404 on other org's data)
# 6. Bulk operations respect boundaries
# 7. OrganizationManager filtering
# 8. Foreign key relationships
# 9. API list endpoints isolation
# 10. API detail endpoints isolation
# 11. Manager enforcement# Create two organizations
org1 = Organization.objects.create(name='Org 1', slug='org1')
org2 = Organization.objects.create(name='Org 2', slug='org2')
# Create data for each
password1 = Password.objects.create(organization=org1, title='Org1 Secret')
password2 = Password.objects.create(organization=org2, title='Org2 Secret')
# Verify isolation
assert Password.objects.filter(organization=org1).count() == 1
assert Password.objects.filter(organization=org2).count() == 1
# Cross-org query returns empty
assert Password.objects.filter(organization=org1, id=password2.id).count() == 0# config/settings.py
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
] if not DEBUG else [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer', # Only in dev
],
}Why: DRF's browsable API has had XSS vulnerabilities (CVE fixed in 3.15.2). Disabling in production reduces attack surface.
| Scope | Rate Limit | Purpose | Applied To |
|---|---|---|---|
anon |
50/hour | Per-IP limit for unauthenticated users | All API endpoints |
user |
1000/hour | Per-user limit for authenticated users | All API endpoints |
login |
10/hour | Brute force protection | Login endpoint |
password_reset |
5/hour | Abuse prevention | Password reset endpoint |
token |
20/hour | API key operation limits | Token create/refresh/revoke |
ai_request |
100/day | Cost protection (daily) | AI endpoints |
ai_burst |
10/minute | Cost protection (burst) | AI endpoints |
# In your view
from api.throttles import LoginThrottle, AIRequestThrottle, AIBurstThrottle
class LoginView(APIView):
throttle_classes = [LoginThrottle]
class FloorplanGenerateView(APIView):
throttle_classes = [AIRequestThrottle, AIBurstThrottle]# Generate API key for user
from api.models import APIKey
api_key = APIKey.objects.create(
user=request.user,
name='My API Key',
organization=request.organization
)
print(f'Key: {api_key.key}') # Only shown once
# Use API key
curl -H "Authorization: Api-Key YOUR_KEY_HERE" https://api.example.com/passwords/Security Notes:
- API keys signed with
API_KEY_SECRET(separate fromSECRET_KEY) - Keys can be revoked without changing user password
- Rate-limited via DRF throttles
- Audit logged
AI_MAX_DAILY_REQUESTS_PER_USER=100
AI_MAX_DAILY_REQUESTS_PER_ORG=1000How It Works:
- Redis-backed counters with 24-hour TTL
- Middleware checks before processing request
- Returns 429 with reset time when exceeded
Error Response:
{
"error": "Daily AI request limit exceeded",
"limit": 100,
"used": 100,
"reset_in_hours": 18.5
}AI_MAX_DAILY_SPEND_PER_USER=10.00 # USD
AI_MAX_DAILY_SPEND_PER_ORG=100.00 # USDHow It Works:
- Track estimated cost per request
- Accumulate daily spend in Redis
- Block when cap exceeded
AI_BURST_LIMIT=10 # requests per minuteHow It Works:
- Prevents rapid-fire API abuse
- DRF throttle with 1-minute window
- Separate from daily limit
AI_PII_REDACTION_ENABLED=TruePatterns Detected:
- Emails:
user@example.com→[REDACTED_EMAIL] - Phones:
555-123-4567→[REDACTED_PHONE] - SSNs:
123-45-6789→[REDACTED_SSN] - Credit Cards:
4111-1111-1111-1111→[REDACTED_CARD] - API Keys: Long alphanumeric →
[REDACTED_KEY]
Usage:
from core.ai_abuse_control import PIIRedactor
# Redact before sending to AI
prompt = "Contact John at john@example.com or call 555-123-4567"
safe_prompt = PIIRedactor.redact(prompt)
# Result: "Contact John at [REDACTED_EMAIL] or call [REDACTED_PHONE]"
# Check for PII
pii_check = PIIRedactor.check_for_pii(prompt)
# Result: {'has_email': True, 'has_phone': True, 'has_ssn': False, ...}from core.ai_abuse_control import get_ai_usage_stats
# Get current usage
stats = get_ai_usage_stats(
user=request.user,
organization=request.organization
)
# Returns:
{
'user': {
'requests_used': 15,
'requests_limit': 100,
'spend_used': 2.50,
'spend_limit': 10.00
},
'organization': {
'requests_used': 250,
'requests_limit': 1000,
'spend_used': 45.00,
'spend_limit': 100.00
}
}# core/ai_abuse_control.py
class AIAbuseControlMiddleware:
def __init__(self, get_response):
self.ai_endpoints = [
'/locations/generate-floorplan/',
'/api/ai/',
]Add your AI endpoints to this list to enable protection.
Environment Variable
↓
APP_MASTER_KEY (plaintext in env)
↓
PBKDF2-SHA256 (100,000 iterations)
↓
Derived 32-byte Key
↓
Fernet Cipher (AES-256-GCM)
↓
Encrypted Secrets (database)
- ✅ Vault passwords (vault.Password.encrypted_password)
- ✅ PSA connection credentials (integrations.PSAConnection.encrypted_credentials)
- ✅ RMM connection credentials (integrations.RMMConnection.encrypted_credentials)
- ✅ OAuth tokens (stored in credentials JSON)
- ✅ API keys from external services (stored in credentials JSON)
- ✅ Webhook secrets (if configured)
# Generate new master key
python manage.py secrets generate-key
# Output: Base64-encoded 256-bit key
# Example: gAAAAABhkj3KLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456=
# Set as environment variable
export APP_MASTER_KEY='gAAAAABhkj3...'When to Rotate:
- Annually (scheduled maintenance)
- After suspected key compromise
- When employee with key access leaves
- After security incident
How to Rotate:
# Step 1: Generate new key
NEW_KEY=$(python manage.py secrets generate-key)
echo "New key: $NEW_KEY"
# Step 2: Rotate all secrets (re-encrypt with new key)
python manage.py secrets rotate \
--old-key "$APP_MASTER_KEY" \
--new-key "$NEW_KEY"
# Output:
# Rotating secrets...
# Rotation complete: 156 secrets updated
# PSAConnections: 12 updated
# RMMConnections: 8 updated
# Passwords: 136 updated
# Step 3: Update environment variable
export APP_MASTER_KEY="$NEW_KEY"
# Step 4: Update .env file or secret manager
echo "APP_MASTER_KEY=$NEW_KEY" >> .env.prod
# Step 5: Restart application
systemctl restart clientst0r-gunicorn
# Step 6: Verify
python manage.py secrets validate
# Output: All secrets configured correctly# Check configuration
python manage.py secrets validate
# Checks:
# ✓ APP_MASTER_KEY is set
# ✓ APP_MASTER_KEY can encrypt/decrypt
# ✓ API_KEY_SECRET is set and differs from SECRET_KEY
# ✓ SECRET_KEY is not using insecure default
# ✓ ANTHROPIC_API_KEY is set (if using AI)
# Example output (healthy):
All secrets configured correctly
# Example output (errors):
Validation failed:
- APP_MASTER_KEY not set in production
- SECRET_KEY and API_KEY_SECRET must be differentfrom core.secrets_management import sanitize_log_data
# Remove secrets before logging
data = {
'username': 'admin',
'password': 'secret123',
'api_key': 'sk-ant-...',
'email': 'user@example.com'
}
safe_data = sanitize_log_data(data)
# Result:
# {
# 'username': 'admin',
# 'password': '[REDACTED]',
# 'api_key': '[REDACTED]',
# 'email': 'user@example.com'
# }
logger.info(f'User data: {safe_data}')Sensitive Keys (Auto-Redacted):
- password, secret, token, key
- api_key, api_secret, client_secret
- access_token, refresh_token, private_key
- app_master_key, db_password, anthropic_api_key
Purpose: Force HTTPS for all connections
Configuration:
SECURE_HSTS_SECONDS=31536000 # 1 year (production)
SECURE_HSTS_INCLUDE_SUBDOMAINS=True
SECURE_HSTS_PRELOAD=False # Optional, irreversibleResponse Header:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Rollout Plan:
# Week 1: Test
SECURE_HSTS_SECONDS=300 # 5 minutes
# Week 2
SECURE_HSTS_SECONDS=86400 # 1 day
# Month 1
SECURE_HSTS_SECONDS=2592000 # 30 days
# Month 6+: Production
SECURE_HSTS_SECONDS=31536000 # 1 yearHSTS Preload (Optional, Advanced):
- Irreversible: Once in preload list, can't be removed for months
- Requirements: 1+ year max-age, includeSubDomains, preload directive
- Submit: https://hstspreload.org/
- Only enable if 100% sure HTTPS will work forever
Purpose: Prevent XSS, clickjacking, code injection
Configuration:
# config/settings.py
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "cdn.jsdelivr.net")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "cdn.jsdelivr.net")
CSP_FONT_SRC = ("'self'", "data:", "cdn.jsdelivr.net")
CSP_FRAME_SRC = ("'self'", "https://embed.diagrams.net")
CSP_FRAME_ANCESTORS = ("'none'",) # Clickjacking protection
CSP_CONNECT_SRC = ("'self'", "https://embed.diagrams.net")
CSP_IMG_SRC = ("'self'", "data:", "https://api.qrserver.com")
CSP_OBJECT_SRC = ("'none'",) # Block plugins
CSP_BASE_URI = ("'self'",)
CSP_FORM_ACTION = ("'self'",)
CSP_UPGRADE_INSECURE_REQUESTS = True # Upgrade HTTP → HTTPSResponse Header:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; ...
Future Improvement (Remove unsafe-inline):
# Use nonces for inline scripts
CSP_SCRIPT_SRC = ("'self'", "'nonce-{nonce}'", "cdn.jsdelivr.net")
# In template:
<script nonce="{{ request.csp_nonce }}">
// Inline JS
</script>Purpose: Disable unnecessary browser features
Configuration:
PERMISSIONS_POLICY = {
'geolocation': [], # Disable geolocation
'microphone': [], # Disable microphone
'camera': [], # Disable camera
'payment': [], # Disable payment API
'usb': [], # Disable USB access
'interest-cohort': [], # Disable FLoC (privacy)
}Response Header:
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=(), interest-cohort=()
If You Need Features:
PERMISSIONS_POLICY = {
'geolocation': ['self'], # Allow geolocation on same origin
'camera': ['self', 'https://trusted-video-site.com'], # Allow camera
}Purpose: Control what URL information is leaked in Referer header
Configuration:
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'Options:
no-referrer: Never send referrersame-origin: Send only for same-origin requestsstrict-origin: Send only origin (no path) for HTTPS → HTTPSstrict-origin-when-cross-origin: Full URL for same-origin, origin only for cross-origin (RECOMMENDED)
Purpose: Prevent clickjacking
Configuration:
X_FRAME_OPTIONS = 'DENY'Options:
DENY: Cannot be framed (recommended)SAMEORIGIN: Can only be framed by same originALLOW-FROM uri: Can be framed by specific URI (deprecated, use CSP frame-ancestors)
Purpose: Prevent MIME sniffing
Configuration:
SECURE_CONTENT_TYPE_NOSNIFF = TrueResponse Header:
X-Content-Type-Options: nosniff
# config/settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '50/hour', # Anonymous (per-IP)
'user': '1000/hour', # Authenticated users
'login': '10/hour', # Login attempts
'password_reset': '5/hour', # Password reset
'token': '20/hour', # API tokens
'ai_request': '100/day', # AI requests (daily)
'ai_burst': '10/minute', # AI requests (burst)
}
}# api/throttles.py
from rest_framework.throttling import UserRateThrottle
class LoginThrottle(UserRateThrottle):
scope = 'login'
class AIRequestThrottle(UserRateThrottle):
scope = 'ai_request'# config/settings.py
AXES_FAILURE_LIMIT = 5 # Lock after 5 failed attempts
AXES_COOLOFF_TIME = 1 # Lock for 1 hour
AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP = True
AXES_RESET_ON_SUCCESS = TrueHow It Works:
- Tracks failed login attempts per username + IP combo
- Locks account for 1 hour after 5 failures
- Resets counter on successful login
- Stored in database (persistent across restarts)
View Locked Accounts:
# Django admin → Axes → Access attempts
# Or command line:
python manage.py axes_reset # Clear all locks
python manage.py axes_reset_user username # Clear specific userConfiguration:
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 12}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]Requirements:
- ✅ Minimum 12 characters
- ✅ Not similar to username/email/first name/last name
- ✅ Not in common password list (10,000+ passwords)
- ✅ Not entirely numeric
- ✅ Argon2 hashing (OWASP recommended)
Configuration:
REQUIRE_2FA = True # Enforce for all users
TWO_FACTOR_PATCH_ADMIN = False
LOGIN_URL = 'two_factor:login'Setup Process:
- User scans QR code with authenticator app (Google Authenticator, Authy, etc.)
- Enters 6-digit code to verify
- Backup codes generated (10 single-use codes)
- 2FA required on every login
Disable 2FA (Admin Action):
# In Django admin or management command
from two_factor.models import Device
# Remove user's device
Device.objects.filter(user=user).delete()
# User must re-setup 2FA on next loginFeatures:
- Separate from user passwords
- Can be revoked without changing password
- Rate-limited via DRF throttles
- Audit logged
- Named (e.g., "Mobile App", "CI/CD")
Generate:
from api.models import APIKey
api_key = APIKey.objects.create(
user=request.user,
name='My App',
organization=request.organization
)
print(f'Key: {api_key.key}') # Only shown once, hash storedUse:
curl -H "Authorization: Api-Key YOUR_KEY_HERE" \
https://api.example.com/passwords/Revoke:
api_key.delete() # Or mark inactiveConfiguration:
# Azure AD settings in Django admin
AZURE_AD_TENANT_ID=<your-tenant-id>
AZURE_AD_CLIENT_ID=<your-client-id>
AZURE_AD_CLIENT_SECRET=<your-client-secret>
AZURE_AD_REDIRECT_URI=https://clientst0r.example.com/accounts/azure/callback/
AZURE_AD_AUTO_CREATE_USERS=True # Auto-create on first loginFlow:
- User clicks "Login with Microsoft"
- Redirects to Azure AD
- User authenticates with Microsoft
- Callback to Client St0r with OAuth code
- Exchange code for access token
- Fetch user info from Microsoft Graph API
- Create/update user in Client St0r
- Log in user
Security:
- OAuth 2.0 / OpenID Connect
- No passwords stored in Client St0r
- Updates user info on each login
- Audit logged
What Gets Logged:
- ✅ Login/logout (success and failure)
- ✅ Password changes
- ✅ 2FA setup/changes
- ✅ API key operations (create, revoke)
- ✅ Data exports
- ✅ Permission changes
- ✅ User creation/deletion
- ✅ Organization changes
- ✅ Cross-org access attempts (should be 0)
- ✅ AI requests
- ✅ Configuration changes
Audit Log Model:
class AuditLog(models.Model):
# Who
user = ForeignKey(User)
username = CharField(max_length=150) # Preserved even if user deleted
# What
action = CharField(max_length=50) # create, read, update, delete, login, etc.
object_type = CharField(max_length=100)
object_id = PositiveIntegerField()
description = TextField()
# Where
organization = ForeignKey(Organization)
ip_address = GenericIPAddressField()
user_agent = TextField()
path = CharField(max_length=500)
# When
timestamp = DateTimeField(auto_now_add=True)
# Additional
extra_data = JSONField()
success = BooleanField(default=True)Query Audit Logs:
# Failed login attempts
AuditLog.objects.filter(action='login', success=False)
# Recent password changes
AuditLog.objects.filter(action='update', object_type='password').order_by('-timestamp')[:10]
# User activity
AuditLog.objects.filter(user=user).order_by('-timestamp')[:100]
# Organization activity
AuditLog.objects.filter(organization=org).order_by('-timestamp')Configuration:
# Django admin → Settings → Snyk Security
SNYK_API_TOKEN=<your-snyk-token> # Optional for enhanced features
SNYK_ENABLED=True
SNYK_SCAN_SCHEDULE='daily' # Options: daily, weekly, monthly
SNYK_AUTO_FIX=False # Enable automated dependency updates
SNYK_SEVERITY_THRESHOLD='low' # Alert on: critical, high, medium, lowWhat Gets Scanned:
- Python Dependencies - All packages in requirements.txt and installed packages
- JavaScript Dependencies - package.json, package-lock.json, node_modules
- Docker Images - Container security scanning (if using Docker deployment)
- Infrastructure as Code - Kubernetes/Terraform configs (if present)
- License Compliance - Check for problematic licenses
Scan Process:
- Automatic Scans - Runs on configured schedule via systemd timer
- Manual Scans - One-click from Admin → Settings → Snyk Security → Run Scan
- GitHub Integration - Automatic scans on pull requests (if GitHub Actions enabled)
- Result Processing:
- Parses JSON output from Snyk CLI
- Categorizes by severity (critical, high, medium, low)
- Stores in database with timestamp
- Tracks trends and changes over time
- Generates remediation recommendations
Web UI Dashboard (Admin → Settings → Snyk Security):
- Total Vulnerability Count - Current known vulnerabilities
- Severity Breakdown - Critical, high, medium, low counts with color coding
- Trend Analysis - Graph showing vulnerability trends (improving/worsening)
- Recent Scan History - List of recent scans with timestamps and results
- Detailed Findings - Package name, current version, vulnerable version, fixed version
- Remediation Advice - Specific upgrade commands and breaking change warnings
- Severity Filters - Filter view by severity level
- Export Options - Download scan results as JSON/CSV
Features:
- Zero-Config Mode - Works without API token (limited features)
- Enhanced Mode - With API token: deeper scans, more context, automated fixes
- Email Alerts - Notify admins when critical vulnerabilities found
- Threshold Alerts - Only alert on specified severity levels
- Ignore List - Mark specific vulnerabilities as accepted risk
- Scheduled Reports - Weekly/monthly summary emails
Run Scan:
# Manual scan via CLI
python manage.py run_snyk_scan
# With options
python manage.py run_snyk_scan --severity-threshold high --json
# Or via Web UI
# Admin → Settings → Snyk Security → Run Scan Now button
# Check status
python manage.py check_snyk_statusIntegration with CI/CD:
- GitHub Actions workflow included (
.github/workflows/snyk.yml) - Automatic scans on push to main branch
- PR checks for new vulnerabilities
- Fail build on critical vulnerabilities (configurable)
/var/log/itdocs/
├── django.log # Application logs
├── gunicorn-access.log # HTTP access logs
├── gunicorn-error.log # HTTP error logs
└── audit.log # Security audit trail (if separate)
Log Rotation:
# /etc/logrotate.d/clientst0r
/var/log/itdocs/*.log {
daily
rotate 30
compress
delaycompress
notifempty
create 0644 www-data www-data
sharedscripts
postrotate
systemctl reload clientst0r-gunicorn
endscript
}Monitor For:
- Spike in failed login attempts (check Axes logs)
- Unusual API usage patterns (check audit logs)
- AI usage spikes (check AI usage stats)
- Cross-org access attempts (should be 0, check audit logs)
- Snyk critical vulnerabilities
- Unusual IP addresses in audit logs
Tools:
- Django admin → Audit Logs
- Django admin → Axes → Access Attempts
- Security Dashboard → Vulnerability Scans
- Grep logs:
grep "FAILED LOGIN" /var/log/itdocs/django.log
If Compromised:
- Disable Affected Accounts:
user.is_active = False
user.save()- Block Attacker IPs:
# Firewall
iptables -A INPUT -s <attacker-ip> -j DROP
# Or fail2ban
fail2ban-client set clientst0r banip <attacker-ip>- Revoke API Keys:
APIKey.objects.filter(user=compromised_user).delete()- Change Passwords:
# Force password reset
user.set_unusable_password()
user.save()
# Email user with reset link- Remove 2FA Devices (if compromised):
Device.objects.filter(user=user).delete()- Patch Vulnerabilities:
# Check Snyk dashboard for recommendations
pip install --upgrade <vulnerable-package>
# Or apply Snyk automated PR
git pull origin snyk-fix-<vuln-id>- Rotate Secrets:
# Rotate encryption key
python manage.py secrets rotate --old-key $OLD --new-key $NEW
# Update environment variable
export APP_MASTER_KEY=$NEW_KEY
# Restart
systemctl restart clientst0r-gunicorn- Reset 2FA:
# Force all users to re-setup 2FA
Device.objects.all().delete()- Review Audit Logs:
# Check for data exfiltration
AuditLog.objects.filter(
action='export',
timestamp__gte=incident_start_time
)- Restore from Backup (if data corruption):
# Database restore
mysql -u root -p clientst0r < backup_2026-01-14.sql
# File restore
rsync -av /backups/media/ /var/lib/itdocs/uploads/- Re-enable Accounts:
user.is_active = True
user.save()- Verify Tenant Isolation:
python manage.py test core.tests.test_tenant_isolation -v 2Document:
- What happened?
- When did it happen?
- How was it detected?
- What was compromised?
- How was it contained?
- What was the root cause?
- How can we prevent this in the future?
Update:
- Security procedures
- Monitoring/alerting rules
- Rate limits (if abuse)
- AI spend caps (if cost abuse)
- Firewall rules
- Access controls
See separate section below for copy/paste implementation guide.
Includes:
- MariaDB hardening (TLS, least-privilege, backups)
- Gunicorn configuration review
- Build security (Semgrep, Gitleaks, SBOM)
- GitHub Actions pinning
- Supply chain security
Last Updated: 2026-01-18 Version: 2.24.148 Maintainer: Client St0r Security Team
- Browsable API disabled automatically in production (JSON-only when DEBUG=False)
- Granular throttling scopes:
- Anonymous: 50/hour
- Login: 10/hour
- Password reset: 5/hour
- Token ops: 20/hour
- AI requests: 100/day + 10/min burst
- Custom throttle classes in
api/throttles.py
- HSTS enabled in production (max-age=31536000)
- SSL redirect enabled in production
- Proxy SSL header configured
- Referrer-Policy: strict-origin-when-cross-origin
- CSP hardening: frame-ancestors 'none', object-src 'none', base-uri 'self', form-action 'self'
- Permissions-Policy defaults deny sensitive browser capabilities
- Custom header middleware:
core/security_headers_middleware.py
- Automated suite:
core/tests/test_tenant_isolation.py - Verifies org boundary enforcement across:
- passwords, assets, documents, audit logs
- API endpoints (403/404 cross-org)
- bulk operations
- manager filtering and foreign key constraints
- Centralized encryption/decryption utilities:
core/secrets_management.py - Key derivation: PBKDF2-SHA256
- Rotation command re-encrypts all encrypted fields:
- vault passwords, PSA/RMM credentials, encrypted fields
- Log sanitization utilities included
- Per-user and per-org request limits + burst control
- Per-user and per-org spend caps (budget protection)
- Prompt size limits
- PII redaction (email/phone/SSN/card/key patterns)
- Standard 429 response payload includes reset window info
- Validate:
python manage.py secrets validate - Generate key:
python manage.py secrets generate-key - Rotate:
python manage.py secrets rotate --old-key ... --new-key ...
- Run isolation tests:
python manage.py test core.tests.test_tenant_isolation -v 2
- Validate response headers and CSP after deployment (CSP changes require careful rollout)
See: docs/PHASE2_SECURITY.md for full implementation guide
- ✅ Semgrep (SAST with OWASP Top 10 + Python rules)
- ✅ Gitleaks (secrets scanning)
- ✅ CodeQL (deep static analysis)
- ✅ SBOM generation (CycloneDX format)
- ✅ pip-audit (PyPI vulnerability checks)
- ✅ Manual dependency management (pip-audit + pip list --outdated)
- ✅ Pre-commit hooks (Ruff + Bandit + Gitleaks)
- ⏳ Pin GitHub Actions to commit SHAs (optional, manual)
- ⏳ Least-privilege DB users (requires manual SQL)
- ⏳ TLS to database (if remote, requires CA cert)
- ⏳ Encrypted backups + restore tests (ops task)
- ⏳ CSP reporting endpoint (Report-Only rollout)
- ⏳ Per-endpoint request body limits
- ⏳ Upload validation (MIME + magic bytes)
- ⏳ API key hashing (store only hashed keys)
Activation: Most Phase 2 features activate automatically on next push. See Phase 2 docs for manual steps.