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

add certutils.CertReloader #11

Closed
wants to merge 2 commits into from
Closed

add certutils.CertReloader #11

wants to merge 2 commits into from

Conversation

joemiller
Copy link
Contributor

CertReloader provides a background go-routine that will automatically
reload TLS key+cert files when they're changed on disk. It implements the
GetCertificate() and GetClientCertificate() funcs used by tls.Config to
dynamically load key+cert data as needed.

Example provided as usage/documentation.

Copy link
Contributor

@spheromak spheromak left a comment

Choose a reason for hiding this comment

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

This is awesome. I only have a semi-nit re: logging

}

func (cr *CertReloader) setCertificate() error {
log.Println("Loading TLS certificates...")
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think libraries should be logging. It should at least be a toggle. Could also make the constructor take a logger.

P.S. I really wish go authors had made log an interface.

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 agree and spent a better part of sunday thinking about it. I'll drop it for now.

CertReloader provides a background go-routine that will automatically
reload TLS key+cert files when they're changed on disk. It implements
the GetCertificate() and GetClientCertificate() funcs used by tls.Config
to dynamically load key+cert data as needed.

Example provided as usage/documentation.
Copy link

@kf6nux kf6nux left a comment

Choose a reason for hiding this comment

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

Really excited about this feature.

I left some comments/concerns in the review. Don't let my comments block a merge if its current form fixes a problem for us.

Thanks for doing this @joemiller ! 🚀

if err != nil {
return err
}
err = cr.watcher.Add(cr.certFile)
Copy link

@kf6nux kf6nux Sep 6, 2017

Choose a reason for hiding this comment

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

It seems that if cr.watcher.Add(cr.certFile) errs, the watchCertificate() goroutine cannot recover. On the next iteration of the loop, it will wait forever to receive from either channel (and neither of which will ever send).

if err != nil {
cr.Error <- err
}
err = cr.resetWatcher()
Copy link

Choose a reason for hiding this comment

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

Why are we resetting the watcher after every event?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unknown. This code is copied from kelseyhightower's POC code (link is at top of the source code). I've poked around and I have not figured out why he was doing that yet.

Copy link

Choose a reason for hiding this comment

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

I couldn't find documentation to support my conjecture, but I suspect it's either a lazy debounce or a multi-platform support hack (it looks like it shouldn't be required on linux as it uses inotify_add_watch(2) which watches paths).

return nil, err
}

go cr.watchCertificate()
Copy link

Choose a reason for hiding this comment

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

Should we wrap cr.watchCertificate() with a func to catch its error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sounds reasonable

cr := &CertReloader{
certFile: certFile,
keyFile: keyFile,
Error: make(chan error, 10),
Copy link

Choose a reason for hiding this comment

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

The watchCertificate() goroutine will stop processing when this buffer fills. Perhaps we should add a timeout to sending on this channel so we drop errors when the user of the library fails to read from this channel?

Copy link
Contributor Author

@joemiller joemiller Sep 6, 2017

Choose a reason for hiding this comment

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

In general I am super uncomfortable with the Error channel. I spent half my labor day weekend trying to think of a better way to handle this. I would prefer to find a way to make the reloader uber simple and uber transparent to callers. That is, I want it to be just as easy as using TLS with the standard "load once" approach.

I'm open to suggestions on how to simplify the error handling.. some current thoughts, though not super happy about any of them:

  1. just fatal out on errors.. but having a library exit your app is no good.
  2. just log the errors ... optionally allow the user to supply a logger, else use a default logger
  3. continue to provide the Error channel for callers that want to implement their own handling of errors. For callers that don't consume the error channel, setup a go routine that simply logs errors for them.
  4. no Error channel, but force the caller to provide a callback func to execute on any errors.

all of these have some kind of additional problem/tradeoffs though.

thoughts? /cc @spheromak too

Copy link

@kf6nux kf6nux Sep 6, 2017

Choose a reason for hiding this comment

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

# 1 has another option:
Rather than hiding the watcher goroutine from the user, require the user to call it. Then the watcher goroutine could bubble errors back to the user of the library merely by exiting (and leave it up to the user of the library how they want to handle that).

Other functions in the stdlib follow a similar pattern to the above (e.g. http.ListenAndServe).

# 2 and # 4 are similar, and either would be fine IMHO. Slight preference for the callback though as I think it offers some implicit intuitive flexibility (e.g. only call the callback if callback exists, otherwise ignore the error). This pattern is found in the stdlib too (it's what we're implementing in this PR with tls.Config)

# 3 would be a little quirky IMHO. I think I would expect a library I use not to log unless I ask it to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tnx @kf6nux , good ideas. I'll probably not be able to revisit this until the weekend. When I do i think i will explore your suggestion #1 first.

@joemiller
Copy link
Contributor Author

Found this, https://github.com/cloudflare/certinel/blob/master/README.md at first glance it looks to meet the same objective. Open sourced by cloudflare. It implements the "provide a callback for file read errors" approach. I will try this out and if it meets the goal I'm going to close this pr

@spheromak
Copy link
Contributor

@joemiller should we come back to this with the certinel or just close this out with that being our way forward ?

@joemiller joemiller closed this Dec 11, 2019
@joemiller joemiller deleted the cert-reloader branch December 11, 2019 20:20
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.

3 participants