-
Notifications
You must be signed in to change notification settings - Fork 32
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
implement the On-Demand TLS feature #63
base: main
Are you sure you want to change the base?
Changes from all commits
2825c98
2a30eb8
46d3d38
ec013ea
f91ed90
2e0ef39
c258d11
2e6581c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,16 @@ | ||
package server | ||
|
||
import ( | ||
"context" | ||
"crypto/sha256" | ||
"encoding/hex" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"log/slog" | ||
"net" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
"path" | ||
"strings" | ||
|
@@ -66,6 +69,7 @@ type HealthCheckConfig struct { | |
|
||
type ServiceOptions struct { | ||
TLSEnabled bool `json:"tls_enabled"` | ||
TLSOnDemandUrl string `json:"tls_on_demand_url"` | ||
TLSCertificatePath string `json:"tls_certificate_path"` | ||
TLSPrivateKeyPath string `json:"tls_private_key_path"` | ||
ACMEDirectory string `json:"acme_directory"` | ||
|
@@ -318,14 +322,49 @@ func (s *Service) createCertManager(hosts []string, options ServiceOptions) (Cer | |
} | ||
} | ||
|
||
hostPolicy, err := s.createAutoCertHostPolicy(hosts, options) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &autocert.Manager{ | ||
Prompt: autocert.AcceptTOS, | ||
Cache: autocert.DirCache(options.ScopedCachePath()), | ||
HostPolicy: autocert.HostWhitelist(hosts...), | ||
HostPolicy: hostPolicy, | ||
Client: &acme.Client{DirectoryURL: options.ACMEDirectory}, | ||
}, nil | ||
} | ||
|
||
func (s *Service) createAutoCertHostPolicy(hosts []string, options ServiceOptions) (autocert.HostPolicy, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be nice to have some test coverage of this, now that's getting a little more involved than just a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. I'll try to write some. |
||
if options.TLSOnDemandUrl == "" { | ||
return autocert.HostWhitelist(hosts...), nil | ||
} | ||
|
||
_, err := url.ParseRequestURI(options.TLSOnDemandUrl) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd expect that the on demand URL will usually point to an endpoint in the application that's deployed, rather than to some other external app. In which case, it would be simpler for this to be a path rather than an absolute URL. We'd then automatically call it on the currently deployed target (a bit like how the health check paths work). That way you don't have to worry about having a stable hostname to reach for all versions of the app, etc., because the proxy takes care of that for you. I'm not sure if there's a common enough need to support an external on demand URL as well, but for simplicity's sake it would be nice to have this be path-only if possible. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👌 I haven't thought about it!!! In my production case, it would work perfectly because my Rails app was always in charge of returning that list of hostnames (even when I was hosting it with k8s). Perhaps (it's a guess), we should keep the URL as well for developers who prefers to move the responsibility of this endpoint to another app (and probably deployed by Kamal too) for performance or architecture reasons. Let's keep it simple in a first time so let's use the path only :-) (I will make the modifications) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good! |
||
if err != nil { | ||
slog.Error("Unable to parse the tls_on_demand_url URL") | ||
return nil, err | ||
} | ||
|
||
slog.Info("Will use the tls_on_demand_url URL", "url", options.TLSOnDemandUrl) | ||
|
||
return func(ctx context.Context, host string) error { | ||
slog.Info("Get a certificate", "host", host) | ||
|
||
resp, err := http.Get(fmt.Sprintf("%s?host=%s", options.TLSOnDemandUrl, url.QueryEscape(host))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be worth having a timeout on this request. I don't think it needs to be user-configurable; just something reasonable like a couple seconds. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. I'm wondering that even 2 or 2 seconds is probably too long and the TLS certificate generation will fail. (I'll make the modifications) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a couple of seconds should work OK, it just all leads to a slower experience for the client for the first request when a new cert is first generated. The initial cert generation typically takes a few seconds anyway. But, it could be worth testing that out to be sure. |
||
if err != nil { | ||
slog.Error("Unable to reach the TLS on demand URL", host, err) | ||
return err | ||
} | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return fmt.Errorf("%s is not allowed to get a certificate", host) | ||
} | ||
|
||
return nil | ||
}, nil | ||
} | ||
|
||
func (s *Service) createMiddleware(options ServiceOptions, certManager CertManager) (http.Handler, error) { | ||
var err error | ||
var handler http.Handler = http.HandlerFunc(s.serviceRequestWithTarget) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't need to set an empty
--host
here. We could modify this restriction so that--host
is not required with the On Demand feature.This probably applies when using custom certificates as well, come to think of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed! But internally, we should use the empty string (or any *) as the host for the internal router, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly! If it’s empty it will match everything, so if they are using on-demand TLS we can let them omit the flag to do that.
Wildcard routing could still be useful to allow multiple apps to run side by side, even if one or more of them is using on-demand TLS. So people should be allowed to specify the host string when they want. It’s just optional in that case.