feat(dashboard): require basic auth for external clients only
- Allow direct access from localhost and private RFC1918 networks - Enforce HTTP Basic Auth for non-private/external source IPs - Read dashboard credentials from compose env vars - Add .env.example entries for DASHBOARD_AUTH_USERNAME/PASSWORD - Update README and DASHBOARD docs
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# Dashboard auth for external clients (non-RFC1918 source IPs)
|
||||
DASHBOARD_AUTH_USERNAME=admin
|
||||
DASHBOARD_AUTH_PASSWORD=change-me-now
|
||||
14
DASHBOARD.md
14
DASHBOARD.md
@@ -154,8 +154,14 @@ curl http://localhost:8080/api/health | jq
|
||||
|
||||
## Security Note
|
||||
|
||||
The dashboard is exposed on `0.0.0.0:8080` making it accessible from your network. If you're running this on a public server, consider:
|
||||
The dashboard is exposed on `0.0.0.0:8080`.
|
||||
Requests from localhost/LAN private ranges are allowed directly.
|
||||
Requests from public/external IPs require HTTP Basic Auth.
|
||||
|
||||
1. Using a reverse proxy (nginx) with authentication
|
||||
2. Restricting access with firewall rules
|
||||
3. Using HTTPS with SSL certificates
|
||||
Set credentials with:
|
||||
|
||||
```bash
|
||||
# .env (copy from .env.example)
|
||||
DASHBOARD_AUTH_USERNAME=admin
|
||||
DASHBOARD_AUTH_PASSWORD=change-me-now
|
||||
```
|
||||
|
||||
@@ -242,6 +242,7 @@ For your ElectrumX server to be accessible from the internet, you **must** confi
|
||||
**Security Notes:**
|
||||
- Only forward port **8080** if you want the dashboard accessible from internet (not recommended without authentication)
|
||||
- Consider using a VPN for dashboard access instead
|
||||
- External dashboard clients (public IPs) require Basic Auth. Configure `DASHBOARD_AUTH_USERNAME` and `DASHBOARD_AUTH_PASSWORD` in `.env` (see `.env.example`).
|
||||
- Ports **50001** and **50002** need to be public for Electrum wallets to connect
|
||||
- Port **2333** is required for the node to sync with the Palladium network
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ services:
|
||||
PALLADIUM_RPC_PORT: "2332"
|
||||
ELECTRUMX_RPC_HOST: "electrumx"
|
||||
ELECTRUMX_RPC_PORT: "8000"
|
||||
DASHBOARD_AUTH_USERNAME: "${DASHBOARD_AUTH_USERNAME:-admin}"
|
||||
DASHBOARD_AUTH_PASSWORD: "${DASHBOARD_AUTH_PASSWORD:-change-me-now}"
|
||||
|
||||
volumes:
|
||||
- ./.palladium/palladium.conf:/palladium-config/palladium.conf:ro
|
||||
|
||||
@@ -12,6 +12,9 @@ import time
|
||||
import copy
|
||||
import threading
|
||||
import ssl
|
||||
import ipaddress
|
||||
import base64
|
||||
import hmac
|
||||
from datetime import datetime
|
||||
import psutil
|
||||
import socket
|
||||
@@ -19,6 +22,66 @@ import socket
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
TRUSTED_CLIENT_NETWORKS = (
|
||||
ipaddress.ip_network('127.0.0.0/8'),
|
||||
ipaddress.ip_network('10.0.0.0/8'),
|
||||
ipaddress.ip_network('172.16.0.0/12'),
|
||||
ipaddress.ip_network('192.168.0.0/16'),
|
||||
ipaddress.ip_network('::1/128'),
|
||||
)
|
||||
|
||||
|
||||
def is_trusted_client_ip(ip_text):
|
||||
"""Allow direct access for localhost and private RFC1918 LAN clients."""
|
||||
if not ip_text:
|
||||
return False
|
||||
try:
|
||||
ip_obj = ipaddress.ip_address(ip_text.strip())
|
||||
if getattr(ip_obj, 'ipv4_mapped', None):
|
||||
ip_obj = ip_obj.ipv4_mapped
|
||||
return any(ip_obj in network for network in TRUSTED_CLIENT_NETWORKS)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def unauthorized_response():
|
||||
response = jsonify({'error': 'Authentication required'})
|
||||
response.status_code = 401
|
||||
response.headers['WWW-Authenticate'] = 'Basic realm="Palladium Dashboard"'
|
||||
return response
|
||||
|
||||
|
||||
@app.before_request
|
||||
def enforce_external_auth():
|
||||
"""
|
||||
Localhost and LAN clients can access directly.
|
||||
External clients must authenticate via Basic Auth credentials from env vars.
|
||||
"""
|
||||
client_ip = request.remote_addr or ''
|
||||
if is_trusted_client_ip(client_ip):
|
||||
return None
|
||||
|
||||
expected_user = os.getenv('DASHBOARD_AUTH_USERNAME', '').strip()
|
||||
expected_pass = os.getenv('DASHBOARD_AUTH_PASSWORD', '').strip()
|
||||
if not expected_user or not expected_pass:
|
||||
return jsonify({'error': 'Dashboard auth is not configured'}), 503
|
||||
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Basic '):
|
||||
return unauthorized_response()
|
||||
|
||||
try:
|
||||
decoded = base64.b64decode(auth_header.split(' ', 1)[1]).decode('utf-8')
|
||||
username, password = decoded.split(':', 1)
|
||||
except Exception:
|
||||
return unauthorized_response()
|
||||
|
||||
user_ok = hmac.compare_digest(username, expected_user)
|
||||
pass_ok = hmac.compare_digest(password, expected_pass)
|
||||
if not (user_ok and pass_ok):
|
||||
return unauthorized_response()
|
||||
return None
|
||||
|
||||
|
||||
@app.after_request
|
||||
def disable_api_cache(response):
|
||||
|
||||
Reference in New Issue
Block a user