Skip to content

caddytls: Regularly reload static certificates #6941

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

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

pascalgn
Copy link
Contributor

@pascalgn pascalgn commented Apr 7, 2025

Fixes #6933

storageCleanTicker *time.Ticker
storageCleanStop chan struct{}
logger *zap.Logger
events *caddyevents.App
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it fine to add magic as a new field here? 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, likely not, since certmagic configs are mostly ephemeral and not tied to an entire TLS app config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! There isn't any state (other than certCache) stored in it, so in the reloadUnmanagedCertificates function, I can simply create a new object, right?

func reloadUnmanagedCertificates() {
	certCacheMu.RLock()
	magic := certmagic.New(certCache, certmagic.Config{
		Storage: ctx.Storage(),
		Logger:  t.logger,
		OnEvent: t.onEvent,
		OCSP: certmagic.OCSPConfig{
			DisableStapling: t.DisableOCSPStapling,
		},
		DisableStorageCheck: t.DisableStorageCheck,
	})
	certCacheMu.RUnlock()
	// add/remove certs from magic...
}

if unmanaged > 0 {
t.regularlyReloadUnmanagedCertificates()
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this reloading should always run (if explicitly enabled via config), independent of loaded certs

}

func (t *TLS) regularlyReloadUnmanagedCertificates() {
t.unmanagedCertsTicker = time.NewTicker(2 * time.Hour)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be configurable, same as StorageCleanInterval, but it needs to be explicitly enabled in the config. It should probably be disabled by default, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'd have this behavior be off by default.

}
}
return nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also try to remove expired certs from the cache in this function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we'd definitely want to remove the prior certificate.

Copy link
Member

@mholt mholt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking the initiative on this!

I haven't had much time to give this some thought, but, I think there's several ways to do this. Note that cert loader modules don't necessarily get their certificates from disk. I think if we are going to load certs from disk, we should be careful to initiate this only from modules which load from disk.

I was kind of thinking of something that watched the file system (fsnotify, I guess?) for changes, and then reloads only in that case. But yes, as you mentioned in a comment, we'd also need to remove the old cert from the cache. It's not my favorite, but it'd be opt-in I guess.

The ticker approach isn't my favorite either: it does a lot of unnecessary work. It's also not as effective... if a cert is updated, it might not be reflected in Caddy for up to the ticker interval. i.e. it's kind of slow. And then it has to load the cert every time, since you're not sure if it changed or not until you load it and check. (Well, I guess you could stat it, but still... almost all stats will end up being unnecessary.)

So I almost wonder if an approach that uses fsnotify in the relevant cert loader modules' Provision() method (and probably also cleaning things up in Cleanup()) would be a more conventional approach? What do you think?

storageCleanTicker *time.Ticker
storageCleanStop chan struct{}
logger *zap.Logger
events *caddyevents.App
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, likely not, since certmagic configs are mostly ephemeral and not tied to an entire TLS app config.

}
}
return nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we'd definitely want to remove the prior certificate.

}

func (t *TLS) regularlyReloadUnmanagedCertificates() {
t.unmanagedCertsTicker = time.NewTicker(2 * time.Hour)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'd have this behavior be off by default.

@pascalgn pascalgn force-pushed the update-unmanaged-certificates branch from e1aa980 to a874bb3 Compare April 9, 2025 10:36
@pascalgn
Copy link
Contributor Author

pascalgn commented Apr 9, 2025

@mholt I pushed some changes. I changed CertificateLoader like this:

type CertificateLoader interface {
	Initialize(updateCertificates func(add []Certificate, remove []string) error) error
}

That way, existing loaders (e.g. StorageLoader) will simply call the updateCertificates callback during the Initialize method, but other loaders (i.e. FolderLoader) will call it during Initialize, but also afterwards, during fsnotify events.

Would it make sense like that?

@mholt
Copy link
Member

mholt commented Apr 15, 2025

I was thinking more of using each loader modules' Provision() and Cleanup() methods to start/stop a goroutine that does the watching and reloading.

As for removing... let me think more on that.

@pascalgn
Copy link
Contributor Author

When moving the code into the Provision method, how would it access the certCache?

@mholt
Copy link
Member

mholt commented Apr 15, 2025

Via the tls app, so, ctx.App("tls") (then type-assert).

We might need to add a method or two to the caddytls.TLS app type which can expose the cache or at least the operations we need on it: https://pkg.go.dev/github.com/caddyserver/caddy/v2/modules/caddytls#TLS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Automatic reload of TLS certificates from filesystem
2 participants