mirror of
https://github.com/caddyserver/caddy.git
synced 2026-10-05 20:31:45 -04:00
httpcaddyfile: new tls_automate_names global option (#8015)
* httpcaddyfile: new tls_automate_names global option
Provisioning a certificate for a name that is not served requires giving
it a site block of its own:
*.example.com {
}
foo.example.com {
respond "Real site"
}
That asks the tls app for the wildcard, but it also adds a route to the
http app, so every name pointed at the server that has no site block of
its own -- bar.example.com here -- gets an empty but valid response
rather than no match at all. Wanting a certificate and wanting to serve
a name are separate things, and the Caddyfile had no way to say only the
first.
Name them in the new global option instead:
{
tls_automate_names *.example.com
}
foo.example.com {
respond "Real site"
}
The names are added to the automate certificate loader and to an
automation policy built from the global options, so a name listed here
is managed exactly as it would be from a site block, with the same
issuers; the only difference in the adapted config is that no route is
added for it. Repeating the option appends rather than replaces, so a
long list can be split over several lines.
Names that cannot get a public certificate are given the internal
issuer, the same treatment a site block gives them. Names that already
appear in the automate list are skipped, but a name that also has a site
block is left listed here as well: a site block for http:// only is not
managed by auto-HTTPS, so dropping the name because a block exists could
silently leave it without a certificate.
The option needs no http app at all, so a config consisting only of
global options now adapts to a tls app on its own -- enough to keep
certificates renewed for a mail or XMPP server, or for names served by a
layer 4 app.
Closes #7122
* httpcaddyfile: reuse an existing policy for an automated name
A name given to tls_automate_names may already have an automation policy
from its own site block. Adding a second policy for the same subject is
not just redundant: the adapter rejects overlapping subjects, so
{
email nobody@example.com
tls_automate_names foo.example.com
}
foo.example.com {
tls {
ca https://acme.example.test/directory
}
}
failed to adapt at all, with "hostname appears in more than one
automation policy, making certificate management ambiguous".
Keep such a name in the automate loader but leave its policy alone. The
site block's policy is the more specific of the two, and the loader entry
is still wanted, since a site block served only over HTTP is not managed
by auto-HTTPS.
* Pin that tls_automate_names overrides auto_https off
The names given to the option are managed whether or not auto-HTTPS is
disabled: unlike the hostless-key block above it, that code is not gated
on auto_https, because naming a subject explicitly is a stronger signal
than the general switch. Nothing enforced it, so add the adaptation
fixture @steadytao asked for.
This commit is contained in:
1 parent
256df3c8ad
commit
5ee9a2d424
7 files changed
+418
-6
No files matched your search
@@ -65,6 +65,7 @@ func init() {
|
||||
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
||||
RegisterGlobalOption("dns", parseOptDNS)
|
||||
RegisterGlobalOption("tls_resolvers", parseOptTLSResolvers)
|
||||
RegisterGlobalOption("tls_automate_names", parseOptAutomateNames)
|
||||
RegisterGlobalOption("ech", parseOptECH)
|
||||
RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio)
|
||||
}
|
||||
@@ -320,6 +321,22 @@ func parseOptTLSResolvers(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||
return resolvers, nil
|
||||
}
|
||||
|
||||
// parseOptAutomateNames parses the tls_automate_names global option, which
|
||||
// names subjects to manage certificates for without serving them. Repeating
|
||||
// the option appends to the list rather than replacing it, so a long list can
|
||||
// be split over several lines.
|
||||
func parseOptAutomateNames(d *caddyfile.Dispenser, existing any) (any, error) {
|
||||
d.Next() // consume option name
|
||||
names := d.RemainingArgs()
|
||||
if len(names) == 0 {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
if previous, ok := existing.([]string); ok {
|
||||
names = append(previous, names...)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||
d.Next() // consume option name
|
||||
|
||||
|
||||
@@ -424,6 +424,58 @@ func (st ServerType) buildTLSApp(
|
||||
}
|
||||
al = append(al, name)
|
||||
}
|
||||
// names from the tls_automate_names global option are managed without a
|
||||
// site block of their own, so that asking for a certificate does not also
|
||||
// mean serving the name; like force_automate, an explicitly listed name is
|
||||
// managed even where auto-HTTPS would not have chosen it. Names that cannot
|
||||
// get a public certificate are given the internal issuer, the same
|
||||
// treatment they would get from a site block.
|
||||
if automateNames, ok := options["tls_automate_names"].([]string); ok {
|
||||
var publicNames []string
|
||||
for _, name := range automateNames {
|
||||
if slices.Contains(al, name) {
|
||||
continue
|
||||
}
|
||||
al = append(al, name)
|
||||
// a name that a site block already wrote a policy for keeps that
|
||||
// policy: it is more specific than anything the global options can
|
||||
// say, and a second policy naming the same subject is ambiguous --
|
||||
// adapting would fail outright. The name still belongs in the
|
||||
// automate loader, since a site block served only over HTTP does
|
||||
// not get its certificate managed by auto-HTTPS.
|
||||
if automationPolicyExistsForSubject(tlsApp.Automation, name) {
|
||||
continue
|
||||
}
|
||||
if certmagic.SubjectQualifiesForPublicCert(name) {
|
||||
publicNames = append(publicNames, name)
|
||||
} else {
|
||||
internalAP.SubjectsRaw = append(internalAP.SubjectsRaw, name)
|
||||
}
|
||||
}
|
||||
// the names still need an automation policy of their own, or they would
|
||||
// miss the issuer configured by global options -- the catch-all policy
|
||||
// that would otherwise carry it is dropped once every other policy
|
||||
// names its subjects. Consolidation folds this back into an identical
|
||||
// policy, so a name listed here ends up in the same place it would have
|
||||
// had it been given a site block.
|
||||
if len(publicNames) > 0 {
|
||||
// only worth a policy if it would carry something: either global
|
||||
// automation options, or ACME defaults filled in further below.
|
||||
// Without either, the names are managed with the defaults anyway,
|
||||
// and an empty policy would just be noise in the output.
|
||||
ap, err := newBaseAutomationPolicy(options, warnings, hasGlobalACMEDefaults(options))
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
if ap != nil {
|
||||
ap.SubjectsRaw = publicNames
|
||||
if tlsApp.Automation == nil {
|
||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.Sort(al) // to stabilize the adapt output
|
||||
if len(al) > 0 {
|
||||
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
|
||||
@@ -440,12 +492,7 @@ func (st ServerType) buildTLSApp(
|
||||
if tlsApp.Automation != nil {
|
||||
globalEmail := options["email"]
|
||||
globalACMECA := options["acme_ca"]
|
||||
globalACMECARoot := options["acme_ca_root"]
|
||||
_, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
|
||||
globalACMEEAB := options["acme_eab"]
|
||||
globalPreferredChains := options["preferred_chains"]
|
||||
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil
|
||||
if hasGlobalACMEDefaults {
|
||||
if hasGlobalACMEDefaults(options) {
|
||||
for i := range tlsApp.Automation.Policies {
|
||||
ap := tlsApp.Automation.Policies[i]
|
||||
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
|
||||
@@ -895,6 +942,33 @@ func appendUniqueStrings(existing []string, additions ...string) []string {
|
||||
return existing
|
||||
}
|
||||
|
||||
// automationPolicyExistsForSubject reports whether some automation policy
|
||||
// already names subject. Subjects are compared exactly, which is the same
|
||||
// comparison the adapter uses to reject overlapping policies; a catch-all
|
||||
// policy names no subjects and so never matches.
|
||||
func automationPolicyExistsForSubject(automation *caddytls.AutomationConfig, subject string) bool {
|
||||
if automation == nil {
|
||||
return false
|
||||
}
|
||||
return slices.ContainsFunc(automation.Policies, func(ap *caddytls.AutomationPolicy) bool {
|
||||
return slices.Contains(ap.SubjectsRaw, subject)
|
||||
})
|
||||
}
|
||||
|
||||
// hasGlobalACMEDefaults reports whether any global option is set that an
|
||||
// automation policy without issuers of its own would later be filled in with.
|
||||
// A policy is worth creating for a subject when this is true, even if the
|
||||
// policy is otherwise empty at the time it is made.
|
||||
func hasGlobalACMEDefaults(options map[string]any) bool {
|
||||
_, hasACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
|
||||
return options["email"] != nil ||
|
||||
options["acme_ca"] != nil ||
|
||||
options["acme_ca_root"] != nil ||
|
||||
hasACMEDNS ||
|
||||
options["acme_eab"] != nil ||
|
||||
options["preferred_chains"] != nil
|
||||
}
|
||||
|
||||
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
||||
// its values from the global options map. It should be used as the base
|
||||
// for any other automation policies. A nil policy (and no error) will be
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
email nobody@example.com
|
||||
tls_automate_names *.example.com
|
||||
}
|
||||
|
||||
foo.example.com {
|
||||
respond "Hello world"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"foo.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Hello world",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"certificates": {
|
||||
"automate": [
|
||||
"*.example.com"
|
||||
]
|
||||
},
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"foo.example.com",
|
||||
"*.example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"email": "nobody@example.com",
|
||||
"module": "acme"
|
||||
},
|
||||
{
|
||||
"ca": "https://acme.zerossl.com/v2/DV90",
|
||||
"email": "nobody@example.com",
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
auto_https off
|
||||
tls_automate_names mail.example.com
|
||||
}
|
||||
|
||||
foo.example.com {
|
||||
respond "hi"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"foo.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "hi",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"tls_connection_policies": [
|
||||
{}
|
||||
],
|
||||
"automatic_https": {
|
||||
"disable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"certificates": {
|
||||
"automate": [
|
||||
"mail.example.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
{
|
||||
email nobody@example.com
|
||||
tls_automate_names foo.example.com bar.example.com
|
||||
}
|
||||
|
||||
foo.example.com {
|
||||
tls {
|
||||
ca https://acme.example.test/directory
|
||||
}
|
||||
respond "served with its own issuer"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"foo.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "served with its own issuer",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"certificates": {
|
||||
"automate": [
|
||||
"bar.example.com",
|
||||
"foo.example.com"
|
||||
]
|
||||
},
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"foo.example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"ca": "https://acme.example.test/directory",
|
||||
"email": "nobody@example.com",
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"subjects": [
|
||||
"bar.example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"email": "nobody@example.com",
|
||||
"module": "acme"
|
||||
},
|
||||
{
|
||||
"ca": "https://acme.zerossl.com/v2/DV90",
|
||||
"email": "nobody@example.com",
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
tls_automate_names localhost 192.168.1.5 real.example.com
|
||||
tls_automate_names second.example.com
|
||||
}
|
||||
|
||||
example.org {
|
||||
respond "hi"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.org"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "hi",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"certificates": {
|
||||
"automate": [
|
||||
"192.168.1.5",
|
||||
"localhost",
|
||||
"real.example.com",
|
||||
"second.example.com"
|
||||
]
|
||||
},
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost",
|
||||
"192.168.1.5"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"module": "internal"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
tls_automate_names mail.example.com xmpp.example.com
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"tls": {
|
||||
"certificates": {
|
||||
"automate": [
|
||||
"mail.example.com",
|
||||
"xmpp.example.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user