www/caddy: Allow access lists in handlers (#4245)

* www/caddy: Allow access lists in handlers. The new unique_suffix constructs the named matcher so that it is always unique. Also small bug that the invert did not render was fixed.

* www/caddy: Create inline access list logic that matches in the scope of a handler before the reverse_proxy directive, effectively blocking connections on matched IPs before they process further.

* www/caddy: Rework access list feature to use a single macro and work the same in domains, subdomains and handlers. Remove abort statement from general settings since the new logic makes it mandatory.

* www/caddy: Actually add the new template to the commit

* www/caddy: Hide Access List Response Code in advanced mode, since abort is now the standard action. Add changelog.

* www/caddy: Remove default values from Access List Response, since abort is the default. Add it as hint to dialogAccessList. Fix two small bugs: Layer4 ports do not render since the generalSettings was missing, Subdomains do not have ports, so replace with description.
This commit is contained in:
Monviech
2024-09-22 09:53:55 +02:00
committed by GitHub
parent 7faded335d
commit b8699cde8c
7 changed files with 58 additions and 59 deletions
+6
View File
@@ -16,9 +16,15 @@ Plugin Changelog
1.7.1
* Add: Frontend HTTP Version can be selected in General Settings, can be used to disable QUIC protocol
* Add: Access Lists can now be defined per HTTP Handler in advanced mode
* Change: Caddy Domains widget will now open links to managed websites in new browser tabs
* Change: To select tls_insecure_skip_verify in a HTTP Handler, the protocol https:// must be chosen
* Change: Abort from General Settings has been removed, it is now automatically added to all Access Lists
* Cleanup: Caddyfile template logic for Access Lists has been rewritten
* Cleanup: TLS checkboxes have been converted to dropdowns with http/https for clarity
* Cleanup: Layer4, Domain and Handle dialogues have been cleaned up, some options are now hidden in advanced mode
* Fix: Layer4 default ports did not render due to regression in previous version
* Fix: Invert in Access Lists did not render due to regression in previous version
1.7.0
@@ -23,13 +23,16 @@
<id>accesslist.HttpResponseCode</id>
<label>HTTP Response Code</label>
<type>text</type>
<help><![CDATA[Set a custom HTTP response code that should be returned to the requesting client when the access list does not match. Setting this will replace "Abort Connections" only for this access list. All clients will stay connected but will receive the response code.]]></help>
<hint>abort</hint>
<help><![CDATA[Set a custom HTTP response code that should be returned to the requesting client when the access list does not match. If empty, the connection is aborted with no response.]]></help>
<advanced>true</advanced>
</field>
<field>
<id>accesslist.HttpResponseMessage</id>
<label>HTTP Response Message</label>
<type>text</type>
<help><![CDATA[Set a custom HTTP response message in addition to the HTTP response code.]]></help>
<help><![CDATA[Set a custom HTTP response message in addition to the HTTP response code. If empty, the default HTTP response message for the chosen HTTP response code will be used.]]></help>
<advanced>true</advanced>
</field>
<field>
<id>accesslist.description</id>
@@ -53,6 +53,13 @@
<label>Access</label>
<advanced>true</advanced>
</field>
<field>
<id>handle.accesslist</id>
<label>Access List</label>
<type>dropdown</type>
<help><![CDATA[Select an Access List to restrict access to this handler. If unset, any local or remote client is allowed access.]]></help>
<advanced>true</advanced>
</field>
<field>
<id>handle.ForwardAuth</id>
<label>Forward Auth</label>
@@ -59,12 +59,6 @@
<type>dropdown</type>
<help><![CDATA[Select an Access List to set IP ranges of Trusted Proxies. If Caddy is not the first server being connected to by clients (for example, when a "CDN" is in front of Caddy), configure "Trusted Proxies" with a list of IP ranges (CIDRs) from which incoming requests are trusted to have sent good values for these headers. Additionally, set the same Access List to the domains the Trusted Proxies connect to.]]></help>
</field>
<field>
<id>caddy.general.abort</id>
<label>Abort Connections</label>
<type>checkbox</type>
<help><![CDATA[Abort all connections that don't have a matching Handle or Access List. This option does not conflict with "Let's Encrypt" or "ZeroSSL". Disable it for troubleshooting purposes, e.g., testing if the "Domain" works and the automatic certificate has been installed. For production use, enabling this option is recommended.]]></help>
</field>
<field>
<id>caddy.general.GracePeriod</id>
<label>Grace Period</label>
@@ -84,7 +84,6 @@
</reverseproxy>
</Model>
</accesslist>
<abort type="BooleanField"/>
<DisableSuperuser type="OptionField">
<Required>Y</Required>
<Default>0</Default>
@@ -288,7 +287,7 @@
<reverseproxy>
<source>OPNsense.Caddy.Caddy</source>
<items>reverseproxy.subdomain</items>
<display>FromDomain,FromPort</display>
<display>FromDomain,description</display>
<display_format>%s %s</display_format>
</reverseproxy>
</Model>
@@ -305,6 +304,16 @@
<Mask>/^(\/.*)?$/u</Mask>
<ValidationMessage>Please enter a valid Path that starts with '/'.</ValidationMessage>
</HandlePath>
<accesslist type="ModelRelationField">
<Model>
<reverseproxy>
<source>OPNsense.Caddy.Caddy</source>
<items>reverseproxy.accesslist</items>
<display>accesslistName,description</display>
<display_format>%s %s</display_format>
</reverseproxy>
</Model>
</accesslist>
<header type="ModelRelationField">
<Model>
<reverseproxy>
@@ -474,6 +474,7 @@
<th data-column-id="ToPath" data-type="string" data-visible="false">{{ lang._('Upstream Path') }}</th>
<th data-column-id="PassiveHealthFailDuration" data-type="string" data-visible="false">{{ lang._('Upstream Fail Duration') }}</th>
<th data-column-id="ForwardAuth" data-type="boolean" data-formatter="boolean" data-visible="false">{{ lang._('Forward Auth') }}</th>
<th data-column-id="accesslist" data-type="string" data-visible="false">{{ lang._('Access List') }}</th>
<th data-column-id="HttpVersion" data-type="string" data-visible="false">{{ lang._('HTTP Version') }}</th>
<th data-column-id="HttpKeepalive" data-type="string" data-visible="false">{{ lang._('HTTP Keepalive') }}</th>
<th data-column-id="HttpTlsTrustedCaCerts" data-type="string" data-visible="false">{{ lang._('TLS Trust Pool') }}</th>
@@ -234,7 +234,7 @@
# There are no regressions to set these ports up.
#}
{% if enableLayer4|default("0") == "1" %}
{% if generalSettings.EnableLayer4|default("0") == "1" %}
# Layer4 default HTTP port
{% if httpPort %}
:{{ httpPort }} {
@@ -379,6 +379,8 @@ http://{{ domain }} {
#}
{% macro reverse_proxy_configuration(handle) %}
{{ handle.HandleType }} {{ handle.HandlePath|default("") }} {
{{ handle_accesslist(handle.accesslist) }}
{# All IPs not matched by accesslist will continue processing #}
{% if handle.ForwardAuth|default("0") == "1" %}
{% include "OPNsense/Caddy/includeAuthProvider" %}
{% endif %}
@@ -433,25 +435,29 @@ http://{{ domain }} {
{#
# Macro: handle_accesslist
# Purpose: Manages the logic for access lists, including configuration and handling.
# Purpose: Manages the logic for access lists, used for handlers, domains and subdomains
# Parameters:
# @param accesslist (string): The UUID of the access list to be applied.
# @param content (string): The content to be wrapped in the access list handler.
# @param invert (boolean): A flag that inverts the logic of the access list.
#}
{% macro handle_accesslist(accesslist, content, invert=false) %}
{% macro handle_accesslist(accesslist) %}
{# Access list logic for dropping connections based on IP #}
{% if accesslist %}
{% set accesslist_obj = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', accesslist) | first %}
{% set client_ips = accesslist_obj.clientIps.split(',') %}
{% set client_ips_space_separated = client_ips | join(' ') %}
@{{ accesslist_obj['@uuid'] }} {
{{ 'not' if invert or accesslist_obj.accesslistenvert|default("0") == "1" else '' }} client_ip {{ client_ips_space_separated }}
}
handle @{{ accesslist_obj['@uuid'] }} {
{{ content }}
}
{% else %}
{{ content }}
{% if accesslist_obj %}
{% set client_ips = accesslist_obj.clientIps.split(',') | join(' ') %}
@{{ accesslist_obj['@uuid'] }} {
{# Non-inverted access lists have "not" as default, inverted ones '' #}
{{ 'not' if accesslist_obj.accesslistInvert|default("0") == "0" else '' }} client_ip {{ client_ips }}
}
{# When IP is matched, abort or send response code. This will end processing and drop the request #}
handle @{{ accesslist_obj['@uuid'] }} {
{% if accesslist_obj.HttpResponseCode %}
respond {{ '"' + accesslist_obj.HttpResponseMessage + '"' if accesslist_obj.HttpResponseMessage else '' }} {{ accesslist_obj.HttpResponseCode }}
{% else %}
abort
{% endif %}
}
{% endif %}
{% endif %}
{% endmacro %}
@@ -474,25 +480,6 @@ http://{{ domain }} {
{% endif %}
{% endmacro %}
{#
# Macro: handle_response
# Purpose: Manages the response logic for access lists and aborts.
# Parameters:
# @param accesslist (object): The access list object containing response settings.
# @param general_abort (string): The global abort setting.
#}
{% macro handle_response(accesslist, general_abort) %}
{% if accesslist %}
{% if accesslist.HttpResponseCode or accesslist.HttpResponseMessage %}
respond {{ '"' + accesslist.HttpResponseMessage|default('') + '"' if accesslist.HttpResponseMessage else '' }} {{ accesslist.HttpResponseCode|default(403) }}
{% elif general_abort == "1" %}
abort
{% endif %}
{% elif general_abort == "1" %}
abort
{% endif %}
{% endmacro %}
{#
# Macro: render_handles
# Purpose: Renders the handles in the correct order (path-specific first, then catch-all),
@@ -567,25 +554,17 @@ http://{{ domain }} {
}
handle @{{ subdomain['@uuid'] }} {
{% set subdomain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('subdomain', 'equalto', subdomain['@uuid']) | list %}
{% set subdomain_content %}
{{ render_handles(subdomain_handles, subdomain.basicauth) }}
{% endset %}
{{ handle_accesslist(subdomain.accesslist, subdomain_content) }}
{% set accesslist = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', subdomain.accesslist) | first %}
{{ handle_response(accesslist, Pischem.caddy.general.abort|default("0")) }}
{{ handle_accesslist(subdomain.accesslist) }}
{# All IPs not matched by accesslist will continue processing #}
{{ render_handles(subdomain_handles, subdomain.basicauth) }}
}
{% endif %}
{% endfor %}
{% set wildcard_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('reverse', 'equalto', reverse['@uuid']) | selectattr('subdomain', 'undefined') | list %}
{% set wildcard_content %}
{{ render_handles(wildcard_handles, reverse.basicauth) }}
{% endset %}
{{ handle_accesslist(reverse.accesslist, wildcard_content) }}
{% set accesslist = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', reverse.accesslist) | first %}
{{ handle_response(accesslist, Pischem.caddy.general.abort|default("0")) }}
{% set domain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('reverse', 'equalto', reverse['@uuid']) | selectattr('subdomain', 'undefined') | list %}
{{ handle_accesslist(reverse.accesslist) }}
{# All IPs not matched by accesslist will continue processing #}
{{ render_handles(domain_handles, reverse.basicauth) }}
}
{% endif %}
{% endfor %}