Skip to content

Commit

Permalink
Implement auto_https prefer_wildcard option
Browse files Browse the repository at this point in the history
  • Loading branch information
francislavoie committed Mar 4, 2024
1 parent 88f8c04 commit 962efa3
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 4 deletions.
30 changes: 29 additions & 1 deletion caddyconfig/httpcaddyfile/httptype.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,12 @@ func (st *ServerType) serversFromPairings(
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
srv.AutoHTTPS.IgnoreLoadedCerts = true

case "prefer_wildcard":
if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
srv.AutoHTTPS.PreferWildcard = true
}
}

Expand Down Expand Up @@ -725,6 +731,13 @@ func (st *ServerType) serversFromPairings(
}
}

wildcardHosts := []string{}
for _, addr := range sblock.keys {
if strings.HasPrefix(addr.Host, "*.") {
wildcardHosts = append(wildcardHosts, addr.Host[2:])
}
}

for _, addr := range sblock.keys {
// if server only uses HTTP port, auto-HTTPS will not apply
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
Expand All @@ -740,6 +753,18 @@ func (st *ServerType) serversFromPairings(
}
}

// If prefer wildcard is enabled, then we add hosts that are
// already covered by the wildcard to the skip list
if srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard && addr.Scheme == "https" {
baseDomain := addr.Host
if idx := strings.Index(baseDomain, "."); idx != -1 {
baseDomain = baseDomain[idx+1:]
}
if !strings.HasPrefix(addr.Host, "*.") && sliceContains(wildcardHosts, baseDomain) {
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
}
}

// If TLS is specified as directive, it will also result in 1 or more connection policy being created
// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
// specifying prefix "https://"
Expand Down Expand Up @@ -878,7 +903,10 @@ func (st *ServerType) serversFromPairings(
if addressQualifiesForTLS &&
!hasCatchAllTLSConnPolicy &&
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI})
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{
DefaultSNI: defaultSNI,
FallbackSNI: fallbackSNI,
})
}

// tidy things up a bit
Expand Down
3 changes: 2 additions & 1 deletion caddyconfig/httpcaddyfile/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,10 +417,11 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
case "disable_redirects":
case "disable_certs":
case "ignore_loaded_certs":
case "prefer_wildcard":
break

default:
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'")
}
}
return val, nil
Expand Down
32 changes: 30 additions & 2 deletions caddyconfig/httpcaddyfile/tlsapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,10 @@ func (st ServerType) buildTLSApp(
}

// consolidate automation policies that are the exact same
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
tlsApp.Automation.Policies = consolidateAutomationPolicies(
tlsApp.Automation.Policies,
sliceContains(autoHTTPS, "prefer_wildcard"),
)

// ensure automation policies don't overlap subjects (this should be
// an error at provision-time as well, but catch it in the adapt phase
Expand Down Expand Up @@ -535,7 +538,7 @@ func newBaseAutomationPolicy(options map[string]any, warnings []caddyconfig.Warn

// consolidateAutomationPolicies combines automation policies that are the same,
// for a cleaner overall output.
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy, preferWildcard bool) []*caddytls.AutomationPolicy {
// sort from most specific to least specific; we depend on this ordering
sort.SliceStable(aps, func(i, j int) bool {
if automationPolicyIsSubset(aps[i], aps[j]) {
Expand Down Expand Up @@ -620,6 +623,31 @@ outer:
j--
}
}

if preferWildcard {
// remove subjects from i if they're covered by a wildcard in j
iSubjs := aps[i].SubjectsRaw
for iSubj := 0; iSubj < len(iSubjs); iSubj++ {
for jSubj := range aps[j].SubjectsRaw {
if !strings.HasPrefix(aps[j].SubjectsRaw[jSubj], "*.") {
continue
}
if certmagic.MatchWildcard(aps[i].SubjectsRaw[iSubj], aps[j].SubjectsRaw[jSubj]) {
iSubjs = append(iSubjs[:iSubj], iSubjs[iSubj+1:]...)
iSubj--
break
}
}
}
aps[i].SubjectsRaw = iSubjs

// remove i if it has no subjects left
if len(aps[i].SubjectsRaw) == 0 {
aps = append(aps[:i], aps[i+1:]...)
i--
continue outer
}
}
}
}

Expand Down
27 changes: 27 additions & 0 deletions modules/caddyhttp/autohttps.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ type AutoHTTPSConfig struct {
// enabled. To force automated certificate management
// regardless of loaded certificates, set this to true.
IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`

// If true, automatic HTTPS will prefer wildcard names
// and ignore non-wildcard names if both are available.
// This allows for writing a config with top-level host
// matchers without having those names produce certificates.
PreferWildcard bool `json:"prefer_wildcard,omitempty"`
}

// Skipped returns true if name is in skipSlice, which
Expand Down Expand Up @@ -167,6 +173,27 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
}
}

if srv.AutoHTTPS.PreferWildcard {
wildcards := make(map[string]struct{})
for d := range serverDomainSet {
if strings.HasPrefix(d, "*.") {
wildcards[d[2:]] = struct{}{}
}
}
for d := range serverDomainSet {
if strings.HasPrefix(d, "*.") {
continue
}
base := d
if idx := strings.Index(d, "."); idx != -1 {
base = d[idx+1:]
}
if _, ok := wildcards[base]; ok {
delete(serverDomainSet, d)
}
}
}

// nothing more to do here if there are no domains that qualify for
// automatic HTTPS and there are no explicit TLS connection policies:
// if there is at least one domain but no TLS conn policy (F&&T), we'll
Expand Down

0 comments on commit 962efa3

Please sign in to comment.