feat: complete nginx migration from caddy
- Replace Caddy reverse proxy with Nginx (nginx:alpine) - Add nginx.conf with HTTP/HTTPS, gzip, and proxy settings - Add nginx-custom-domains.conf template for custom domains - Update docker-compose.yml to use Nginx service - Add ProxyFix middleware to Flask app for proper header handling - Create nginx_config_reader.py utility to read Nginx configuration - Update admin blueprint to display Nginx status in https_config page - Add Nginx configuration display to https_config.html template - Generate self-signed SSL certificates for localhost - Add utility scripts: generate_nginx_certs.sh - Add documentation: NGINX_SETUP_QUICK.md, PROXY_FIX_SETUP.md - All containers now running, HTTPS working, HTTP redirects to HTTPS - Session cookies marked as Secure - Security headers properly configured
This commit is contained in:
84
NGINX_SETUP_QUICK.md
Normal file
84
NGINX_SETUP_QUICK.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Quick Start: Nginx Setup for DigiServer v2
|
||||||
|
|
||||||
|
## Pre-requisites
|
||||||
|
- SSL certificates in `./data/nginx-ssl/cert.pem` and `./data/nginx-ssl/key.pem`
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- Port 80 and 443 available
|
||||||
|
|
||||||
|
## Quick Setup (3 steps)
|
||||||
|
|
||||||
|
### 1. Generate Self-Signed Certificates
|
||||||
|
```bash
|
||||||
|
./generate_nginx_certs.sh localhost 365
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Update Nginx Configuration
|
||||||
|
- Edit `nginx.conf` to set your domain:
|
||||||
|
```nginx
|
||||||
|
server_name localhost; # Change to your domain
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start Docker Compose
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Check if Nginx is running
|
||||||
|
```bash
|
||||||
|
docker ps | grep nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test HTTP → HTTPS redirect
|
||||||
|
```bash
|
||||||
|
curl -L http://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test HTTPS (with self-signed cert)
|
||||||
|
```bash
|
||||||
|
curl -k https://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs
|
||||||
|
```bash
|
||||||
|
docker logs digiserver-nginx
|
||||||
|
docker exec digiserver-nginx tail -f /var/log/nginx/access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Production Certificates
|
||||||
|
|
||||||
|
### Option A: Let's Encrypt (Free)
|
||||||
|
1. Install certbot: `apt-get install certbot`
|
||||||
|
2. Generate cert: `certbot certonly --standalone -d your-domain.com`
|
||||||
|
3. Copy cert: `cp /etc/letsencrypt/live/your-domain.com/fullchain.pem ./data/nginx-ssl/cert.pem`
|
||||||
|
4. Copy key: `cp /etc/letsencrypt/live/your-domain.com/privkey.pem ./data/nginx-ssl/key.pem`
|
||||||
|
5. Fix permissions: `sudo chown 101:101 ./data/nginx-ssl/*`
|
||||||
|
6. Reload: `docker exec digiserver-nginx nginx -s reload`
|
||||||
|
|
||||||
|
### Option B: Commercial Certificate
|
||||||
|
1. Place your certificate files in `./data/nginx-ssl/cert.pem` and `./data/nginx-ssl/key.pem`
|
||||||
|
2. Fix permissions: `sudo chown 101:101 ./data/nginx-ssl/*`
|
||||||
|
3. Reload: `docker exec digiserver-nginx nginx -s reload`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| Port 80/443 in use | `sudo netstat -tlnp \| grep :80` or `:443` |
|
||||||
|
| Certificate permission denied | `sudo chown 101:101 ./data/nginx-ssl/*` |
|
||||||
|
| Nginx won't start | `docker logs digiserver-nginx` |
|
||||||
|
| Connection refused | Check firewall: `sudo ufw allow 80/tcp && sudo ufw allow 443/tcp` |
|
||||||
|
|
||||||
|
## File Locations
|
||||||
|
- Main config: `./nginx.conf`
|
||||||
|
- SSL certs: `./data/nginx-ssl/`
|
||||||
|
- Logs: `./data/nginx-logs/`
|
||||||
|
- Custom domains: `./nginx-custom-domains.conf` (auto-generated)
|
||||||
|
|
||||||
|
## Next: Production Setup
|
||||||
|
1. Update `.env` with your DOMAIN and EMAIL
|
||||||
|
2. Configure HTTPS settings in admin panel
|
||||||
|
3. Run: `python nginx_manager.py generate`
|
||||||
|
4. Test: `docker exec digiserver-nginx nginx -t`
|
||||||
|
5. Reload: `docker exec digiserver-nginx nginx -s reload`
|
||||||
56
PROXY_FIX_SETUP.md
Normal file
56
PROXY_FIX_SETUP.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# ProxyFix Middleware Setup - DigiServer v2
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
ProxyFix middleware is now properly configured in the Flask app to handle reverse proxy headers from Nginx (or Caddy). This ensures correct handling of:
|
||||||
|
- **X-Real-IP**: Client's real IP address
|
||||||
|
- **X-Forwarded-For**: List of IPs in the proxy chain
|
||||||
|
- **X-Forwarded-Proto**: Original protocol (http/https)
|
||||||
|
- **X-Forwarded-Host**: Original hostname
|
||||||
|
|
||||||
|
## Configuration Details
|
||||||
|
|
||||||
|
### Flask App (app/app.py)
|
||||||
|
```python
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `x_for=1`: Trust one proxy for X-Forwarded-For header
|
||||||
|
- `x_proto=1`: Trust proxy for X-Forwarded-Proto header
|
||||||
|
- `x_host=1`: Trust proxy for X-Forwarded-Host header
|
||||||
|
- `x_port=1`: Trust proxy for X-Forwarded-Port header
|
||||||
|
|
||||||
|
### Config Settings (app/config.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Reverse proxy trust (for Nginx/Caddy with ProxyFix middleware)
|
||||||
|
TRUSTED_PROXIES = os.getenv('TRUSTED_PROXIES', '127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16')
|
||||||
|
PREFERRED_URL_SCHEME = os.getenv('PREFERRED_URL_SCHEME', 'https')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing ProxyFix
|
||||||
|
|
||||||
|
### 1. Test Real Client IP
|
||||||
|
```bash
|
||||||
|
docker exec digiserver-app flask shell
|
||||||
|
>>> from flask import request
|
||||||
|
>>> request.remote_addr # Should show client IP
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test URL Scheme
|
||||||
|
```bash
|
||||||
|
docker exec digiserver-app flask shell
|
||||||
|
>>> from flask import url_for
|
||||||
|
>>> url_for('auth.login', _external=True) # Should use https://
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
- [x] ProxyFix imported in app.py
|
||||||
|
- [x] app.wsgi_app wrapped with ProxyFix
|
||||||
|
- [x] TRUSTED_PROXIES configured
|
||||||
|
- [x] PREFERRED_URL_SCHEME set to 'https'
|
||||||
|
- [x] SESSION_COOKIE_SECURE=True in ProductionConfig
|
||||||
|
- [x] Nginx headers configured correctly
|
||||||
@@ -4,6 +4,7 @@ Modern Flask application with blueprint architecture
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from flask import Flask, render_template
|
from flask import Flask, render_template
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from app.config import DevelopmentConfig, ProductionConfig, TestingConfig
|
from app.config import DevelopmentConfig, ProductionConfig, TestingConfig
|
||||||
@@ -37,6 +38,10 @@ def create_app(config_name=None):
|
|||||||
|
|
||||||
app.config.from_object(config)
|
app.config.from_object(config)
|
||||||
|
|
||||||
|
# Apply ProxyFix middleware for reverse proxy (Nginx/Caddy)
|
||||||
|
# This ensures proper handling of X-Forwarded-* headers
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||||
|
|
||||||
# Initialize extensions
|
# Initialize extensions
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
bcrypt.init_app(app)
|
bcrypt.init_app(app)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from app.extensions import db, bcrypt
|
|||||||
from app.models import User, Player, Group, Content, ServerLog, Playlist, HTTPSConfig
|
from app.models import User, Player, Group, Content, ServerLog, Playlist, HTTPSConfig
|
||||||
from app.utils.logger import log_action
|
from app.utils.logger import log_action
|
||||||
from app.utils.caddy_manager import CaddyConfigGenerator
|
from app.utils.caddy_manager import CaddyConfigGenerator
|
||||||
|
from app.utils.nginx_config_reader import get_nginx_status
|
||||||
|
|
||||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||||
|
|
||||||
@@ -870,10 +871,14 @@ def https_config():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
log_action('info', f'HTTPS status auto-corrected to enabled (detected from request)')
|
log_action('info', f'HTTPS status auto-corrected to enabled (detected from request)')
|
||||||
|
|
||||||
|
# Get Nginx configuration status
|
||||||
|
nginx_status = get_nginx_status()
|
||||||
|
|
||||||
return render_template('admin/https_config.html',
|
return render_template('admin/https_config.html',
|
||||||
config=config,
|
config=config,
|
||||||
is_https_active=is_https_active,
|
is_https_active=is_https_active,
|
||||||
current_host=current_host)
|
current_host=current_host,
|
||||||
|
nginx_status=nginx_status)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_action('error', f'Error loading HTTPS config page: {str(e)}')
|
log_action('error', f'Error loading HTTPS config page: {str(e)}')
|
||||||
flash('Error loading HTTPS configuration page.', 'danger')
|
flash('Error loading HTTPS configuration page.', 'danger')
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ class Config:
|
|||||||
SESSION_COOKIE_HTTPONLY = True
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
|
||||||
|
# Reverse proxy trust (for Nginx/Caddy with ProxyFix middleware)
|
||||||
|
# These are set by werkzeug.middleware.proxy_fix
|
||||||
|
TRUSTED_PROXIES = os.getenv('TRUSTED_PROXIES', '127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16')
|
||||||
|
PREFERRED_URL_SCHEME = os.getenv('PREFERRED_URL_SCHEME', 'https')
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
SEND_FILE_MAX_AGE_DEFAULT = 300 # 5 minutes for static files
|
SEND_FILE_MAX_AGE_DEFAULT = 300 # 5 minutes for static files
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,95 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Nginx Status Card -->
|
||||||
|
<div class="card nginx-status-card">
|
||||||
|
<h2>🔧 Nginx Reverse Proxy Status</h2>
|
||||||
|
{% if nginx_status.available %}
|
||||||
|
<div class="nginx-status-content">
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
<span class="badge badge-success">✅ Nginx Configured</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Configuration Path:</strong>
|
||||||
|
<code>{{ nginx_status.path }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if nginx_status.ssl_enabled %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>SSL/TLS:</strong>
|
||||||
|
<span class="badge badge-success">🔒 Enabled</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>SSL/TLS:</strong>
|
||||||
|
<span class="badge badge-warning">⚠️ Not Configured</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.http_ports %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>HTTP Ports:</strong>
|
||||||
|
<code>{{ nginx_status.http_ports|join(', ') }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.https_ports %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>HTTPS Ports:</strong>
|
||||||
|
<code>{{ nginx_status.https_ports|join(', ') }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.server_names %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Server Names:</strong>
|
||||||
|
{% for name in nginx_status.server_names %}
|
||||||
|
<code>{{ name }}</code>{% if not loop.last %}<br>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.upstream_servers %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Upstream Servers:</strong>
|
||||||
|
{% for server in nginx_status.upstream_servers %}
|
||||||
|
<code>{{ server }}</code>{% if not loop.last %}<br>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.ssl_protocols %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>SSL Protocols:</strong>
|
||||||
|
<code>{{ nginx_status.ssl_protocols|join(', ') }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.client_max_body_size %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Max Body Size:</strong>
|
||||||
|
<code>{{ nginx_status.client_max_body_size }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.gzip_enabled %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Gzip Compression:</strong>
|
||||||
|
<span class="badge badge-success">✅ Enabled</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="status-disabled">
|
||||||
|
<p>⚠️ <strong>Nginx configuration not accessible</strong></p>
|
||||||
|
<p>Error: {{ nginx_status.error|default('Unknown error') }}</p>
|
||||||
|
<p style="font-size: 12px; color: #666;">Path checked: {{ nginx_status.path }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Information Section -->
|
<!-- Information Section -->
|
||||||
<div class="card info-card">
|
<div class="card info-card">
|
||||||
<h2>ℹ️ Important Information</h2>
|
<h2>ℹ️ Important Information</h2>
|
||||||
@@ -466,6 +555,45 @@
|
|||||||
color: #0066cc;
|
color: #0066cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nginx-status-card {
|
||||||
|
background: linear-gradient(135deg, #f0f7ff 0%, #e7f3ff 100%);
|
||||||
|
border-left: 5px solid #0066cc;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nginx-status-card h2 {
|
||||||
|
color: #0066cc;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nginx-status-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
padding: 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-left: 3px solid #0066cc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item strong {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 150px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item code {
|
||||||
|
background: #f0f7ff;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #0066cc;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.info-sections {
|
.info-sections {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
|||||||
120
app/utils/nginx_config_reader.py
Normal file
120
app/utils/nginx_config_reader.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"""Nginx configuration reader utility."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
|
||||||
|
|
||||||
|
class NginxConfigReader:
|
||||||
|
"""Read and parse Nginx configuration files."""
|
||||||
|
|
||||||
|
def __init__(self, config_path: str = '/etc/nginx/nginx.conf'):
|
||||||
|
"""Initialize Nginx config reader."""
|
||||||
|
self.config_path = config_path
|
||||||
|
self.config_content = None
|
||||||
|
self.is_available = os.path.exists(config_path)
|
||||||
|
|
||||||
|
if self.is_available:
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
self.config_content = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
self.is_available = False
|
||||||
|
self.error = str(e)
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get Nginx configuration status."""
|
||||||
|
if not self.is_available:
|
||||||
|
return {
|
||||||
|
'available': False,
|
||||||
|
'error': 'Nginx configuration not found',
|
||||||
|
'path': self.config_path
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'available': True,
|
||||||
|
'path': self.config_path,
|
||||||
|
'file_exists': os.path.exists(self.config_path),
|
||||||
|
'ssl_enabled': self._check_ssl_enabled(),
|
||||||
|
'http_ports': self._extract_http_ports(),
|
||||||
|
'https_ports': self._extract_https_ports(),
|
||||||
|
'upstream_servers': self._extract_upstream_servers(),
|
||||||
|
'server_names': self._extract_server_names(),
|
||||||
|
'ssl_protocols': self._extract_ssl_protocols(),
|
||||||
|
'client_max_body_size': self._extract_client_max_body_size(),
|
||||||
|
'gzip_enabled': self._check_gzip_enabled(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _check_ssl_enabled(self) -> bool:
|
||||||
|
"""Check if SSL is enabled."""
|
||||||
|
if not self.config_content:
|
||||||
|
return False
|
||||||
|
return 'ssl_certificate' in self.config_content
|
||||||
|
|
||||||
|
def _extract_http_ports(self) -> List[int]:
|
||||||
|
"""Extract HTTP listening ports."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
pattern = r'listen\s+(\d+)'
|
||||||
|
matches = re.findall(pattern, self.config_content)
|
||||||
|
return sorted(list(set(int(p) for p in matches if int(p) < 1000)))
|
||||||
|
|
||||||
|
def _extract_https_ports(self) -> List[int]:
|
||||||
|
"""Extract HTTPS listening ports."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
pattern = r'listen\s+(\d+).*ssl'
|
||||||
|
matches = re.findall(pattern, self.config_content)
|
||||||
|
return sorted(list(set(int(p) for p in matches)))
|
||||||
|
|
||||||
|
def _extract_upstream_servers(self) -> List[str]:
|
||||||
|
"""Extract upstream servers."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
upstream_match = re.search(r'upstream\s+\w+\s*{([^}]+)}', self.config_content)
|
||||||
|
if upstream_match:
|
||||||
|
upstream_content = upstream_match.group(1)
|
||||||
|
servers = re.findall(r'server\s+([^\s;]+)', upstream_content)
|
||||||
|
return servers
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _extract_server_names(self) -> List[str]:
|
||||||
|
"""Extract server names."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
pattern = r'server_name\s+([^;]+);'
|
||||||
|
matches = re.findall(pattern, self.config_content)
|
||||||
|
result = []
|
||||||
|
for match in matches:
|
||||||
|
names = match.strip().split()
|
||||||
|
result.extend(names)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _extract_ssl_protocols(self) -> List[str]:
|
||||||
|
"""Extract SSL protocols."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
pattern = r'ssl_protocols\s+([^;]+);'
|
||||||
|
match = re.search(pattern, self.config_content)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip().split()
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _extract_client_max_body_size(self) -> Optional[str]:
|
||||||
|
"""Extract client max body size."""
|
||||||
|
if not self.config_content:
|
||||||
|
return None
|
||||||
|
pattern = r'client_max_body_size\s+([^;]+);'
|
||||||
|
match = re.search(pattern, self.config_content)
|
||||||
|
return match.group(1).strip() if match else None
|
||||||
|
|
||||||
|
def _check_gzip_enabled(self) -> bool:
|
||||||
|
"""Check if gzip is enabled."""
|
||||||
|
if not self.config_content:
|
||||||
|
return False
|
||||||
|
return bool(re.search(r'gzip\s+on\s*;', self.config_content))
|
||||||
|
|
||||||
|
|
||||||
|
def get_nginx_status() -> Dict[str, Any]:
|
||||||
|
"""Get Nginx configuration status."""
|
||||||
|
reader = NginxConfigReader()
|
||||||
|
return reader.get_status()
|
||||||
@@ -26,19 +26,19 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- digiserver-network
|
- digiserver-network
|
||||||
|
|
||||||
# Caddy reverse proxy with automatic HTTPS
|
# Nginx reverse proxy with HTTPS support
|
||||||
caddy:
|
nginx:
|
||||||
image: caddy:2-alpine
|
image: nginx:alpine
|
||||||
container_name: digiserver-caddy
|
container_name: digiserver-nginx
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
- "443:443/udp" # HTTP/3 support
|
|
||||||
- "2019:2019" # Caddy admin API
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/Caddyfile:/etc/caddy/Caddyfile:ro
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
- ./data/caddy-data:/data
|
- ./nginx-custom-domains.conf:/etc/nginx/conf.d/custom-domains.conf:rw
|
||||||
- ./data/caddy-config:/config
|
- ./data/nginx-ssl:/etc/nginx/ssl:ro
|
||||||
|
- ./data/nginx-logs:/var/log/nginx
|
||||||
|
- ./data/certbot:/var/www/certbot:ro # For Let's Encrypt ACME challenges
|
||||||
environment:
|
environment:
|
||||||
- DOMAIN=${DOMAIN:-localhost}
|
- DOMAIN=${DOMAIN:-localhost}
|
||||||
- EMAIL=${EMAIL:-admin@localhost}
|
- EMAIL=${EMAIL:-admin@localhost}
|
||||||
@@ -46,6 +46,12 @@ services:
|
|||||||
digiserver-app:
|
digiserver-app:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
networks:
|
networks:
|
||||||
- digiserver-network
|
- digiserver-network
|
||||||
|
|
||||||
|
|||||||
30
generate_nginx_certs.sh
Executable file
30
generate_nginx_certs.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Generate self-signed SSL certificates for Nginx
|
||||||
|
# Usage: ./generate_nginx_certs.sh [domain] [days]
|
||||||
|
|
||||||
|
DOMAIN=${1:-localhost}
|
||||||
|
DAYS=${2:-365}
|
||||||
|
CERT_DIR="./data/nginx-ssl"
|
||||||
|
|
||||||
|
echo "🔐 Generating self-signed SSL certificate for Nginx"
|
||||||
|
echo "Domain: $DOMAIN"
|
||||||
|
echo "Valid for: $DAYS days"
|
||||||
|
echo "Certificate directory: $CERT_DIR"
|
||||||
|
|
||||||
|
# Create directory if it doesnt exist
|
||||||
|
mkdir -p "$CERT_DIR"
|
||||||
|
|
||||||
|
# Generate private key and certificate
|
||||||
|
openssl req -x509 -nodes -days "$DAYS" \
|
||||||
|
-newkey rsa:2048 \
|
||||||
|
-keyout "$CERT_DIR/key.pem" \
|
||||||
|
-out "$CERT_DIR/cert.pem" \
|
||||||
|
-subj "/CN=$DOMAIN/O=DigiServer/C=US"
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chmod 644 "$CERT_DIR/cert.pem"
|
||||||
|
chmod 600 "$CERT_DIR/key.pem"
|
||||||
|
|
||||||
|
echo "✅ Certificates generated successfully!"
|
||||||
|
echo "Certificate: $CERT_DIR/cert.pem"
|
||||||
|
echo "Key: $CERT_DIR/key.pem"
|
||||||
21
nginx-custom-domains.conf
Normal file
21
nginx-custom-domains.conf
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Nginx configuration for custom HTTPS domains
|
||||||
|
# This file will be dynamically generated based on HTTPSConfig database entries
|
||||||
|
# Include this in your nginx.conf with: include /etc/nginx/conf.d/custom-domains.conf;
|
||||||
|
|
||||||
|
# Example entry for custom domain:
|
||||||
|
# server {
|
||||||
|
# listen 443 ssl http2;
|
||||||
|
# listen [::]:443 ssl http2;
|
||||||
|
# server_name digiserver.example.com;
|
||||||
|
#
|
||||||
|
# ssl_certificate /etc/nginx/ssl/custom/cert.pem;
|
||||||
|
# ssl_certificate_key /etc/nginx/ssl/custom/key.pem;
|
||||||
|
#
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://digiserver_app;
|
||||||
|
# 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;
|
||||||
|
# }
|
||||||
|
# }
|
||||||
117
nginx.conf
Normal file
117
nginx.conf
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
use epoll;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
client_max_body_size 2048M;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml;
|
||||||
|
|
||||||
|
# Upstream to Flask application
|
||||||
|
upstream digiserver_app {
|
||||||
|
server digiserver-app:5000;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP Server - redirect to HTTPS
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Allow ACME challenges for Let's Encrypt
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect HTTP to HTTPS for non-ACME requests
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS Server (with self-signed cert by default)
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2 default_server;
|
||||||
|
listen [::]:443 ssl http2 default_server;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# SSL certificate paths (will be volume-mounted)
|
||||||
|
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||||
|
|
||||||
|
# SSL Configuration
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" 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;
|
||||||
|
|
||||||
|
# Proxy settings
|
||||||
|
location / {
|
||||||
|
proxy_pass http://digiserver_app;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
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 X-Forwarded-Host $server_name;
|
||||||
|
|
||||||
|
# Timeouts for large uploads
|
||||||
|
proxy_connect_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
|
# Buffering
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 4 256k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static files caching
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
proxy_pass http://digiserver_app;
|
||||||
|
proxy_cache_valid 200 60d;
|
||||||
|
expires 60d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Additional server blocks for custom domains can be included here
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user