Reference Guide Detailed deployment notes with production context and concrete examples.

Flask Security Checklist

If you're preparing a Flask app for production and need to reduce security risk, this guide gives you a practical hardening checklist. Use it to lock down configuration, transport security, process permissions, and common deployment weak points before launch or during an audit.

Quick Fix / Quick Setup

Apply the highest-impact controls first:

bash
# 1) Ensure Flask is not running in debug mode
export FLASK_ENV=production
export DEBUG=0

# 2) Generate a strong secret key
python - <<'PY'
import secrets
print(secrets.token_hex(32))
PY

# 3) Restrict Gunicorn/systemd process user
sudo systemctl edit gunicorn
# Add or verify:
# [Service]
# User=www-data
# Group=www-data
# NoNewPrivileges=true
# PrivateTmp=true

# 4) Enable firewall for web traffic only
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

# 5) Check HTTPS and security headers
curl -I https://your-domain.com

# 6) Validate Nginx config before reload
sudo nginx -t && sudo systemctl reload nginx

This covers the most common production gaps first: debug mode, weak secrets, over-privileged processes, unnecessary open ports, and missing HTTPS validation.

What’s Happening

Flask production security depends on multiple layers: app configuration, Gunicorn/systemd isolation, Nginx edge controls, firewall rules, secrets handling, and restricted database access.

Most production security failures come from deployment mistakes, not Flask itself: debug mode left on, exposed .env files, public Gunicorn binds, missing HTTPS, weak upload handling, or excessive service permissions.

