From b8af35970ea0963cb4cdf405334f5294311e2382 Mon Sep 17 00:00:00 2001
From: Monviech <79600909+Monviech@users.noreply.github.com>
Date: Wed, 7 May 2025 10:58:59 +0200
Subject: [PATCH] www/caddy: Change template generation of wildcard+subdomain
pattern to support ECH and mTLS (#4673)
* www/caddy: Change wildcard domain with subdomain pattern to align with caddy-v2.10.0
* www/caddy: Allow Client Auth (mTLS) inside subdomains as supported by new wildcard domain and subdomain pattern
* www/caddy: Add encrypted client hello (ECH)
---
.../OPNsense/Caddy/forms/dialogSubdomain.xml | 19 +++
.../OPNsense/Caddy/forms/general.xml | 10 ++
.../mvc/app/models/OPNsense/Caddy/Caddy.xml | 15 ++
.../scripts/OPNsense/Caddy/caddy_certs.php | 13 ++
.../templates/OPNsense/Caddy/Caddyfile | 128 +++++++++++-------
5 files changed, 133 insertions(+), 52 deletions(-)
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml
index 558c702a2..ccfc1c2ba 100644
--- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml
+++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml
@@ -81,4 +81,23 @@
false
+
+ subdomain.ClientAuthTrustPool
+
+ select_multiple
+
+
+ false
+
+
+
+ subdomain.ClientAuthMode
+
+ dropdown
+ true
+
+
+ false
+
+
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml
index b6f0c86d1..2d5a1167b 100644
--- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml
+++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml
@@ -180,6 +180,16 @@
0
+
+ header
+
+
+
+ caddy.general.TlsDnsEchDomain
+
+ text
+
+
diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml
index 3e1fdc217..7e9d03c24 100644
--- a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml
+++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml
@@ -109,6 +109,9 @@
N
+
+ N
+
@@ -337,6 +340,18 @@
+
+ require_and_verify
+
+ request
+ require
+ verify_if_given
+
+
+
+ ca
+ Y
+
diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php
index a6494eb97..0c14379c4 100755
--- a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php
+++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php
@@ -100,6 +100,19 @@ foreach ((new Caddy())->reverseproxy->reverse->iterateItems() as $reverseItem) {
}
}
+foreach ((new Caddy())->reverseproxy->subdomain->iterateItems() as $subdomainItem) {
+ $caCertField = (string)$subdomainItem->ClientAuthTrustPool;
+
+ if (!empty($caCertField)) {
+ $refs = array_map('trim', explode(',', $caCertField));
+ foreach ($refs as $ref) {
+ if (!empty($ref)) {
+ $caCertRefs[] = $ref;
+ }
+ }
+ }
+}
+
$caCertRefs = array_unique($caCertRefs);
foreach ((new Ca())->ca->iterateItems() as $caItem) {
diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile
index 8e027500b..acbf13bc6 100644
--- a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile
+++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile
@@ -1,5 +1,5 @@
{#
-# Copyright (c) 2023-2024 Cedrik Pischem
+# Copyright (c) 2023-2025 Cedrik Pischem
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -220,6 +220,19 @@
}
{% endif %}
+ {#
+ # Section: Encrypted ClientHello (ECH) Configuration
+ # https://caddyserver.com/docs/caddyfile/options#ech
+ #}
+ {% if generalSettings.TlsDnsEchDomain|default("") and dnsProvider %}
+ dns {{ dnsProvider }} {% if dnsProvider not in dnsProviderSpecialConfig %}{{ dnsApiKey }}{% else %}{
+ {% set context_var = 'dnsProviderSpecialLogic' %}
+ {% include "OPNsense/Caddy/includeDnsProvider" %}
+ }
+ {% endif +%}
+ ech {{ generalSettings.TlsDnsEchDomain }}
+ {% endif %}
+
{#
# Section: ACME Email, Auto HTTPS selection and global import statement
# Purpose: The ACME email is optional for receiving certificate notices.
@@ -304,21 +317,21 @@ http://{{ domain }} {
# certificates with the DNS-01 challenge. Refer to Dynamic DNS section for more details.
#}
{% macro tls_configuration(
- customCert,
- dnsChallenge,
- clientAuthTrustPool,
- clientAuthMode,
- dnsProvider,
- dnsApiKey,
- dnsSecretApiKey,
- tlsDnsOptionalField1,
- tlsDnsOptionalField2,
- tlsDnsOptionalField3,
- tlsDnsOptionalField4,
- tlsDnsPropagationTimeout,
- tlsDnsPropagationTimeoutPeriod,
- tlsDnsPropagationDelay,
- tlsDnsPropagationResolvers
+ customCert="",
+ dnsChallenge="0",
+ clientAuthTrustPool="",
+ clientAuthMode="",
+ dnsProvider="",
+ dnsApiKey="",
+ dnsSecretApiKey="",
+ tlsDnsOptionalField1="",
+ tlsDnsOptionalField2="",
+ tlsDnsOptionalField3="",
+ tlsDnsOptionalField4="",
+ tlsDnsPropagationTimeout="",
+ tlsDnsPropagationTimeoutPeriod="",
+ tlsDnsPropagationDelay="",
+ tlsDnsPropagationResolvers=""
) %}
{% if customCert or (dnsChallenge == "1" and dnsProvider) or clientAuthTrustPool %}
tls {% if customCert %}/var/db/caddy/data/caddy/certificates/temp/{{ customCert }}.pem /var/db/caddy/data/caddy/certificates/temp/{{ customCert }}.key{% endif %} {
@@ -586,13 +599,10 @@ http://{{ domain }} {
# - render_handles: Renders the handles in the correct order, including basic authentication.
# - reverse_proxy_configuration: Sets up the handle with reverse proxy configurations.
# - handle_response: Manages the response logic for access lists and aborts.
-# Important Details:
-# - Order of Path specific Handles: Prioritizes order of specific path handles over catch-all handles.
-# - Order of Wildcard Domains and Subdomains: Handles for wildcard domains come after all subdomains.
#}
+
{% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %}
{% if reverse.enabled|default("0") == "1" %}
- # Reverse Proxy Domain: "{{ reverse['@uuid'] }}"
{% if reverse.DisableTls|default("0") == "1" %}http://{% endif %}{{ reverse.FromDomain|default("") }}{% if reverse.FromPort %}:{{ reverse.FromPort }}{% endif %} {
{% if reverse.AccessLog|default("0") == "1" %}
{% if generalSettings.LogAccessPlain|default("0") == "0" %}
@@ -606,44 +616,58 @@ http://{{ domain }} {
{% endif %}
{% endif %}
{{ tls_configuration(
- reverse.CustomCertificate|default(""),
- reverse.DnsChallenge|default("0"),
- reverse.ClientAuthTrustPool|default(""),
- reverse.ClientAuthMode|default(""),
- generalSettings.TlsDnsProvider,
- generalSettings.TlsDnsApiKey,
- generalSettings.TlsDnsSecretApiKey,
- generalSettings.TlsDnsOptionalField1,
- generalSettings.TlsDnsOptionalField2,
- generalSettings.TlsDnsOptionalField3,
- generalSettings.TlsDnsOptionalField4,
- generalSettings.TlsDnsPropagationTimeout,
- generalSettings.TlsDnsPropagationTimeoutPeriod,
- generalSettings.TlsDnsPropagationDelay,
- generalSettings.TlsDnsPropagationResolvers
+ customCert=reverse.CustomCertificate|default(""),
+ dnsChallenge=reverse.DnsChallenge|default("0"),
+ clientAuthTrustPool=reverse.ClientAuthTrustPool|default(""),
+ clientAuthMode=reverse.ClientAuthMode|default(""),
+ dnsProvider=generalSettings.TlsDnsProvider,
+ dnsApiKey=generalSettings.TlsDnsApiKey,
+ dnsSecretApiKey=generalSettings.TlsDnsSecretApiKey,
+ tlsDnsOptionalField1=generalSettings.TlsDnsOptionalField1,
+ tlsDnsOptionalField2=generalSettings.TlsDnsOptionalField2,
+ tlsDnsOptionalField3=generalSettings.TlsDnsOptionalField3,
+ tlsDnsOptionalField4=generalSettings.TlsDnsOptionalField4,
+ tlsDnsPropagationTimeout=generalSettings.TlsDnsPropagationTimeout,
+ tlsDnsPropagationTimeoutPeriod=generalSettings.TlsDnsPropagationTimeoutPeriod,
+ tlsDnsPropagationDelay=generalSettings.TlsDnsPropagationDelay,
+ tlsDnsPropagationResolvers=generalSettings.TlsDnsPropagationResolvers
) }}
-
- {% for subdomain in helpers.toList('Pischem.caddy.reverseproxy.subdomain') %}
- {% if subdomain.enabled|default("0") == "1" and subdomain.reverse == reverse['@uuid'] %}
- @{{ subdomain['@uuid'] }} {
- host {{ subdomain.FromDomain }}
- }
- handle @{{ subdomain['@uuid'] }} {
- {% set subdomain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('subdomain', 'equalto', subdomain['@uuid']) | list %}
- {{ handle_accesslist(subdomain.accesslist, subdomain.FromDomain) }}
- {# All IPs not matched by accesslist will continue processing #}
- {{ render_basic_auth(subdomain.basicauth) }}
- {{ render_handles(subdomain_handles) }}
- }
- {% endif %}
- {% endfor %}
-
{% set domain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('reverse', 'equalto', reverse['@uuid']) | selectattr('subdomain', 'undefined') | list %}
{{ handle_accesslist(reverse.accesslist, reverse.FromDomain) }}
- {# All IPs not matched by accesslist will continue processing #}
{{ render_basic_auth(reverse.basicauth) }}
{{ render_handles(domain_handles) }}
}
+
+ {% endif %}
+{% endfor %}
+
+{% for subdomain in helpers.toList('Pischem.caddy.reverseproxy.subdomain') %}
+ {% if subdomain.enabled|default("0") == "1" %}
+ {% set reverse = helpers.toList('Pischem.caddy.reverseproxy.reverse') | selectattr('@uuid', 'equalto', subdomain.reverse) | first %}
+ {% if reverse and reverse.enabled|default("0") == "1" %}
+ {% if reverse.DisableTls|default("0") == "1" %}http://{% endif %}{{ subdomain.FromDomain|default("") }}{% if reverse.FromPort %}:{{ reverse.FromPort }}{% endif %} {
+ {% if reverse.AccessLog|default("0") == "1" %}
+ {% if generalSettings.LogAccessPlain|default("0") == "0" %}
+ log {{ subdomain['@uuid'] }}
+ {% else %}
+ log {
+ output file /var/log/caddy/access/{{ subdomain['@uuid'] }}.log {
+ roll_keep_for {{ generalSettings.LogAccessPlainKeep|default("10") }}d
+ }
+ }
+ {% endif %}
+ {% endif %}
+ {{ tls_configuration(
+ clientAuthTrustPool=subdomain.ClientAuthTrustPool|default(""),
+ clientAuthMode=subdomain.ClientAuthMode|default("")
+ ) }}
+ {% set subdomain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('subdomain', 'equalto', subdomain['@uuid']) | list %}
+ {{ handle_accesslist(subdomain.accesslist, subdomain.FromDomain) }}
+ {{ render_basic_auth(subdomain.basicauth) }}
+ {{ render_handles(subdomain_handles) }}
+ }
+
+ {% endif %}
{% endif %}
{% endfor %}