This document covers security best practices for deploying and operating SHATTERED.
- Quick Security Checklist
- Authentication
- Authorization & Roles
- Multi-Tenancy
- Network Security
- Rate Limiting
- Audit Logging
- Production Deployment
- Security Headers
- Data Protection
- Reporting Vulnerabilities
Before deploying to production, verify:
-
AUTH_SECRET_KEYis set to a secure random value (not the default) -
CORS_ORIGINSis set to your actual domain (not*) - PostgreSQL, Redis, and Qdrant are not exposed externally
- HTTPS is enabled via Traefik or your own reverse proxy
- Default admin password has been changed after setup
- Rate limiting is enabled
- Audit logging is enabled
SHATTERED uses JWT (JSON Web Tokens) for stateless authentication:
- Tokens are signed with
AUTH_SECRET_KEY - Default expiration: 1 hour (configurable via
JWT_LIFETIME_SECONDS) - Tokens are stored in browser localStorage
# Generate a secure key (minimum 32 bytes)
python -c "import secrets; print(secrets.token_urlsafe(32))"Important:
- Never use the default key in production
- Rotate keys periodically (existing sessions will be invalidated)
- Keep the key secret - do not commit to version control
On first access, SHATTERED prompts for initial configuration:
- Tenant Creation: Organization name and settings
- Admin Account: Email, password, and display name
This is a one-time setup. After completion, the setup endpoint is disabled.
- Minimum 8 characters
- No complexity requirements enforced (users should choose strong passwords)
- Passwords are hashed with bcrypt before storage
| Role | Description | Capabilities |
|---|---|---|
| Admin | Full system access | User management, settings, audit logs, all features |
| Analyst | Standard user | Read/write access to documents and analyses |
| Viewer | Read-only access | View documents and analyses, no modifications |
Endpoints are protected by role requirements:
# Admin-only endpoints
@router.get("/users", dependencies=[Depends(require_admin)])
# Any authenticated user
@router.get("/documents", dependencies=[Depends(require_auth)])ProtectedRoute: Requires authenticationAdminRoute: Requires admin role
Unauthorized access redirects to login.
Each tenant has completely isolated data:
- All database tables include
tenant_idcolumn - Queries automatically filter by tenant context
- Cross-tenant data access is prevented at the database layer
- User authenticates → JWT includes
tenant_id - Request middleware extracts tenant from JWT
- Database queries automatically scope to tenant
- Response contains only tenant's data
All shard queries use tenant helpers:
# Automatically includes WHERE tenant_id = ?
rows = await self.tenant_fetch("SELECT * FROM my_table")| Mode | Ports Exposed | Use Case |
|---|---|---|
| Development | 8100 (app), 5432 (postgres), 6379 (redis), 6333 (qdrant) | Local development |
| Production | 80, 443 only | Internet-facing deployment |
Using docker-compose.traefik.yml:
- Only Traefik exposes ports 80/443
- PostgreSQL, Redis, Qdrant are internal only
- Application communicates via Docker network
# Allow only HTTP/HTTPS
ufw allow 80/tcp
ufw allow 443/tcp
ufw deny 5432/tcp # Block external PostgreSQL
ufw deny 6379/tcp # Block external Redis
ufw deny 6333/tcp # Block external QdrantRATE_LIMIT_DEFAULT=100/minute # General API endpoints
RATE_LIMIT_UPLOAD=20/minute # File upload endpoints
RATE_LIMIT_AUTH=10/minute # Login/authentication endpointsResponses include:
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Requests remainingX-RateLimit-Reset: Time until limit resets
When rate limited, clients receive:
{
"detail": "Rate limit exceeded. Retry after X seconds."
}| Event Type | Trigger |
|---|---|
tenant.created |
Initial setup completed |
user.created |
New user created by admin |
user.updated |
User profile modified |
user.deleted |
User removed from system |
user.deactivated |
User account disabled |
user.reactivated |
User account re-enabled |
user.role.changed |
User role modified |
Each entry includes:
- Timestamp (UTC)
- Event type
- Actor (user who performed action)
- Target (affected resource)
- IP address
- User agent
- Additional metadata
Audit logs are stored indefinitely. Consider implementing log rotation for long-running deployments.
- UI: Settings → Audit Log (admin only)
- API:
GET /api/auth/audit - Export: CSV or JSON via API
SHATTERED includes Traefik configuration for production:
docker compose -f docker-compose.yml -f docker-compose.traefik.yml up -d- Automatic certificate provisioning
- Automatic renewal (30 days before expiry)
- HTTP-01 challenge (requires port 80 access)
# Create with correct permissions
touch traefik/acme.json
chmod 600 traefik/acme.jsonImportant: acme.json must have 600 permissions or Traefik will refuse to start.
- TLS 1.2 and 1.3 only (older versions disabled)
- Strong cipher suites
- HSTS enabled with 1-year max-age
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains; preload |
Force HTTPS |
X-Content-Type-Options |
nosniff |
Prevent MIME sniffing |
X-Frame-Options |
DENY |
Prevent clickjacking |
X-XSS-Protection |
1; mode=block |
XSS filter (legacy) |
Referrer-Policy |
strict-origin-when-cross-origin |
Limit referrer info |
Content-Security-Policy |
See below | Control resource loading |
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self' ws: wss:;
frame-ancestors 'none';
Note: unsafe-inline and unsafe-eval are required for React development builds. Consider removing for production builds with pre-built assets.
- Database files stored on Docker volumes
- Consider full-disk encryption for sensitive deployments
- Regular backups recommended
- HTTPS enforced via Traefik in production
- Internal Docker network for service-to-service communication
- No sensitive data in URLs or query parameters
- Passwords hashed with bcrypt
- JWT secrets never logged
- API keys stored in environment variables (not in database)
- Filename sanitization (special characters removed)
- Path traversal prevention
- File type validation
- Configurable allowed directories
# Allows localhost variants
CORS_ORIGINS=http://localhost:3100,http://127.0.0.1:3100# Restrict to your domain only
CORS_ORIGINS=https://your-domain.comThe docker-compose.traefik.yml automatically sets:
CORS_ORIGINS=https://${DOMAIN}| Variable | Description |
|---|---|
AUTH_SECRET_KEY |
JWT signing key (generate securely) |
POSTGRES_PASSWORD |
Database password |
| Variable | Description |
|---|---|
DOMAIN |
Your domain name |
ACME_EMAIL |
Let's Encrypt notification email |
CORS_ORIGINS |
Allowed CORS origins |
For production:
- Use Docker secrets or environment files
- Never commit
.envto version control - Consider HashiCorp Vault or similar for enterprise
If you discover a security vulnerability:
- Do not open a public GitHub issue
- Email security concerns to the maintainer
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We aim to respond within 48 hours and provide a fix within 7 days for critical issues.
- JWT authentication
- Role-based access control
- Multi-tenant data isolation
- Rate limiting
- CORS configuration
- Security headers
- XSS prevention (DOMPurify)
- Path traversal protection
- Audit logging
- HTTPS via Traefik
- Login attempt logging
- Session management (logout all devices)
- Two-factor authentication (2FA)
- API key authentication
- IP allowlisting
- Automated security scanning
Last updated: January 2026