mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-04-26 10:50:43 +00:00
*.tld feature zone protection
This commit is contained in:
parent
68f34a0697
commit
c946815838
13 changed files with 773 additions and 79 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
|
@ -10,17 +10,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [v3.0.3] - 2025-10-03
|
||||
|
||||
### Added
|
||||
- Introduced dual-mode Access Group builder with Public (`bypass`) and Authenticated (`allow`) tabs, tailored helper text, and mode-specific validation.
|
||||
- Surfaced contextual colour-coded alerts and consistent dropdown menus across Access Policies and the main dashboard for quicker policy reviews.
|
||||
- **Dual-Mode Access Group Builder:** Introduced dedicated Public (`bypass`) and Authenticated (`allow`) tabs with tailored helper text and mode-specific validation.
|
||||
- **System-Managed Default Bypass Policy:** Automatic creation of non-deletable `public-default-bypass` reusable policy used across all public/bypass access rules, eliminating duplicate bypass policies in Cloudflare.
|
||||
- **Zone Default Policies Section:** New UI section on Access Policies page displaying all DNS zones with their wildcard protection status (`*.domain.com` policies).
|
||||
- **Zone Policy Creation Modal:** One-click creation of zone-level wildcard policies with access group selection, providing security safety net for all subdomains.
|
||||
- **Visual Protection Indicators:** Green "Protected" and yellow "Not Protected" badges show zone security status at a glance.
|
||||
- **Contextual UI Elements:** Colour-coded alerts and consistent dropdown menus across Access Policies and dashboard for quicker policy reviews.
|
||||
|
||||
### Changed
|
||||
- Each Access Group now syncs to a reusable Cloudflare Access Policy, enabling one-to-many reuse, bi-directional edits, and automatic migration of legacy inline policies (including `block` → `deny` conversion).
|
||||
- Access Policies UI and dashboard now share the same three-dot action menus and Cloudflare dashboard shortcuts for a uniform workflow.
|
||||
- **Reusable Policy Architecture:** Each Access Group now syncs to a reusable Cloudflare Access Policy, enabling one-to-many reuse, bi-directional edits, and automatic migration of legacy inline policies (including `block` → `deny` conversion).
|
||||
- **Simplified Manual Rule Creation:** Removed "Authenticate by Email" and "Default *.tld" quick-create options from manual rule modals to enforce proper access policy design workflow.
|
||||
- **Bypass Rule Implementation:** All rules using "Bypass" option now reference the centralized `public-default-bypass` system policy instead of creating inline policies.
|
||||
- **Policy Creation Workflow:** Complex authentication scenarios now require creating an Access Policy first, then applying it to services—enforcing "single source of truth" principle.
|
||||
- **Unified UI Style:** Access Policies UI and dashboard now share the same three-dot action menus and Cloudflare dashboard shortcuts for uniform workflow.
|
||||
|
||||
### Security
|
||||
- **Zone-Level Protection:** Zone Default Policies feature enables protection of all subdomains (including undocumented ones) through `*.domain.com` wildcard policies, preventing accidental exposure.
|
||||
- **Default Policy Protection:** System-managed `public-default-bypass` policy cannot be deleted through UI or backend, ensuring critical infrastructure remains intact.
|
||||
|
||||
### Fixed
|
||||
- Public Access Groups now issue Cloudflare `bypass` decisions as intended instead of incorrectly falling back to `allow`.
|
||||
- Simplified country filtering to remove redundant double-blocking logic when combining geo rules with public mode.
|
||||
- Reusable policy synchronisation now preserves all decision types (`bypass`, `allow`, `deny`) when pushing or importing definitions.
|
||||
- **Public Access Groups:** Now correctly issue Cloudflare `bypass` decisions as intended instead of incorrectly falling back to `allow`.
|
||||
- **Country Filtering:** Simplified country filtering to remove redundant double-blocking logic when combining geo rules with public mode.
|
||||
- **Policy Synchronization:** Reusable policy synchronisation now preserves all decision types (`bypass`, `allow`, `deny`) when pushing or importing definitions.
|
||||
- **Duplicate Policy Reduction:** Eliminates creation of multiple identical bypass policies—all public services now share one canonical policy.
|
||||
- **Policy Consistency:** Ensures consistent public access behavior across all services using the centralized system policy.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -33,13 +33,15 @@ DockFlare is a powerful, self-hosted ingress controller that simplifies Cloudfla
|
|||
|
||||
It enables secure, hassle-free public access to both Dockerized and non-Dockerized applications with minimal direct interaction with Cloudflare, making it the perfect tool for centralizing and streamlining your access management.
|
||||
|
||||
### ✨ What's New in DockFlare 3.0.3: Access Modes & Reusable Policies
|
||||
### ✨ What's New in DockFlare 3.0.3: Access Modes, Reusable Policies & Zone Security
|
||||
|
||||
DockFlare 3.0.3 makes it easier to design access control that matches real-world needs.
|
||||
DockFlare 3.0.3 makes it easier to design access control that matches real-world needs, with cleaner policy management and enhanced zone-level security.
|
||||
|
||||
- **Two access modes**: Access Groups now offer dedicated Public and Authenticated tabs. Pick `Public` for Cloudflare Access `bypass` (no login, optional geo blocks) or `Authenticated` for `allow` policies that require mail/domain logins.
|
||||
- **Reusable Cloudflare policies**: DockFlare syncs every Access Group to a reusable Access Policy, so you can reference the same rules across multiple applications, edit them centrally, and keep the Cloudflare dashboard tidy.
|
||||
- **Refined policy builder**: The modal dynamically validates required emails in Authenticated mode, hides irrelevant options for Public mode, and colour-codes the guidance copy to explain each decision.
|
||||
- **System-managed default bypass policy**: A single, non-deletable `public-default-bypass` policy is automatically created and reused across all public services, eliminating duplicate bypass policies.
|
||||
- **Zone Default Policies (*.tld wildcards)**: New section on the Access Policies page shows which DNS zones have wildcard protection. Create `*.yourdomain.com` policies with one click to ensure all subdomains are protected by default—even ones you forget to configure explicitly.
|
||||
- **Simplified UI**: Removed confusing quick-create options ("Email Auth", "Default *.tld") from manual rule creation. Complex policies should be designed on the Access Policies page and then applied to services.
|
||||
- **Migration-ready**: Legacy inline policies automatically convert to reusable policies during the next sync, and DockFlare harmonizes Cloudflare `block` decisions with the newer `deny` verb.
|
||||
|
||||
### ✨ What's New in DockFlare 3.0: Multi-Server & Agent Release
|
||||
|
|
|
|||
|
|
@ -158,10 +158,79 @@ def load_state():
|
|||
except Exception as e_load_unexp:
|
||||
logging.error(f"LOAD_STATE: Unexpected error loading state: {e_load_unexp}. Starting fresh (already cleared).", exc_info=True)
|
||||
|
||||
def ensure_default_bypass_policy(flask_app=None):
|
||||
|
||||
from app.core import reusable_policies
|
||||
|
||||
default_bypass_id = "public-default-bypass"
|
||||
policy_name = "Default Public Access (Bypass)"
|
||||
|
||||
with state_lock:
|
||||
# Check if policy exists in local state
|
||||
if default_bypass_id not in access_groups:
|
||||
logging.info(f"Creating default bypass access group in state: {default_bypass_id}")
|
||||
|
||||
# Create in Cloudflare first
|
||||
cf_policy_id = None
|
||||
if flask_app:
|
||||
with flask_app.app_context():
|
||||
try:
|
||||
cf_policy = reusable_policies.create_reusable_policy(
|
||||
name=policy_name,
|
||||
decision="bypass",
|
||||
include_rules=[{"everyone": {}}]
|
||||
)
|
||||
if cf_policy and cf_policy.get("id"):
|
||||
cf_policy_id = cf_policy["id"]
|
||||
logging.info(f"Created default bypass policy in Cloudflare with ID: {cf_policy_id}")
|
||||
else:
|
||||
logging.warning(f"Failed to create default bypass policy in Cloudflare, will create local reference only")
|
||||
except Exception as e:
|
||||
logging.error(f"Error creating default bypass policy in Cloudflare: {e}", exc_info=True)
|
||||
|
||||
# Create local state entry
|
||||
access_groups[default_bypass_id] = {
|
||||
"id": cf_policy_id if cf_policy_id else default_bypass_id,
|
||||
"display_name": policy_name,
|
||||
"session_duration": "24h",
|
||||
"app_launcher_visible": False,
|
||||
"auto_redirect_to_identity": False,
|
||||
"public_mode": True,
|
||||
"policies": [
|
||||
{
|
||||
"name": policy_name,
|
||||
"decision": "bypass",
|
||||
"include": [{"everyone": {}}]
|
||||
}
|
||||
],
|
||||
"system_policy": True, # Mark as system policy
|
||||
"deletable": False, # Cannot be deleted via UI
|
||||
"cf_policy_id": cf_policy_id # Store the actual Cloudflare policy ID
|
||||
}
|
||||
save_state()
|
||||
logging.info(f"Default bypass policy '{default_bypass_id}' created successfully in local state.")
|
||||
else:
|
||||
logging.debug(f"Default bypass policy '{default_bypass_id}' already exists in local state.")
|
||||
|
||||
# Verify it exists in Cloudflare
|
||||
existing_policy = access_groups[default_bypass_id]
|
||||
cf_policy_id = existing_policy.get("cf_policy_id") or existing_policy.get("id")
|
||||
|
||||
if flask_app and cf_policy_id != default_bypass_id: # Has a real CF ID
|
||||
with flask_app.app_context():
|
||||
try:
|
||||
cf_policy = reusable_policies.get_reusable_policy(cf_policy_id)
|
||||
if cf_policy:
|
||||
logging.debug(f"Verified default bypass policy exists in Cloudflare: {cf_policy_id}")
|
||||
else:
|
||||
logging.warning(f"Default bypass policy not found in Cloudflare, may need recreation")
|
||||
except Exception as e:
|
||||
logging.error(f"Error verifying default bypass policy in Cloudflare: {e}")
|
||||
|
||||
def save_state():
|
||||
global managed_rules, access_groups
|
||||
current_thread_name = threading.current_thread().name
|
||||
|
||||
|
||||
with state_lock:
|
||||
logging.info(f"SAVE_STATE: Start (RLock acquired). THREAD: {current_thread_name}. Items to save: {len(managed_rules)} rules, {len(access_groups)} access groups.")
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from cryptography.fernet import Fernet
|
|||
|
||||
from app import app, docker_client, tunnel_state, cloudflared_agent_state, config
|
||||
|
||||
from app.core.state_manager import load_state
|
||||
from app.core.state_manager import load_state, ensure_default_bypass_policy
|
||||
from app.core.tunnel_manager import (
|
||||
initialize_tunnel,
|
||||
update_cloudflared_container_status,
|
||||
|
|
@ -302,6 +302,9 @@ def main_application_entrypoint():
|
|||
|
||||
load_state()
|
||||
logging.info("Initial state loading from file complete.")
|
||||
|
||||
ensure_default_bypass_policy(flask_app=app)
|
||||
logging.info("Default bypass policy initialization complete.")
|
||||
|
||||
if docker_client:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -99,12 +99,14 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
<div class="divider my-1"></div>
|
||||
<li {{ 'class="disabled"' if group_id in used_group_ids else '' }}>
|
||||
<a class="text-error delete-access-group-btn" data-group-id="{{ group_id }}" data-group-name="{{ details.display_name }}" {{ 'title="Cannot delete: group is in use"' if group_id in used_group_ids else '' }}>
|
||||
<li {{ 'class="disabled"' if group_id in used_group_ids or details.get('system_policy') or not details.get('deletable', True) else '' }}>
|
||||
<a class="text-error delete-access-group-btn" data-group-id="{{ group_id }}" data-group-name="{{ details.display_name }}" {{ 'title="Cannot delete: system policy"' if details.get('system_policy') or not details.get('deletable', True) else ('title="Cannot delete: group is in use"' if group_id in used_group_ids else '') }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</svg>
|
||||
{% if group_id in used_group_ids %}
|
||||
{% if details.get('system_policy') or not details.get('deletable', True) %}
|
||||
<span class="opacity-50">Delete (system policy)</span>
|
||||
{% elif group_id in used_group_ids %}
|
||||
<span class="opacity-50">Delete (policy is in use)</span>
|
||||
{% else %}
|
||||
Delete
|
||||
|
|
@ -149,11 +151,131 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Zone Default Policies Section -->
|
||||
<section class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-base-300 pb-3 mb-6">
|
||||
<div>
|
||||
<h2 class="card-title text-2xl sm:text-3xl">
|
||||
Zone Default Policies (*.tld Wildcards)
|
||||
</h2>
|
||||
<p class="text-sm opacity-70 mt-1">Protect all subdomains of your zones with a wildcard <code>*.domain.com</code> access policy.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if zone_policies %}
|
||||
<div class="overflow-x-auto -mx-6 sm:-mx-8">
|
||||
<table class="table table-zebra table-sm w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="p-3">Zone Name</th>
|
||||
<th class="p-3">Wildcard Hostname</th>
|
||||
<th class="p-3">Status</th>
|
||||
<th class="p-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for zone in zone_policies %}
|
||||
<tr>
|
||||
<td class="p-3 font-medium">{{ zone.zone_name }}</td>
|
||||
<td class="p-3"><code class="text-xs">*.{{ zone.zone_name }}</code></td>
|
||||
<td class="p-3">
|
||||
{% if zone.has_default_policy %}
|
||||
<span class="badge badge-success gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
||||
</svg>
|
||||
Protected
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
Not Protected
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="p-3">
|
||||
{% if not zone.has_default_policy %}
|
||||
<button class="btn btn-xs btn-primary create-zone-policy-btn" data-zone-name="{{ zone.zone_name }}" data-zone-id="{{ zone.zone_id }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3 mr-1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
Create Policy
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="https://one.dash.cloudflare.com/{{ ACCOUNT_ID_FOR_DISPLAY }}/access/apps" target="_blank" rel="noopener noreferrer" class="btn btn-xs btn-ghost">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3 mr-1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
View in CF
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center opacity-70 py-8">
|
||||
<p>No DNS zones found in your Cloudflare account.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ super() }}
|
||||
{% include 'modals/_access_group_modal.html' %}
|
||||
|
||||
<!-- Zone Default Policy Creation Modal -->
|
||||
<dialog id="zone-policy-modal" class="modal">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
<h3 class="font-bold text-lg mb-4">Create Zone Default Policy</h3>
|
||||
<form method="POST" action="{{ url_for('web.create_zone_default_policy') }}" id="zone-policy-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="zone_name" id="zone-policy-zone-name"/>
|
||||
<input type="hidden" name="zone_id" id="zone-policy-zone-id"/>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="alert alert-info">
|
||||
<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>This will create a wildcard Access Application for <strong><code id="zone-policy-hostname-display"></code></strong> to protect all subdomains.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="zone-policy-access-group">
|
||||
<span class="label-text font-medium">Select Access Policy</span>
|
||||
</label>
|
||||
<select name="access_group_id" id="zone-policy-access-group" class="select select-bordered w-full" required>
|
||||
<option value="">-- Select an Access Policy --</option>
|
||||
{% for group_id, details in access_groups.items()|sort %}
|
||||
<option value="{{ group_id }}">{{ details.display_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">The access policy that will protect <code>*.zone.com</code></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick="document.getElementById('zone-policy-modal').close()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create Zone Policy</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
|
@ -501,6 +623,21 @@
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle zone policy creation
|
||||
document.querySelectorAll('.create-zone-policy-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const zoneName = btn.getAttribute('data-zone-name');
|
||||
const zoneId = btn.getAttribute('data-zone-id');
|
||||
|
||||
document.getElementById('zone-policy-zone-name').value = zoneName;
|
||||
document.getElementById('zone-policy-zone-id').value = zoneId;
|
||||
document.getElementById('zone-policy-hostname-display').textContent = `*.${zoneName}`;
|
||||
|
||||
document.getElementById('zone-policy-modal').showModal();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -97,3 +97,46 @@ This policy keeps your marketing site public while limiting traffic from specifi
|
|||
* **Blocked countries:** `RU`, `CN`, `KP`
|
||||
|
||||
The resulting reusable policy issues a Cloudflare `bypass` decision for everyone, excluding the listed countries. Combine it with other groups if you need to layer additional controls (`dockflare.access.groups=public-eu,admin-users`).
|
||||
|
||||
---
|
||||
|
||||
## Zone Default Policies - Security Best Practice
|
||||
|
||||
### What Are Zone Default Policies?
|
||||
|
||||
Zone Default Policies are wildcard `*.domain.com` Access Applications that protect ALL subdomains of a DNS zone, including ones you haven't explicitly configured yet.
|
||||
|
||||
### Why You Need Them
|
||||
|
||||
**The Problem:** If you forget to add an Access policy to a service, it's exposed publicly by default.
|
||||
|
||||
**The Solution:** A zone-level wildcard policy acts as a safety net. Even if you forget to configure `forgotten-service.yourdomain.com`, the `*.yourdomain.com` policy will catch it.
|
||||
|
||||
### How to Set Them Up
|
||||
|
||||
1. Navigate to **Access Policies** page
|
||||
2. Scroll to **Zone Default Policies (*.tld Wildcards)** section
|
||||
3. Look for zones with "Not Protected" ⚠️ badge
|
||||
4. Click **Create Policy**
|
||||
5. Select appropriate access group:
|
||||
- **For public domains:** Use `public-default-bypass`
|
||||
- **For internal domains:** Use an authentication policy
|
||||
- **For mixed-use:** Use your most restrictive policy
|
||||
|
||||
### Best Practices
|
||||
|
||||
- ✅ **Always create zone policies** for production domains
|
||||
- ✅ **Use authentication policies** for internal/private zones
|
||||
- ✅ **Use public bypass** only for truly public zones
|
||||
- ✅ **Review regularly** - check zone protection status monthly
|
||||
- ⚠️ **Remember priority** - Specific hostname policies override wildcard policies
|
||||
|
||||
### Policy Priority Order
|
||||
|
||||
Cloudflare evaluates Access policies in this order:
|
||||
|
||||
1. **Exact hostname match** (e.g., `app.example.com`) - Highest priority
|
||||
2. **Wildcard match** (e.g., `*.example.com`) - Fallback
|
||||
3. **No match** = Public access (no Access App) - Default
|
||||
|
||||
This means you can have a restrictive zone default policy and still create specific exceptions for individual services.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,30 @@ These labels allow you to dynamically create and manage Cloudflare Access applic
|
|||
|
||||
**Note:** It is highly recommended to use **Access Groups** (`dockflare.access.group`) for managing policies. DockFlare 3.0.3 synchronises every Access Group to a named reusable Cloudflare Access Policy, giving you one-to-many reuse and bi-directional edits. Using individual labels is best for one-off, unique configurations. If `dockflare.access.group` or `dockflare.access.groups` is used, all other `dockflare.access.*` labels are ignored.
|
||||
|
||||
### Important Changes in v3.0.3
|
||||
|
||||
#### System Default Bypass Policy
|
||||
|
||||
Starting in v3.0.3, when you use `dockflare.access.policy=bypass`, your service will reference the system-managed `public-default-bypass` reusable policy instead of creating an inline policy. This keeps your Cloudflare dashboard clean.
|
||||
|
||||
- **Before v3.0.3:** Each bypass rule created a separate inline policy
|
||||
- **v3.0.3+:** All bypass rules share one canonical `public-default-bypass` policy
|
||||
|
||||
#### Simplified Access Configuration
|
||||
|
||||
For complex access scenarios (email/domain authentication, IP whitelisting, etc.), it's now recommended to:
|
||||
|
||||
1. Create an Access Group on the **Access Policies** page
|
||||
2. Reference it with `dockflare.access.group=your-group-id`
|
||||
|
||||
Quick-create options have been removed from the UI to encourage this best-practice workflow.
|
||||
|
||||
#### Zone Default Policy Label
|
||||
|
||||
The `dockflare.access.policy=default_tld` label still works and will inherit protection from your zone's `*.domain.com` wildcard policy. If no zone policy exists, the service will be public (no Access App).
|
||||
|
||||
**Recommendation:** Create zone default policies for all your domains in the UI for better security.
|
||||
|
||||
| Label | Description | Example |
|
||||
| :--- | :--- | :--- |
|
||||
| `dockflare.access.group` | The ID of a single, pre-configured Access Group to apply to this service. The ID can be found on the "Access Policies" page in the DockFlare UI. | `dockflare.access.group=internal-tools-policy` |
|
||||
|
|
|
|||
|
|
@ -42,3 +42,42 @@ DockFlare uses a flexible, layered approach to configuration, giving you both au
|
|||
* **Revert** a service's configuration back to what is defined in its Docker labels, discarding any UI overrides.
|
||||
|
||||
This layered model allows you to "set it and forget it" with Docker labels for most services, while still having the power to handle exceptions and complex scenarios through the Web UI.
|
||||
|
||||
---
|
||||
|
||||
## Access Policy Architecture (v3.0.3+)
|
||||
|
||||
### Reusable Policy System
|
||||
|
||||
DockFlare now uses a **reusable policy architecture** that aligns with Cloudflare's best practices:
|
||||
|
||||
1. **Access Groups** → Sync to → **Cloudflare Reusable Policies**
|
||||
2. **Access Applications** → Reference → **Reusable Policy IDs**
|
||||
3. **Single source of truth** - update once, applies everywhere
|
||||
|
||||
This architecture eliminates policy duplication and allows you to manage policies from either DockFlare or the Cloudflare dashboard with full bi-directional sync.
|
||||
|
||||
### System-Managed Policies
|
||||
|
||||
DockFlare automatically manages certain policies for consistency:
|
||||
|
||||
- **`public-default-bypass`**: Created on startup, used by all bypass rules
|
||||
- Non-deletable system policy
|
||||
- Synced to Cloudflare on first Access Policies page visit
|
||||
- Referenced by all services using "Bypass" access
|
||||
- Prevents duplicate bypass policies in your Cloudflare dashboard
|
||||
|
||||
### Zone Default Policies
|
||||
|
||||
Zone-level wildcard policies (`*.domain.com`) provide layered security through policy priority:
|
||||
|
||||
1. **Specific hostname policy** (e.g., `app.example.com`) - Highest priority
|
||||
2. **Zone wildcard policy** (e.g., `*.example.com`) - Fallback
|
||||
3. **No policy** = Public access (no Access App) - Default
|
||||
|
||||
This ensures forgotten or undocumented services are still protected by the zone-level policy, acting as a security safety net.
|
||||
|
||||
**Example:**
|
||||
- Zone policy: `*.internal.company.com` → Requires company email authentication
|
||||
- Specific service: `public-demo.internal.company.com` → Uses `public-default-bypass`
|
||||
- Forgotten service: `test.internal.company.com` → Protected by zone policy (requires auth)
|
||||
|
|
|
|||
|
|
@ -25,12 +25,30 @@ The UI gives you full control over your ingress rules.
|
|||
|
||||
## Access Policies Page
|
||||
|
||||
This page is the central location for managing your reusable **Access Groups**. From here, you can:
|
||||
This page is the central location for managing your reusable **Access Groups** and securing your DNS zones with wildcard policies.
|
||||
|
||||
### Advanced Access Policies
|
||||
|
||||
From the Access Groups section, you can:
|
||||
* **Create** new Access Groups using the two-tab modal (Authenticated vs Public). Guidance banners update per tab so you understand when DockFlare will emit a Cloudflare `allow` or `bypass` decision.
|
||||
* **Edit** existing Access Groups. The modal enforces mode-specific validation (emails required for Authenticated) and keeps Geo/IP settings visible for both modes.
|
||||
* **Delete** Access Groups that are no longer in use. DockFlare keeps track of the linked reusable Cloudflare policy and removes it when you drop the group.
|
||||
* **Delete** Access Groups that are no longer in use (system policies like `public-default-bypass` cannot be deleted).
|
||||
* **Sync from Cloudflare** to import existing DockFlare reusable policies from your account.
|
||||
* Use the action menu beside each entry to open the matching policy directly in the Cloudflare dashboard via the Cloudflare icon shortcut.
|
||||
|
||||
**Note:** The `public-default-bypass` system policy is automatically created and managed by DockFlare. All services using "Bypass" access reference this single policy, keeping your Cloudflare dashboard clean.
|
||||
|
||||
### Zone Default Policies (*.tld Wildcards)
|
||||
|
||||
The second section shows **Zone Default Policies** - a security best practice feature that protects all subdomains:
|
||||
|
||||
* **Protection Status:** Visual badges show which DNS zones have wildcard `*.domain.com` policies (Protected 🛡️) and which don't (Not Protected ⚠️).
|
||||
* **Create Zone Policy:** Click "Create Policy" on any unprotected zone to create a wildcard Access Application.
|
||||
* **Select Policy:** Choose which Access Group should protect all subdomains of the zone (can be public bypass, authentication, or any custom policy).
|
||||
* **Security Safety Net:** Even if you forget to add a policy to a specific service, the zone-level wildcard policy will catch it.
|
||||
|
||||
**Best Practice:** Create zone default policies for all your domains. For public-facing domains, use the default bypass policy. For internal/private domains, use an authentication policy. This ensures no subdomain is accidentally exposed.
|
||||
|
||||
For more details, see the [Access Policy Best Practices & Examples](Access-Policy-Best-Practices.md) guide.
|
||||
|
||||
## Settings Page
|
||||
|
|
|
|||
215
dockflare/app/templates/docs/Zone-Default-Policies.md
Normal file
215
dockflare/app/templates/docs/Zone-Default-Policies.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Zone Default Policies - Wildcard Protection
|
||||
|
||||
## Overview
|
||||
|
||||
Zone Default Policies are a security best-practice feature that uses Cloudflare Access wildcard applications (`*.domain.com`) to protect all subdomains of a DNS zone automatically.
|
||||
|
||||
## The Problem This Solves
|
||||
|
||||
Without zone default policies:
|
||||
- Forgotten services are publicly exposed
|
||||
- New subdomains have no protection until manually configured
|
||||
- Typos in hostname configurations bypass access controls
|
||||
- Documentation drift leads to security gaps
|
||||
|
||||
## How It Works
|
||||
|
||||
### Policy Priority
|
||||
|
||||
Cloudflare evaluates Access policies in this order:
|
||||
|
||||
1. **Exact hostname match** (e.g., `app.example.com`)
|
||||
2. **Wildcard match** (e.g., `*.example.com`)
|
||||
3. **No match** = Public access (no Access App)
|
||||
|
||||
### DockFlare Implementation
|
||||
|
||||
DockFlare's **Zone Default Policies** section:
|
||||
- Lists all your Cloudflare DNS zones
|
||||
- Shows protection status with visual badges
|
||||
- Allows one-click creation of `*.zone.com` policies
|
||||
- Lets you choose which Access Group protects the zone
|
||||
|
||||
## Setup Guide
|
||||
|
||||
### Step 1: Review Your Zones
|
||||
|
||||
1. Navigate to **Access Policies** page
|
||||
2. Scroll to **Zone Default Policies (*.tld Wildcards)**
|
||||
3. Review protection status:
|
||||
- 🛡️ **Green "Protected"** - Zone has wildcard policy
|
||||
- ⚠️ **Yellow "Not Protected"** - Zone is vulnerable
|
||||
|
||||
### Step 2: Create Zone Policies
|
||||
|
||||
For each unprotected zone:
|
||||
|
||||
1. Click **Create Policy** button
|
||||
2. Modal shows `*.zone-name.com` hostname
|
||||
3. Select appropriate Access Policy:
|
||||
- **Public zones** → `public-default-bypass`
|
||||
- **Internal zones** → Authentication policy
|
||||
- **Mixed zones** → Most restrictive policy
|
||||
4. Click **Create Zone Policy**
|
||||
|
||||
### Step 3: Verify in Cloudflare
|
||||
|
||||
1. Open Cloudflare Zero Trust dashboard
|
||||
2. Navigate to Access → Applications
|
||||
3. Look for applications named `Zone Default: *.domain.com`
|
||||
4. Verify policy is correct
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
### Production Environments
|
||||
|
||||
✅ **Always enable zone default policies**
|
||||
- Prevents accidental exposure
|
||||
- Catches configuration mistakes
|
||||
- Protects against subdomain discovery attacks
|
||||
|
||||
### Policy Selection Strategy
|
||||
|
||||
- **Public content domains** (blogs, marketing): `public-default-bypass`
|
||||
- **Internal tools domains**: Email/domain authentication
|
||||
- **Sensitive data domains**: MFA-enabled authentication
|
||||
- **Development domains**: Lock down with strictest policy
|
||||
|
||||
### Monitoring
|
||||
|
||||
Regularly review:
|
||||
- Which zones have protection (**Access Policies** page)
|
||||
- Access Application logs in Cloudflare
|
||||
- List of active subdomains vs configured policies
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Policy already exists" error
|
||||
|
||||
A `*.domain.com` Access Application already exists. This could be:
|
||||
- Created manually in Cloudflare
|
||||
- Created by DockFlare previously
|
||||
- Created by another tool
|
||||
|
||||
**Solution:** Manage it directly in Cloudflare, or delete and recreate via DockFlare.
|
||||
|
||||
### Service still accessible without authentication
|
||||
|
||||
Check policy priority:
|
||||
1. Verify service has specific hostname policy
|
||||
2. Confirm zone wildcard exists and is configured correctly
|
||||
3. Check Cloudflare Access logs for policy evaluation order
|
||||
4. Ensure DNS record points to correct tunnel
|
||||
|
||||
### Zone not showing in list
|
||||
|
||||
Possible causes:
|
||||
- DNS zone not in your Cloudflare account
|
||||
- API token lacks `Zone:Zone:Read` permission
|
||||
- Zone is paused or deleted
|
||||
|
||||
**Solution:** Verify zone exists in Cloudflare dashboard and API token has correct permissions.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Create zone policies first** - Before adding services
|
||||
2. **Use authentication for internal zones** - Never use bypass
|
||||
3. **Document exceptions** - If a zone doesn't need protection, document why
|
||||
4. **Regular audits** - Monthly review of zone protection status
|
||||
5. **Test before production** - Verify wildcard policy doesn't break existing services
|
||||
6. **Principle of least privilege** - Use most restrictive policy that still allows legitimate access
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Public Blog Zone
|
||||
```
|
||||
Zone: blog.example.com
|
||||
Policy: public-default-bypass
|
||||
Result: All subdomains publicly accessible (*.blog.example.com)
|
||||
```
|
||||
|
||||
### Internal Tools Zone
|
||||
```
|
||||
Zone: internal.company.com
|
||||
Policy: Company Email Authentication
|
||||
Result: All subdomains require @company.com email (*.internal.company.com)
|
||||
```
|
||||
|
||||
### Mixed Development Zone
|
||||
```
|
||||
Zone: dev.company.com
|
||||
Policy: Developer Team Authentication
|
||||
Result: All dev services protected by default (*.dev.company.com)
|
||||
Specific overrides: public-demo.dev.company.com → public-default-bypass
|
||||
```
|
||||
|
||||
## Understanding Policy Priority
|
||||
|
||||
### Scenario 1: Specific Policy Overrides Wildcard
|
||||
|
||||
**Setup:**
|
||||
- Zone policy: `*.example.com` → Requires authentication
|
||||
- Specific policy: `blog.example.com` → `public-default-bypass`
|
||||
|
||||
**Result:**
|
||||
- `blog.example.com` → Public (specific policy wins)
|
||||
- `api.example.com` → Requires auth (wildcard catches it)
|
||||
- `forgotten.example.com` → Requires auth (wildcard catches it)
|
||||
|
||||
### Scenario 2: Wildcard as Safety Net
|
||||
|
||||
**Setup:**
|
||||
- Zone policy: `*.internal.company.com` → Requires @company.com email
|
||||
- Specific policy: None for `test-server.internal.company.com`
|
||||
|
||||
**Result:**
|
||||
- `test-server.internal.company.com` → Requires auth (wildcard protects it)
|
||||
- Even if you forgot to configure it, the zone policy protects it
|
||||
|
||||
### Scenario 3: No Protection
|
||||
|
||||
**Setup:**
|
||||
- Zone policy: None for `*.risky-domain.com`
|
||||
- Specific policy: `app.risky-domain.com` → Authentication
|
||||
|
||||
**Result:**
|
||||
- `app.risky-domain.com` → Requires auth (specific policy)
|
||||
- `forgotten.risky-domain.com` → ⚠️ **PUBLIC** (no wildcard to catch it)
|
||||
|
||||
## Integration with DockFlare Labels
|
||||
|
||||
### Using `default_tld` Label
|
||||
|
||||
The `dockflare.access.policy=default_tld` label tells DockFlare to use the zone's wildcard policy:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
my-service:
|
||||
image: nginx
|
||||
labels:
|
||||
- "dockflare.enable=true"
|
||||
- "dockflare.hostname=new-app.internal.company.com"
|
||||
- "dockflare.service=http://my-service:80"
|
||||
- "dockflare.access.policy=default_tld"
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- If `*.internal.company.com` exists → Inherits that policy
|
||||
- If no zone policy exists → Service is public (no Access App created)
|
||||
|
||||
### Recommendation
|
||||
|
||||
Instead of relying on `default_tld` label:
|
||||
1. Create zone default policies in the UI
|
||||
2. Let the wildcard policy automatically protect all services
|
||||
3. Only create specific policies for exceptions
|
||||
|
||||
This ensures better security by default.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Access Policy Best Practices](Access-Policy-Best-Practices.md)
|
||||
- [Using the Web UI](Using-the-Web-UI.md)
|
||||
- [Container Labels](Container-Labels.md)
|
||||
- [How DockFlare Works](How-DockFlare-Works.md)
|
||||
- [Security Architecture](Security-Architecture.md)
|
||||
|
|
@ -15,6 +15,7 @@ This documentation provides comprehensive information for DockFlare. Whether you
|
|||
* [How DockFlare Works](How-DockFlare-Works.md)
|
||||
* [DockFlare Agent & Multi-Server Architecture](Multi-Server-Agent.md)
|
||||
* [Access Policy Best Practices](Access-Policy-Best-Practices.md)
|
||||
* [Zone Default Policies](Zone-Default-Policies.md)
|
||||
* [Internal vs External `cloudflared`](Internal-vs-External-cloudflared.md)
|
||||
* [State-Persistence](State-Persistence.md)
|
||||
* **Configuration**
|
||||
|
|
|
|||
|
|
@ -457,15 +457,8 @@
|
|||
<select name="manual_access_policy_type" id="manual_access_policy_type" class="select select-bordered w-full policy-type-select">
|
||||
<option value="none" selected>None (Public - No App)</option>
|
||||
<option value="bypass">Bypass (Public App)</option>
|
||||
<option value="authenticate_email">Authenticate by Email</option>
|
||||
<option value="default_tld">Use Default *.tld Policy</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="auth-email-field hidden">
|
||||
<label class="label" for="manual_auth_email"><span class="label-text">Allowed Email(s) or Domain(s)</span></label>
|
||||
<input type="text" id="manual_auth_email" name="manual_auth_email" placeholder="user@example.com, @domain.com" class="input input-bordered w-full" />
|
||||
<div class="label"><span class="label-text-alt">Comma-separated. e.g., test@example.com, @another.org</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -614,14 +607,8 @@
|
|||
<select name="edit_access_policy_type" id="edit_manual_access_policy_type" class="select select-bordered w-full policy-type-select">
|
||||
<option value="none">None (Public - No App)</option>
|
||||
<option value="bypass">Bypass (Public App)</option>
|
||||
<option value="authenticate_email">Authenticate by Email</option>
|
||||
<option value="default_tld">Use Default *.tld Policy</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="auth-email-field hidden">
|
||||
<label class="label" for="edit_manual_auth_email"><span class="label-text">Allowed Email(s) or Domain(s)</span></label>
|
||||
<input type="text" id="edit_manual_auth_email" name="edit_auth_email" placeholder="user@example.com, @domain.com" class="input input-bordered w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -439,13 +439,37 @@ from cryptography.fernet import Fernet
|
|||
@login_required
|
||||
def access_policies_page():
|
||||
"""Renders the Access Policies page."""
|
||||
from app.core import reusable_policies
|
||||
|
||||
default_bypass_id = "public-default-bypass"
|
||||
if default_bypass_id in access_groups:
|
||||
policy = access_groups[default_bypass_id]
|
||||
cf_policy_id = policy.get("cf_policy_id")
|
||||
|
||||
# If no Cloudflare policy ID, create it now
|
||||
if not cf_policy_id or cf_policy_id == default_bypass_id:
|
||||
try:
|
||||
cf_policy = reusable_policies.create_reusable_policy(
|
||||
name=policy.get("display_name", "Default Public Access (Bypass)"),
|
||||
decision="bypass",
|
||||
include_rules=[{"everyone": {}}]
|
||||
)
|
||||
if cf_policy and cf_policy.get("id"):
|
||||
with state_lock:
|
||||
access_groups[default_bypass_id]["cf_policy_id"] = cf_policy["id"]
|
||||
access_groups[default_bypass_id]["id"] = cf_policy["id"]
|
||||
save_state()
|
||||
logging.info(f"Synced default bypass policy to Cloudflare with ID: {cf_policy['id']}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to sync default bypass policy to Cloudflare: {e}", exc_info=True)
|
||||
|
||||
groups_for_template = {}
|
||||
used_group_ids = set()
|
||||
group_usage = {} # Maps group_id -> list of hostnames using it
|
||||
group_usage = {}
|
||||
|
||||
with state_lock:
|
||||
for rule in managed_rules.values():
|
||||
# Include both docker and agent-sourced rules
|
||||
|
||||
if rule.get('source') in ['docker', 'agent']:
|
||||
hostname = rule.get('hostname', 'Unknown')
|
||||
group_id_val = rule.get('access_group_id')
|
||||
|
|
@ -471,12 +495,30 @@ def access_policies_page():
|
|||
|
||||
cf_account_id = current_app.config.get('CF_ACCOUNT_ID', '')
|
||||
|
||||
|
||||
zone_policies = []
|
||||
try:
|
||||
zones = list_account_zones()
|
||||
for zone in zones or []:
|
||||
zone_name = zone.get('name')
|
||||
if zone_name:
|
||||
has_policy = check_for_tld_access_policy(zone_name)
|
||||
zone_policies.append({
|
||||
'zone_name': zone_name,
|
||||
'zone_id': zone.get('id'),
|
||||
'has_default_policy': has_policy
|
||||
})
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching zone default policies: {e}", exc_info=True)
|
||||
zone_policies = []
|
||||
|
||||
return render_template(
|
||||
'access_policies.html',
|
||||
access_groups=groups_for_template,
|
||||
used_group_ids=used_group_ids,
|
||||
group_usage=group_usage,
|
||||
countries=countries,
|
||||
zone_policies=zone_policies,
|
||||
ACCOUNT_ID_FOR_DISPLAY=cf_account_id if cf_account_id else "Not Configured"
|
||||
)
|
||||
|
||||
|
|
@ -1321,33 +1363,47 @@ def ui_add_manual_rule_route():
|
|||
cloudflared_agent_state["last_action_status"] = "Error: Failed to create/update Access App for group(s)."
|
||||
|
||||
elif manual_access_policy_type and manual_access_policy_type != 'none':
|
||||
cf_access_policies = []
|
||||
if manual_access_policy_type == "bypass":
|
||||
logging.warning(f"Bypass policy requested for manual rule {full_hostname}. This is insecure and deprecated. Converting to 'allow'.")
|
||||
cf_access_policies = [{"name": "UI Manual Authenticated Access", "decision": "allow", "include": [{"everyone": {}}]}]
|
||||
elif manual_access_policy_type == "authenticate_email":
|
||||
if not manual_auth_email:
|
||||
cloudflared_agent_state["last_action_status"] = "Error: Email is required for this policy type."
|
||||
return redirect(url_for('web.status_page'))
|
||||
cf_access_policies = [
|
||||
{"name": f"UI Allow Access for {manual_auth_email}", "decision": "allow", "include": [{"email": {"email": manual_auth_email}}]}
|
||||
]
|
||||
cf_access_policies.append({"name": "UI Deny Fallback", "decision": "deny", "include": [{"everyone": {}}]})
|
||||
|
||||
default_bypass_id = "public-default-bypass"
|
||||
if default_bypass_id in access_groups:
|
||||
default_bypass_group = access_groups[default_bypass_id]
|
||||
cf_policy_id = default_bypass_group.get("cf_policy_id") or default_bypass_group.get("id")
|
||||
|
||||
existing_app = find_cloudflare_access_application_by_hostname(full_hostname)
|
||||
if existing_app:
|
||||
app_result = update_cloudflare_access_application(
|
||||
existing_app['id'], full_hostname, f"DockFlare-{full_hostname}", "24h", False, [full_hostname], cf_access_policies, None, False, False
|
||||
)
|
||||
else:
|
||||
app_result = create_cloudflare_access_application(
|
||||
full_hostname, f"DockFlare-{full_hostname}", "24h", False, [full_hostname], cf_access_policies, None, False, False
|
||||
)
|
||||
if app_result:
|
||||
access_app_id = app_result.get('id')
|
||||
access_policy_type = manual_access_policy_type
|
||||
else:
|
||||
cloudflared_agent_state["last_action_status"] = "Error: Failed to create Access App for manual policy."
|
||||
access_group_id = [default_bypass_id]
|
||||
access_policy_type = "group"
|
||||
desired_app_name = f"DockFlare-{full_hostname}"
|
||||
|
||||
access_app_config_hash = generate_access_app_config_hash(
|
||||
policy_type="group", session_duration="24h",
|
||||
app_launcher_visible=False,
|
||||
allowed_idps_str=None,
|
||||
auto_redirect_to_identity=False,
|
||||
custom_access_rules_str=json.dumps([cf_policy_id], sort_keys=True),
|
||||
group_id=default_bypass_id
|
||||
)
|
||||
|
||||
existing_app = find_cloudflare_access_application_by_hostname(full_hostname)
|
||||
if existing_app:
|
||||
app_result = update_cloudflare_access_application(
|
||||
existing_app['id'], full_hostname, desired_app_name, "24h",
|
||||
False, [full_hostname], [cf_policy_id],
|
||||
None, False, True
|
||||
)
|
||||
else:
|
||||
app_result = create_cloudflare_access_application(
|
||||
full_hostname, desired_app_name, "24h",
|
||||
False, [full_hostname], [cf_policy_id],
|
||||
None, False, True
|
||||
)
|
||||
|
||||
if app_result:
|
||||
access_app_id = app_result.get('id')
|
||||
else:
|
||||
cloudflared_agent_state["last_action_status"] = "Error: Failed to create/update Access App with default bypass policy."
|
||||
else:
|
||||
cloudflared_agent_state["last_action_status"] = "Error: Default bypass policy not found."
|
||||
return redirect(url_for('web.status_page'))
|
||||
|
||||
with state_lock:
|
||||
existing_rule = managed_rules.get(key_for_managed_rules)
|
||||
|
|
@ -1576,31 +1632,47 @@ def ui_edit_manual_rule_route():
|
|||
if app_result:
|
||||
access_app_id = app_result.get('id')
|
||||
elif manual_access_policy_type and manual_access_policy_type != 'none':
|
||||
cf_access_policies = []
|
||||
if manual_access_policy_type == "bypass":
|
||||
logging.warning(f"Bypass policy requested for edited rule {full_hostname}. This is insecure and deprecated. Converting to 'allow'.")
|
||||
cf_access_policies = [{"name": "UI Manual Authenticated Access", "decision": "allow", "include": [{"everyone": {}}]}]
|
||||
elif manual_access_policy_type == "authenticate_email":
|
||||
if not manual_auth_email:
|
||||
cloudflared_agent_state["last_action_status"] = "Error: Email required for policy."
|
||||
return redirect(url_for('web.status_page'))
|
||||
cf_access_policies = [
|
||||
{"name": f"UI Allow Email {manual_auth_email}", "decision": "allow", "include": [{"email": {"email": manual_auth_email}}]},
|
||||
{"name": "UI Deny Fallback", "decision": "deny", "include": [{"everyone": {}}]}
|
||||
]
|
||||
if cf_access_policies:
|
||||
existing_app = find_cloudflare_access_application_by_hostname(full_hostname)
|
||||
if existing_app:
|
||||
app_result = update_cloudflare_access_application(
|
||||
existing_app['id'], full_hostname, f"DockFlare-{full_hostname}", "24h", False, [full_hostname], cf_access_policies, None, False, False
|
||||
# Use the default bypass reusable policy
|
||||
default_bypass_id = "public-default-bypass"
|
||||
if default_bypass_id in access_groups:
|
||||
default_bypass_group = access_groups[default_bypass_id]
|
||||
cf_policy_id = default_bypass_group.get("cf_policy_id") or default_bypass_group.get("id")
|
||||
|
||||
access_group_id = [default_bypass_id]
|
||||
access_policy_type = "group"
|
||||
desired_app_name = f"DockFlare-{full_hostname}"
|
||||
|
||||
access_app_config_hash = generate_access_app_config_hash(
|
||||
policy_type="group", session_duration="24h",
|
||||
app_launcher_visible=False,
|
||||
allowed_idps_str=None,
|
||||
auto_redirect_to_identity=False,
|
||||
custom_access_rules_str=json.dumps([cf_policy_id], sort_keys=True),
|
||||
group_id=default_bypass_id
|
||||
)
|
||||
|
||||
existing_app = find_cloudflare_access_application_by_hostname(full_hostname)
|
||||
if existing_app:
|
||||
app_result = update_cloudflare_access_application(
|
||||
existing_app['id'], full_hostname, desired_app_name, "24h",
|
||||
False, [full_hostname], [cf_policy_id],
|
||||
None, False, True
|
||||
)
|
||||
else:
|
||||
app_result = create_cloudflare_access_application(
|
||||
full_hostname, desired_app_name, "24h",
|
||||
False, [full_hostname], [cf_policy_id],
|
||||
None, False, True
|
||||
)
|
||||
|
||||
if app_result:
|
||||
access_app_id = app_result.get('id')
|
||||
else:
|
||||
cloudflared_agent_state["last_action_status"] = "Error: Failed to update Access App with default bypass policy."
|
||||
else:
|
||||
app_result = create_cloudflare_access_application(
|
||||
full_hostname, f"DockFlare-{full_hostname}", "24h", False, [full_hostname], cf_access_policies, None, False, False
|
||||
)
|
||||
if app_result:
|
||||
access_app_id = app_result.get('id')
|
||||
access_policy_type = manual_access_policy_type
|
||||
cloudflared_agent_state["last_action_status"] = "Error: Default bypass policy not found."
|
||||
return redirect(url_for('web.status_page'))
|
||||
except Exception as e:
|
||||
logging.error(f"Error updating access app during manual edit: {e}", exc_info=True)
|
||||
cloudflared_agent_state["last_action_status"] = "Error: Failed to update access app."
|
||||
|
|
@ -1868,6 +1940,11 @@ def delete_access_group(group_id):
|
|||
flash(f"Error: Access Group with ID '{group_id}' not found.", "error")
|
||||
return redirect(url_for('web.access_policies_page'))
|
||||
|
||||
# Check if this is a system policy that cannot be deleted
|
||||
if access_groups[group_id].get('system_policy') or not access_groups[group_id].get('deletable', True):
|
||||
flash(f"Error: Cannot delete system policy '{access_groups[group_id]['display_name']}'.", "error")
|
||||
return redirect(url_for('web.access_policies_page'))
|
||||
|
||||
is_in_use = any(
|
||||
(isinstance(rule.get('access_group_id'), list) and group_id in rule.get('access_group_id')) or \
|
||||
(rule.get('access_group_id') == group_id)
|
||||
|
|
@ -1899,6 +1976,72 @@ def delete_access_group(group_id):
|
|||
flash(f"Success: Access Group '{display_name}' has been deleted.", "success")
|
||||
return redirect(url_for('web.access_policies_page'))
|
||||
|
||||
@bp.route('/ui/zone-policies/create', methods=['POST'])
|
||||
def create_zone_default_policy():
|
||||
"""Creates a wildcard *.zone.com Access Application using a selected access group."""
|
||||
zone_name = request.form.get('zone_name', '').strip()
|
||||
zone_id = request.form.get('zone_id', '').strip()
|
||||
access_group_id = request.form.get('access_group_id', '').strip()
|
||||
|
||||
if not zone_name or not access_group_id:
|
||||
flash("Error: Zone name and access policy are required.", "error")
|
||||
return redirect(url_for('web.access_policies_page'))
|
||||
|
||||
with state_lock:
|
||||
if access_group_id not in access_groups:
|
||||
flash(f"Error: Access policy '{access_group_id}' not found.", "error")
|
||||
return redirect(url_for('web.access_policies_page'))
|
||||
|
||||
group = access_groups[access_group_id]
|
||||
|
||||
wildcard_hostname = f"*.{zone_name}"
|
||||
|
||||
try:
|
||||
# Check if it already exists
|
||||
existing = find_cloudflare_access_application_by_hostname(wildcard_hostname)
|
||||
if existing:
|
||||
flash(f"A wildcard policy for '{wildcard_hostname}' already exists.", "warning")
|
||||
return redirect(url_for('web.access_policies_page'))
|
||||
|
||||
# Get the Cloudflare policy ID
|
||||
from app.core import reusable_policies
|
||||
cf_policy_id = group.get("cf_policy_id") or group.get("id")
|
||||
|
||||
# Sync to Cloudflare if needed
|
||||
if not cf_policy_id or cf_policy_id == access_group_id:
|
||||
policy_id = reusable_policies.sync_access_group_to_reusable_policy(access_group_id, group)
|
||||
if policy_id:
|
||||
cf_policy_id = policy_id
|
||||
with state_lock:
|
||||
access_groups[access_group_id]["cf_policy_id"] = policy_id
|
||||
access_groups[access_group_id]["id"] = policy_id
|
||||
save_state()
|
||||
|
||||
# Create the Access Application
|
||||
app_name = f"Zone Default: {wildcard_hostname}"
|
||||
session_duration = group.get("session_duration", "24h")
|
||||
app_launcher_visible = group.get("app_launcher_visible", False)
|
||||
auto_redirect = group.get("auto_redirect_to_identity", False)
|
||||
allowed_idps = group.get("allowed_idps")
|
||||
|
||||
app_result = create_cloudflare_access_application(
|
||||
wildcard_hostname, app_name, session_duration,
|
||||
app_launcher_visible, [wildcard_hostname], [cf_policy_id],
|
||||
allowed_idps, auto_redirect, True
|
||||
)
|
||||
|
||||
if app_result:
|
||||
flash(f"Success: Created zone default policy for '{wildcard_hostname}'.", "success")
|
||||
logging.info(f"Created zone default policy for {wildcard_hostname} with Access App ID {app_result.get('id')}")
|
||||
else:
|
||||
flash(f"Error: Failed to create Access Application for '{wildcard_hostname}'.", "error")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error creating zone default policy for {wildcard_hostname}: {e}", exc_info=True)
|
||||
flash(f"Error: Failed to create zone policy. {str(e)}", "error")
|
||||
|
||||
return redirect(url_for('web.access_policies_page'))
|
||||
|
||||
@bp.route('/ui/access-groups/sync-from-cloudflare', methods=['POST'])
|
||||
def sync_access_groups_from_cloudflare():
|
||||
from app import config
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue