Add Cloudflare/nginx proxy support and production setup

- Updated ProxyFix configuration for Cloudflare + nginx
- Added custom IP detection for real client IPs
- Updated rate limiting to use real IPs
- Added nginx configuration example
- Added Cloudflare setup guide
- Added production server setup script
This commit is contained in:
2025-09-20 20:07:28 +01:00
parent f409977257
commit 3dbb181cb0
5 changed files with 348 additions and 5 deletions

146
CLOUDFLARE_SETUP.md Normal file
View File

@@ -0,0 +1,146 @@
# Cloudflare + nginx Setup Guide for ircquotes
## Overview
This setup ensures that your ircquotes application can see real client IP addresses even when behind:
1. **Cloudflare** (CDN/Proxy)
2. **nginx** (Reverse Proxy)
3. **Gunicorn** (WSGI Server)
## Architecture
```
Client → Cloudflare → nginx → Gunicorn → ircquotes
```
## Setup Steps
### 1. Cloudflare Configuration
#### Enable Proxy (Orange Cloud)
- Set your DNS record to "Proxied" (orange cloud icon)
- This routes traffic through Cloudflare's edge servers
#### Recommended Cloudflare Settings:
- **SSL/TLS**: Full (Strict) if you have SSL on origin
- **Security Level**: Medium
- **Bot Fight Mode**: Enabled
- **Rate Limiting**: Configure as needed
- **Page Rules**: Optional caching rules
#### Important Headers:
Cloudflare automatically adds these headers:
- `CF-Connecting-IP`: Real client IP address
- `CF-Ray`: Request identifier
- `CF-Visitor`: Visitor information
### 2. nginx Configuration
Copy the provided `nginx-ircquotes.conf` to your nginx sites:
```bash
sudo cp nginx-ircquotes.conf /etc/nginx/sites-available/ircquotes
sudo ln -s /etc/nginx/sites-available/ircquotes /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
**Key nginx features:**
- ✅ Cloudflare IP range restoration
- ✅ Real IP detection via CF-Connecting-IP
- ✅ Additional rate limiting layer
- ✅ Security headers
- ✅ Gzip compression
- ✅ Static file optimization
### 3. Application Configuration
The ircquotes app is already configured to:
- ✅ Use `CF-Connecting-IP` header (Cloudflare's real IP)
- ✅ Fall back to `X-Forwarded-For` and `X-Real-IP`
- ✅ Handle 2-proxy setup (Cloudflare + nginx)
- ✅ Rate limit by real client IP
### 4. Verification
To verify real IPs are being detected:
1. **Check application logs**:
```bash
tail -f /var/log/ircquotes/access.log
```
2. **Test from different locations**:
- Visit your site from different networks
- Check admin panel for real IPs in quote submissions
- Verify rate limiting works per real IP
3. **Debug headers** (temporary debug route):
```python
@app.route('/debug-headers')
def debug_headers():
return jsonify({
'real_ip': get_real_ip(),
'cf_connecting_ip': request.headers.get('CF-Connecting-IP'),
'x_forwarded_for': request.headers.get('X-Forwarded-For'),
'x_real_ip': request.headers.get('X-Real-IP'),
'remote_addr': request.remote_addr
})
```
### 5. Security Considerations
#### Cloudflare Settings:
- Enable **DDoS Protection**
- Configure **WAF Rules** for your application
- Set up **Rate Limiting** at Cloudflare level
- Enable **Bot Management** if available
#### nginx Security:
- Keep Cloudflare IP ranges updated
- Monitor for suspicious patterns
- Implement additional rate limiting
- Regular security updates
#### Application Security:
- All security features already implemented
- Rate limiting per real IP
- CSRF protection enabled
- Input validation active
## Troubleshooting
### IPs showing as 127.0.0.1:
1. Check nginx is passing headers correctly
2. Verify Cloudflare IP ranges in nginx config
3. Ensure ProxyFix is configured for 2 proxies
4. Check `CF-Connecting-IP` header presence
### Rate limiting not working:
1. Verify real IP detection is working
2. Check rate limiting configuration
3. Monitor nginx and application logs
4. Test with different source IPs
### Performance issues:
1. Enable nginx caching for static files
2. Configure Cloudflare caching rules
3. Monitor Gunicorn worker count
4. Check database connection pooling
## Monitoring
Recommended monitoring:
- **Application logs**: Real IP addresses in logs
- **nginx access logs**: Request patterns
- **Cloudflare Analytics**: Traffic patterns
- **Rate limiting metrics**: Blocked vs allowed requests
## Production Checklist
- [ ] Cloudflare proxy enabled (orange cloud)
- [ ] nginx configuration deployed
- [ ] Real IP detection working
- [ ] Rate limiting functional
- [ ] Security headers present
- [ ] SSL/TLS configured
- [ ] Monitoring in place
- [ ] Backup and recovery tested

39
app.py
View File

@@ -16,6 +16,34 @@ from sqlalchemy.engine import Engine
import sqlite3 import sqlite3
from config_loader import config # Import configuration system from config_loader import config # Import configuration system
def get_real_ip():
"""
Get the real client IP address considering Cloudflare and nginx reverse proxy.
Checks headers in order of priority:
1. CF-Connecting-IP (Cloudflare's real IP header)
2. X-Forwarded-For (standard proxy header)
3. X-Real-IP (nginx real IP header)
4. request.remote_addr (fallback)
"""
# Cloudflare provides the real IP in CF-Connecting-IP header
cf_ip = request.headers.get('CF-Connecting-IP')
if cf_ip:
return cf_ip
# Check X-Forwarded-For (may contain multiple IPs, first is original client)
forwarded_for = request.headers.get('X-Forwarded-For')
if forwarded_for:
# Take the first IP in the chain (original client)
return forwarded_for.split(',')[0].strip()
# Check X-Real-IP (nginx header)
real_ip = request.headers.get('X-Real-IP')
if real_ip:
return real_ip
# Fallback to request.remote_addr or default
return request.remote_addr or '127.0.0.1'
# Configure SQLite for better concurrency # Configure SQLite for better concurrency
@event.listens_for(Engine, "connect") @event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record): def set_sqlite_pragma(dbapi_connection, connection_record):
@@ -60,13 +88,14 @@ csrf.exempt('get_top_quotes')
csrf.exempt('search_quotes') csrf.exempt('search_quotes')
csrf.exempt('get_stats') csrf.exempt('get_stats')
# Initialize rate limiter # Initialize rate limiter with custom IP detection
limiter = Limiter(app, key_func=get_remote_address) limiter = Limiter(app, key_func=get_real_ip)
db = SQLAlchemy(app) db = SQLAlchemy(app)
# Apply ProxyFix middleware # Apply ProxyFix middleware for Cloudflare + nginx setup
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1) # x_for=2: nginx (1) + Cloudflare (1) = 2 proxies in X-Forwarded-For chain
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2, x_proto=1, x_host=1, x_port=1, x_prefix=1)
# Initialize Argon2 password hasher # Initialize Argon2 password hasher
ph = PasswordHasher() ph = PasswordHasher()
@@ -152,7 +181,7 @@ def submit():
flash("Invalid content detected. Please remove any script tags or JavaScript.", 'error') flash("Invalid content detected. Please remove any script tags or JavaScript.", 'error')
return redirect(url_for('submit')) return redirect(url_for('submit'))
ip_address = request.headers.get('CF-Connecting-IP', request.remote_addr) # Get the user's IP address ip_address = get_real_ip() # Get the real user's IP address
user_agent = request.headers.get('User-Agent') # Get the user's browser info user_agent = request.headers.get('User-Agent') # Get the user's browser info
new_quote = Quote(text=quote_text, ip_address=ip_address, user_agent=user_agent) new_quote = Quote(text=quote_text, ip_address=ip_address, user_agent=user_agent)

View File

@@ -17,6 +17,12 @@
"session_cookie_secure": false, "session_cookie_secure": false,
"session_cookie_httponly": true, "session_cookie_httponly": true,
"session_cookie_samesite": "Lax", "session_cookie_samesite": "Lax",
"proxy_setup": {
"behind_cloudflare": true,
"behind_nginx": true,
"trusted_proxies": 2,
"cloudflare_ip_header": "CF-Connecting-IP"
},
"security_headers": { "security_headers": {
"x_content_type_options": "nosniff", "x_content_type_options": "nosniff",
"x_frame_options": "DENY", "x_frame_options": "DENY",

112
nginx-ircquotes.conf Normal file
View File

@@ -0,0 +1,112 @@
# nginx configuration for ircquotes behind Cloudflare
# Place this in /etc/nginx/sites-available/ircquotes
server {
listen 80;
server_name your-domain.com; # Replace with your actual domain
# Cloudflare real IP restoration
# Get latest Cloudflare IP ranges from: https://www.cloudflare.com/ips/
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
# IPv6 ranges (optional)
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
# Use Cloudflare's CF-Connecting-IP header for real IP
real_ip_header CF-Connecting-IP;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript;
# Rate limiting (additional layer)
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
# Main application
location / {
proxy_pass http://127.0.0.1:5050;
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;
# Pass Cloudflare headers
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;
proxy_set_header CF-Ray $http_cf_ray;
proxy_set_header CF-Visitor $http_cf_visitor;
# Timeouts
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# Rate limit login endpoint
location /login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://127.0.0.1:5050;
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_set_header CF-Connecting-IP $http_cf_connecting_ip;
}
# Rate limit API endpoints
location /api/ {
limit_req zone=api burst=10 nodelay;
proxy_pass http://127.0.0.1:5050;
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_set_header CF-Connecting-IP $http_cf_connecting_ip;
}
# Static files (optional optimization)
location /static/ {
proxy_pass http://127.0.0.1:5050;
proxy_set_header Host $host;
# Cache static files
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
proxy_pass http://127.0.0.1:5050;
proxy_set_header Host $host;
}
}

50
setup.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# ircquotes production server setup script
# Run this after cloning the repository
echo "Setting up ircquotes on production server..."
# Create instance directory
echo "Creating instance directory..."
mkdir -p instance
# Generate secret key
echo "Generating Flask secret key..."
python3 -c "import secrets; print(secrets.token_hex(32))" > instance/flask_secret_key
# Create empty database file
echo "Creating database file..."
touch instance/quotes.db
# Set permissions
echo "Setting file permissions..."
chmod 600 instance/flask_secret_key
chmod 664 instance/quotes.db
# Create virtual environment
echo "Creating virtual environment..."
python3 -m venv .venv
# Activate and install dependencies
echo "Installing dependencies..."
source .venv/bin/activate
pip install -r requirements.txt
# Initialize database
echo "Initializing database..."
python -c "from app import app, db; app.app_context().push(); db.create_all(); print('Database initialized successfully!')"
echo ""
echo "Setup complete! You can now:"
echo "1. Configure admin credentials:"
echo " python generate_password.py"
echo " python config_manager.py admin.username 'yourusername'"
echo " python config_manager.py admin.password_hash 'generated_hash'"
echo ""
echo "2. Start the application:"
echo " source .venv/bin/activate"
echo " gunicorn --config gunicorn.conf.py app:app"
echo ""
echo "3. Or run in development mode:"
echo " python app.py"