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, } }