Skip to main content

Secure Configuration in Python: Environment and Hardening

Secure configuration is the practice of setting up applications and infrastructure so that default behavior is secure and dangerous features are disabled. A single misconfiguration (debug mode on in production, exposed admin panel, weak CORS policy) can undermine all other security measures. Most breaches include a misconfiguration as a contributing factor. Python frameworks like Flask and Django have secure defaults, but developers must explicitly configure them for production environments. This article covers the most critical configuration decisions for Python applications.

Debug Mode: The Most Dangerous Misconfiguration

Debug mode is invaluable during development: it provides detailed error messages, enables code reloading, and allows interactive debugging. In production, debug mode is a liability. When enabled, it exposes:

  • Full stack traces (revealing code structure and internal libraries).
  • Environment variables (including secrets like API keys).
  • The interactive debugger (allowing arbitrary code execution).
  • Database queries (revealing schema and business logic).

Flask debug mode on in production has resulted in countless breaches:

# INSECURE — debug mode on in production
from flask import Flask
app = Flask(__name__)
app.run(debug=True) # NEVER do this in production!

# SECURE — debug based on environment
from os import getenv
app = Flask(__name__)
app.config['DEBUG'] = getenv('FLASK_DEBUG', 'False') == 'True'
if app.config['DEBUG']:
print("Running in debug mode")
else:
print("Running in production mode")

app.run()

Django also has a DEBUG setting:

# Django settings.py
# INSECURE
DEBUG = True

# SECURE
DEBUG = False # Always False in production
# For development, set DEBUG=True via environment variable or local settings file
# .env: DEBUG=True (never committed to git)
# settings.py: DEBUG = os.getenv('DEBUG', 'False') == 'True'

Set DEBUG = False in all production configurations. Use separate settings files for development and production:

# settings/base.py (shared settings)
DEBUG = False
ALLOWED_HOSTS = []

# settings/development.py (local development)
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']

# settings/production.py (production)
from .base import *
DEBUG = False
ALLOWED_HOSTS = [os.getenv('ALLOWED_HOST', '')]
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True

HTTPS Enforcement and Security Headers

Always transmit data over HTTPS (TLS/SSL). HTTP is vulnerable to man-in-the-middle attacks where attackers intercept and modify traffic. For Flask and Django, enforce HTTPS by redirecting HTTP to HTTPS and setting the Strict-Transport-Security header:

# Flask: enforce HTTPS
from flask import Flask
from flask_talisman import Talisman

app = Flask(__name__)

# Enforce HTTPS and set security headers
Talisman(app,
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000, # 1 year in seconds
)

@app.before_request
def redirect_to_https():
if not request.is_secure and not app.debug:
url = request.url.replace('http://', 'https://', 1)
return redirect(url, code=301)

Django enforces HTTPS via settings:

# Django settings.py
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True # Add to HSTS preload list

# Also set in web server (Nginx, Apache)
# Redirect HTTP to HTTPS at the web server level

The Strict-Transport-Security (HSTS) header tells browsers to always use HTTPS for your domain. Without it, attackers can downgrade HTTPS to HTTP on the user's first visit.

Content Security Policy and Additional Headers

Content Security Policy (CSP) restricts which resources (scripts, stylesheets, images) can be loaded, mitigating XSS attacks. Other security headers prevent clickjacking, MIME type sniffing, and other attacks:

# Flask: set security headers
from flask import Flask

app = Flask(__name__)

@app.after_request
def set_security_headers(response):
# Prevent clickjacking (UI redressing)
response.headers['X-Frame-Options'] = 'SAMEORIGIN'

# Prevent MIME type sniffing
response.headers['X-Content-Type-Options'] = 'nosniff'

# Enable XSS protection in older browsers
response.headers['X-XSS-Protection'] = '1; mode=block'

# Content Security Policy
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self' 'unsafe-inline'"

# Referrer Policy
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'

return response

For Django, use the django-csp package:

# Django settings.py
MIDDLEWARE = [
'csp.middleware.CSPMiddleware',
# ... other middleware
]

CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "trusted-cdn.com")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")

File Permissions and Secret Access

Even with secrets management, file permissions matter. Config files, logs, and private keys should not be readable by other users on the system:

# Set restrictive file permissions
chmod 600 .env # Only the owner can read
chmod 600 /etc/app/secrets.conf # Only the owner and root
chmod 700 /var/log/app/ # Only the owner can access logs

