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
+}