@marcel91200 / caddy Mirror · read-only / commits / 25c6901e
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 03:30 UTC

httpcaddyfile: isolate TLS policies across listeners (#8192)

Zen Dodd committed Oct 12, 2026 at 16:38 UTC 25c6901e05214573b87c48e07e3913e319032ec6
2 files changed +99 -1
caddyconfig/httpcaddyfile/httptype.go
+19 -1
@@ -804,7 +804,12 @@ func (st *ServerType) serversFromPairings(
804 if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
805 // tls connection policies
806 for _, cpVal := range cpVals {
807 - cp := cpVal.Value.(*caddytls.ConnectionPolicy)
807 + // Policies are parsed once per original server block. Each listener
808 + // rewrites and consolidates them independently.
809 + cp, err := cloneConnectionPolicy(cpVal.Value.(*caddytls.ConnectionPolicy))
810 + if err != nil {
811 + return nil, fmt.Errorf("cloning TLS connection policy: %v", err)
812 + }
813
814 // make sure the policy covers all hostnames from the block
815 for _, h := range hosts {
@@ -1129,6 +1134,19 @@ func (st *ServerType) serversFromPairings(
1134 return servers, nil
1135 }
1136
1137 +func cloneConnectionPolicy(cp *caddytls.ConnectionPolicy) (*caddytls.ConnectionPolicy, error) {
1138 + encoded, err := json.Marshal(cp)
1139 + if err != nil {
1140 + return nil, err
1141 + }
1142 +
1143 + cloned := new(caddytls.ConnectionPolicy)
1144 + if err := json.Unmarshal(encoded, cloned); err != nil {
1145 + return nil, err
1146 + }
1147 + return cloned, nil
1148 +}
1149 +
1150 // sniNames returns the server names a connection policy's sni matcher matches.
1151 // The bool is false when the policy has no sni matcher, or when it does not
1152 // decode - the latter is unexpected enough to warn about rather than silently
caddyconfig/httpcaddyfile/httptype_test.go
+80
@@ -2,6 +2,7 @@ package httpcaddyfile
2
3 import (
4 "encoding/json"
5 + "slices"
6 "strings"
7 "testing"
8
@@ -323,3 +324,82 @@ func TestDefaultSNIWithoutHTTPS(t *testing.T) {
324 t.Errorf("Expected default_sni 'my-sni.com' in TLS connection policies, but it was missing. Generated JSON: %s", string(result))
325 }
326 }
327 +
328 +func TestTLSConnectionPoliciesAreIsolatedAcrossListeners(t *testing.T) {
329 + caddyfileStr := `(mtls) {
330 + tls {
331 + client_auth {
332 + mode require_and_verify
333 + trust_pool file /tmp/root.crt
334 + }
335 + }
336 + }
337 +
338 + abc.domain.com, abc.domain.com:2000 {
339 + import mtls
340 + respond "first"
341 + }
342 +
343 + domain.domain.com {
344 + import mtls
345 + respond "second"
346 + }
347 +
348 + ghi.domain.com, ghi.domain.com:2000 {
349 + import mtls
350 + respond "third"
351 + }`
352 +
353 + adapter := caddyfile.Adapter{ServerType: ServerType{}}
354 + result, _, err := adapter.Adapt([]byte(caddyfileStr), nil)
355 + if err != nil {
356 + t.Fatalf("Failed to adapt Caddyfile: %v", err)
357 + }
358 +
359 + var config struct {
360 + Apps struct {
361 + HTTP struct {
362 + Servers map[string]*caddyhttp.Server `json:"servers"`
363 + } `json:"http"`
364 + } `json:"apps"`
365 + }
366 + if err := json.Unmarshal(result, &config); err != nil {
367 + t.Fatalf("Failed to unmarshal JSON config: %v", err)
368 + }
369 +
370 + for listener, expectedNames := range map[string][]string{
371 + ":2000": {"abc.domain.com", "ghi.domain.com"},
372 + ":443": {"abc.domain.com", "domain.domain.com", "ghi.domain.com"},
373 + } {
374 + var server *caddyhttp.Server
375 + for _, candidate := range config.Apps.HTTP.Servers {
376 + if slices.Contains(candidate.Listen, listener) {
377 + server = candidate
378 + break
379 + }
380 + }
381 + if server == nil {
382 + t.Fatalf("Expected a server listening on %s", listener)
383 + }
384 +
385 + var names []string
386 + for _, policy := range server.TLSConnPolicies {
387 + if policy.ClientAuthentication == nil {
388 + continue
389 + }
390 + if policy.ClientAuthentication.Mode != "require_and_verify" {
391 + t.Fatalf("Expected %s client authentication on %s, got %s", "require_and_verify", listener, policy.ClientAuthentication.Mode)
392 + }
393 + var policyNames []string
394 + if err := json.Unmarshal(policy.MatchersRaw["sni"], &policyNames); err != nil {
395 + t.Fatalf("Failed to unmarshal SNI matcher for %s: %v", listener, err)
396 + }
397 + names = append(names, policyNames...)
398 + }
399 +
400 + slices.Sort(names)
401 + if !slices.Equal(names, expectedNames) {
402 + t.Errorf("Expected client authentication for %v on %s, got %v", expectedNames, listener, names)
403 + }
404 + }
405 +}