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)
This commit is contained in:
Monviech
2025-05-07 10:58:59 +02:00
committed by GitHub
parent 70ced7841e
commit b8af35970e
5 changed files with 133 additions and 52 deletions
@@ -81,4 +81,23 @@
<visible>false</visible>
</grid_view>
</field>
<field>
<id>subdomain.ClientAuthTrustPool</id>
<label>Client Auth Trust Pool</label>
<type>select_multiple</type>
<help><![CDATA[Choose multiple CAs or self-signed certificates from "System - Trust - Authorities". Client Auth is activated as soon as at least one certificate has been chosen. Important: Certificate revocation lists are not evaluated. If you need granular control, provide individual self-signed certificates for each device, and unset them to block access. Though keep in mind that if no certificate is left in this field, Client Auth will be deactivated.]]></help>
<grid_view>
<visible>false</visible>
</grid_view>
</field>
<field>
<id>subdomain.ClientAuthMode</id>
<label>Client Auth Mode</label>
<type>dropdown</type>
<advanced>true</advanced>
<help><![CDATA["request" - Ask clients for a certificate, but allow even if there isn't one; do not verify it. "require" - Require clients to present a certificate, but do not verify it. "verify_if_given" - Ask clients for a certificate; allow even if there isn't one, but verify it if there is. "require_and_verify" - Require clients to present a valid certificate that is verified.]]></help>
<grid_view>
<visible>false</visible>
</grid_view>
</field>
</form>
@@ -180,6 +180,16 @@
<hint>0</hint>
<help><![CDATA[propagation_delay is a duration value in seconds that sets how long to wait before starting DNS TXT records propagation checks when using the DNS challenge.]]></help>
</field>
<field>
<type>header</type>
<label>ECH</label>
</field>
<field>
<id>caddy.general.TlsDnsEchDomain</id>
<label>ECH Domain</label>
<type>text</type>
<help><![CDATA[Enables Encrypted ClientHello (ECH) by using the specified public domain name as the plaintext server name (SNI) in TLS handshakes. More information: https://caddyserver.com/docs/caddyfile/options#ech]]></help>
</field>
</tab>
<tab id="general-dynamicdns" description="Dynamic DNS">
<field>
@@ -109,6 +109,9 @@
<TlsDnsPropagationResolvers type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
</TlsDnsPropagationResolvers>
<TlsDnsEchDomain type="HostnameField">
<IpAllowed>N</IpAllowed>
</TlsDnsEchDomain>
<accesslist type="ModelRelationField">
<Model>
<reverseproxy>
@@ -337,6 +340,18 @@
<description type="DescriptionField"/>
<DynDns type="BooleanField"/>
<AcmePassthrough type="HostnameField"/>
<ClientAuthMode type="OptionField">
<BlankDesc>require_and_verify</BlankDesc>
<OptionValues>
<request>request</request>
<require>require</require>
<verify_if_given>verify_if_given</verify_if_given>
</OptionValues>
</ClientAuthMode>
<ClientAuthTrustPool type="CertificateField">
<Type>ca</Type>
<Multiple>Y</Multiple>
</ClientAuthTrustPool>
</subdomain>
<handle type="ArrayField">
<enabled type="BooleanField">
@@ -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) {
@@ -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 %}