optional disable of password auth

This commit is contained in:
ChrispyBacon-dev 2025-08-12 15:56:48 +02:00
parent 2189e77b74
commit 6e4d1a0920
5 changed files with 148 additions and 74 deletions

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="flex justify-center items-center min-h-screen">
<div class="card w-96 bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">{{ title }}</h2>
<p>Password login has been disabled by the administrator.</p>
<p>Please use an alternative login method, such as a Cloudflare Access policy.</p>
<div class="card-actions justify-end">
<a href="{{ url_for('web.status_page') }}" class="btn btn-primary">Go to Status Page</a>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -287,19 +287,42 @@
</form>
</div>
<div>
<h3 class="font-semibold text-lg mb-2">Password Reset</h3>
<div role="alert" class="alert alert-info text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>
To reset your password, you must have filesystem access to the DockFlare container.
<br>
1. Stop the DockFlare container.
<br>
2. Delete the `dockflare_config.dat` and `dockflare.key` files from your persistent data volume.
<br>
3. Restart the container. You will be prompted to go through the initial setup again.
</span>
</div>
<h3 class="font-semibold text-lg mb-2">Disable Password Login</h3>
<form method="POST" action="{{ url_for('web.settings_page') }}" class="space-y-4 protocol-aware-form">
{{ security_settings_form.hidden_tag() }}
<div class="form-control">
<label class="label cursor-pointer p-0">
<span class="label-text">{{ security_settings_form.disable_password_login.label.text }}</span>
{{ security_settings_form.disable_password_login(class="toggle toggle-primary") }}
</label>
<div class="text-xs opacity-70 mt-1">{{ security_settings_form.disable_password_login.description }}</div>
</div>
<div role="alert" class="alert alert-warning text-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<span><strong>Security Warning:</strong>When disabling password login, you become responsible for securing DockFlare access. Best practice is to use a Cloudflare Tunnel with an Access Policy and ensure Docker ports are not exposed, preventing access from the local network (LAN).</span>
</div>
<div class="card-actions">
{{ security_settings_form.submit_security_settings(class="btn btn-warning") }}
</div>
</form>
</div>
</div>
<div class="mt-8">
<h3 class="font-semibold text-lg mb-2">Password Reset</h3>
<div role="alert" class="alert alert-info text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>
To reset your password, you must have filesystem access to the DockFlare container.
<br>
1. Stop the DockFlare container.
<br>
2. Delete the `dockflare_config.dat` and `dockflare.key` files from your persistent data volume.
<br>
3. Restart the container. You will be prompted to go through the initial setup again.
</span>
</div>
</div>
</div>

View file

