From 3dbb181cb025db732f1d6d101ed4b223829138d3 Mon Sep 17 00:00:00 2001 From: ComputerTech312 Date: Sat, 20 Sep 2025 20:07:28 +0100 Subject: [PATCH] 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 --- CLOUDFLARE_SETUP.md | 146 +++++++++++++++++++++++++++++++++++++++++++ app.py | 39 ++++++++++-- config.json | 6 ++ nginx-ircquotes.conf | 112 +++++++++++++++++++++++++++++++++ setup.sh | 50 +++++++++++++++ 5 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 CLOUDFLARE_SETUP.md create mode 100644 nginx-ircquotes.conf create mode 100644 setup.sh diff --git a/CLOUDFLARE_SETUP.md b/CLOUDFLARE_SETUP.md new file mode 100644 index 0000000..9b78dd9 --- /dev/null +++ b/CLOUDFLARE_SETUP.md @@ -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 \ No newline at end of file diff --git a/app.py b/app.py index 12c74e9..4c2b06e 100644 --- a/app.py +++ b/app.py @@ -16,6 +16,34 @@ from sqlalchemy.engine import Engine import sqlite3 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 @event.listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection, connection_record): @@ -60,13 +88,14 @@ csrf.exempt('get_top_quotes') csrf.exempt('search_quotes') csrf.exempt('get_stats') -# Initialize rate limiter -limiter = Limiter(app, key_func=get_remote_address) +# Initialize rate limiter with custom IP detection +limiter = Limiter(app, key_func=get_real_ip) db = SQLAlchemy(app) -# Apply ProxyFix middleware -app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1) +# Apply ProxyFix middleware for Cloudflare + nginx setup +# 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 ph = PasswordHasher() @@ -152,7 +181,7 @@ def submit(): flash("Invalid content detected. Please remove any script tags or JavaScript.", 'error') 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 new_quote = Quote(text=quote_text, ip_address=ip_address, user_agent=user_agent) diff --git a/config.json b/config.json index 4851bc5..7cdf494 100644 --- a/config.json +++ b/config.json @@ -17,6 +17,12 @@ "session_cookie_secure": false, "session_cookie_httponly": true, "session_cookie_samesite": "Lax", + "proxy_setup": { + "behind_cloudflare": true, + "behind_nginx": true, + "trusted_proxies": 2, + "cloudflare_ip_header": "CF-Connecting-IP" + }, "security_headers": { "x_content_type_options": "nosniff", "x_frame_options": "DENY", diff --git a/nginx-ircquotes.conf b/nginx-ircquotes.conf new file mode 100644 index 0000000..7049828 --- /dev/null +++ b/nginx-ircquotes.conf @@ -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; + } +} \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..36241db --- /dev/null +++ b/setup.sh @@ -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" \ No newline at end of file