Security & SSL Configuration
Comprehensive guide to securing your self-hosted ANTE ERP installation.
Security Overview
A secure ANTE ERP installation requires:
- SSL/HTTPS - Encrypted communications
- Strong Passwords - All services and accounts
- Firewall Configuration - Restricted access
- Regular Updates - Security patches
- Access Control - User permissions
- Audit Logging - Activity monitoring
SSL/HTTPS Setup
Option 1: Nginx Reverse Proxy with Let's Encrypt (Recommended)
This is the most common and recommended setup for production.
Step 1: Install Nginx
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y nginx
# CentOS/RHEL
sudo yum install -y nginx
# Start and enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginxStep 2: Install Certbot
# Ubuntu/Debian
sudo apt-get install -y certbot python3-certbot-nginx
# CentOS/RHEL
sudo yum install -y certbot python3-certbot-nginxStep 3: Configure Domain DNS
Ensure your domain points to your server:
# Check DNS resolution
dig +short erp.yourcompany.com
# Should return your server's IP addressStep 4: Create Nginx Configuration
Create /etc/nginx/sites-available/ante-erp:
# HTTP - Redirect to HTTPS
server {
listen 80;
listen [::]:80;
server_name erp.yourcompany.com;
# Let's Encrypt challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect all other traffic to HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}
# HTTPS - Main configuration
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name erp.yourcompany.com;
# SSL Certificates (will be configured by certbot)
ssl_certificate /etc/letsencrypt/live/erp.yourcompany.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/erp.yourcompany.com/privkey.pem;
# SSL Configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
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 "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https:; font-src 'self' data:;" always;
# Max upload size
client_max_body_size 100M;
client_body_timeout 300s;
# Frontend
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# Backend API
location /api/ {
proxy_pass http://localhost:3001/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# WebSocket
location /socket.io/ {
proxy_pass http://localhost:4001/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket timeouts
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
# Health check endpoint
location /health {
proxy_pass http://localhost:3001/health;
access_log off;
}
# Static file caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://localhost:8080;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Logs
access_log /var/log/nginx/ante-erp-access.log;
error_log /var/log/nginx/ante-erp-error.log;
}Step 5: Enable Configuration
# Create symlink
sudo ln -s /etc/nginx/sites-available/ante-erp /etc/nginx/sites-enabled/
# Test configuration
sudo nginx -t
# If test passes, reload Nginx
sudo systemctl reload nginxStep 6: Obtain SSL Certificate
# Get certificate
sudo certbot --nginx -d erp.yourcompany.com
# Follow prompts:
# - Enter email address
# - Agree to terms
# - Choose redirect option (option 2)
# Certbot will automatically update your Nginx configurationStep 7: Test Auto-Renewal
# Test renewal process
sudo certbot renew --dry-run
# Certbot automatically sets up a systemd timer for renewal
# Verify it's active:
sudo systemctl status certbot.timerStep 8: Update ANTE Configuration
Update your .env file:
FRONTEND_URL=https://erp.yourcompany.com
API_URL=https://erp.yourcompany.com/api
SOCKET_URL=wss://erp.yourcompany.com/socket.ioRestart services:
docker compose restart backend frontendOption 2: Docker with SSL Termination
Use Nginx in Docker with automatic SSL.
Create docker-compose.ssl.yml:
version: '3.8'
services:
nginx:
image: nginx:alpine
container_name: ante-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- certbot_data:/var/www/certbot:ro
- certbot_conf:/etc/letsencrypt:ro
depends_on:
- frontend
- backend
networks:
- ante-network
certbot:
image: certbot/certbot:latest
container_name: ante-certbot
volumes:
- certbot_data:/var/www/certbot
- certbot_conf:/etc/letsencrypt
command: certonly --webroot --webroot-path=/var/www/certbot --email admin@yourcompany.com --agree-tos --no-eff-email -d erp.yourcompany.com
volumes:
certbot_data:
certbot_conf:Firewall Configuration
UFW (Ubuntu/Debian)
# Install UFW
sudo apt-get install ufw
# Default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (IMPORTANT - do this first!)
sudo ufw allow 22/tcp
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# If not using reverse proxy, allow direct access
# sudo ufw allow 8080/tcp # Frontend
# sudo ufw allow 3001/tcp # Backend
# sudo ufw allow 4001/tcp # WebSocket
# Enable firewall
sudo ufw enable
# Check status
sudo ufw status verboseFirewalld (CentOS/RHEL)
# Install firewalld
sudo yum install firewalld
# Start and enable
sudo systemctl start firewalld
sudo systemctl enable firewalld
# Allow HTTP and HTTPS
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
# Allow SSH
sudo firewall-cmd --permanent --add-service=ssh
# Reload firewall
sudo firewall-cmd --reload
# Check status
sudo firewall-cmd --list-allDocker and Firewall
Docker can bypass UFW rules. To fix this:
# Edit Docker daemon config
sudo nano /etc/docker/daemon.jsonAdd:
{
"iptables": false
}Restart Docker:
sudo systemctl restart dockerPassword Security
Generate Strong Passwords
# Generate passwords for .env file
echo "DB_PASSWORD=$(openssl rand -base64 32)"
echo "REDIS_PASSWORD=$(openssl rand -base64 32)"
echo "MONGO_PASSWORD=$(openssl rand -base64 32)"
echo "JWT_SECRET=$(openssl rand -base64 64)"Password Policy Configuration
In .env:
# Enforce strong passwords
PASSWORD_MIN_LENGTH=12
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_NUMBER=true
PASSWORD_REQUIRE_SPECIAL=true
PASSWORD_MAX_AGE_DAYS=90
# Prevent password reuse
PASSWORD_HISTORY_COUNT=5
# Account lockout
MAX_LOGIN_ATTEMPTS=5
LOCKOUT_DURATION_MINUTES=30Access Control
Database Security
PostgreSQL
-- Create read-only user for reporting
CREATE USER readonly WITH PASSWORD 'strong_password';
GRANT CONNECT ON DATABASE ante_db TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
-- Revoke unnecessary permissions
REVOKE ALL ON SCHEMA public FROM PUBLIC;MongoDB
// Create read-only user
db.createUser({
user: "readonly",
pwd: "strong_password",
roles: [{ role: "read", db: "ante" }]
})
// Create backup user
db.createUser({
user: "backup",
pwd: "strong_password",
roles: [{ role: "backup", db: "admin" }]
})Network Security
Restrict Database Access
In docker-compose.yml, don't expose database ports:
services:
postgres:
# Remove or comment out ports section
# ports:
# - "5433:5432"
# Database only accessible within Docker networkIf you must expose ports, bind to localhost only:
services:
postgres:
ports:
- "127.0.0.1:5433:5432" # Only accessible from localhostUse Docker Networks
Isolate services:
networks:
frontend-network:
driver: bridge
backend-network:
driver: bridge
database-network:
driver: bridge
services:
frontend:
networks:
- frontend-network
backend:
networks:
- frontend-network
- backend-network
- database-network
postgres:
networks:
- database-networkSecurity Headers
Already configured in Nginx above, but for reference:
# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Enable XSS protection
add_header X-XSS-Protection "1; mode=block" always;
# HTTP Strict Transport Security
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Referrer Policy
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;Rate Limiting
Nginx Rate Limiting
Add to Nginx configuration:
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
server {
# Apply rate limiting
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
# ... rest of config
}
location /api/auth/login {
limit_req zone=login_limit burst=3 nodelay;
# ... rest of config
}
}Application-Level Rate Limiting
Configure in .env:
RATE_LIMIT_ENABLED=true
RATE_LIMIT_WINDOW=900000 # 15 minutes
RATE_LIMIT_MAX_REQUESTS=100Audit Logging
Enable Audit Logs
In .env:
AUDIT_LOG_ENABLED=true
AUDIT_LOG_LEVEL=info
AUDIT_LOG_FILE=/app/logs/audit.log
# Events to log
AUDIT_LOG_AUTH=true
AUDIT_LOG_DATA_CHANGES=true
AUDIT_LOG_ADMIN_ACTIONS=true
AUDIT_LOG_FILE_ACCESS=trueView Audit Logs
# View recent audit events
docker compose exec backend tail -f /app/logs/audit.log
# Search for specific user activity
docker compose exec backend grep "user:john.doe" /app/logs/audit.log
# View failed login attempts
docker compose exec backend grep "LOGIN_FAILED" /app/logs/audit.logSecurity Scanning
Scan Docker Images
# Install Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Scan images
trivy image ghcr.io/gtplusnet/ante-self-hosted-backend:latest
trivy image ghcr.io/gtplusnet/ante-self-hosted-frontend-main:latest
# Scan for critical vulnerabilities only
trivy image --severity CRITICAL ghcr.io/gtplusnet/ante-self-hosted-backend:latestRegular Security Updates
# Update system packages
sudo apt-get update && sudo apt-get upgrade -y
# Update Docker images
docker compose pull
docker compose up -d
# Restart containers with new images
docker compose restartBackup Encryption
Encrypt sensitive backups:
# Encrypt backup
gpg --symmetric --cipher-algo AES256 backup_20251027.sql.gz
# Decrypt backup
gpg --decrypt backup_20251027.sql.gz.gpg > backup_20251027.sql.gzSecurity Checklist
Before going to production:
- [ ] SSL/HTTPS configured and tested
- [ ] All default passwords changed
- [ ] Strong passwords for all services
- [ ] Firewall configured and enabled
- [ ] Database ports not exposed publicly
- [ ] Security headers configured
- [ ] Rate limiting enabled
- [ ] Audit logging enabled
- [ ] Automated backups configured
- [ ] Backup encryption enabled
- [ ] SSL certificate auto-renewal tested
- [ ] Security scanning performed
- [ ] Access control policies defined
- [ ] Admin users use strong passwords
- [ ] Two-factor authentication considered
- [ ] Regular update schedule established
Security Best Practices
Regular Maintenance
Weekly:
- Review audit logs
- Check for failed login attempts
- Monitor disk space
Monthly:
- Update Docker images
- Review user access
- Test backup restore
- Scan for vulnerabilities
Quarterly:
- Rotate passwords and keys
- Review security policies
- Update SSL certificates (automatic with Let's Encrypt)
- Security audit
Incident Response
If you detect a security incident:
Isolate: Stop affected services
bashdocker compose stop backend frontendInvestigate: Check logs
bashdocker compose logs backend > incident_logs.txtBackup: Save current state
bashdocker compose exec postgres pg_dump -U ante ante_db > incident_backup.sqlRemediate: Fix vulnerability
Restore: Bring services back online
Monitor: Watch for further issues
Next Steps
Last Updated: October 27, 2025
