Security Best Practices
Securing your self-hosted AFFiNE instance is crucial for protecting user data and maintaining trust. This guide covers essential security measures and best practices.
Network Security
HTTPS/TLS
Always use HTTPS in production:
Configure reverse proxy with TLS
Use Let’s Encrypt for free SSL certificates: sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d affine.yourdomain.com
Update AFFiNE configuration
AFFINE_SERVER_HTTPS = true
AFFINE_SERVER_HOST = affine.yourdomain.com
Enable HSTS
Add to Nginx configuration: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Never run AFFiNE in production without HTTPS. Credentials and data will be transmitted in plain text.
Firewall Configuration
Limit network access to essential ports:
# Allow SSH (change default port if possible)
sudo ufw allow 22/tcp
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable firewall
sudo ufw enable
# Verify rules
sudo ufw status
Network Isolation
Isolate services using Docker networks:
networks :
frontend :
driver : bridge
backend :
driver : bridge
internal : true
services :
affine :
networks :
- frontend
- backend
postgres :
networks :
- backend
redis :
networks :
- backend
This prevents direct access to database and Redis from outside the Docker network.
Authentication & Authorization
Strong Password Policy
Enforce strong passwords:
{
"auth" : {
"passwordRequirements" : {
"min" : 12 ,
"max" : 128
}
}
}
Consider requiring passwords with at least 12 characters including uppercase, lowercase, numbers, and symbols.
Disable Public Signup
For private deployments, disable public registration:
{
"auth" : {
"allowSignup" : false ,
"allowSignupForOauth" : false
}
}
Create accounts manually or via invitation only.
Email Verification
Require email verification before access:
{
"auth" : {
"requireEmailVerification" : true
}
}
Session Security
Configure secure session settings:
{
"auth" : {
"session" : {
"ttl" : 604800 ,
"ttr" : 86400
}
}
}
ttl: Session lifetime (7 days)
ttr: Time to refresh (1 day)
Shorter TTL values improve security but reduce convenience.
OAuth Authentication
Use OAuth providers for centralized authentication:
Google Workspace
OIDC (Keycloak, Auth0, etc.)
{
"oauth" : {
"providers" : {
"google" : {
"clientId" : "your-client-id.apps.googleusercontent.com" ,
"clientSecret" : "your-client-secret"
}
}
},
"auth" : {
"requireEmailDomainVerification" : true
}
}
Database Security
Strong Database Password
Use a strong, unique password for PostgreSQL:
# Generate a secure password
openssl rand -base64 32
Set in .env:
DB_PASSWORD = your_generated_secure_password_here
Network Access Control
Restrict PostgreSQL access:
postgres :
image : pgvector/pgvector:pg16
networks :
- backend # Not exposed to frontend network
# Don't expose ports to host
# ports:
# - "5432:5432" # NEVER do this in production
Encryption at Rest
For sensitive data, use encrypted volumes:
# Create encrypted volume using LUKS
sudo cryptsetup luksFormat /dev/sdb
sudo cryptsetup luksOpen /dev/sdb affine_encrypted
sudo mkfs.ext4 /dev/mapper/affine_encrypted
sudo mount /dev/mapper/affine_encrypted /var/lib/affine
Regular Backups
Encrypt backups before storing:
# Backup and encrypt
docker compose exec postgres pg_dump -U affine affine | \
gzip | \
gpg --encrypt --recipient admin@yourdomain.com \
> affine-backup- $( date +%Y%m%d ) .sql.gz.gpg
Database User Permissions
Use least privilege principle:
-- Create read-only user for reporting
CREATE USER affine_readonly WITH PASSWORD 'secure_password' ;
GRANT CONNECT ON DATABASE affine TO affine_readonly;
GRANT USAGE ON SCHEMA public TO affine_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO affine_readonly;
Application Security
Private Key Security
The private key is used for signing tokens and encrypting sensitive data:
Generate secure key
openssl genrsa -out private.key 4096
Store securely
chmod 600 private.key
sudo chown root:root private.key
Set in environment
AFFINE_PRIVATE_KEY = $( cat private.key )
Backup the key
Store encrypted backup in secure location (password manager, vault, etc.).
If the private key is compromised, all sessions and encrypted data may be at risk. Rotate immediately if compromise is suspected.
Rate Limiting
Protect against brute force and DoS attacks:
{
"throttle" : {
"enabled" : true ,
"throttlers" : {
"default" : {
"ttl" : 60 ,
"limit" : 60
},
"strict" : {
"ttl" : 60 ,
"limit" : 10
}
}
}
}
For additional protection, use Nginx rate limiting:
http {
limit_req_zone $ binary_remote_addr zone=affine_limit:10m rate=10r/s;
server {
location / {
limit_req zone=affine_limit burst=20 nodelay;
proxy_pass http://localhost:3010;
}
}
}
CAPTCHA Protection
Enable CAPTCHA for authentication:
{
"captcha" : {
"enabled" : true ,
"config" : {
"turnstile" : {
"secret" : "your-cloudflare-turnstile-secret"
},
"challenge" : {
"bits" : 20
}
}
}
}
Content Security Policy
Add CSP headers in Nginx:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss:; frame-ancestors 'none';" always;
Add comprehensive security headers:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
Storage Security
File Upload Restrictions
Set maximum upload size:
client_max_body_size 100M ;
Configure in WebSocket settings:
{
"websocket" : {
"maxHttpBufferSize" : 100000000
}
}
S3 Security
When using S3-compatible storage:
{
"Version" : "2012-10-17" ,
"Statement" : [
{
"Effect" : "Allow" ,
"Principal" : {
"AWS" : "arn:aws:iam::ACCOUNT:user/affine"
},
"Action" : [
"s3:GetObject" ,
"s3:PutObject" ,
"s3:DeleteObject"
],
"Resource" : "arn:aws:s3:::your-bucket/*"
}
]
}
Enable server-side encryption: {
"storages" : {
"blob" : {
"storage" : {
"provider" : "aws-s3" ,
"config" : {
"serverSideEncryption" : "AES256"
}
}
}
}
}
Use IAM roles instead of access keys when possible: {
"storages" : {
"blob" : {
"storage" : {
"provider" : "aws-s3" ,
"config" : {
"credentials" : {
"fromInstanceMetadata" : true
}
}
}
}
}
}
Monitoring & Auditing
Enable Audit Logging
Monitor security-relevant events:
# Monitor authentication attempts
docker compose logs -f affine | grep -i "auth\|login\|password"
# Monitor failed access attempts
docker compose logs -f affine | grep -i "unauthorized\|forbidden\|denied"
Intrusion Detection
Install and configure Fail2Ban:
sudo apt install fail2ban
# Create filter for AFFiNE
sudo cat > /etc/fail2ban/filter.d/affine.conf << EOF
[Definition]
failregex = ^.*Authentication failed.*<HOST>.*$
ignoreregex =
EOF
# Create jail
sudo cat > /etc/fail2ban/jail.d/affine.conf << EOF
[affine]
enabled = true
port = http,https
filter = affine
logpath = /var/log/affine/access.log
maxretry = 5
bantime = 3600
EOF
sudo systemctl restart fail2ban
Security Scanning
Regularly scan for vulnerabilities:
# Scan Docker images
docker scout cves ghcr.io/toeverything/affine:stable
# Update base images
docker compose pull
docker compose up -d
Compliance & Privacy
Data Privacy
Implement data export functionality
Provide account deletion with data removal
Document data processing activities
Implement data retention policies
Enable encryption at rest and in transit
Ensure data stays in required regions: {
"storages" : {
"blob" : {
"storage" : {
"provider" : "aws-s3" ,
"config" : {
"region" : "eu-central-1"
}
}
}
}
}
Access Control
Implement role-based access control:
Regular users: Standard workspace access
Workspace admins: Manage workspace members
System admins: Server configuration access
Document who has access to:
Server infrastructure
Database backups
Encryption keys
Configuration files
Incident Response
Security Incident Plan
Detect
Monitor logs and metrics
Set up alerts for suspicious activity
Regular security audits
Contain
Isolate affected systems
Block malicious IPs
Disable compromised accounts
Investigate
Collect logs and evidence
Identify attack vector
Assess impact and data exposure
Remediate
Patch vulnerabilities
Rotate credentials
Update security controls
Recover
Restore from clean backups
Verify system integrity
Resume normal operations
Review
Document incident
Update procedures
Notify affected parties if required
Compromised Credentials
If credentials are compromised:
# 1. Rotate database password
docker compose exec postgres psql -U postgres -c "ALTER USER affine PASSWORD 'new_secure_password';"
# 2. Update environment
vim .env # Update DB_PASSWORD
# 3. Rotate private key
mv ~/.affine/self-host/config/private.key ~/.affine/self-host/config/private.key.old
openssl genrsa -out ~/.affine/self-host/config/private.key 4096
# 4. Restart services
docker compose down
docker compose up -d
# 5. Force logout all users (sessions invalidated by key rotation)
Security Checklist
Before going to production:
Security Resources
Security is an ongoing process, not a one-time setup. Regularly review and update your security measures.
Next Steps