From 877ecf32b4d18107dee9b905330f15b79127e0a0 Mon Sep 17 00:00:00 2001 From: Rune Olsen Date: Fri, 23 Jan 2026 13:32:19 +0100 Subject: [PATCH] Fixed login with only ip:port access --- Dockerfile | 1 + PROXY_SETUP.md | 111 +++++++++++++++++++++++++++++++++- README.md | 25 +++++++- app.py | 145 +++++++++++++++++++-------------------------- docker-compose.yml | 21 ++++++- 5 files changed, 214 insertions(+), 89 deletions(-) diff --git a/Dockerfile b/Dockerfile index b1fd91d..82c0b28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ EXPOSE 5172 ENV PYTHONUNBUFFERED=1 ENV FLASK_DEBUG=False ENV FLASK_ENV=production +ENV ENABLE_PROXY=false # Run the application via entrypoint ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/PROXY_SETUP.md b/PROXY_SETUP.md index d51426e..dbe9fc8 100644 --- a/PROXY_SETUP.md +++ b/PROXY_SETUP.md @@ -2,7 +2,114 @@ This guide helps you configure reverse proxies (Nginx, Traefik, Zoraxy, Authelia, Caddy, etc.) to work with Mailcow Alias Manager. -## ✅ Built-in Proxy Support +--- + +## 🔧 ENABLE_PROXY Configuration + +The application supports **two access modes** controlled by the `ENABLE_PROXY` environment variable in `docker-compose.yml`: + +### **Mode 1: Direct Access (ENABLE_PROXY=false)** - DEFAULT + +Use this when accessing the application directly via IP:port **without a reverse proxy**. + +**docker-compose.yml:** +```yaml +environment: + - ENABLE_PROXY=false # Default setting +``` + +**Access:** `http://192.168.1.100:5172` (replace with your server IP) + +**Features:** +- ✅ Works over HTTP (no HTTPS required) +- ✅ Standard cookie behavior (SameSite=Lax) +- ✅ No proxy configuration needed +- ✅ Simple login flow +- ✅ Perfect for internal/LAN access + +**When to use:** +- Accessing from internal network only +- No reverse proxy in place +- Testing or development +- Simple single-server setup + +--- + +### **Mode 2: Proxy Access (ENABLE_PROXY=true)** + +Use this when running **behind a reverse proxy** (Authelia, Zoraxy, Nginx, Traefik, Caddy). + +**docker-compose.yml:** +```yaml +environment: + - ENABLE_PROXY=true +``` + +**Access:** `https://alias.yourdomain.com` (through your reverse proxy) + +**Features:** +- ✅ ProxyFix middleware handles X-Forwarded-* headers +- ✅ HTTPS redirect support +- ✅ Secure cookies (HTTPS only) +- ✅ Works with authentication proxies (Authelia) +- ✅ Multi-proxy chain support + +**When to use:** +- Accessing from internet via domain name +- Behind Nginx, Traefik, Caddy, HAProxy +- Behind authentication proxy (Authelia, Authentik) +- SSL/TLS termination at proxy +- Production deployments with HTTPS + +--- + +### **Switching Between Modes** + +To switch from one mode to another: + +1. **Edit `docker-compose.yml`** + ```yaml + # Change this line: + - ENABLE_PROXY=false # or true + ``` + +2. **Restart the container** + ```bash + docker compose down + docker compose up -d + ``` + +3. **Verify mode in logs** + ```bash + docker compose logs mailcow-alias-manager | grep "ACCESS MODE" + ``` + + You should see either: + - `ACCESS MODE: Direct IP:Port (ENABLE_PROXY=false)` + - `ACCESS MODE: Reverse Proxy (ENABLE_PROXY=true)` + +4. **Clear browser cookies** (IMPORTANT!) + - Press F12 → Application → Cookies + - Delete all cookies for your domain + - Close and reopen browser + +5. **Login again** + +--- + +### **Quick Reference Table** + +| Access Method | ENABLE_PROXY | Access URL | Cookie Mode | +|--------------|--------------|------------|-------------| +| Direct IP:port | `false` (default) | `http://192.168.1.100:5172` | HTTP, SameSite=Lax | +| Nginx/Traefik | `true` | `https://alias.example.com` | HTTPS, SameSite=None | +| Authelia + Zoraxy | `true` | `https://alias.example.com` | HTTPS, SameSite=None | +| Caddy | `true` | `https://alias.example.com` | HTTPS, SameSite=None | +| Local dev (`python3 app.py`) | N/A (not set) | `http://localhost:5172` | HTTP, SameSite=Lax | + +--- + +## ✅ Built-in Proxy Support (When ENABLE_PROXY=true) The application includes **ProxyFix middleware** that automatically handles: - ✅ HTTPS detection via `X-Forwarded-Proto` @@ -10,7 +117,7 @@ The application includes **ProxyFix middleware** that automatically handles: - ✅ Client IP forwarding via `X-Forwarded-For` - ✅ Path prefix support via `X-Forwarded-Prefix` -**No configuration changes needed in most cases!** +**Works with 2 proxies in chain** (e.g., Zoraxy → Authelia → App) --- diff --git a/README.md b/README.md index 5334d4b..a4a6848 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,30 @@ The application will be available at: `http://localhost:5172` ## Configuration -### 1. Configure Mailcow Connection +### 1. Access Mode Configuration + +The application supports two access modes: + +**Direct Access (Default)** - Access via IP:port +- Set `ENABLE_PROXY=false` in `docker-compose.yml` (default) +- Access at: `http://your-server-ip:5172` +- Works over HTTP (no HTTPS required) +- Perfect for internal/LAN access + +**Proxy Access** - Access via reverse proxy (Authelia, Nginx, Traefik, etc.) +- Set `ENABLE_PROXY=true` in `docker-compose.yml` +- Access at: `https://alias.yourdomain.com` (through your proxy) +- Requires HTTPS +- See [PROXY_SETUP.md](PROXY_SETUP.md) for detailed configuration + +**To switch modes:** +1. Edit `docker-compose.yml` and change `ENABLE_PROXY` value +2. Restart: `docker compose down && docker compose up -d` +3. Clear browser cookies and login again + +--- + +### 2. Configure Mailcow Connection You can configure your Mailcow server connection either: diff --git a/app.py b/app.py index 1210e5b..3b93f00 100644 --- a/app.py +++ b/app.py @@ -21,71 +21,78 @@ logger = logging.getLogger('mailcow-alias-manager') 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 proxy mode based on environment variable +# ENABLE_PROXY=true: Behind reverse proxy (Authelia/Zoraxy/Nginx/etc.) +# ENABLE_PROXY=false: Direct IP:port access (default) +ENABLE_PROXY = os.getenv('ENABLE_PROXY', 'false').lower() in ('true', '1', 'yes') + +if ENABLE_PROXY: + # Mode: Behind reverse proxy (Authelia/Zoraxy/Nginx/etc.) + logger.info("=" * 60) + logger.info(" ACCESS MODE: Reverse Proxy (ENABLE_PROXY=true)") + logger.info("=" * 60) + logger.info(" Configuration:") + logger.info(" - ProxyFix middleware: ACTIVE") + logger.info(" - Trusted proxies: 2 (e.g., Zoraxy + Authelia)") + logger.info(" - Cookie security: HTTPS only (Secure=True)") + logger.info(" - SameSite policy: None (cross-origin auth)") + logger.info(" - URL scheme: https://") + logger.info("=" * 60) + + app.wsgi_app = ProxyFix( + app.wsgi_app, + x_for=2, # Trust X-Forwarded-For with 2 proxies + x_proto=2, # Trust X-Forwarded-Proto (http/https) + x_host=2, # Trust X-Forwarded-Host + x_prefix=2 # Trust X-Forwarded-Prefix + ) + + cookie_secure = True + cookie_samesite = 'None' + preferred_scheme = 'https' +else: + # Mode: Direct IP:port access (no proxy) + logger.info("=" * 60) + logger.info(" ACCESS MODE: Direct IP:Port (ENABLE_PROXY=false)") + logger.info("=" * 60) + logger.info(" Configuration:") + logger.info(" - ProxyFix middleware: DISABLED") + logger.info(" - Cookie security: HTTP allowed (Secure=False)") + logger.info(" - SameSite policy: Lax (standard mode)") + logger.info(" - URL scheme: http://") + logger.info(" - Access via: http://your-ip:5172") + logger.info("=" * 60) + + # Don't apply ProxyFix for direct access + cookie_secure = False + cookie_samesite = 'Lax' + preferred_scheme = 'http' # Initialize database on startup malias_w.init_database() -# Session configuration optimized for reverse proxy with Authelia +# Session configuration - dynamic based on proxy mode 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_NAME='malias_session', + SESSION_COOKIE_SECURE=cookie_secure, SESSION_COOKIE_HTTPONLY=True, - SESSION_COOKIE_SAMESITE='None', # Required for authentication proxies + SESSION_COOKIE_SAMESITE=cookie_samesite, SESSION_COOKIE_PATH='/', - SESSION_COOKIE_DOMAIN=None, # Let browser auto-set domain - SESSION_REFRESH_EACH_REQUEST=True, # Keep session alive - PREFERRED_URL_SCHEME='https' + SESSION_COOKIE_DOMAIN=None, + SESSION_REFRESH_EACH_REQUEST=True, + PREFERRED_URL_SCHEME=preferred_scheme ) -# 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) + if ENABLE_PROXY: + # Set CORS headers for proxy mode (needed for Authelia/Zoraxy) + response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*') + response.headers['Access-Control-Allow-Credentials'] = 'true' - # 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 + # Cache control (applies to both modes) response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' @@ -213,21 +220,6 @@ def login(): # 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 else: logger.warning("Login failed: Invalid password") @@ -307,24 +299,7 @@ def index(): session['user_token'] = secrets.token_urlsafe(32) session['auth_method'] = 'authelia' session.modified = True - - # 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 + return render_template('index.html') # Check if logged in if not session.get('logged_in'): @@ -608,7 +583,7 @@ def debug_info(): 'app_config': { 'DEBUG': app.debug, 'PREFERRED_URL_SCHEME': app.config['PREFERRED_URL_SCHEME'], - 'ZORAXY_COOKIE_FIX': ZORAXY_COOKIE_FIX + 'ENABLE_PROXY': ENABLE_PROXY } } diff --git a/docker-compose.yml b/docker-compose.yml index 9a91e74..ed7b2a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,4 +43,23 @@ services: # 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 + - SECRET_KEY=dca6920115b8bbc13c346c75d668a49849590c77d9baaf903296582f24ff816a + + # Reverse Proxy Mode + # ------------------ + # Set to 'true' when accessing through reverse proxy (Authelia, Zoraxy, Nginx, etc.) + # Set to 'false' for direct IP:port access (e.g., http://192.168.1.100:5172) + # + # ENABLE_PROXY=true (Proxy Mode): + # - ProxyFix middleware enabled (handles X-Forwarded-* headers) + # - Secure cookies required (HTTPS only) + # - SameSite=None (allows cross-origin authentication) + # - Access via: https://alias.yourdomain.com + # + # ENABLE_PROXY=false (Direct Mode) - DEFAULT: + # - ProxyFix middleware disabled + # - Standard cookies (HTTP allowed) + # - SameSite=Lax (standard browser behavior) + # - Access via: http://your-server-ip:5172 + # + - ENABLE_PROXY=false \ No newline at end of file