Step-by-Step Guide

  1. Disable development settings
    Confirm Flask debug mode is off and do not use the Werkzeug development server in production.
    bash
    export FLASK_ENV=production
    export DEBUG=0
    

    In your Flask config:
    python
    DEBUG = False
    TESTING = False
    
  2. Set a strong SECRET_KEY outside source control
    Generate a key:
    bash
    python - <<'PY'
    import secrets
    print(secrets.token_hex(32))
    PY
    

    Load it from the environment:
    python
    import os
    SECRET_KEY = os.environ["SECRET_KEY"]
    

    If you need a full setup pattern, use Flask Environment Variables and Secrets Setup.
  3. Move credentials out of the repository
    Do not hardcode database URLs, API keys, SMTP passwords, or token secrets.
    Example systemd environment file:
    ini
    # /etc/myapp/myapp.env
    SECRET_KEY=replace_me
    DATABASE_URL=postgresql://appuser:strongpassword@127.0.0.1/mydb
    

    Restrict permissions:
    bash
    sudo chown root:www-data /etc/myapp/myapp.env
    sudo chmod 640 /etc/myapp/myapp.env
    
  4. Run Gunicorn as a dedicated non-root user
    In your systemd service:
    ini
    [Service]
    User=www-data
    Group=www-data
    WorkingDirectory=/srv/myapp
    EnvironmentFile=/etc/myapp/myapp.env
    ExecStart=/srv/myapp/venv/bin/gunicorn --workers 3 --bind unix:/run/gunicorn.sock wsgi:app
    NoNewPrivileges=true
    PrivateTmp=true
    ProtectSystem=full
    ProtectHome=true
    

    Reload and restart:
    bash
    sudo systemctl daemon-reload
    sudo systemctl restart gunicorn
    sudo systemctl status gunicorn --no-pager
    
  5. Restrict file permissions
    Application code should not be world-writable. Secrets files should be readable only by root or the service group.
    bash
    sudo find /srv/myapp -type f -perm /o+w
    sudo chmod 640 /etc/myapp/myapp.env
    sudo chmod 755 /srv/myapp
    
  6. Terminate TLS at Nginx
    Enforce HTTPS and use a valid certificate.
    Example server blocks:
    nginx
    server {
        listen 80;
        server_name your-domain.com www.your-domain.com;
        return 301 https://$host$request_uri;
    }
    
    server {
        listen 443 ssl http2;
        server_name your-domain.com www.your-domain.com;
    
        ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    
        location / {
            include proxy_params;
            proxy_pass http://unix:/run/gunicorn.sock;
        }
    }
    

    Validate:
    bash
    sudo nginx -t && sudo systemctl reload nginx
    curl -I http://your-domain.com
    curl -I https://your-domain.com
    

    Also review Flask HTTPS and Domain Checklist.
  7. Enable secure cookie settings
    In Flask config:
    python
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = "Lax"
    REMEMBER_COOKIE_SECURE = True
    REMEMBER_COOKIE_HTTPONLY = True
    REMEMBER_COOKIE_SAMESITE = "Lax"
    
  8. Configure proxy awareness correctly
    If Nginx terminates TLS, Flask must trust only the expected proxy headers.
    python
    from werkzeug.middleware.proxy_fix import ProxyFix
    app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
    

    Do this only when Flask is behind your trusted reverse proxy and Gunicorn is not publicly exposed.
  9. Add core Nginx security headers
    Add headers in the TLS server block:
    nginx
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    

    If compatible with your frontend, add a Content Security Policy:
    nginx
    add_header Content-Security-Policy "default-src 'self';" always;
    
  10. Lock down allowed hosts and domain routing
    Use only expected server_name values and remove default server blocks that can expose unintended apps.
    bash
    sudo grep -R "server_name" /etc/nginx/sites-enabled
    
  11. Restrict network exposure
    Expose only 80 and 443 publicly. Bind Gunicorn to a Unix socket or 127.0.0.1.
    bash
    ss -tulpn
    sudo ufw status verbose
    

    Gunicorn example:
    bash
    gunicorn --bind 127.0.0.1:8000 wsgi:app
    

    Preferred:
    bash
    gunicorn --bind unix:/run/gunicorn.sock wsgi:app
    
  12. Harden file uploads
    Enforce size limits and sanitize names.
    In Flask:
    python
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024
    

    Use safe filenames:
    python
    from werkzeug.utils import secure_filename
    filename = secure_filename(upload.filename)
    

    In Nginx:
    nginx
    client_max_body_size 16M;
    

    Store uploads outside executable paths and avoid serving raw user uploads directly unless required.
  13. Secure the database connection
    Use least-privilege database users and do not expose the database publicly unless necessary.
    Check listening addresses:
    bash
    ss -tulpn | grep 5432
    ss -tulpn | grep 3306
    

    Restrict access with firewall rules and database bind settings.
  14. Install dependency and OS updates
    Review outdated Python packages:
    bash
    python -m pip list --outdated
    python -m pip audit
    

    Review system packages:
    bash
    sudo apt list --upgradable
    
  15. Add logging and monitoring
    Review logs regularly:
    bash
    sudo journalctl -u gunicorn -n 100 --no-pager
    sudo tail -n 100 /var/log/nginx/access.log
    sudo tail -n 100 /var/log/nginx/error.log
    

    Track:
    • app exceptions
    • repeated 4xx/5xx spikes
    • auth failures
    • abnormal request rates
    • unexpected upload attempts
  16. Set rate limits and request size limits at Nginx
    Example:
    nginx
    http {
        limit_req_zone $binary_remote_addr zone=app_limit:10m rate=10r/s;
    }
    
    server {
        client_max_body_size 16M;
    
        location /login {
            limit_req zone=app_limit burst=20 nodelay;
            include proxy_params;
            proxy_pass http://unix:/run/gunicorn.sock;
        }
    }
    
  17. Review admin interfaces and background tools
    Protect dashboards, queue monitors, internal APIs, and task runners with authentication and network restrictions. Do not leave admin paths publicly accessible by default.
  18. Back up critical data securely
    Ensure backups exist, are access-controlled, and can be restored.
    Validation should include:
    • backup schedule exists
    • restore test completed
    • backup storage permissions restricted
    • encryption used where required
  19. Scan your deployment
    Validate runtime configuration and exposure.
    bash
    curl -I https://your-domain.com
    ss -tulpn
    sudo nginx -t
    python -m pip audit
    
  20. Re-run the checklist after every infrastructure or app change
    Re-check security after:
    • Nginx changes
    • DNS updates
    • new upload features
    • CI/CD changes
    • service user changes
    • added background workers

Common Causes

  • Debug mode left enabled → exposes stack traces and unsafe debugger behavior → set production config explicitly and verify runtime values.
  • SECRET_KEY committed to source control or reused across environments → weakens session and token integrity → rotate the key and load it from environment variables.
  • Gunicorn running as root → increases blast radius if compromised → run under a dedicated service account.
  • Gunicorn bound to a public interface → bypasses Nginx protections → bind to a Unix socket or 127.0.0.1.
  • Missing HTTPS redirect or invalid certificate → breaks transport security and browser trust → fix Nginx TLS config and renewal.
  • Environment files readable by all users → secrets can leak locally → restrict ownership and permissions.
  • Database port exposed publicly → increases attack surface → restrict inbound access and use least-privilege credentials.
  • File uploads stored unsafely → allows malicious file handling or overwrite issues → validate, sanitize, and isolate uploads.
  • Missing security headers → reduces browser-side protections → configure headers in Nginx or Flask.
  • Outdated dependencies or OS packages → known vulnerabilities remain exploitable → patch regularly and audit installed packages.

