Fixed login with only ip:port access

This commit is contained in:
2026-01-23 13:32:19 +01:00
parent 3b78959901
commit 877ecf32b4
5 changed files with 214 additions and 89 deletions

View File

@@ -29,6 +29,7 @@ EXPOSE 5172
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV FLASK_DEBUG=False ENV FLASK_DEBUG=False
ENV FLASK_ENV=production ENV FLASK_ENV=production
ENV ENABLE_PROXY=false
# Run the application via entrypoint # Run the application via entrypoint
ENTRYPOINT ["./docker-entrypoint.sh"] ENTRYPOINT ["./docker-entrypoint.sh"]

View File

@@ -2,7 +2,114 @@
This guide helps you configure reverse proxies (Nginx, Traefik, Zoraxy, Authelia, Caddy, etc.) to work with Mailcow Alias Manager. 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: The application includes **ProxyFix middleware** that automatically handles:
- ✅ HTTPS detection via `X-Forwarded-Proto` - ✅ 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` - ✅ Client IP forwarding via `X-Forwarded-For`
- ✅ Path prefix support via `X-Forwarded-Prefix` - ✅ 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)
--- ---

View File

@@ -68,7 +68,30 @@ The application will be available at: `http://localhost:5172`
## Configuration ## 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: You can configure your Mailcow server connection either:

129
app.py
View File

@@ -21,71 +21,78 @@ logger = logging.getLogger('mailcow-alias-manager')
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY', 'malias-default-secret-key-please-change') # Consistent secret key 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 # Configure proxy mode based on environment variable
# Increasing the number of proxies to handle Zoraxy->Authelia->App chain # 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 = ProxyFix(
app.wsgi_app, app.wsgi_app,
x_for=2, # Trust X-Forwarded-For with 2 proxies (Zoraxy + Authelia) x_for=2, # Trust X-Forwarded-For with 2 proxies
x_proto=2, # Trust X-Forwarded-Proto (http/https) x_proto=2, # Trust X-Forwarded-Proto (http/https)
x_host=2, # Trust X-Forwarded-Host x_host=2, # Trust X-Forwarded-Host
x_prefix=2 # Trust X-Forwarded-Prefix 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 # Initialize database on startup
malias_w.init_database() malias_w.init_database()
# Session configuration optimized for reverse proxy with Authelia # Session configuration - dynamic based on proxy mode
app.config.update( app.config.update(
PERMANENT_SESSION_LIFETIME=timedelta(hours=24), PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
SESSION_COOKIE_NAME='malias_session', # Unique name to avoid conflicts with Authelia SESSION_COOKIE_NAME='malias_session',
SESSION_COOKIE_SECURE=True, # Always use secure cookies with Authelia SESSION_COOKIE_SECURE=cookie_secure,
SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='None', # Required for authentication proxies SESSION_COOKIE_SAMESITE=cookie_samesite,
SESSION_COOKIE_PATH='/', SESSION_COOKIE_PATH='/',
SESSION_COOKIE_DOMAIN=None, # Let browser auto-set domain SESSION_COOKIE_DOMAIN=None,
SESSION_REFRESH_EACH_REQUEST=True, # Keep session alive SESSION_REFRESH_EACH_REQUEST=True,
PREFERRED_URL_SCHEME='https' 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 @app.after_request
def after_request(response): def after_request(response):
"""Process the response before it's sent""" """Process the response before it's sent"""
# Apply special cookie fix for Zoraxy if ENABLE_PROXY:
response = fix_cookie_for_zoraxy(response) # Set CORS headers for proxy mode (needed for Authelia/Zoraxy)
# 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-Origin'] = request.headers.get('Origin', '*')
response.headers['Access-Control-Allow-Credentials'] = 'true' 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['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache' response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0' response.headers['Expires'] = '0'
@@ -213,21 +220,6 @@ def login():
# Return JSON response # Return JSON response
response = jsonify({'status': 'success', 'message': 'Login successful'}) 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 response
else: else:
logger.warning("Login failed: Invalid password") logger.warning("Login failed: Invalid password")
@@ -307,24 +299,7 @@ def index():
session['user_token'] = secrets.token_urlsafe(32) session['user_token'] = secrets.token_urlsafe(32)
session['auth_method'] = 'authelia' session['auth_method'] = 'authelia'
session.modified = True session.modified = True
return render_template('index.html')
# 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
# Check if logged in # Check if logged in
if not session.get('logged_in'): if not session.get('logged_in'):
@@ -608,7 +583,7 @@ def debug_info():
'app_config': { 'app_config': {
'DEBUG': app.debug, 'DEBUG': app.debug,
'PREFERRED_URL_SCHEME': app.config['PREFERRED_URL_SCHEME'], 'PREFERRED_URL_SCHEME': app.config['PREFERRED_URL_SCHEME'],
'ZORAXY_COOKIE_FIX': ZORAXY_COOKIE_FIX 'ENABLE_PROXY': ENABLE_PROXY
} }
} }

View File

@@ -44,3 +44,22 @@ services:
# All workers must use the SAME secret key to decode session cookies # All workers must use the SAME secret key to decode session cookies
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" # Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
- SECRET_KEY=dca6920115b8bbc13c346c75d668a49849590c77d9baaf903296582f24ff816a - 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