diff --git a/README.md b/README.md index c5981675f5..2a8dae5c1e 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,17 @@ Let's Encrypt client and ACME library written in Go. - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses - Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension - Support [draft-aaron-acme-profiles-00](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/): Profiles Extension +- Comes with about [150 DNS providers](https://go-acme.github.io/lego/dns) - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates - Revoke certificates -- Robust implementation of all ACME challenges +- Robust implementation of ACME challenges: - HTTP (http-01) - DNS (dns-01) - TLS (tls-alpn-01) - SAN certificate support - [CNAME support](https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme.html) by default -- Comes with multiple optional [DNS providers](https://go-acme.github.io/lego/dns) - [Custom challenge solvers](https://go-acme.github.io/lego/usage/library/writing-a-challenge-solver/) - Certificate bundling - OCSP helper function diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 44a468eda1..d2150d07ef 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -182,6 +182,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "ACME_DNS_STORAGE_PATH": The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates.`) ew.writeln() + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ACME_DNS_ALLOWLIST": Source networks using CIDR notation (multiple values should be separated with a comma).`) + ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/acme-dns`) diff --git a/docs/content/_index.md b/docs/content/_index.md index 7cb903aacf..497e7e1686 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -14,17 +14,17 @@ Let's Encrypt client and ACME library written in Go. - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses - Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension - Support [draft-aaron-acme-profiles-00](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/): Profiles Extension +- Comes with about [150 DNS providers]({{% ref "dns" %}}) - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates - Revoke certificates -- Robust implementation of all ACME challenges +- Robust implementation of ACME challenges: - HTTP (http-01) - DNS (dns-01) - TLS (tls-alpn-01) - SAN certificate support - [CNAME support](https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme.html) by default -- Comes with multiple optional [DNS providers]({{% ref "dns" %}}) - [Custom challenge solvers]({{% ref "usage/library/Writing-a-Challenge-Solver" %}}) - Certificate bundling - OCSP helper function diff --git a/docs/content/dns/zz_gen_acme-dns.md b/docs/content/dns/zz_gen_acme-dns.md index be901c5125..cb3d240166 100644 --- a/docs/content/dns/zz_gen_acme-dns.md +++ b/docs/content/dns/zz_gen_acme-dns.md @@ -52,6 +52,14 @@ The environment variable names can be suffixed by `_FILE` to reference a file in More information [here]({{% ref "dns#configuration-and-credentials" %}}). +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `ACME_DNS_ALLOWLIST` | Source networks using CIDR notation (multiple values should be separated with a comma). | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). diff --git a/providers/dns/acmedns/acmedns.go b/providers/dns/acmedns/acmedns.go index 9dd63d0a2d..8dedeb1284 100644 --- a/providers/dns/acmedns/acmedns.go +++ b/providers/dns/acmedns/acmedns.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "strings" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" @@ -23,6 +24,10 @@ const ( // (e.g. https://acmedns.your-domain.com). EnvAPIBase = envNamespace + "API_BASE" + // EnvAllowList are source networks using CIDR notation, + // e.g. "192.168.100.1/24,1.2.3.4/32,2002:c0a8:2a00::0/40". + EnvAllowList = envNamespace + "ALLOWLIST" + // EnvStoragePath is the environment variable name for the ACME-DNS JSON account data file. // A per-domain account will be registered/persisted to this file and used for TXT updates. EnvStoragePath = envNamespace + "STORAGE_PATH" @@ -34,6 +39,19 @@ const ( var _ challenge.Provider = (*DNSProvider)(nil) +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIBase string + AllowList []string + StoragePath string + StorageBaseURL string +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{} +} + // acmeDNSClient is an interface describing the goacmedns.Client functions the DNSProvider uses. // It makes it easier for tests to shim a mock Client into the DNSProvider. type acmeDNSClient interface { @@ -47,58 +65,67 @@ type acmeDNSClient interface { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { + config *Config client acmeDNSClient storage goacmedns.Storage } -// NewDNSProvider creates an ACME-DNS provider using file based account storage. -// Its configuration is loaded from the environment by reading EnvAPIBase and EnvStoragePath. +// NewDNSProvider returns a DNSProvider instance configured for Joohoi's acme-dns. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIBase) if err != nil { return nil, fmt.Errorf("acme-dns: %w", err) } - storagePath := env.GetOrFile(EnvStoragePath) - storageBaseURL := env.GetOrFile(EnvStorageBaseURL) + config := NewDefaultConfig() + config.APIBase = values[EnvAPIBase] + config.StoragePath = env.GetOrFile(EnvStoragePath) + config.StorageBaseURL = env.GetOrFile(EnvStorageBaseURL) - if storagePath == "" && storageBaseURL == "" { - return nil, fmt.Errorf("acme-dns: %s or %s environment variables not set", EnvStoragePath, EnvStorageBaseURL) + allowList := env.GetOrFile(EnvAllowList) + if allowList != "" { + config.AllowList = strings.Split(allowList, ",") } - if storagePath != "" && storageBaseURL != "" { - return nil, fmt.Errorf("acme-dns: %s or %s environment variables cannot be used at the same time", EnvStoragePath, EnvStorageBaseURL) - } + return NewDNSProviderConfig(config) +} - var st goacmedns.Storage - if storagePath != "" { - st = storage.NewFile(values[EnvStoragePath], 0o600) - } else { - st, err = internal.NewHTTPStorage(storageBaseURL) - if err != nil { - return nil, fmt.Errorf("acme-dns: new HTTP storage: %w", err) - } +// NewDNSProviderConfig return a DNSProvider instance configured for Joohoi's acme-dns. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("acme-dns: the configuration of the DNS provider is nil") } - client, err := goacmedns.NewClient(values[EnvAPIBase]) + st, err := getStorage(config) if err != nil { return nil, fmt.Errorf("acme-dns: %w", err) } - return NewDNSProviderClient(client, st) + client, err := goacmedns.NewClient(config.APIBase) + if err != nil { + return nil, fmt.Errorf("acme-dns: new client: %w", err) + } + + return &DNSProvider{ + config: config, + client: client, + storage: st, + }, nil } // NewDNSProviderClient creates an ACME-DNS DNSProvider with the given acmeDNSClient and [goacmedns.Storage]. +// Deprecated: use [NewDNSProviderConfig] instead. func NewDNSProviderClient(client acmeDNSClient, storage goacmedns.Storage) (*DNSProvider, error) { if client == nil { - return nil, errors.New("ACME-DNS Client must be not nil") + return nil, errors.New("acme-dns: Client must be not nil") } if storage == nil { - return nil, errors.New("ACME-DNS Storage must be not nil") + return nil, errors.New("acme-dns: Storage must be not nil") } return &DNSProvider{ + config: NewDefaultConfig(), client: client, storage: storage, }, nil @@ -172,8 +199,7 @@ func (d *DNSProvider) CleanUp(_, _, _ string) error { // the one-time manual CNAME setup required to complete setup of the ACME-DNS hook for the domain. // If any other error occurs it is returned as-is. func (d *DNSProvider) register(ctx context.Context, domain, fqdn string) error { - // TODO(@cpu): Read CIDR whitelists from the environment - newAcct, err := d.client.RegisterAccount(ctx, nil) + newAcct, err := d.client.RegisterAccount(ctx, d.config.AllowList) if err != nil { return err } @@ -207,3 +233,24 @@ func (d *DNSProvider) register(ctx context.Context, domain, fqdn string) error { Target: newAcct.FullDomain, } } + +func getStorage(config *Config) (goacmedns.Storage, error) { + if config.StoragePath == "" && config.StorageBaseURL == "" { + return nil, errors.New("storagePath or storageBaseURL is not set") + } + + if config.StoragePath != "" && config.StorageBaseURL != "" { + return nil, errors.New("storagePath and storageBaseURL cannot be used at the same time") + } + + if config.StoragePath != "" { + return storage.NewFile(config.StoragePath, 0o600), nil + } + + st, err := internal.NewHTTPStorage(config.StorageBaseURL) + if err != nil { + return nil, fmt.Errorf("new HTTP storage: %w", err) + } + + return st, nil +} diff --git a/providers/dns/acmedns/acmedns.toml b/providers/dns/acmedns/acmedns.toml index 12d690414c..6d68a013d1 100644 --- a/providers/dns/acmedns/acmedns.toml +++ b/providers/dns/acmedns/acmedns.toml @@ -22,6 +22,8 @@ lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com ACME_DNS_API_BASE = "The ACME-DNS API address" ACME_DNS_STORAGE_PATH = "The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates." ACME_DNS_STORAGE_BASE_URL = "The ACME-DNS JSON account data server." + [Configuration.Additional] + ACME_DNS_ALLOWLIST = "Source networks using CIDR notation (multiple values should be separated with a comma)." [Links] API = "https://github.com/joohoi/acme-dns#api" diff --git a/providers/dns/acmedns/acmedns_test.go b/providers/dns/acmedns/acmedns_test.go index e21a10522e..76aa097f56 100644 --- a/providers/dns/acmedns/acmedns_test.go +++ b/providers/dns/acmedns/acmedns_test.go @@ -68,17 +68,20 @@ func TestPresent(t *testing.T) { for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { - dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)}) - require.NoError(t, err) + p := &DNSProvider{ + config: NewDefaultConfig(), + client: test.Client, + storage: mockStorage{make(map[string]goacmedns.Account)}, + } // override the storage mock if required by the test case. if test.Storage != nil { - dp.storage = test.Storage + p.storage = test.Storage } // call Present. The token argument can be garbage because the ACME-DNS // provider does not use it. - err = dp.Present(egDomain, "foo", egKeyAuth) + err := p.Present(egDomain, "foo", egKeyAuth) if test.ExpectedError != nil { assert.Equal(t, test.ExpectedError, err) } else { @@ -134,16 +137,19 @@ func TestRegister(t *testing.T) { for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { - dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)}) - require.NoError(t, err) + p := &DNSProvider{ + config: NewDefaultConfig(), + client: test.Client, + storage: mockStorage{make(map[string]goacmedns.Account)}, + } // override the storage mock if required by the testcase. if test.Storage != nil { - dp.storage = test.Storage + p.storage = test.Storage } // Call register for the example domain/fqdn. - err = dp.register(context.Background(), egDomain, egFQDN) + err := p.register(context.Background(), egDomain, egFQDN) if test.ExpectedError != nil { assert.Equal(t, test.ExpectedError, err) } else {