Security Best Practices
This guide provides comprehensive security best practices for deploying and maintaining a secure Fluxbase instance.
Table of Contents
Section titled “Table of Contents”- Authentication & Authorization
- SAML SSO Security
- GraphQL API Security
- Database Security
- Network Security
- Secrets Management
- Input Validation
- Error Handling
- Logging & Monitoring
- Deployment Security
- Incident Response
Authentication & Authorization
Section titled “Authentication & Authorization”1. Enforce Strong Password Policies
Section titled “1. Enforce Strong Password Policies”auth: 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 # Force password rotationClient-side validation:
function validatePassword(password: string): string[] { const errors: string[] = [];
if (password.length < 12) { errors.push("Password must be at least 12 characters"); } if (!/[A-Z]/.test(password)) { errors.push("Password must contain an uppercase letter"); } if (!/[a-z]/.test(password)) { errors.push("Password must contain a lowercase letter"); } if (!/[0-9]/.test(password)) { errors.push("Password must contain a number"); } if (!/[!@#$%^&*]/.test(password)) { errors.push("Password must contain a special character"); }
return errors;}2. Implement Multi-Factor Authentication
Section titled “2. Implement Multi-Factor Authentication”// Enable 2FA for userconst { qr_code, secret } = await client.auth.setup2FA();
// Display QR code to usershowQRCode(qr_code);
// Verify and enableawait client.auth.enable2FA({ code: userEnteredCode });Enforce 2FA for sensitive operations:
auth: require_2fa_for_admins: true require_2fa_for_sensitive_ops: true3. Use Short-Lived Tokens
Section titled “3. Use Short-Lived Tokens”auth: access_token_expiry: "15m" # Short-lived access tokens refresh_token_expiry: "7d" # Longer refresh tokens refresh_token_rotation: true # Rotate on each use4. Implement Token Blacklisting
Section titled “4. Implement Token Blacklisting”// Logout revokes tokensawait client.auth.signOut(); // Token added to blacklist
// Verify token isn't blacklisted in middlewareconst isBlacklisted = await checkTokenBlacklist(token);if (isBlacklisted) { throw new Error("Token has been revoked");}5. Limit Failed Login Attempts
Section titled “5. Limit Failed Login Attempts”auth: max_login_attempts: 5 lockout_duration: "15m" lockout_type: "ip_and_email" # Lock both IP and email6. Implement Row Level Security
Section titled “6. Implement Row Level Security”-- Enable RLS on ALL user tablesALTER TABLE public.user_data ENABLE ROW LEVEL SECURITY;ALTER TABLE public.user_data FORCE ROW LEVEL SECURITY;
-- Create isolation policyCREATE POLICY user_isolation ON public.user_data FOR ALL USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());SAML SSO Security
Section titled “SAML SSO Security”When configuring SAML SSO providers, follow these security best practices to prevent common attacks.
1. Disable IdP-Initiated SSO
Section titled “1. Disable IdP-Initiated SSO”IdP-initiated SSO is vulnerable to assertion replay attacks. Only enable it if your IdP requires it.
auth: saml_providers: - name: okta enabled: true allow_idp_initiated: false # Recommended - prevents replay attacks2. Validate Audience Restrictions
Section titled “2. Validate Audience Restrictions”Fluxbase automatically validates that SAML assertions are intended for your service provider by checking the Audience element. This prevents attackers from replaying assertions meant for other applications.
3. Configure RelayState Redirect Whitelist
Section titled “3. Configure RelayState Redirect Whitelist”Prevent open redirect attacks by configuring allowed redirect hosts for SAML authentication.
auth: saml_providers: - name: okta enabled: true # Only allow redirects to your application domains allowed_redirect_hosts: - "app.example.com" - "dashboard.example.com"Without this configuration, only relative URLs (same-origin) are allowed for redirects.
4. Require HTTPS for Metadata URLs
Section titled “4. Require HTTPS for Metadata URLs”Always use HTTPS for IdP metadata URLs to prevent man-in-the-middle attacks.
auth: saml_providers: - name: okta enabled: true idp_metadata_url: "https://company.okta.com/app/xxx/sso/saml/metadata" allow_insecure_metadata_url: false # Default - requires HTTPS5. User Attribute Sanitization
Section titled “5. User Attribute Sanitization”Fluxbase automatically sanitizes user attributes (like display names) from SAML assertions to prevent XSS attacks from malicious IdP responses.
GraphQL API Security
Section titled “GraphQL API Security”Fluxbase implements several security measures for the GraphQL API to prevent abuse and resource exhaustion.
1. Query Depth Limiting
Section titled “1. Query Depth Limiting”Deeply nested queries can cause exponential resource consumption. Configure maximum query depth.
graphql: enabled: true max_depth: 10 # Maximum nesting depthExample rejected query (depth > 5):
{ users { # depth 1 posts { # depth 2 comments { # depth 3 author { # depth 4 posts { # depth 5 title # depth 6 - REJECTED } } } } }}2. Query Complexity Analysis
Section titled “2. Query Complexity Analysis”Fluxbase calculates a complexity score for each query based on fields and list traversals.
graphql: max_complexity: 1000 # Maximum complexity scoreThe complexity score accounts for:
- Base cost per field (1 point)
- Higher cost for list fields (10 points)
- Multiplied cost for nested lists
3. Disable Introspection in Production
Section titled “3. Disable Introspection in Production”GraphQL introspection exposes your entire schema. Disable it in production.
graphql: introspection: false # Disable schema introspection4. Row Level Security (RLS) Enforcement
Section titled “4. Row Level Security (RLS) Enforcement”The GraphQL endpoint enforces PostgreSQL Row Level Security for all operations:
How it works:
- GraphQL resolvers execute queries with
SET LOCAL ROLEset to the appropriate database role - JWT claims are passed via
request.jwt.claimsfor use in RLS policies - All queries, mutations, and foreign key traversals respect RLS policies
- Anonymous requests use the
anonrole - Authenticated users use the
authenticatedrole - Service keys use the
service_role(bypasses RLS)
Role Mapping:
| Application Role | Database Role |
|---|---|
service_role, dashboard_admin | service_role (bypasses RLS) |
anon, empty | anon |
All others (user, admin, etc.) | authenticated |
Configure RLS on your tables:
-- Enable RLS on tables accessed via GraphQLALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;ALTER TABLE public.posts FORCE ROW LEVEL SECURITY;
-- Create appropriate policiesCREATE POLICY posts_select ON public.posts FOR SELECT USING (user_id = auth.uid() OR is_public = true);
-- Use JWT claims for fine-grained accessCREATE POLICY admin_full_access ON public.posts FOR ALL USING ( current_setting('request.jwt.claims', true)::jsonb->>'role' = 'admin' );Important: Foreign key traversals also respect RLS. A user cannot access related records through GraphQL joins if they don’t have permission to view those records directly.
Database Security
Section titled “Database Security”1. Use Principle of Least Privilege
Section titled “1. Use Principle of Least Privilege”-- Create application user with minimal permissionsCREATE USER fluxbase_app WITH PASSWORD 'secure_password';
-- Grant only necessary permissionsGRANT CONNECT ON DATABASE fluxbase TO fluxbase_app;GRANT USAGE ON SCHEMA public TO fluxbase_app;GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fluxbase_app;
-- Revoke dangerous permissionsREVOKE CREATE ON SCHEMA public FROM fluxbase_app;REVOKE ALL ON SCHEMA pg_catalog FROM fluxbase_app;
-- For read-only operationsCREATE USER readonly_user WITH PASSWORD 'secure_password';GRANT CONNECT ON DATABASE fluxbase TO readonly_user;GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;2. Enable Encrypted Connections
Section titled “2. Enable Encrypted Connections”database: url: "postgres://user:pass@host:5432/db?sslmode=require" max_connections: 50 ssl_mode: "require" # or "verify-full" for productionPostgreSQL SSL Configuration:
# postgresql.confssl = onssl_cert_file = '/path/to/server.crt'ssl_key_file = '/path/to/server.key'ssl_ca_file = '/path/to/root.crt'3. Regular Backups with Encryption
Section titled “3. Regular Backups with Encryption”#!/bin/bash# Backup with encryptionpg_dump -U postgres fluxbase | \ gpg --encrypt --recipient admin@example.com > \ /backups/fluxbase-$(date +%Y%m%d).sql.gpg
# Rotate old backupsfind /backups -name "fluxbase-*.sql.gpg" -mtime +30 -deleteAutomated backups:
# crontab0 2 * * * /usr/local/bin/backup.sh4. Audit Database Access
Section titled “4. Audit Database Access”-- Enable audit loggingCREATE EXTENSION IF NOT EXISTS pgaudit;
-- Configure audit settingsALTER SYSTEM SET pgaudit.log = 'write, ddl';ALTER SYSTEM SET pgaudit.log_catalog = off;ALTER SYSTEM SET pgaudit.log_parameter = on;
-- Reload configurationSELECT pg_reload_conf();5. Protect Against SQL Injection
Section titled “5. Protect Against SQL Injection”// ✅ GOOD: Parameterized queriesconst { data } = await client .from("users") .select("*") .eq("email", userEmail) // Safely parameterized .execute();
// ❌ BAD: String concatenationconst query = `SELECT * FROM users WHERE email = '${userEmail}'`;// NEVER DO THIS!Network Security
Section titled “Network Security”1. Always Use HTTPS in Production
Section titled “1. Always Use HTTPS in Production”server: port: 443 tls: enabled: true cert_file: /etc/letsencrypt/live/example.com/fullchain.pem key_file: /etc/letsencrypt/live/example.com/privkey.pem min_version: "1.2" # TLS 1.2 minimumAutomatic certificate renewal with Let’s Encrypt:
# Install certbotapt-get install certbot
# Get certificatecertbot certonly --standalone -d example.com
# Auto-renewal (crontab)0 0 1 * * certbot renew --quiet && systemctl reload fluxbase2. Configure Firewall Rules
Section titled “2. Configure Firewall Rules”# UFW (Ubuntu)ufw default deny incomingufw default allow outgoingufw allow 22/tcp # SSHufw allow 443/tcp # HTTPSufw allow 80/tcp # HTTP (for Let's Encrypt)ufw enable
# Restrict PostgreSQL accessufw allow from 10.0.0.0/8 to any port 5432iptables alternative:
# Block all incoming except SSH and HTTPSiptables -P INPUT DROPiptables -A INPUT -p tcp --dport 22 -j ACCEPTiptables -A INPUT -p tcp --dport 443 -j ACCEPTiptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT3. Enable Rate Limiting
Section titled “3. Enable Rate Limiting”rate_limiting: enabled: true per_minute: 60 per_hour: 1000
# Stricter limits for sensitive endpoints endpoints: - path: "/api/v1/auth/login" per_minute: 5 per_hour: 20
- path: "/api/v1/auth/signup" per_minute: 3 per_hour: 10
- path: "/api/v1/auth/password/reset" per_minute: 2 per_hour: 5Learn more about Rate Limiting →
4. Configure CORS Properly
Section titled “4. Configure CORS Properly”server: cors: # ✅ GOOD: Specific origins allowed_origins: - "https://yourdomain.com" - "https://www.yourdomain.com"
# ❌ BAD: Wildcard allows any origin # allowed_origins: ["*"]
allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] allowed_headers: ["Content-Type", "Authorization", "X-CSRF-Token"] allow_credentials: true # Required for cookies max_age: 36005. Implement Security Headers
Section titled “5. Implement Security Headers”security: headers: content_security_policy: "default-src 'self'" x_frame_options: "DENY" strict_transport_security: "max-age=31536000; includeSubDomains"Learn more about Security Headers →
Secrets Management
Section titled “Secrets Management”1. Never Commit Secrets to Git
Section titled “1. Never Commit Secrets to Git”.env.env.local.env.*.localfluxbase.yaml*.pem*.keysecrets/Check for committed secrets:
# Use git-secretsgit secrets --scan
# Use truffleHogtrufflehog filesystem .2. Use Environment Variables
Section titled “2. Use Environment Variables”# fluxbase.yaml - reference environment variablesdatabase: url: ${DATABASE_URL}
auth: jwt_secret: ${JWT_SECRET}
email: smtp_password: ${SMTP_PASSWORD}# .env (never commit!)DATABASE_URL=postgres://user:pass@host/dbJWT_SECRET=your-super-secret-key-min-32-charsSMTP_PASSWORD=smtp-password-here3. Use Secrets Management Services
Section titled “3. Use Secrets Management Services”Docker Secrets:
# Create secretecho "my-jwt-secret" | docker secret create jwt_secret -
# Use in docker-compose.ymlservices: fluxbase: secrets: - jwt_secret environment: JWT_SECRET_FILE: /run/secrets/jwt_secretKubernetes Secrets:
# Create secretkubectl create secret generic fluxbase-secrets \ --from-literal=jwt-secret=my-jwt-secret \ --from-literal=database-url=postgres://...
# Use in deploymentenv: - name: JWT_SECRET valueFrom: secretKeyRef: name: fluxbase-secrets key: jwt-secretAWS Secrets Manager:
import { SecretsManager } from "@aws-sdk/client-secrets-manager";
const client = new SecretsManager({ region: "us-east-1" });
async function getSecret(secretName: string): Promise<string> { const response = await client.getSecretValue({ SecretId: secretName }); return response.SecretString || "";}
// Use in applicationconst jwtSecret = await getSecret("fluxbase/jwt-secret");4. Rotate Secrets Regularly
Section titled “4. Rotate Secrets Regularly”#!/bin/bash# Generate new JWT secretNEW_SECRET=$(openssl rand -base64 32)
# Update in secrets managerkubectl patch secret fluxbase-secrets \ -p="{\"data\":{\"jwt-secret\":\"$(echo -n $NEW_SECRET | base64)\"}}"
# Rolling restartkubectl rollout restart deployment/fluxbase5. Limit Secret Access
Section titled “5. Limit Secret Access”# Kubernetes RBACapiVersion: v1kind: ServiceAccountmetadata: name: fluxbase---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: secret-readerrules:- apiGroups: [""] resources: ["secrets"] resourceNames: ["fluxbase-secrets"] verbs: ["get"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: read-secretssubjects:- kind: ServiceAccount name: fluxbaseroleRef: kind: Role name: secret-reader apiGroup: rbac.authorization.k8s.ioInput Validation
Section titled “Input Validation”1. Validate All User Input
Section titled “1. Validate All User Input”import validator from "validator";
interface CreateUserInput { email: string; name: string; age?: number; website?: string;}
function validateUserInput(input: CreateUserInput): string[] { const errors: string[] = [];
// Email validation if (!validator.isEmail(input.email)) { errors.push("Invalid email address"); }
// Name validation if (!input.name || input.name.length < 2 || input.name.length > 100) { errors.push("Name must be between 2 and 100 characters"); }
// Age validation if (input.age !== undefined) { if (!Number.isInteger(input.age) || input.age < 0 || input.age > 150) { errors.push("Age must be between 0 and 150"); } }
// URL validation if (input.website && !validator.isURL(input.website)) { errors.push("Invalid website URL"); }
return errors;}2. Sanitize User Input
Section titled “2. Sanitize User Input”import DOMPurify from "isomorphic-dompurify";
// Sanitize HTMLconst safeHTML = DOMPurify.sanitize(userInput);
// Escape for SQL (use parameterized queries instead!)import escape from "pg-escape";
// Validate and sanitize file uploadsfunction validateUpload(file: File): boolean { const allowedTypes = ["image/jpeg", "image/png", "image/gif"]; const maxSize = 5 * 1024 * 1024; // 5MB
if (!allowedTypes.includes(file.type)) { throw new Error("Invalid file type"); }
if (file.size > maxSize) { throw new Error("File too large"); }
return true;}3. Use Type Validation Libraries
Section titled “3. Use Type Validation Libraries”import { z } from "zod";
// Define schemaconst userSchema = z.object({ email: z.string().email(), name: z.string().min(2).max(100), age: z.number().int().min(0).max(150).optional(), website: z.string().url().optional(),});
// Validate inputtry { const validatedData = userSchema.parse(userInput); // Use validatedData safely} catch (error) { if (error instanceof z.ZodError) { console.error("Validation errors:", error.errors); }}4. Implement Rate Limiting for Forms
Section titled “4. Implement Rate Limiting for Forms”// Client-side debouncingimport { debounce } from "lodash";
const debouncedSubmit = debounce( async (data) => { await submitForm(data); }, 1000, { leading: true, trailing: false });Error Handling
Section titled “Error Handling”1. Don’t Leak Sensitive Information
Section titled “1. Don’t Leak Sensitive Information”// ✅ GOOD: Generic error messagestry { await client.auth.signIn({ email, password });} catch (error) { throw new Error("Invalid email or password");}
// ❌ BAD: Reveals whether user existstry { const user = await findUser(email); if (!user) { throw new Error("User not found"); } if (!verifyPassword(password, user.password_hash)) { throw new Error("Incorrect password"); }} catch (error) { // Reveals too much information throw error;}2. Log Errors Securely
Section titled “2. Log Errors Securely”// ✅ GOOD: Log without sensitive datalogger.error("Authentication failed", { ip: req.ip, user_agent: req.headers["user-agent"], timestamp: new Date().toISOString(),});
// ❌ BAD: Logs sensitive datalogger.error("Authentication failed", { email: req.body.email, password: req.body.password, // NEVER LOG PASSWORDS! token: req.headers.authorization,});3. Implement Error Boundaries
Section titled “3. Implement Error Boundaries”// Global error handlerapp.use((err: Error, req, res, next) => { // Log full error internally logger.error("Unhandled error", { error: err, stack: err.stack });
// Return generic error to client res.status(500).json({ error: "Internal server error", message: process.env.NODE_ENV === "development" ? err.message : "An unexpected error occurred", });});Logging & Monitoring
Section titled “Logging & Monitoring”1. Enable Audit Logging
Section titled “1. Enable Audit Logging”logging: level: "info" audit_enabled: true audit_log_file: "/var/log/fluxbase/audit.log" audit_events: - "auth.login" - "auth.logout" - "auth.signup" - "auth.password_reset" - "admin.user_create" - "admin.user_delete" - "admin.role_change"2. Monitor Security Events
Section titled “2. Monitor Security Events”// Set up alerts for suspicious activityconst alerts = { failed_logins: { threshold: 10, window: "5m", action: "notify_admin", }, unusual_api_activity: { threshold: 1000, window: "1m", action: "rate_limit", }, admin_actions: { threshold: 1, window: "0s", action: "log_and_notify", },};3. Use Structured Logging
Section titled “3. Use Structured Logging”import winston from "winston";
const logger = winston.createLogger({ format: winston.format.json(), transports: [ new winston.transports.File({ filename: "error.log", level: "error" }), new winston.transports.File({ filename: "combined.log" }), ],});
logger.info("User logged in", { user_id: user.id, ip: req.ip, user_agent: req.headers["user-agent"], timestamp: new Date().toISOString(),});4. Monitor Performance Metrics
Section titled “4. Monitor Performance Metrics”# Enable Prometheus metricsmonitoring: enabled: true port: 9090 path: "/metrics"
# Monitor key metricsmetrics: - request_duration - request_count - error_rate - active_connections - database_query_time - cache_hit_rateDeployment Security
Section titled “Deployment Security”1. Use Container Security Scanning
Section titled “1. Use Container Security Scanning”# Scan Docker imagesdocker scan fluxbase/fluxbase:latest
# Use Trivytrivy image fluxbase/fluxbase:latest
# Use Snyksnyk container test fluxbase/fluxbase:latest2. Run as Non-Root User
Section titled “2. Run as Non-Root User”# DockerfileFROM node:25-alpine
# Create non-root userRUN addgroup -g 1001 -S fluxbase && \ adduser -S fluxbase -u 1001
# Switch to non-root userUSER fluxbase
# Run applicationCMD ["node", "server.js"]3. Use Read-Only File System
Section titled “3. Use Read-Only File System”services: fluxbase: image: ghcr.io/fluxbase-eu/fluxbase:latest:latest read_only: true tmpfs: - /tmp - /var/run4. Limit Container Resources
Section titled “4. Limit Container Resources”services: fluxbase: deploy: resources: limits: cpus: "2" memory: 2G reservations: cpus: "1" memory: 1G5. Enable Security Contexts (Kubernetes)
Section titled “5. Enable Security Contexts (Kubernetes)”apiVersion: apps/v1kind: Deploymentspec: template: spec: securityContext: runAsNonRoot: true runAsUser: 1001 fsGroup: 1001 containers: - name: fluxbase securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALLIncident Response
Section titled “Incident Response”1. Create Incident Response Plan
Section titled “1. Create Incident Response Plan”# Incident Response Plan
## Detection
- Monitor logs for suspicious activity- Set up automated alerts- Regular security audits
## Containment
1. Isolate affected systems2. Block malicious IPs3. Revoke compromised credentials4. Enable additional logging
## Eradication
1. Identify root cause2. Patch vulnerabilities3. Remove malicious code4. Update security rules
## Recovery
1. Restore from clean backups2. Verify system integrity3. Monitor for persistence4. Gradually restore services
## Post-Incident
1. Document incident2. Update security measures3. Train team members4. Conduct retrospective2. Prepare Recovery Procedures
Section titled “2. Prepare Recovery Procedures”#!/bin/bash# 1. Stop servicessystemctl stop fluxbase
# 2. Restore database from backuppg_restore -U postgres -d fluxbase /backups/latest.dump
# 3. Verify integritypsql -U postgres -d fluxbase -c "SELECT COUNT(*) FROM auth.users"
# 4. Start servicessystemctl start fluxbase
# 5. Verify functionalitycurl https://api.example.com/health3. Maintain Security Contacts
Section titled “3. Maintain Security Contacts”security_team: - name: "Security Lead" email: "security@example.com" phone: "+1-555-0100"
- name: "Infrastructure Lead" email: "infra@example.com" phone: "+1-555-0101"
external_contacts: - name: "Security Researcher" email: "researcher@example.com"
- name: "Hosting Provider" email: "support@hosting.com" phone: "+1-555-0200"Security Checklist
Section titled “Security Checklist”Pre-Production
Section titled “Pre-Production”- Strong password policies enforced
- 2FA enabled for admin accounts
- HTTPS/TLS configured with valid certificates
- Database connections encrypted
- RLS policies reviewed and tested
- Rate limiting configured
- Security headers configured
- CORS properly configured
- Secrets stored securely (not in code)
- Input validation implemented
- Error handling doesn’t leak information
- Audit logging enabled
- Backup strategy implemented
- Firewall rules configured
- Container security scanning passed
- Dependency vulnerabilities resolved
- Penetration testing completed
Post-Production
Section titled “Post-Production”- Monitor logs daily
- Review audit logs weekly
- Update dependencies monthly
- Rotate secrets quarterly
- Conduct security audits annually
- Test backups monthly
- Review access controls quarterly
- Update incident response plan annually
- Train team on security quarterly
Further Reading
Section titled “Further Reading”- Security Overview
- CSRF Protection
- Security Headers
- Row Level Security
- Rate Limiting
- OWASP Top 10
- CWE Top 25
Summary
Section titled “Summary”Security is a continuous process, not a one-time task:
- ✅ Authentication: Strong passwords, 2FA, short-lived tokens
- ✅ Authorization: RLS, RBAC, least privilege
- ✅ Network: HTTPS, firewall, rate limiting
- ✅ Secrets: Environment variables, secrets management
- ✅ Validation: Input validation, sanitization, type checking
- ✅ Errors: Generic messages, secure logging
- ✅ Monitoring: Audit logs, alerts, metrics
- ✅ Deployment: Container security, non-root user
- ✅ Response: Incident plan, recovery procedures
Follow these best practices and stay vigilant to maintain a secure Fluxbase instance.