@ -17,6 +17,11 @@ class LoginForm(FlaskForm):
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""Handles the user login process."""
if current_app.config.get('DISABLE_PASSWORD_LOGIN'):
flash('Password login is disabled. Please use an alternative login method.', 'warning')
# Still render a basic page, but without the form.
return render_template('auth/login_disabled.html', title="Login Disabled")
if current_user.is_authenticated:
return redirect(url_for('web.status_page'))

View file

@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# app/web/forms.py
from flask_wtf import FlaskForm
from wtforms import PasswordField, SubmitField, StringField, IntegerField
from wtforms import PasswordField, SubmitField, StringField, IntegerField, BooleanField
from wtforms.validators import DataRequired, EqualTo, Length, Optional
class SettingsForm(FlaskForm):
@ -39,6 +39,14 @@ class SettingsForm(FlaskForm):
)
submit_settings = SubmitField('Save General Settings')
class SecuritySettingsForm(FlaskForm):
"""Form for editing security settings."""
disable_password_login = BooleanField(
'Disable Password Login',
description="If selected, password-based login will be disabled. Access to DockFlare will only be possible through a Cloudflare Access policy."
)
submit_security_settings = SubmitField('Save Security Settings')
class ChangePasswordForm(FlaskForm):
"""Form for changing the user's password."""
current_password = PasswordField(

View file

@ -217,7 +217,7 @@ def status_page():
CF_ZONE_ID_CONFIGURED=bool(current_app.config.get('CF_ZONE_ID'))
)
from app.web.forms import ChangePasswordForm, SettingsForm
from app.web.forms import ChangePasswordForm, SecuritySettingsForm, SettingsForm
from werkzeug.security import check_password_hash, generate_password_hash
from cryptography.fernet import Fernet
@ -225,88 +225,107 @@ from cryptography.fernet import Fernet
@login_required
def settings_page():
"""Renders and handles the main settings page."""
settings_form = SettingsForm()
settings_form = SettingsForm(prefix='general')
change_password_form = ChangePasswordForm()
if settings_form.submit_settings.data and settings_form.validate_on_submit():
data_path = os.path.dirname(config.STATE_FILE_PATH)
key_file = os.path.join(data_path, 'dockflare.key')
config_file = os.path.join(data_path, 'dockflare_config.dat')
security_settings_form = SecuritySettingsForm(prefix='security')
try:
with open(key_file, 'rb') as f:
key = f.read()
fernet = Fernet(key)
# Distinguish between form submissions
if request.method == 'POST':
if settings_form.submit_settings.data and settings_form.validate():
data_path = os.path.dirname(config.STATE_FILE_PATH)
key_file = os.path.join(data_path, 'dockflare.key')
config_file = os.path.join(data_path, 'dockflare_config.dat')
with open(config_file, 'rb') as f:
decrypted_data = fernet.decrypt(f.read())
config_data = json.loads(decrypted_data)
try:
with open(key_file, 'rb') as f:
key = f.read()
fernet = Fernet(key)
with open(config_file, 'rb') as f:
decrypted_data = fernet.decrypt(f.read())
config_data = json.loads(decrypted_data)
original_tunnel_name = config_data.get('tunnel_name')
new_tunnel_name = settings_form.tunnel_name.data
tunnel_name_changed = original_tunnel_name != new_tunnel_name
original_tunnel_name = config_data.get('tunnel_name')
new_tunnel_name = settings_form.tunnel_name.data
tunnel_name_changed = original_tunnel_name != new_tunnel_name
config_data['tunnel_name'] = new_tunnel_name
config_data['cf_zone_id'] = settings_form.cf_zone_id.data
config_data['tunnel_dns_scan_zone_names'] = settings_form.tunnel_dns_scan_zone_names.data
config_data['grace_period_seconds'] = settings_form.grace_period_seconds.data
config_data['tunnel_name'] = new_tunnel_name
config_data['cf_zone_id'] = settings_form.cf_zone_id.data
config_data['tunnel_dns_scan_zone_names'] = settings_form.tunnel_dns_scan_zone_names.data
config_data['grace_period_seconds'] = settings_form.grace_period_seconds.data
encrypted_payload = fernet.encrypt(json.dumps(config_data).encode('utf-8'))
with open(config_file, 'wb') as f:
f.write(encrypted_payload)
from app import config as config_module
current_app.config['TUNNEL_NAME'] = new_tunnel_name
config_module.TUNNEL_NAME = new_tunnel_name
current_app.config['CLOUDFLARED_CONTAINER_NAME'] = f"cloudflared-agent-{new_tunnel_name}"
config_module.CLOUDFLARED_CONTAINER_NAME = f"cloudflared-agent-{new_tunnel_name}"
current_app.config['CF_ZONE_ID'] = config_data['cf_zone_id']
config_module.CF_ZONE_ID = config_data['cf_zone_id']
scan_zones_str = config_data.get('tunnel_dns_scan_zone_names', '')
current_app.config['TUNNEL_DNS_SCAN_ZONE_NAMES'] = [name.strip() for name in scan_zones_str.split(',') if name.strip()]
config_module.TUNNEL_DNS_SCAN_ZONE_NAMES = current_app.config['TUNNEL_DNS_SCAN_ZONE_NAMES']
current_app.config['GRACE_PERIOD_SECONDS'] = int(config_data.get('grace_period_seconds', 28800))
config_module.GRACE_PERIOD_SECONDS = current_app.config['GRACE_PERIOD_SECONDS']
encrypted_payload = fernet.encrypt(json.dumps(config_data).encode('utf-8'))
with open(config_file, 'wb') as f:
f.write(encrypted_payload)
flash('General settings updated successfully.', 'success')
if tunnel_name_changed and not config.USE_EXTERNAL_CLOUDFLARED:
flash('Tunnel name changed. Restarting the agent to apply changes...', 'info')
logging.info(f"Tunnel name changed from '{original_tunnel_name}' to '{new_tunnel_name}'. Triggering agent restart.")
def restart_agent_task():
stop_cloudflared_container()
time.sleep(5)
initialize_tunnel()
start_cloudflared_container()
from app import config as config_module
current_app.config['TUNNEL_NAME'] = new_tunnel_name
config_module.TUNNEL_NAME = new_tunnel_name
current_app.config['CLOUDFLARED_CONTAINER_NAME'] = f"cloudflared-agent-{new_tunnel_name}"
config_module.CLOUDFLARED_CONTAINER_NAME = f"cloudflared-agent-{new_tunnel_name}"
from threading import Thread
restart_thread = Thread(target=restart_agent_task)
restart_thread.start()
current_app.config['CF_ZONE_ID'] = config_data['cf_zone_id']
config_module.CF_ZONE_ID = config_data['cf_zone_id']
scan_zones_str = config_data.get('tunnel_dns_scan_zone_names', '')
current_app.config['TUNNEL_DNS_SCAN_ZONE_NAMES'] = [name.strip() for name in scan_zones_str.split(',') if name.strip()]
config_module.TUNNEL_DNS_SCAN_ZONE_NAMES = current_app.config['TUNNEL_DNS_SCAN_ZONE_NAMES']
return redirect(url_for('web.settings_page'))
except Exception as e:
logging.error(f"Failed to update settings in config file: {e}", exc_info=True)
flash('An error occurred while saving settings.', 'danger')
elif security_settings_form.submit_security_settings.data and security_settings_form.validate():
data_path = os.path.dirname(config.STATE_FILE_PATH)
key_file = os.path.join(data_path, 'dockflare.key')
config_file = os.path.join(data_path, 'dockflare_config.dat')
try:
with open(key_file, 'rb') as f:
key = f.read()
fernet = Fernet(key)
current_app.config['GRACE_PERIOD_SECONDS'] = int(config_data.get('grace_period_seconds', 28800))
config_module.GRACE_PERIOD_SECONDS = current_app.config['GRACE_PERIOD_SECONDS']
with open(config_file, 'rb') as f:
decrypted_data = fernet.decrypt(f.read())
config_data = json.loads(decrypted_data)
flash('General settings updated successfully.', 'success')
config_data['disable_password_login'] = security_settings_form.disable_password_login.data
encrypted_payload = fernet.encrypt(json.dumps(config_data).encode('utf-8'))
with open(config_file, 'wb') as f:
f.write(encrypted_payload)
if tunnel_name_changed and not config.USE_EXTERNAL_CLOUDFLARED:
flash('Tunnel name changed. Restarting the agent to apply changes...', 'info')
logging.info(f"Tunnel name changed from '{original_tunnel_name}' to '{new_tunnel_name}'. Triggering agent restart.")
current_app.config['DISABLE_PASSWORD_LOGIN'] = config_data['disable_password_login']
flash('Security settings updated successfully.', 'success')
return redirect(url_for('web.settings_page'))
except Exception as e:
logging.error(f"Failed to update security settings in config file: {e}", exc_info=True)
flash('An error occurred while saving security settings.', 'danger')
def restart_agent_task():
stop_cloudflared_container()
time.sleep(5)
initialize_tunnel()
start_cloudflared_container()
from threading import Thread
restart_thread = Thread(target=restart_agent_task)
restart_thread.start()
return redirect(url_for('web.settings_page'))
except Exception as e:
logging.error(f"Failed to update settings in config file: {e}", exc_info=True)
flash('An error occurred while saving settings.', 'danger')
# Populate forms for GET request
if request.method == 'GET':
settings_form.tunnel_name.data = current_app.config.get('TUNNEL_NAME')
settings_form.cf_zone_id.data = current_app.config.get('CF_ZONE_ID')
settings_form.tunnel_dns_scan_zone_names.data = ','.join(current_app.config.get('TUNNEL_DNS_SCAN_ZONE_NAMES', []))
settings_form.grace_period_seconds.data = current_app.config.get('GRACE_PERIOD_SECONDS')
security_settings_form.disable_password_login.data = current_app.config.get('DISABLE_PASSWORD_LOGIN', False)
groups_for_template = {}
used_group_ids = set()
@ -330,6 +349,7 @@ def settings_page():
'settings.html',
settings_form=settings_form,
change_password_form=change_password_form,
security_settings_form=security_settings_form,
access_groups=groups_for_template,
used_group_ids=used_group_ids,
all_account_tunnels=all_account_tunnels_list,