Fixed login with only ip:port access
This commit is contained in:
@@ -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"]
|
||||||
|
|||||||
111
PROXY_SETUP.md
111
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.
|
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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -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:
|
||||||
|
|
||||||
|
|||||||
145
app.py
145
app.py
@@ -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.)
|
||||||
app.wsgi_app = ProxyFix(
|
# ENABLE_PROXY=false: Direct IP:port access (default)
|
||||||
app.wsgi_app,
|
ENABLE_PROXY = os.getenv('ENABLE_PROXY', 'false').lower() in ('true', '1', 'yes')
|
||||||
x_for=2, # Trust X-Forwarded-For with 2 proxies (Zoraxy + Authelia)
|
|
||||||
x_proto=2, # Trust X-Forwarded-Proto (http/https)
|
if ENABLE_PROXY:
|
||||||
x_host=2, # Trust X-Forwarded-Host
|
# Mode: Behind reverse proxy (Authelia/Zoraxy/Nginx/etc.)
|
||||||
x_prefix=2 # Trust X-Forwarded-Prefix
|
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
|
# 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)
|
||||||
|
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
|
# Cache control (applies to both modes)
|
||||||
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['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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user