From 2973603b947f7a5607c49018b6f9d217dc2a1d3d Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Tue, 5 Mar 2024 11:17:26 +0000 Subject: [PATCH] Support additional ACME providers By default, SSL certificates are provisioned using Let's Encrypt's production directory. If the only configuration provided is `SSL_DOMAIN`, that's the behaviour we want. However, it can also be useful to specify an alternate provider, so we'll expose a few extra environment variable to make that possible: `ACME_DIRECTORY` can be used to specify a different directory to use. For example, the Let's Encrypt staging environment, or another provider entirely. Some providers require EAB credentials in order to issue certificates. To support these providers, we allow prodiving them in `EAB_KID` and `EAB_HMAC_KEY` --- README.md | 3 +++ internal/config.go | 25 +++++++++++++++++-------- internal/config_test.go | 2 ++ internal/server.go | 32 +++++++++++++++++++++++++++++--- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3f23188..44f3fec 100644 --- a/README.md +++ b/README.md @@ -72,4 +72,7 @@ version. | `HTTP_IDLE_TIMEOUT` | The maximum time in seconds that a client can be idle before the connection is closed. | 60 | | `HTTP_READ_TIMEOUT` | The maximum time in seconds that a client can take to send the request headers. | 30 | | `HTTP_WRITE_TIMEOUT` | The maximum time in seconds during which the client must read the response. | 30 | +| `ACME_DIRECTORY` | The URL of the ACME directory to use for SSL certificate provisioning. | `https://acme-v02.api.letsencrypt.org/directory` (Let's Encrypt production) | +| `EAB_KID` | The EAB key identifier to use when provisioning SSL certificates, if required. | None | +| `EAB_HMAC_KEY` | The Base64-encoded EAB HMAC key to use when provisioning SSL certificates, if required. | None | | `DEBUG` | Set to `1` or `true` to enable debug logging. | Disabled | diff --git a/internal/config.go b/internal/config.go index 36aece4..eff8ce4 100644 --- a/internal/config.go +++ b/internal/config.go @@ -6,6 +6,8 @@ import ( "os" "strconv" "time" + + "golang.org/x/crypto/acme" ) const ( @@ -20,8 +22,9 @@ const ( defaultMaxCacheItemSizeBytes = 1 * MB defaultMaxRequestBody = 0 - defaultStoragePath = "./storage/thruster" - defaultBadGatewayPage = "./public/502.html" + defaultACMEDirectoryURL = acme.LetsEncryptURL + defaultStoragePath = "./storage/thruster" + defaultBadGatewayPage = "./public/502.html" defaultHttpPort = 80 defaultHttpsPort = 443 @@ -42,9 +45,12 @@ type Config struct { XSendfileEnabled bool MaxRequestBody int - SSLDomain string - StoragePath string - BadGatewayPage string + SSLDomain string + ACMEDirectoryURL string + EAB_KID string + EAB_HMACKey string + StoragePath string + BadGatewayPage string HttpPort int HttpsPort int @@ -75,9 +81,12 @@ func NewConfig() (*Config, error) { XSendfileEnabled: getEnvBool("X_SENDFILE_ENABLED", true), MaxRequestBody: getEnvInt("MAX_REQUEST_BODY", defaultMaxRequestBody), - SSLDomain: getEnvString("SSL_DOMAIN", ""), - StoragePath: getEnvString("STORAGE_PATH", defaultStoragePath), - BadGatewayPage: getEnvString("BAD_GATEWAY_PAGE", defaultBadGatewayPage), + SSLDomain: getEnvString("SSL_DOMAIN", ""), + ACMEDirectoryURL: getEnvString("ACME_DIRECTORY", defaultACMEDirectoryURL), + EAB_KID: getEnvString("EAB_KID", ""), + EAB_HMACKey: getEnvString("EAB_HMAC_KEY", ""), + StoragePath: getEnvString("STORAGE_PATH", defaultStoragePath), + BadGatewayPage: getEnvString("BAD_GATEWAY_PAGE", defaultBadGatewayPage), HttpPort: getEnvInt("HTTP_PORT", defaultHttpPort), HttpsPort: getEnvInt("HTTPS_PORT", defaultHttpsPort), diff --git a/internal/config_test.go b/internal/config_test.go index 89a8c7a..cd8f7b0 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -28,6 +28,7 @@ func TestConfig_override_defaults_with_env_vars(t *testing.T) { usingEnvVar(t, "HTTP_READ_TIMEOUT", "5") usingEnvVar(t, "X_SENDFILE_ENABLED", "0") usingEnvVar(t, "DEBUG", "1") + usingEnvVar(t, "ACME_DIRECTORY", "https://acme-staging-v02.api.letsencrypt.org/directory") c, err := NewConfig() require.NoError(t, err) @@ -37,6 +38,7 @@ func TestConfig_override_defaults_with_env_vars(t *testing.T) { assert.Equal(t, 5*time.Second, c.HttpReadTimeout) assert.Equal(t, false, c.XSendfileEnabled) assert.Equal(t, slog.LevelDebug, c.LogLevel) + assert.Equal(t, "https://acme-staging-v02.api.letsencrypt.org/directory", c.ACMEDirectoryURL) } func TestConfig_override_defaults_with_env_vars_using_prefix(t *testing.T) { diff --git a/internal/server.go b/internal/server.go index e0753e5..e58c58f 100644 --- a/internal/server.go +++ b/internal/server.go @@ -2,12 +2,14 @@ package internal import ( "context" + "encoding/base64" "fmt" "log/slog" "net" "net/http" "time" + "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" ) @@ -68,10 +70,34 @@ func (s *Server) Stop() { } func (s *Server) certManager() *autocert.Manager { + client := &acme.Client{DirectoryURL: s.config.ACMEDirectoryURL} + binding := s.externalAccountBinding() + + slog.Debug("SSL: initializing", "directory", client.DirectoryURL, "using_eab", binding != nil) + return &autocert.Manager{ - Cache: autocert.DirCache(s.config.StoragePath), - HostPolicy: autocert.HostWhitelist(s.config.SSLDomain), - Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(s.config.StoragePath), + Client: client, + ExternalAccountBinding: binding, + HostPolicy: autocert.HostWhitelist(s.config.SSLDomain), + Prompt: autocert.AcceptTOS, + } +} + +func (s *Server) externalAccountBinding() *acme.ExternalAccountBinding { + if s.config.EAB_KID == "" || s.config.EAB_HMACKey == "" { + return nil + } + + key, err := base64.RawURLEncoding.DecodeString(s.config.EAB_HMACKey) + if err != nil { + slog.Error("Error decoding EAB_HMACKey", "error", err) + return nil + } + + return &acme.ExternalAccountBinding{ + KID: s.config.EAB_KID, + Key: key, } }