# Verify permissions
ls -la .env
-rw------- 1 user group 123 Jun 02 12:00 .env

In Docker containers, avoid running as root:

# Dockerfile
FROM python:3.11-slim

RUN useradd -m -u 1000 appuser

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .
RUN chown -R appuser:appuser /app

USER appuser
CMD ["python", "app.py"]

Database Security: Connection Pooling and Timeouts

Configure database connections securely:

# SQLAlchemy: secure database configuration
from sqlalchemy import create_engine

engine = create_engine(
'postgresql://user:password@localhost/dbname',
# Connection pooling
pool_size=5, # Maximum 5 connections
max_overflow=10, # Additional temporary connections
pool_pre_ping=True, # Verify connections are alive before use
# Timeouts
connect_args={'connect_timeout': 5}, # 5-second connection timeout
# SSL for remote connections
echo=False, # Disable SQL logging in production
)

# Never log SQL queries in production (they may contain sensitive data)
# Set echo=False

Use SSL/TLS for remote database connections:

# PostgreSQL with SSL
conn_string = 'postgresql://user:password@remote-host/dbname?sslmode=require'
engine = create_engine(conn_string, connect_args={
'sslmode': 'require',
'sslcert': '/path/to/client-cert.pem',
'sslkey': '/path/to/client-key.pem',
'sslrootcert': '/path/to/ca-cert.pem',
})

Rate Limiting and DDoS Mitigation

Prevent brute-force and denial-of-service attacks by rate limiting:

# Flask rate limiting
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute") # Max 5 login attempts per minute per IP
def login():
# ... login logic
pass

@app.route('/api/data', methods=['GET'])
@limiter.limit("100 per hour")
def get_data():
# ... get data logic
pass

For Django, use django-ratelimit:

from django_ratelimit.decorators import ratelimit

@ratelimit(key='ip', rate='5/m') # 5 requests per minute per IP
def login(request):
# ... login logic
pass

For distributed applications, use a Redis-backed rate limiter that enforces limits across multiple servers.

Configuration Management and Infrastructure-as-Code

Use infrastructure-as-code tools (Terraform, CloudFormation, Helm) to enforce secure configurations:

# Terraform: secure Flask application on AWS
resource "aws_security_group" "app_sg" {
name = "app-security-group"

ingress {
from_port = 443 # HTTPS only
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 80 # Redirect HTTP to HTTPS
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.app.arn
port = 443
protocol = "HTTPS"
certificate_arn = aws_acm_certificate.app.arn

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}

Key Takeaways

  • Disable debug mode in production; it exposes stack traces, environment variables, and allows code execution.
  • Enforce HTTPS using Strict-Transport-Security headers and redirects; HTTP is vulnerable to man-in-the-middle attacks.
  • Set security headers (CSP, X-Frame-Options, X-Content-Type-Options) to mitigate XSS, clickjacking, and MIME sniffing.
  • Configure file permissions (600 for .env, 700 for directories) to restrict access to secrets and logs.
  • Use rate limiting on sensitive endpoints (login, password reset) to prevent brute-force attacks.
  • Implement infrastructure-as-code to ensure consistent, secure configurations across all environments.

Frequently Asked Questions

Should I run my Python app as root?

No. Always run applications as a low-privilege user (e.g., appuser in Docker, www-data in traditional deployments). If the application is compromised, attackers only gain that user's privileges, not root.

How do I handle configuration differences between development and production?

Use environment-specific settings files (e.g., settings/development.py vs settings/production.py) or environment variables to override defaults. Never commit production secrets to Git.

Is it safe to expose status endpoints that show uptime and health?

Yes, but be careful not to expose sensitive information (database version, internal IPs, secret counts). Status endpoints should report only the minimum information needed (healthy/unhealthy).

Do I need a Web Application Firewall (WAF)?

A WAF (e.g., AWS WAF, Cloudflare) provides an additional layer of defense against common attacks (SQL injection, XSS). It is not a replacement for secure code, but it is a good defense-in-depth measure, especially for public-facing applications.

How often should I rotate TLS certificates?

Modern certificates are valid for 1 year. Tools like certbot automatically renew Let's Encrypt certificates every 90 days. Set a reminder to rotate manually issued certificates before expiration.

Further Reading