Skip to content

Commit

Permalink
implement the On-Demand TLS functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
did committed Nov 1, 2024
1 parent e584373 commit 2825c98
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 2 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ applications. To enable this, add the `--tls` flag when deploying an instance:
kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com --tls


## On-demand TLS

In addition of the automatic TLS functionality, Kamal Proxy can also dynamically obtain a TLS certificate
from any host allowed by an external API endpoint of your choice.
This avoids hard-coding hosts in the configuration, especially when you don't know the hosts at the startup.

kamal-proxy deploy service1 --target web-1:3000 --host "" --tls --tls-on-demand-url=localhost:4567/check

The On-demand URL endpoint will have to answer a 200 HTTP status code.
Kamal Proxy will call the on-demand URL with a query string of `?host=` containing the host received by Kamal Proxy.

It also must respond as fast as possible, a couple of milliseconds top.


### Custom TLS certificate

When you obtained your TLS certificate manually, manage your own certificate authority,
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func newDeployCommand() *deployCommand {
deployCommand.cmd.Flags().StringSliceVar(&deployCommand.args.Hosts, "host", []string{}, "Host(s) to serve this target on (empty for wildcard)")

deployCommand.cmd.Flags().BoolVar(&deployCommand.args.ServiceOptions.TLSEnabled, "tls", false, "Configure TLS for this target (requires a non-empty host)")
deployCommand.cmd.Flags().StringVar(&deployCommand.args.ServiceOptions.TLSOnDemandUrl, "tls-on-demand-url", "", "Will make an HTTP request to the given URL, asking whether a host is allowed to have a certificate issued.")
deployCommand.cmd.Flags().BoolVar(&deployCommand.tlsStaging, "tls-staging", false, "Use Let's Encrypt staging environment for certificate provisioning")
deployCommand.cmd.Flags().StringVar(&deployCommand.args.ServiceOptions.TLSCertificatePath, "tls-certificate-path", "", "Configure custom TLS certificate path (PEM format)")
deployCommand.cmd.Flags().StringVar(&deployCommand.args.ServiceOptions.TLSPrivateKeyPath, "tls-private-key-path", "", "Configure custom TLS private key path (PEM format)")
Expand Down
75 changes: 73 additions & 2 deletions internal/server/service.go
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"
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -313,19 +317,86 @@ func (s *Service) createCertManager(hosts []string, options ServiceOptions) (Cer
// Ensure we're not trying to use Let's Encrypt to fetch a wildcard domain,
// as that is not supported with the challenge types that we use.
for _, host := range hosts {
if strings.Contains(host, "*") {
if strings.Contains(host, "*") && options.TLSOnDemandUrl == "" {
return nil, ErrorAutomaticTLSDoesNotSupportWildcards
}
}

// // TODO:
// // - create a func instead to return the host policy
// // - create the function calling the on demand url
// var hostPolicy = autocert.HostWhitelist(hosts...)

// // https://stackoverflow.com/questions/52129908/can-i-have-a-dynamic-host-policty-with-autocert

// // Wildcard hosts!!! we can handle them now!
// if len(hosts) == 0 && options.TLSOnDemandUrl != "" {
// fmt.Println("πŸš€πŸš€πŸš€ Registering a custom hostPolicy for", hosts)
// hostPolicy = func(ctx context.Context, host string) error {
// slog.Debug("Contacting", options.TLSOnDemandUrl, host)

// resp, err := http.Get(fmt.Sprintf("%s?domain=%s", options.TLSOnDemandUrl, url.QueryEscape(host)))

// if err != nil {
// // the TLS on demand URL is not reachable
// slog.Error("Unable to reach the TLS on demand URL", host, err)
// return err
// }

// if resp.StatusCode != 200 && resp.StatusCode != 201 {
// return fmt.Errorf("%s is not allowed to get a certificate", host)
// }

// return nil
// }
// } else {
// fmt.Println("πŸ˜•πŸ˜•πŸ˜•", len(hosts), hosts, options.TLSOnDemandUrl)
// }

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) {
onDemandTls := len(hosts) == 0 && options.TLSOnDemandUrl != ""

if !onDemandTls {
return autocert.HostWhitelist(hosts...), nil
}

_, err := url.ParseRequestURI(options.TLSOnDemandUrl)

if err != nil {
slog.Error("Unable to parse the tls_on_demand_url URL")
return nil, err
}

return func(ctx context.Context, host string) error {
resp, err := http.Get(fmt.Sprintf("%s?host=%s", options.TLSOnDemandUrl, url.QueryEscape(host)))

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)
Expand Down

0 comments on commit 2825c98

Please sign in to comment.