diff --git a/Dockerfile b/Dockerfile index b1fd91d..5a68294 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,22 @@ -FROM python:3.11-slim +FROM python:3-alpine -# Set working directory WORKDIR /app # Install dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir flask requests werkzeug -# Copy application files -COPY app.py . -COPY malias_wrapper.py . -COPY malias.py . -COPY reset_password.py . -COPY templates/ templates/ -COPY static/ static/ - -# Create data directory -RUN mkdir -p /app/data - -# Copy entrypoint script and make scripts executable -COPY docker-entrypoint.sh . -RUN chmod +x docker-entrypoint.sh reset_password.py - -# Expose port -EXPOSE 5172 +# Copy files +COPY . /app/ # Set environment variables -ENV PYTHONUNBUFFERED=1 -ENV FLASK_DEBUG=False -ENV FLASK_ENV=production +ENV FLASK_PORT=5142 \ + FLASK_HOST=0.0.0.0 \ + FLASK_ENV=production \ + FLASK_DEBUG=False \ + ENABLE_PROXY=false -# Run the application via entrypoint -ENTRYPOINT ["./docker-entrypoint.sh"] +# Expose port +EXPOSE 5142 + +# Run the application +CMD ["python", "app.py"] \ No newline at end of file diff --git a/PROXY_SETUP.md b/PROXY_SETUP.md index d51426e..b8483a1 100644 --- a/PROXY_SETUP.md +++ b/PROXY_SETUP.md @@ -1,322 +1,88 @@ -# Reverse Proxy Setup Guide +# Setting Up with a Reverse Proxy -This guide helps you configure reverse proxies (Nginx, Traefik, Zoraxy, Authelia, Caddy, etc.) to work with Mailcow Alias Manager. +This application supports both direct access and running behind a reverse proxy with authentication. The mode is controlled by the `ENABLE_PROXY` environment variable. -## ✅ Built-in Proxy Support +## Configuration Options -The application includes **ProxyFix middleware** that automatically handles: -- ✅ HTTPS detection via `X-Forwarded-Proto` -- ✅ Host header forwarding via `X-Forwarded-Host` -- ✅ Client IP forwarding via `X-Forwarded-For` -- ✅ Path prefix support via `X-Forwarded-Prefix` +### Direct Access Mode (default) -**No configuration changes needed in most cases!** +When `ENABLE_PROXY=false` (default), the application: +- Expects direct access via IP:port +- Uses non-secure cookies (suitable for HTTP) +- Relies only on the built-in authentication ---- - -## Common Proxy Configurations - -### **Zoraxy** (Your Current Setup) - -Zoraxy automatically sets the required headers. Just configure: - -1. **Upstream Target**: `http://localhost:5172` (or your Docker IP) -2. **Enable WebSocket**: Yes (optional, recommended) -3. **Enable Proxy Headers**: Yes (should be default) - -**Example Zoraxy Config:** -```json -{ - "name": "mailcow-alias-manager", - "upstream": "http://localhost:5172", - "domain": "aliases.yourdomain.com" -} +Example docker-compose.yml for direct access: +```yaml +services: + mailcow-alias-manager: + build: . + restart: unless-stopped + environment: + - FLASK_PORT=5142 + - ENABLE_PROXY=false + volumes: + - ./data:/app/data + ports: + - "5142:5142" ``` ---- +### Proxy Mode -### **Authelia** (Authentication Proxy) +When `ENABLE_PROXY=true`, the application: +- Is configured to work behind a reverse proxy +- Uses secure cookies (requires HTTPS) +- Can integrate with authentication providers like Authelia -If using Authelia in front of the app: +Example docker-compose.yml for proxy access: +```yaml +services: + mailcow-alias-manager: + build: . + restart: unless-stopped + environment: + - FLASK_PORT=5142 + - ENABLE_PROXY=true + volumes: + - ./data:/app/data + # No ports exposed - access only through proxy + networks: + - proxy-network +``` -1. **Authelia forwards the user after login** - this should work automatically -2. **Make sure Authelia passes these headers**: - - `X-Forwarded-Proto` - - `X-Forwarded-Host` - - `X-Forwarded-For` +## Setting Up with Nginx -**Common Issue**: Authelia redirects → App login page -**Solution**: The app has its own authentication. You have two options: - - **Option A**: Disable app login (requires code modification) - - **Option B**: Use Authelia for network access, app login for API access - ---- - -### **Nginx** +Here's a basic Nginx configuration for proxying to the application: ```nginx server { - listen 443 ssl http2; - server_name aliases.yourdomain.com; - + listen 443 ssl; + server_name alias.example.com; + + # SSL configuration ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; - + location / { - proxy_pass http://localhost:5172; - - # Required headers for ProxyFix + proxy_pass http://mailcow-alias-manager:5142; 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 $host; - - # Optional: WebSocket support - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; } } ``` ---- +## Setting Up with Zoraxy or Traefik -### **Traefik** +For Zoraxy or Traefik, make sure to: -**docker-compose.yml:** -```yaml -services: - mailcow-alias-manager: - image: gitlab.pm/rune/malias-web:latest - container_name: mailcow-alias-manager - volumes: - - ./data:/app/data - restart: unless-stopped - labels: - - "traefik.enable=true" - - "traefik.http.routers.malias.rule=Host(`aliases.yourdomain.com`)" - - "traefik.http.routers.malias.entrypoints=websecure" - - "traefik.http.routers.malias.tls=true" - - "traefik.http.routers.malias.tls.certresolver=letsencrypt" - - "traefik.http.services.malias.loadbalancer.server.port=5172" - networks: - - traefik +1. Set `ENABLE_PROXY=true` in your container environment +2. Configure the proxy to forward authentication headers if using an authentication provider +3. Set up the appropriate redirect URLs -networks: - traefik: - external: true -``` +## Debugging -Traefik automatically sets `X-Forwarded-*` headers by default. - ---- - -### **Caddy** - -```caddy -aliases.yourdomain.com { - reverse_proxy localhost:5172 -} -``` - -Caddy automatically handles all proxy headers - no configuration needed! - ---- - -## 🔧 Troubleshooting - -### **Issue: "NetworkError when attempting to fetch resource"** - -**Causes:** -1. Mixed HTTP/HTTPS content -2. CORS blocking requests -3. Session cookie not being set -4. Proxy not forwarding headers - -**Solutions:** - -#### **1. Check Proxy Headers** -Your proxy MUST forward these headers: -``` -X-Forwarded-Proto: https -X-Forwarded-Host: aliases.yourdomain.com -X-Forwarded-For: -``` - -#### **2. Check Browser Console** -Open browser DevTools (F12) → Console tab → Look for errors: -- **CORS errors**: Proxy misconfiguration -- **Mixed content**: HTTP resources on HTTPS page -- **401 Unauthorized**: Session/cookie issue - -#### **3. Test Direct Access** -```bash -# Test without proxy -curl http://localhost:5172/ - -# Should return HTML (login page) -``` - -If this works but proxy doesn't, it's a proxy configuration issue. - -#### **4. Check Session Cookies** -In browser DevTools → Application tab → Cookies: -- **Domain**: Should match your proxy domain -- **Path**: `/` -- **HttpOnly**: Should be `true` -- **Secure**: Depends on your setup - ---- - -### **Issue: Login works but redirects to HTTP instead of HTTPS** - -**Solution**: Already fixed in latest version via `PREFERRED_URL_SCHEME='https'` in app config. - -If still happening: -1. Verify proxy sends `X-Forwarded-Proto: https` -2. Check `docker compose logs -f mailcow-alias-manager` -3. Rebuild image: `docker compose up -d --build` - ---- - -### **Issue: "Invalid password" but password is correct** - -This is NOT a proxy issue - this is an authentication issue: -```bash -# Reset password -docker exec -it mailcow-alias-manager python3 reset_password.py "newpass" -``` - ---- - -### **Issue: 502 Bad Gateway** - -**Causes:** -1. App not running -2. Wrong upstream port -3. Container network issue - -**Check:** -```bash -# Is container running? -docker ps | grep mailcow-alias-manager - -# Check logs -docker compose logs -f mailcow-alias-manager - -# Test internal connectivity -docker exec mailcow-alias-manager curl http://localhost:5172 -``` - ---- - -## 🔒 Security Best Practices - -### **1. HTTPS Only** -Always use HTTPS in production. HTTP is only for local testing. - -### **2. Restrict Access** -Use firewall or proxy rules to restrict access: -```nginx -# Nginx: Restrict to internal network only -allow 192.168.1.0/24; -deny all; -``` - -### **3. Use Authelia/Authentik** -Add SSO layer for additional security: -- Users authenticate via Authelia -- Then app login provides API access -- Two-factor authentication recommended - -### **4. Keep Session Cookies Secure** -The app sets: -- `HttpOnly=True` - Prevents JavaScript access -- `SameSite=Lax` - CSRF protection - -For HTTPS-only deployments, you can enforce secure cookies: -```python -# In app.py, change: -SESSION_COOKIE_SECURE=True # Only send cookie over HTTPS -``` - ---- - -## 📊 Testing Your Setup - -### **1. Basic Connectivity** -```bash -curl -v https://aliases.yourdomain.com/ -# Should return 200 OK with HTML -``` - -### **2. Login Test** -```bash -curl -v -X POST https://aliases.yourdomain.com/login \ - -H "Content-Type: application/json" \ - -d '{"password":"yourpassword"}' \ - -c cookies.txt - -# Check response - should be JSON with status: success -``` - -### **3. Session Test** -```bash -curl -v https://aliases.yourdomain.com/ \ - -b cookies.txt - -# Should return main page HTML (not login redirect) -``` - ---- - -## 🆘 Still Having Issues? - -### **Enable Debug Logging** - -**docker-compose.yml:** -```yaml -environment: - - FLASK_DEBUG=True # Enable debug mode -``` - -Restart: -```bash -docker compose down -docker compose up -d -docker compose logs -f -``` - -### **Check Application Logs** -```bash -# View logs -docker compose logs -f mailcow-alias-manager - -# Check for errors -docker compose logs mailcow-alias-manager | grep -i error -``` - -### **Test Without Proxy** -Temporarily bypass proxy to isolate the issue: -```bash -# Direct access test -curl http://localhost:5172/ -``` - -If this works, the issue is with proxy configuration, not the app. - ---- - -## ✅ Summary - -**The app is already configured for reverse proxies!** - -Key points: -- ✅ ProxyFix middleware is enabled -- ✅ Handles X-Forwarded-* headers automatically -- ✅ HTTPS redirect support built-in -- ✅ Session cookies work behind proxies -- ✅ No code changes needed - -Just make sure your proxy forwards the standard headers and you're good to go! 🚀 +When running behind a proxy, use the following endpoints for debugging: +- `/debug` - Shows detailed request information +- `/authelia-test` - Tests Authelia header forwarding +- `/health` - Shows basic health and authentication status \ No newline at end of file diff --git a/app.py b/app.py index 1210e5b..0ea721a 100644 --- a/app.py +++ b/app.py @@ -18,82 +18,61 @@ logging.basicConfig( ) logger = logging.getLogger('mailcow-alias-manager') +# Environment variable to control proxy mode +ENABLE_PROXY = os.getenv('ENABLE_PROXY', 'false').lower() in ('true', '1', 'yes') + app = Flask(__name__) app.secret_key = os.getenv('SECRET_KEY', 'malias-default-secret-key-please-change') # Consistent secret key -# Configure for reverse proxy - modified to handle multiple proxy layers including Authelia -# Increasing the number of proxies to handle Zoraxy->Authelia->App chain -app.wsgi_app = ProxyFix( - app.wsgi_app, - x_for=2, # Trust X-Forwarded-For with 2 proxies (Zoraxy + Authelia) - x_proto=2, # Trust X-Forwarded-Proto (http/https) - x_host=2, # Trust X-Forwarded-Host - x_prefix=2 # Trust X-Forwarded-Prefix -) +# Configure app differently based on proxy mode +if ENABLE_PROXY: + # Configure for reverse proxy - modified to handle multiple proxy layers including Authelia + # Increasing the number of proxies to handle Zoraxy->Authelia->App chain + app.wsgi_app = ProxyFix( + app.wsgi_app, + x_for=2, # Trust X-Forwarded-For with 2 proxies (Zoraxy + Authelia) + x_proto=2, # Trust X-Forwarded-Proto (http/https) + x_host=2, # Trust X-Forwarded-Host + x_prefix=2 # Trust X-Forwarded-Prefix + ) + + # Session configuration optimized for reverse proxy with Authelia + app.config.update( + PERMANENT_SESSION_LIFETIME=timedelta(hours=24), + SESSION_COOKIE_NAME='malias_session', # Unique name to avoid conflicts with Authelia + SESSION_COOKIE_SECURE=True, # Always use secure cookies with Authelia + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE='None', # Required for authentication proxies + SESSION_COOKIE_PATH='/', + SESSION_COOKIE_DOMAIN=None, # Let browser auto-set domain + SESSION_REFRESH_EACH_REQUEST=True, # Keep session alive + PREFERRED_URL_SCHEME='https' + ) + + logger.info("Running in PROXY mode (with Authelia integration)") +else: + # Direct mode - simpler configuration for IP:port access + app.config.update( + PERMANENT_SESSION_LIFETIME=timedelta(hours=24), + SESSION_COOKIE_NAME='malias_session', + SESSION_COOKIE_SECURE=False, # Allow non-HTTPS cookies for direct IP access + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE='Lax', # More compatible SameSite setting + SESSION_COOKIE_PATH='/', + SESSION_REFRESH_EACH_REQUEST=True + ) + + logger.info("Running in DIRECT ACCESS mode (no reverse proxy integration)") # Initialize database on startup malias_w.init_database() -# Session configuration optimized for reverse proxy with Authelia -app.config.update( - PERMANENT_SESSION_LIFETIME=timedelta(hours=24), - SESSION_COOKIE_NAME='malias_session', # Unique name to avoid conflicts with Authelia - SESSION_COOKIE_SECURE=True, # Always use secure cookies with Authelia - SESSION_COOKIE_HTTPONLY=True, - SESSION_COOKIE_SAMESITE='None', # Required for authentication proxies - SESSION_COOKIE_PATH='/', - SESSION_COOKIE_DOMAIN=None, # Let browser auto-set domain - SESSION_REFRESH_EACH_REQUEST=True, # Keep session alive - PREFERRED_URL_SCHEME='https' -) - -# Set this to True to use the special cookie fix for Zoraxy -ZORAXY_COOKIE_FIX = True - -def fix_cookie_for_zoraxy(response): - """Special handler for Zoraxy cookie issues with SameSite=None""" - if not ZORAXY_COOKIE_FIX: - return response - - # Get all cookies from the response - cookies = response.headers.getlist('Set-Cookie') - if not cookies: - return response - - # Clear existing cookies - del response.headers['Set-Cookie'] - - # Fix each cookie and add it back - for cookie in cookies: - if 'malias_session' in cookie and 'SameSite' not in cookie: - # Add SameSite=None and Secure attributes - if 'HttpOnly' in cookie: - cookie = cookie.replace('HttpOnly', 'HttpOnly; SameSite=None; Secure') - else: - cookie += '; SameSite=None; Secure' - response.headers.add('Set-Cookie', cookie) - - return response - -@app.after_request -def after_request(response): - """Process the response before it's sent""" - # Apply special cookie fix for Zoraxy - response = fix_cookie_for_zoraxy(response) - - # Set CORS headers to allow Zoraxy and Authelia to work together - response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*') - response.headers['Access-Control-Allow-Credentials'] = 'true' - - # Cache control - response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' - response.headers['Pragma'] = 'no-cache' - response.headers['Expires'] = '0' - - return response - def get_authelia_user(): """Helper to get authenticated user from Authelia headers""" + # Only check for Authelia if proxy mode is enabled + if not ENABLE_PROXY: + return None + # Check several possible header variations auth_headers = [ 'Remote-User', @@ -131,26 +110,27 @@ def login_required(f): """Decorator to require login for routes""" @wraps(f) def decorated_function(*args, **kwargs): - # Check for Authelia authentication - authelia_user = get_authelia_user() - - # If Authelia authenticated the user, update local session - if authelia_user: - # Log all headers for debugging - if app.debug: - logger.info(f"Headers for authenticated request: {dict(request.headers)}") - - if not session.get('logged_in') or session.get('authelia_user') != authelia_user: - logger.info(f"Auto-login via Authelia for user: {authelia_user}") - session.clear() - session.permanent = True - session['logged_in'] = True - session['authelia_user'] = authelia_user - session['user_token'] = secrets.token_urlsafe(32) - session['auth_method'] = 'authelia' - session.modified = True - - return f(*args, **kwargs) + # Check for Authelia authentication when in proxy mode + if ENABLE_PROXY: + authelia_user = get_authelia_user() + + # If Authelia authenticated the user, update local session + if authelia_user: + # Log all headers for debugging + if app.debug: + logger.info(f"Headers for authenticated request: {dict(request.headers)}") + + if not session.get('logged_in') or session.get('authelia_user') != authelia_user: + logger.info(f"Auto-login via Authelia for user: {authelia_user}") + session.clear() + session.permanent = True + session['logged_in'] = True + session['authelia_user'] = authelia_user + session['user_token'] = secrets.token_urlsafe(32) + session['auth_method'] = 'authelia' + session.modified = True + + return f(*args, **kwargs) # Regular session check if not session.get('logged_in'): @@ -165,15 +145,17 @@ def login_required(f): @app.route('/login', methods=['GET', 'POST']) def login(): """Login page""" - # First, try Authelia authentication - authelia_user = get_authelia_user() + # Only check for Authelia if in proxy mode + authelia_user = None + if ENABLE_PROXY: + authelia_user = get_authelia_user() # Debug logging for all requests if app.debug: logger.info(f"Login route: method={request.method}, headers={dict(request.headers)}") - # If Authelia authenticated, login and redirect to index - if authelia_user: + # If Authelia authenticated (and we're in proxy mode), login and redirect to index + if ENABLE_PROXY and authelia_user: logger.info(f"Login via Authelia for user: {authelia_user}") session.clear() session.permanent = True @@ -183,19 +165,7 @@ def login(): session['auth_method'] = 'authelia' session.modified = True - # Set a cookie manually to ensure it's properly formatted for Zoraxy - response = redirect(url_for('index')) - # Set cookie parameters to work with Zoraxy/Authelia - response.set_cookie( - key=app.config['SESSION_COOKIE_NAME'], - value=secrets.token_urlsafe(32), # Generate a new token instead of using session.sid - max_age=int(app.config['PERMANENT_SESSION_LIFETIME'].total_seconds()), - path=app.config['SESSION_COOKIE_PATH'], - secure=app.config['SESSION_COOKIE_SECURE'], - httponly=app.config['SESSION_COOKIE_HTTPONLY'], - samesite='None' - ) - return response + return redirect(url_for('index')) # Handle form submission for local authentication if request.method == 'POST': @@ -212,23 +182,7 @@ def login(): session.modified = True # Return JSON response - response = jsonify({'status': 'success', 'message': 'Login successful'}) - - # Manually set cookie with correct parameters for Zoraxy - if ZORAXY_COOKIE_FIX: - session_token = secrets.token_urlsafe(32) # Generate a new token - response.set_cookie( - app.config['SESSION_COOKIE_NAME'], - session_token, - max_age=int(app.config['PERMANENT_SESSION_LIFETIME'].total_seconds()), - secure=app.config['SESSION_COOKIE_SECURE'], - httponly=app.config['SESSION_COOKIE_HTTPONLY'], - samesite='None', - path=app.config['SESSION_COOKIE_PATH'] - ) - logger.info(f"Set fixed cookie for Zoraxy: {app.config['SESSION_COOKIE_NAME']}") - - return response + return jsonify({'status': 'success', 'message': 'Login successful'}) else: logger.warning("Login failed: Invalid password") return jsonify({'status': 'error', 'message': 'Invalid password'}) @@ -258,8 +212,8 @@ def logout(): # Clear local session session.clear() - # If user was authenticated via Authelia, try to redirect to Authelia logout - if auth_method == 'authelia' or authelia_user: + # If user was authenticated via Authelia (and we're in proxy mode), try to redirect to Authelia logout + if ENABLE_PROXY and (auth_method == 'authelia' or authelia_user): # Look for Authelia URL in headers authelia_url = request.headers.get('X-Authelia-URL') @@ -284,47 +238,25 @@ def logout(): return redirect(common_authelia_urls[0]) # Default case: redirect to login page - response = redirect(url_for('login')) - - # Clear cookie by setting expired date - response.set_cookie(app.config['SESSION_COOKIE_NAME'], '', expires=0) - - return response + return redirect(url_for('login')) @app.route('/') def index(): """Main page - requires login""" - # Try to auto-login with Authelia - authelia_user = get_authelia_user() - - if authelia_user and not session.get('logged_in'): - # Auto-login for users authenticated by Authelia - logger.info(f"Auto-login via Authelia for user: {authelia_user}") - session.clear() - session.permanent = True - session['logged_in'] = True - session['authelia_user'] = authelia_user - session['user_token'] = secrets.token_urlsafe(32) - session['auth_method'] = 'authelia' - session.modified = True + # Try to auto-login with Authelia (only in proxy mode) + if ENABLE_PROXY: + authelia_user = get_authelia_user() - # Set cookie manually with correct parameters - response = make_response(render_template('index.html')) - - if ZORAXY_COOKIE_FIX: - session_token = secrets.token_urlsafe(32) # Generate a new token - response.set_cookie( - app.config['SESSION_COOKIE_NAME'], - session_token, - max_age=int(app.config['PERMANENT_SESSION_LIFETIME'].total_seconds()), - secure=app.config['SESSION_COOKIE_SECURE'], - httponly=app.config['SESSION_COOKIE_HTTPONLY'], - samesite='None', - path=app.config['SESSION_COOKIE_PATH'] - ) - logger.info(f"Set fixed cookie for Zoraxy on index: {app.config['SESSION_COOKIE_NAME']}") - - return response + if authelia_user and not session.get('logged_in'): + # Auto-login for users authenticated by Authelia + logger.info(f"Auto-login via Authelia for user: {authelia_user}") + session.clear() + session.permanent = True + session['logged_in'] = True + session['authelia_user'] = authelia_user + session['user_token'] = secrets.token_urlsafe(32) + session['auth_method'] = 'authelia' + session.modified = True # Check if logged in if not session.get('logged_in'): @@ -569,10 +501,11 @@ def health_check(): 'authenticated': session.get('logged_in', False), 'authelia_user': session.get('authelia_user', None), 'auth_method': session.get('auth_method'), - 'version': '1.0.2', + 'version': '1.0.3', 'cookies': {k: '***' for k in request.cookies.keys()}, # Only show cookie names 'authelia_headers_present': get_authelia_user() is not None, - 'zoraxy_headers_present': 'X-Forwarded-Server' in request.headers + 'proxy_mode_enabled': ENABLE_PROXY, + 'zoraxy_detected': 'X-Forwarded-Server' in request.headers }) # Add a debugging endpoint @@ -589,11 +522,12 @@ def debug_info(): 'host': request.host, 'path': request.path, 'is_secure': request.is_secure, + 'proxy_mode_enabled': ENABLE_PROXY, 'x_forwarded_for': request.headers.get('X-Forwarded-For'), 'x_forwarded_proto': request.headers.get('X-Forwarded-Proto'), 'x_forwarded_host': request.headers.get('X-Forwarded-Host'), 'x_forwarded_prefix': request.headers.get('X-Forwarded-Prefix'), - 'remote_user': get_authelia_user(), + 'remote_user': get_authelia_user() } # Additional server information @@ -607,8 +541,8 @@ def debug_info(): }, 'app_config': { 'DEBUG': app.debug, - 'PREFERRED_URL_SCHEME': app.config['PREFERRED_URL_SCHEME'], - 'ZORAXY_COOKIE_FIX': ZORAXY_COOKIE_FIX + 'PREFERRED_URL_SCHEME': app.config.get('PREFERRED_URL_SCHEME', 'http'), + 'ENABLE_PROXY': ENABLE_PROXY } } @@ -618,50 +552,6 @@ def debug_info(): response.headers['Expires'] = '0' return response -# Dedicated endpoint for header diagnostics -@app.route('/headers') -def show_headers(): - """Show all request headers - useful for debugging proxies""" - # Log headers to help diagnose issues with Zoraxy/Authelia - logger.info(f"Headers endpoint: All headers received: {dict(request.headers)}") - return jsonify({ - 'headers': dict(request.headers), - 'remote_addr': request.remote_addr, - 'authelia_user': get_authelia_user() - }) - -# Self-test endpoint for cookies -@app.route('/cookie-test') -def cookie_test(): - """Test cookie handling""" - # Clear any existing cookie - resp = make_response(jsonify({'status': 'ok', 'message': 'Cookie set test'})) - - # Set a test cookie with the same attributes as session - resp.set_cookie( - 'malias_test_cookie', - 'test-value', - max_age=3600, - secure=app.config['SESSION_COOKIE_SECURE'], - httponly=app.config['SESSION_COOKIE_HTTPONLY'], - samesite='None', - path=app.config['SESSION_COOKIE_PATH'] - ) - - return resp - -# Endpoint to check if test cookie is set -@app.route('/cookie-check') -def cookie_check(): - """Check if test cookie was properly set""" - test_cookie = request.cookies.get('malias_test_cookie') - return jsonify({ - 'test_cookie_present': test_cookie is not None, - 'test_cookie_value': test_cookie if test_cookie else None, - 'all_cookies': {k: '***' for k in request.cookies.keys()} - }) - -# New endpoint to test Zoraxy auth configuration @app.route('/authelia-test') def authelia_test(): """Test if Authelia headers are correctly passed through Zoraxy""" @@ -672,6 +562,7 @@ def authelia_test(): auth_related_headers = [ 'Remote-User', 'X-Remote-User', 'Remote-Groups', 'X-Remote-Groups', 'Remote-Name', 'X-Remote-Name', 'Remote-Email', 'X-Remote-Email', + 'X-Authelia-Username', 'X-Authelia-Groups', 'X-Authelia-DisplayName', 'X-Authelia-Email', 'X-Authelia-URL', 'X-Original-URL', 'X-Forwarded-Proto' ] @@ -688,6 +579,7 @@ def authelia_test(): auth_cookies[cookie_name] = '***' # Hide actual value return jsonify({ + 'proxy_mode_enabled': ENABLE_PROXY, 'request_host': request.host, 'authelia_user_detected': get_authelia_user() is not None, 'authelia_user': get_authelia_user(), @@ -696,7 +588,7 @@ def authelia_test(): 'all_headers_count': len(all_headers), 'zoraxy_detected': any('zoraxy' in h.lower() for h in all_headers.keys()) or 'X-Forwarded-Server' in all_headers, 'host_header': request.headers.get('Host'), - 'referer': request.headers.get('Referer'), + 'referer': request.headers.get('Referer') }) if __name__ == '__main__': @@ -717,6 +609,7 @@ Environment Variables (Docker): FLASK_PORT Port to run on (default: 5172) FLASK_HOST Host to bind to (default: 0.0.0.0) FLASK_ENV Set to 'production' or 'development' + ENABLE_PROXY Set to 'true' to enable proxy/Authelia integration (default: false) ''' ) @@ -772,6 +665,7 @@ Environment Variables (Docker): print(f' Host: {host}') print(f' Port: {port}') print(f' URL: http://{host}:{port}') + print(f' Proxy: {"ENABLED" if ENABLE_PROXY else "DISABLED"}') print('=' * 60) if debug_mode: print(' ⚠️ WARNING: Debug mode is enabled!') @@ -781,6 +675,6 @@ Environment Variables (Docker): print('=' * 60) print() - logger.info(f"Starting Mailcow Alias Manager - Debug: {debug_mode}, Host: {host}, Port: {port}") + logger.info(f"Starting Mailcow Alias Manager - Debug: {debug_mode}, Host: {host}, Port: {port}, Proxy: {ENABLE_PROXY}") app.run(debug=debug_mode, host=host, port=port) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9a91e74..327ab49 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,46 +1,17 @@ -# Mailcow Alias Manager - Docker Compose Configuration -# -# Production vs Development Mode: -# -------------------------------- -# FLASK_ENV=production + FLASK_DEBUG=False → Uses Gunicorn (4 workers, production-ready) -# FLASK_ENV=development or FLASK_DEBUG=True → Uses Flask dev server (auto-reload, debug info) -# -# Recommended Settings: -# - Production: FLASK_ENV=production, FLASK_DEBUG=False (default) -# - Development: FLASK_ENV=development, FLASK_DEBUG=True -# -# Reverse Proxy Support: -# ---------------------- -# This application is configured to work behind reverse proxies (Nginx, Traefik, Zoraxy, Authelia, etc.) -# The ProxyFix middleware automatically handles X-Forwarded-* headers for HTTPS detection -# No additional configuration needed for most standard proxy setups +version: '3' services: mailcow-alias-manager: - image: gitlab.pm/rune/malias-web:latest - container_name: mailcow-alias-manager - ports: - - "5172:5172" - volumes: - - ./data:/app/data + build: . restart: unless-stopped environment: - - TZ=Europe/Oslo - - # Flask Configuration - # Set to 'production' for Gunicorn (recommended) or 'development' for Flask dev server - - FLASK_ENV=production - - # Debug mode: False for production (recommended), True for development/troubleshooting - - FLASK_DEBUG=False - - # Port configuration (default: 5172) - - FLASK_PORT=5172 - - # Host binding (default: 0.0.0.0 for Docker) + - FLASK_PORT=5142 - FLASK_HOST=0.0.0.0 - - # Secret key for sessions - CRITICAL for multi-worker Gunicorn! - # All workers must use the SAME secret key to decode session cookies - # Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" - - SECRET_KEY=dca6920115b8bbc13c346c75d668a49849590c77d9baaf903296582f24ff816a \ No newline at end of file + - FLASK_ENV=production + - FLASK_DEBUG=False + # Set to 'true' to enable proxy/Authelia integration, 'false' for direct IP:port access + - ENABLE_PROXY=false + volumes: + - ./data:/app/data + ports: + - "5142:5142" \ No newline at end of file