Security is a top priority in nself deployments. This guide covers security best practices, hardening configurations, and the tools nself provides to help secure your backend infrastructure.
Security is a shared responsibility. While nself provides secure defaults and hardening tools, you are responsible for proper configuration, regular updates, and monitoring of your deployments.
# Run security audit
nself prod check
# Apply all security hardening
nself prod harden
# Generate strong secrets
nself prod secrets generate
# Configure SSL
nself prod ssl request yourdomain.com
# Configure firewall
nself prod firewall configure# Comprehensive security audit
nself prod check
# Output:
# Production Security Audit
# ════════════════════════════════════════════════
#
# Authentication & Authorization
# ─────────────────────────────────────
# ✓ Admin UI disabled
# ✓ Hasura console disabled
# ✓ Dev mode disabled
# ✓ Strong JWT secret (64+ characters)
# ✓ Password policy configured
#
# Network Security
# ─────────────────────────────────────
# ✓ SSL/TLS enabled
# ✓ HSTS enabled
# ✓ Secure headers configured
# ✗ Rate limiting not configured
#
# Data Protection
# ─────────────────────────────────────
# ✓ Database password strong
# ✓ Backups enabled
# ✓ Encryption at rest enabled
# ✗ Audit logging not enabled
#
# Infrastructure
# ─────────────────────────────────────
# ✓ Firewall configured
# ✓ SSH key authentication
# ✓ Non-root containers
# ✗ Container resource limits not set
#
# Score: 85/100
# Critical Issues: 0
# Warnings: 3
# Recommendations: 4# Detailed audit with recommendations
nself prod check --verbose
# Export audit report
nself prod check --output security-report.json
# Check specific category
nself prod check --category authentication
nself prod check --category network
nself prod check --category data# JWT settings for production
HASURA_JWT_KEY=your-secret-key-minimum-64-characters-for-production-use
HASURA_JWT_TYPE=HS256
# Token expiration
AUTH_ACCESS_TOKEN_EXPIRY=15m # Short-lived access tokens
AUTH_REFRESH_TOKEN_EXPIRY=7d # Longer refresh tokens
# Audience and issuer validation
AUTH_JWT_AUDIENCE=https://api.yourdomain.com
AUTH_JWT_ISSUER=https://auth.yourdomain.com# Password requirements
AUTH_PASSWORD_MIN_LENGTH=12
AUTH_PASSWORD_REQUIRE_UPPERCASE=true
AUTH_PASSWORD_REQUIRE_LOWERCASE=true
AUTH_PASSWORD_REQUIRE_NUMBERS=true
AUTH_PASSWORD_REQUIRE_SYMBOLS=true
AUTH_PASSWORD_PREVENT_COMMON=true
# Password history
AUTH_PASSWORD_HISTORY_COUNT=5
AUTH_PASSWORD_MAX_AGE_DAYS=90# Enable MFA for all users
AUTH_MFA_ENABLED=true
AUTH_MFA_REQUIRED_FOR_ROLES=admin,moderator
# TOTP configuration
AUTH_MFA_TOTP_ENABLED=true
AUTH_MFA_TOTP_ISSUER="Your App Name"
# Recovery codes
AUTH_MFA_RECOVERY_CODES_COUNT=10# Authentication rate limiting
AUTH_RATE_LIMIT_ENABLED=true
AUTH_RATE_LIMIT_LOGIN_ATTEMPTS=5
AUTH_RATE_LIMIT_LOGIN_WINDOW=900 # 15 minutes
AUTH_RATE_LIMIT_SIGNUP_ATTEMPTS=3
AUTH_RATE_LIMIT_SIGNUP_WINDOW=3600 # 1 hour
# Account lockout
AUTH_ACCOUNT_LOCKOUT_ENABLED=true
AUTH_ACCOUNT_LOCKOUT_ATTEMPTS=5
AUTH_ACCOUNT_LOCKOUT_DURATION=1800 # 30 minutes# Enable SSL with Let's Encrypt
SSL_ENABLED=true
SSL_PROVIDER=letsencrypt
LETSENCRYPT_EMAIL=admin@yourdomain.com
# TLS protocol versions (disable old versions)
SSL_PROTOCOLS="TLSv1.2 TLSv1.3"
# Strong cipher suites
SSL_CIPHERS="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
SSL_PREFER_SERVER_CIPHERS=true
# HSTS (HTTP Strict Transport Security)
HSTS_ENABLED=true
HSTS_MAX_AGE=31536000 # 1 year
HSTS_INCLUDE_SUBDOMAINS=true
HSTS_PRELOAD=true# Enable security headers
SECURITY_HEADERS_ENABLED=true
# Content Security Policy
CSP_ENABLED=true
CSP_DEFAULT_SRC="'self'"
CSP_SCRIPT_SRC="'self' 'unsafe-inline'"
CSP_STYLE_SRC="'self' 'unsafe-inline'"
CSP_IMG_SRC="'self' data: https:"
CSP_CONNECT_SRC="'self' https://api.yourdomain.com wss://api.yourdomain.com"
# Other security headers
X_FRAME_OPTIONS=DENY
X_CONTENT_TYPE_OPTIONS=nosniff
X_XSS_PROTECTION="1; mode=block"
REFERRER_POLICY=strict-origin-when-cross-origin
PERMISSIONS_POLICY="geolocation=(), microphone=(), camera=()"# CORS settings
CORS_ENABLED=true
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=Authorization,Content-Type,X-Requested-With
CORS_ALLOW_CREDENTIALS=true
CORS_MAX_AGE=86400# Configure firewall via nself
nself prod firewall configure --dry-run
nself prod firewall configure
# Manual firewall rules (UFW)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
# Docker network isolation
DOCKER_NETWORK_INTERNAL=true
EXPOSE_INTERNAL_PORTS=false# Strong database credentials
POSTGRES_PASSWORD=your-strong-password-32-chars-minimum
# Connection security
POSTGRES_SSL_MODE=require
POSTGRES_SSL_CERT=/path/to/server.crt
POSTGRES_SSL_KEY=/path/to/server.key
# Connection restrictions
POSTGRES_ALLOWED_HOSTS=hasura,api-service
POSTGRES_MAX_CONNECTIONS=100
# Audit logging
POSTGRES_LOG_STATEMENT=ddl
POSTGRES_LOG_CONNECTIONS=true
POSTGRES_LOG_DISCONNECTIONS=true# PostgreSQL encryption
POSTGRES_DATA_ENCRYPTION=true
# MinIO encryption
MINIO_SSE_ENABLED=true
MINIO_SSE_MASTER_KEY=your-32-character-master-key
# Backup encryption
BACKUP_ENCRYPTION=true
BACKUP_ENCRYPTION_KEY=your-backup-encryption-key# Generate secure secrets
nself prod secrets generate
# Rotate secrets
nself prod secrets rotate POSTGRES_PASSWORD
nself prod secrets rotate --all
# Validate secrets
nself prod secrets validate
# File permissions for secrets
chmod 600 .environments/prod/.env.secrets# Run containers as non-root
CONTAINER_USER=1000:1000
# Resource limits
POSTGRES_MEMORY_LIMIT=2GB
POSTGRES_CPU_LIMIT=2.0
HASURA_MEMORY_LIMIT=1GB
HASURA_CPU_LIMIT=1.0
# Read-only root filesystem
CONTAINER_READ_ONLY_ROOT=true
# Drop unnecessary capabilities
CONTAINER_DROP_CAPABILITIES=ALL
CONTAINER_ADD_CAPABILITIES=NET_BIND_SERVICE
# Security options
CONTAINER_NO_NEW_PRIVILEGES=true# SSH configuration recommendations
# /etc/ssh/sshd_config
# Disable password authentication
PasswordAuthentication no
PubkeyAuthentication yes
# Disable root login
PermitRootLogin no
# Use SSH key authentication only
ChallengeResponseAuthentication no
# Limit SSH to specific users
AllowUsers deploy
# Change default port (optional)
Port 2222
# Limit authentication attempts
MaxAuthTries 3
LoginGraceTime 60# Apply nself hardening
nself prod harden
# This applies:
# - Disable unnecessary services
# - Configure automatic security updates
# - Set up fail2ban
# - Configure sysctl security parameters
# - Set file permissions
# - Enable audit logging
# Manual hardening steps
# 1. Keep system updated
sudo apt update && sudo apt upgrade
# 2. Install fail2ban
sudo apt install fail2ban
sudo systemctl enable fail2ban
# 3. Configure automatic updates
sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades# Disable development features
HASURA_GRAPHQL_ENABLE_CONSOLE=false
HASURA_GRAPHQL_DEV_MODE=false
HASURA_GRAPHQL_ENABLE_TELEMETRY=false
# Strong admin secret
HASURA_GRAPHQL_ADMIN_SECRET=your-strong-admin-secret-32-chars
# Unauthorized role
HASURA_GRAPHQL_UNAUTHORIZED_ROLE=anonymous
# Connection limits
HASURA_GRAPHQL_WS_CONNECTION_INIT_TIMEOUT=10s
HASURA_GRAPHQL_MAX_CONNECTIONS=50-- Enable row-level security on sensitive tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Create policies
CREATE POLICY users_own_data ON users
FOR ALL
USING (id = current_setting('hasura.user')::uuid);
CREATE POLICY posts_author_access ON posts
FOR ALL
USING (
author_id = current_setting('hasura.user')::uuid
OR status = 'published'
);
-- Admin bypass
CREATE POLICY admin_full_access ON users
FOR ALL
USING (current_setting('hasura.role') = 'admin');// Example Hasura permission configuration
{
"role": "user",
"table": "posts",
"permissions": {
"select": {
"filter": {
"_or": [
{ "author_id": { "_eq": "X-Hasura-User-Id" } },
{ "status": { "_eq": "published" } }
]
},
"columns": ["id", "title", "content", "created_at"],
"limit": 100
},
"insert": {
"check": {
"author_id": { "_eq": "X-Hasura-User-Id" }
},
"columns": ["title", "content"]
},
"update": {
"filter": { "author_id": { "_eq": "X-Hasura-User-Id" } },
"columns": ["title", "content"]
},
"delete": {
"filter": { "author_id": { "_eq": "X-Hasura-User-Id" } }
}
}
}# Enable audit logging
AUDIT_LOG_ENABLED=true
AUDIT_LOG_LEVEL=info
AUDIT_LOG_RETENTION_DAYS=90
# What gets logged:
# - Authentication events (login, logout, failed attempts)
# - Authorization failures
# - Data access (sensitive tables)
# - Configuration changes
# - Admin actions-- Audit log table
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
timestamp TIMESTAMPTZ DEFAULT NOW(),
user_id UUID,
action VARCHAR(50) NOT NULL,
resource VARCHAR(100),
resource_id UUID,
details JSONB,
ip_address INET,
user_agent TEXT,
success BOOLEAN NOT NULL
);
CREATE INDEX idx_audit_logs_timestamp ON audit_logs(timestamp);
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);# Enable security monitoring
SECURITY_MONITORING_ENABLED=true
# Alert thresholds
ALERT_FAILED_LOGIN_THRESHOLD=10
ALERT_FAILED_LOGIN_WINDOW=300 # 5 minutes
ALERT_RATE_LIMIT_THRESHOLD=100
ALERT_UNUSUAL_ACCESS_ENABLED=true
# Alert destinations
ALERT_EMAIL=security@yourdomain.com
ALERT_SLACK_WEBHOOK=https://hooks.slack.com/...
ALERT_PAGERDUTY_KEY=your-pagerduty-key# Install and configure fail2ban
sudo apt install fail2ban
# Create jail configuration
# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 3600
# Restart fail2ban
sudo systemctl restart fail2bannself prod check and address all issues# Check for nself updates
nself update check
# Update nself CLI
nself update
# Update container images
nself update images
# Review security advisories
nself security advisories# Scan container images for vulnerabilities
docker scan hasura/graphql-engine:latest
docker scan postgres:16
# Use Trivy for comprehensive scanning
trivy image hasura/graphql-engine:latest
trivy image postgres:16# Immediately rotate all secrets
nself prod secrets rotate --all --force
# Block all external access (emergency)
nself prod firewall lockdown
# Take system offline for maintenance
nself stop
# Restore from clean backup
nself db restore --clean latest-verified-backup.sql.gzSecurity is an ongoing process, not a one-time configuration. Regularly audit your deployment, stay updated on security best practices, and monitor for potential threats.