Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

autohttps: Implement auto_https prefer_wildcard option #6146

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 49 additions & 7 deletions caddyconfig/httpcaddyfile/httptype.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,8 +520,8 @@ func (st *ServerType) serversFromPairings(
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
}
autoHTTPS := "on"
if ah, ok := options["auto_https"].(string); ok {
autoHTTPS := []string{}
if ah, ok := options["auto_https"].([]string); ok {
autoHTTPS = ah
}

Expand All @@ -548,17 +548,37 @@ func (st *ServerType) serversFromPairings(
}

// handle the auto_https global option
if autoHTTPS != "on" {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
switch autoHTTPS {
for _, val := range autoHTTPS {
switch val {
case "off":
if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
srv.AutoHTTPS.Disabled = true

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

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

case "ignore_loaded_certs":
if srv.AutoHTTPS == nil {
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 @@ -627,7 +647,7 @@ func (st *ServerType) serversFromPairings(
})

var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled

// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
Expand Down Expand Up @@ -711,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 @@ -726,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 @@ -873,7 +912,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
21 changes: 14 additions & 7 deletions caddyconfig/httpcaddyfile/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,15 +433,22 @@ func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {

func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
val := d.RemainingArgs()
if len(val) == 0 {
return "", d.ArgErr()
}
if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
for _, v := range val {
switch v {
case "off":
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', 'ignore_loaded_certs', or 'prefer_wildcard'")
}
}
return val, nil
}
Expand Down
64 changes: 47 additions & 17 deletions caddyconfig/httpcaddyfile/tlsapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,32 +44,34 @@ func (st ServerType) buildTLSApp(
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
autoHTTPS := "on"
if ah, ok := options["auto_https"].(string); ok {
autoHTTPS := []string{}
if ah, ok := options["auto_https"].([]string); ok {
autoHTTPS = ah
}

// find all hosts that share a server block with a hostless
// key, so that they don't get forgotten/omitted by auto-HTTPS
// (since they won't appear in route matchers)
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
if autoHTTPS != "off" {
if !sliceContains(autoHTTPS, "off") {
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
for _, addr := range sb.keys {
if addr.Host == "" {
// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
if addr.Host != "" {
continue
}

// this server block has a hostless key, now
// go through and add all the hosts to the set
for _, otherAddr := range sb.keys {
if otherAddr.Original == addr.Original {
continue
}
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
}
break
}
break
}
}
}
Expand Down Expand Up @@ -344,7 +346,7 @@ func (st ServerType) buildTLSApp(
internalAP := &caddytls.AutomationPolicy{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
}
if autoHTTPS != "off" && autoHTTPS != "disable_certs" {
if !sliceContains(autoHTTPS, "off") && !sliceContains(autoHTTPS, "disable_certs") {
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
Expand Down Expand Up @@ -411,7 +413,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 @@ -539,7 +544,7 @@ func newBaseAutomationPolicy(

// 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 @@ -624,6 +629,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