Debugging Section

Check actual runtime state, not only static config files.

Validate Flask security settings

bash
python - <<'PY'
from app import app
print('DEBUG=', app.config.get('DEBUG'))
print('SESSION_COOKIE_SECURE=', app.config.get('SESSION_COOKIE_SECURE'))
print('SESSION_COOKIE_HTTPONLY=', app.config.get('SESSION_COOKIE_HTTPONLY'))
print('SECRET_KEY_SET=', bool(app.config.get('SECRET_KEY')))
PY

What to look for:

  • DEBUG=False
  • secure cookie flags enabled
  • SECRET_KEY_SET=True

Inspect Gunicorn systemd configuration

bash
sudo systemctl cat gunicorn
sudo systemctl status gunicorn --no-pager
sudo journalctl -u gunicorn -n 100 --no-pager

What to look for:

  • User= and Group= set to a non-root account
  • EnvironmentFile= points to the correct path
  • hardening directives are applied
  • no startup failures from permissions or missing environment variables

Validate Nginx security configuration

bash
sudo nginx -t
sudo systemctl status nginx --no-pager
sudo grep -R "server_name\|ssl_certificate\|add_header\|client_max_body_size" /etc/nginx/sites-enabled /etc/nginx/nginx.conf

What to look for:

  • HTTP to HTTPS redirect exists
  • valid certificate paths
  • headers are present in the TLS block
  • upload/body limits are defined where needed

Test headers and redirects

bash
curl -I http://your-domain.com
curl -I https://your-domain.com

What to look for:

  • 301 or 308 redirect from HTTP to HTTPS
  • Strict-Transport-Security after HTTPS is stable
  • X-Content-Type-Options: nosniff
  • correct Set-Cookie attributes

Verify listening ports and firewall state

bash
ss -tulpn
sudo ufw status verbose

What to look for:

  • only expected public ports exposed
  • Gunicorn not listening on a public interface
  • database ports not publicly reachable unless explicitly required

Check file and secret permissions

bash
ls -lah /path/to/app
ls -lah /path/to/app/.env
stat /path/to/app/.env
sudo -u www-data test -r /path/to/app/.env && echo readable || echo not-readable

What to look for:

  • secrets not world-readable
  • app user can read only what it needs
  • upload directories are writable only where required

Audit package state

bash
python -m pip list --outdated
python -m pip audit
sudo apt list --upgradable

What to look for:

  • vulnerable Python packages
  • pending security updates
  • outdated web stack components

Checklist

  • Flask debug mode is disabled in production.
  • SECRET_KEY is strong and not committed to version control.
  • Database credentials and API secrets are loaded from environment variables or a secure secret source.
  • Gunicorn runs as a dedicated non-root user.
  • systemd service files and environment files have restricted permissions.
  • Gunicorn is bound to a Unix socket or localhost, not a public interface unless required.
  • UFW or cloud firewall exposes only required ports.
  • Nginx is serving a valid TLS certificate.
  • HTTP requests redirect to HTTPS.
  • SESSION_COOKIE_SECURE and SESSION_COOKIE_HTTPONLY are enabled.
  • Security headers are returned by Nginx or the app.
  • File upload limits and validation are configured.
  • Uploaded files are stored with safe permissions and non-executable paths.
  • Database user permissions are limited to application needs.
  • Server packages and Python dependencies are updated.
  • Access logs and error logs are enabled and reviewed.
  • Backups exist and restore testing has been performed.
  • No default credentials, sample configs, or unused services remain on the server.

FAQ

Q: What are the minimum security settings for a Flask production deploy?
A: Disable debug, use a strong SECRET_KEY, run behind Nginx with HTTPS, load secrets from environment variables, run Gunicorn as a non-root user, and restrict public ports.

Q: Should I store .env on the server?
A: Only if necessary, with strict permissions and limited ownership. A secrets manager or protected systemd environment file is safer.

Q: Do I need both firewall rules and Nginx restrictions?
A: Yes. Nginx protects at the web layer; firewall rules reduce exposed network surface.

Q: How do I verify secure cookies are working?
A: Inspect Set-Cookie headers in browser developer tools or with curl and confirm Secure, HttpOnly, and SameSite attributes are present.

Q: Is UFW enough to secure database access?
A: It helps, but also restrict access in cloud security groups, database bind settings, and database user privileges.

Final Takeaway

Flask security in production is mostly a deployment discipline problem: disable debug, protect secrets, enforce HTTPS, isolate services, restrict network exposure, and validate the result continuously.

Use this checklist as a release gate and re-run it after every infrastructure or application change.