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:
146
CLOUDFLARE_SETUP.md
Normal file
146
CLOUDFLARE_SETUP.md
Normal 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
39
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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
112
nginx-ircquotes.conf
Normal file
112
nginx-ircquotes.conf
Normal 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
50
setup.sh
Normal 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"
|
||||
Reference in New Issue
Block a user