diff --git a/README.md b/README.md
index 1a8480b246..cb3f8ab2e7 100644
--- a/README.md
+++ b/README.md
@@ -62,212 +62,212 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
Active24 |
Akamai EdgeDNS |
Alibaba Cloud DNS |
- all-inkl |
+ Alibaba Cloud ESA |
+ | all-inkl |
Amazon Lightsail |
Amazon Route 53 |
Anexia CloudDNS |
- ArvanCloud |
+ | ArvanCloud |
Aurora DNS |
Autodns |
Axelname |
- Azion |
+ | Azion |
Azure (deprecated) |
Azure DNS |
Baidu Cloud |
- Beget.com |
+ | Beget.com |
Binary Lane |
Bindman |
Bluecat |
- BookMyName |
+ | BookMyName |
Brandit (deprecated) |
Bunny |
Checkdomain |
- Civo |
+ | Civo |
Cloud.ru |
CloudDNS |
Cloudflare |
- ClouDNS |
+ | ClouDNS |
CloudXNS (Deprecated) |
ConoHa v2 |
ConoHa v3 |
- Constellix |
+ | Constellix |
Core-Networks |
CPanel/WHM |
Derak Cloud |
- deSEC.io |
+ | deSEC.io |
Designate DNSaaS for Openstack |
Digital Ocean |
DirectAdmin |
- DNS Made Easy |
+ | DNS Made Easy |
dnsHome.de |
DNSimple |
DNSPod (deprecated) |
- Domain Offensive (do.de) |
+ | Domain Offensive (do.de) |
Domeneshop |
DreamHost |
Duck DNS |
- Dyn |
+ | Dyn |
DynDnsFree.de |
Dynu |
EasyDNS |
- Efficient IP |
+ | Efficient IP |
Epik |
Exoscale |
External program |
- F5 XC |
+ | F5 XC |
freemyip.com |
G-Core |
Gandi |
- Gandi Live DNS (v5) |
+ | Gandi Live DNS (v5) |
Glesys |
Go Daddy |
Google Cloud |
- Google Domains |
+ | Google Domains |
Hetzner |
Hosting.de |
Hostinger |
- Hosttech |
+ | Hosttech |
HTTP request |
http.net |
Huawei Cloud |
- Hurricane Electric DNS |
+ | Hurricane Electric DNS |
HyperOne |
IBM Cloud (SoftLayer) |
IIJ DNS Platform Service |
- Infoblox |
+ | Infoblox |
Infomaniak |
Internet Initiative Japan |
Internet.bs |
- INWX |
+ | INWX |
Ionos |
IPv64 |
iwantmyname (Deprecated) |
- Joker |
+ | Joker |
Joohoi's ACME-DNS |
KeyHelp |
Liara |
- Lima-City |
+ | Lima-City |
Linode (v4) |
Liquid Web |
Loopia |
- LuaDNS |
+ | LuaDNS |
Mail-in-a-Box |
ManageEngine CloudDNS |
Manual |
- Metaname |
+ | Metaname |
Metaregistrar |
mijn.host |
Mittwald |
- myaddr.{tools,dev,io} |
+ | myaddr.{tools,dev,io} |
MyDNS.jp |
MythicBeasts |
Name.com |
- Namecheap |
+ | Namecheap |
Namesilo |
NearlyFreeSpeech.NET |
Netcup |
- Netlify |
+ | Netlify |
Nicmanager |
NIFCloud |
Njalla |
- Nodion |
+ | Nodion |
NS1 |
Octenium |
Open Telekom Cloud |
- Oracle Cloud |
+ | Oracle Cloud |
OVH |
plesk.com |
Porkbun |
- PowerDNS |
+ | PowerDNS |
Rackspace |
Rain Yun/雨云 |
RcodeZero |
- reg.ru |
+ | reg.ru |
Regfish |
RFC2136 |
RimuHosting |
- RU CENTER |
+ | RU CENTER |
Sakura Cloud |
Scaleway |
Selectel |
- Selectel v2 |
+ | Selectel v2 |
SelfHost.(de|eu) |
Servercow |
Shellrent |
- Simply.com |
+ | Simply.com |
Sonic |
Spaceship |
Stackpath |
- Technitium |
+ | Technitium |
Tencent Cloud DNS |
Tencent EdgeOne |
Timeweb Cloud |
- TransIP |
+ | TransIP |
UKFast SafeDNS |
Ultradns |
Variomedia |
- VegaDNS |
+ | VegaDNS |
Vercel |
Versio.[nl|eu|uk] |
VinylDNS |
- VK Cloud |
+ | VK Cloud |
Volcano Engine/火山引擎 |
Vscale |
Vultr |
- webnames.ca |
+ | webnames.ca |
webnames.ru |
Websupport |
WEDOS |
- West.cn/西部数码 |
+ | West.cn/西部数码 |
Yandex 360 |
Yandex Cloud |
Yandex PDD |
- Zone.ee |
+ | Zone.ee |
ZoneEdit |
Zonomi |
|
- |
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go
index e21f37e63d..b5c4aaf4d9 100644
--- a/cmd/zz_gen_cmd_dnshelp.go
+++ b/cmd/zz_gen_cmd_dnshelp.go
@@ -16,6 +16,7 @@ func allDNSCodes() string {
"acme-dns",
"active24",
"alidns",
+ "aliesa",
"allinkl",
"anexia",
"arvancloud",
@@ -251,6 +252,29 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/alidns`)
+ case "aliesa":
+ // generated from: providers/dns/aliesa/aliesa.toml
+ ew.writeln(`Configuration for Alibaba Cloud ESA.`)
+ ew.writeln(`Code: 'aliesa'`)
+ ew.writeln(`Since: 'v4.29.0'`)
+ ew.writeln()
+
+ ew.writeln(`Credentials:`)
+ ew.writeln(` - "ALIESA_ACCESS_KEY": Access key ID`)
+ ew.writeln(` - "ALIESA_RAM_ROLE": Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)`)
+ ew.writeln(` - "ALIESA_SECRET_KEY": Access Key secret`)
+ ew.writeln(` - "ALIESA_SECURITY_TOKEN": STS Security Token (optional)`)
+ ew.writeln()
+
+ ew.writeln(`Additional Configuration:`)
+ ew.writeln(` - "ALIESA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
+ ew.writeln(` - "ALIESA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
+ ew.writeln(` - "ALIESA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
+ ew.writeln(` - "ALIESA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)
+
+ ew.writeln()
+ ew.writeln(`More information: https://go-acme.github.io/lego/dns/aliesa`)
+
case "allinkl":
// generated from: providers/dns/allinkl/allinkl.toml
ew.writeln(`Configuration for all-inkl.`)
diff --git a/docs/content/dns/zz_gen_aliesa.md b/docs/content/dns/zz_gen_aliesa.md
new file mode 100644
index 0000000000..6f68adb54a
--- /dev/null
+++ b/docs/content/dns/zz_gen_aliesa.md
@@ -0,0 +1,78 @@
+---
+title: "Alibaba Cloud ESA"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: aliesa
+dnsprovider:
+ since: "v4.29.0"
+ code: "aliesa"
+ url: "https://www.alibabacloud.com/en/product/esa"
+---
+
+
+
+
+
+
+Configuration for [Alibaba Cloud ESA](https://www.alibabacloud.com/en/product/esa).
+
+
+
+
+- Code: `aliesa`
+- Since: v4.29.0
+
+
+Here is an example bash command using the Alibaba Cloud ESA provider:
+
+```bash
+# Setup using instance RAM role
+ALIESA_RAM_ROLE=lego \
+lego --email you@example.com --dns aliesa -d '*.example.com' -d example.com run
+
+# Or, using credentials
+ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \
+ALIESA_SECRET_KEY=your-secret-key \
+ALIESA_SECURITY_TOKEN=your-sts-token \
+lego --email you@example.com --dns aliesa - -d '*.example.com' -d example.com run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `ALIESA_ACCESS_KEY` | Access key ID |
+| `ALIESA_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance) |
+| `ALIESA_SECRET_KEY` | Access Key secret |
+| `ALIESA_SECURITY_TOKEN` | STS Security Token (optional) |
+
+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" %}}).
+
+
+## Additional Configuration
+
+| Environment Variable Name | Description |
+|--------------------------------|-------------|
+| `ALIESA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
+| `ALIESA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
+| `ALIESA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
+| `ALIESA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
+
+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" %}}).
+
+
+
+
+## More information
+
+- [API documentation](https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-overview?spm=a2c63.p38356.help-menu-2673927.d_6_0_0.20b224c28PSZDc#:~:text=DNS-,DNS%20records,-DNS%20records)
+- [Go client](https://github.com/alibabacloud-go/esa-20240910)
+
+
+
+
diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml
index 80cbcaac07..5b1341e448 100644
--- a/docs/data/zz_cli_help.toml
+++ b/docs/data/zz_cli_help.toml
@@ -152,7 +152,7 @@ To display the documentation for a specific DNS provider, run:
$ lego dnshelp -c code
Supported DNS providers:
- acme-dns, active24, alidns, allinkl, anexia, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, conohav3, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
+ acme-dns, active24, alidns, aliesa, allinkl, anexia, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, conohav3, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hostinger, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, keyhelp, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
More information: https://go-acme.github.io/lego/dns
"""
diff --git a/go.mod b/go.mod
index 3dc72c1ffd..578c89ae95 100644
--- a/go.mod
+++ b/go.mod
@@ -31,6 +31,7 @@ require (
github.com/dnsimple/dnsimple-go/v4 v4.0.0
github.com/exoscale/egoscale/v3 v3.1.27
github.com/go-acme/alidns-20150109/v4 v4.6.1
+ github.com/go-acme/esa-20240910/v2 v2.39.4-0.20251105201639-2cc10a8ec341
github.com/go-acme/tencentclouddnspod v1.1.10
github.com/go-acme/tencentedgdeone v1.1.48
github.com/go-jose/go-jose/v4 v4.1.3
diff --git a/go.sum b/go.sum
index a8d61029da..0e90a229f0 100644
--- a/go.sum
+++ b/go.sum
@@ -314,6 +314,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-acme/alidns-20150109/v4 v4.6.1 h1:Dch3aWRcw4U62+jKPjPQN3iW3TPvgIywATbvHzojXeo=
github.com/go-acme/alidns-20150109/v4 v4.6.1/go.mod h1:RBcqBA5IvUWtlpjx6dC6EkPVyBNLQ+mR18XoaP38BFY=
+github.com/go-acme/esa-20240910/v2 v2.39.4-0.20251105201639-2cc10a8ec341 h1:1PFxP0oYr2WuPuwFjGR2oxm0D4G2DBxMj8sJl7PX3xs=
+github.com/go-acme/esa-20240910/v2 v2.39.4-0.20251105201639-2cc10a8ec341/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g=
github.com/go-acme/tencentclouddnspod v1.1.10 h1:ERVJ4mc3cT4Nb3+n6H/c1AwZnChGBqLoymE0NVYscKI=
github.com/go-acme/tencentclouddnspod v1.1.10/go.mod h1:Bo/0YQJ/99FM+44HmCQkByuptX1tJsJ9V14MGV/2Qco=
github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk=
diff --git a/providers/dns/aliesa/aliesa.go b/providers/dns/aliesa/aliesa.go
new file mode 100644
index 0000000000..deb8162da8
--- /dev/null
+++ b/providers/dns/aliesa/aliesa.go
@@ -0,0 +1,251 @@
+// Package aliesa implements a DNS provider for solving the DNS-01 challenge using AlibabaCloud ESA.
+package aliesa
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sync"
+ "time"
+
+ openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
+ "github.com/alibabacloud-go/tea/dara"
+ "github.com/aliyun/credentials-go/credentials"
+ esa "github.com/go-acme/esa-20240910/v2/client"
+ "github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/platform/config/env"
+ "github.com/go-acme/lego/v4/providers/dns/internal/ptr"
+)
+
+// Environment variables names.
+const (
+ envNamespace = "ALIESA_"
+
+ EnvRAMRole = envNamespace + "RAM_ROLE"
+ EnvAccessKey = envNamespace + "ACCESS_KEY"
+ EnvSecretKey = envNamespace + "SECRET_KEY"
+ EnvSecurityToken = envNamespace + "SECURITY_TOKEN"
+ EnvRegionID = envNamespace + "REGION_ID"
+
+ EnvTTL = envNamespace + "TTL"
+ EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
+ EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
+ EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
+)
+
+const defaultRegionID = "cn-hangzhou"
+
+// Config is used to configure the creation of the DNSProvider.
+type Config struct {
+ RAMRole string
+ APIKey string
+ SecretKey string
+ SecurityToken string
+ RegionID string
+
+ PropagationTimeout time.Duration
+ PollingInterval time.Duration
+ TTL int
+ HTTPTimeout time.Duration
+}
+
+// NewDefaultConfig returns a default configuration for the DNSProvider.
+func NewDefaultConfig() *Config {
+ return &Config{
+ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
+ PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
+ HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
+ }
+}
+
+// DNSProvider implements the challenge.Provider interface.
+type DNSProvider struct {
+ config *Config
+ client *esa.Client
+
+ recordIDs map[string]int64
+ recordIDsMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for AlibabaCloud ESA.
+func NewDNSProvider() (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.RegionID = env.GetOrFile(EnvRegionID)
+
+ values, err := env.Get(EnvRAMRole)
+ if err == nil {
+ config.RAMRole = values[EnvRAMRole]
+ return NewDNSProviderConfig(config)
+ }
+
+ values, err = env.Get(EnvAccessKey, EnvSecretKey)
+ if err != nil {
+ return nil, fmt.Errorf("aliesa: %w", err)
+ }
+
+ config.APIKey = values[EnvAccessKey]
+ config.SecretKey = values[EnvSecretKey]
+ config.SecurityToken = env.GetOrFile(EnvSecurityToken)
+
+ return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for AlibabaCloud ESA.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("aliesa: the configuration of the DNS provider is nil")
+ }
+
+ if config.RegionID == "" {
+ config.RegionID = defaultRegionID
+ }
+
+ cfg := new(openapi.Config).
+ SetRegionId(config.RegionID).
+ SetReadTimeout(int(config.HTTPTimeout.Milliseconds()))
+
+ switch {
+ case config.RAMRole != "":
+ // https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance
+ credentialsCfg := new(credentials.Config).
+ SetType("ecs_ram_role").
+ SetRoleName(config.RAMRole)
+
+ credentialClient, err := credentials.NewCredential(credentialsCfg)
+ if err != nil {
+ return nil, fmt.Errorf("aliesa: new credential: %w", err)
+ }
+
+ cfg = cfg.SetCredential(credentialClient)
+
+ case config.APIKey != "" && config.SecretKey != "" && config.SecurityToken != "":
+ cfg = cfg.
+ SetAccessKeyId(config.APIKey).
+ SetAccessKeySecret(config.SecretKey).
+ SetSecurityToken(config.SecurityToken)
+
+ case config.APIKey != "" && config.SecretKey != "":
+ cfg = cfg.
+ SetAccessKeyId(config.APIKey).
+ SetAccessKeySecret(config.SecretKey)
+
+ default:
+ return nil, errors.New("aliesa: ram role or credentials missing")
+ }
+
+ client, err := esa.NewClient(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("aliesa: new client: %w", err)
+ }
+
+ // Workaround to get a regional URL.
+ // https://github.com/alibabacloud-go/esa-20240910/blame/7660e3aab2045d4820e4b83427a154efe0c79319/client/client.go#L27
+ // The `EndpointRule` is hardcoded with an empty string, so the region is ignored.
+ client.Endpoint = nil
+ client.EndpointRule = ptr.Pointer("regional")
+
+ client.Endpoint, err = esa.GetEndpoint(client, dara.String("esa"), client.RegionId, client.EndpointRule, client.Network, client.Suffix, client.EndpointMap, client.Endpoint)
+ if err != nil {
+ return nil, fmt.Errorf("aliesa: get endpoint: %w", err)
+ }
+
+ return &DNSProvider{
+ config: config,
+ client: client,
+ recordIDs: make(map[string]int64),
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ ctx := context.Background()
+
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ siteID, err := d.getSiteID(ctx, info.EffectiveFQDN)
+ if err != nil {
+ return fmt.Errorf("aliesa: %w", err)
+ }
+
+ crReq := new(esa.CreateRecordRequest).
+ SetSiteId(siteID).
+ SetType("TXT").
+ SetRecordName(dns01.UnFqdn(info.EffectiveFQDN)).
+ SetTtl(int32(d.config.TTL)).
+ SetData(new(esa.CreateRecordRequestData).SetValue(info.Value))
+
+ // https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord
+ crResp, err := esa.CreateRecordWithContext(ctx, d.client, crReq, &dara.RuntimeOptions{})
+ if err != nil {
+ return fmt.Errorf("aliesa: create record: %w", err)
+ }
+
+ d.recordIDsMu.Lock()
+ d.recordIDs[token] = ptr.Deref(crResp.Body.GetRecordId())
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ ctx := context.Background()
+
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ // gets the record's unique ID
+ d.recordIDsMu.Lock()
+ recordID, ok := d.recordIDs[token]
+ d.recordIDsMu.Unlock()
+
+ if !ok {
+ return fmt.Errorf("aliesa: unknown record ID for '%s'", info.EffectiveFQDN)
+ }
+
+ drReq := new(esa.DeleteRecordRequest).
+ SetRecordId(recordID)
+
+ // https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-deleterecord
+ _, err := esa.DeleteRecordWithContext(ctx, d.client, drReq, &dara.RuntimeOptions{})
+ if err != nil {
+ return fmt.Errorf("aliesa: delete record: %w", err)
+ }
+
+ return nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS propagation.
+// Adjusting here to cope with spikes in propagation times.
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return d.config.PropagationTimeout, d.config.PollingInterval
+}
+
+func (d *DNSProvider) getSiteID(ctx context.Context, fqdn string) (int64, error) {
+ authZone, err := dns01.FindZoneByFqdn(fqdn)
+ if err != nil {
+ return 0, fmt.Errorf("aliesa: could not find zone for domain %q: %w", fqdn, err)
+ }
+
+ lsReq := new(esa.ListSitesRequest).
+ SetSiteName(dns01.UnFqdn(authZone)).
+ SetSiteSearchType("suffix")
+
+ // https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-listsites
+ lsResp, err := esa.ListSitesWithContext(ctx, d.client, lsReq, &dara.RuntimeOptions{})
+ if err != nil {
+ return 0, fmt.Errorf("list sites: %w", err)
+ }
+
+ for f := range dns01.UnFqdnDomainsSeq(fqdn) {
+ domain := dns01.UnFqdn(f)
+
+ for _, site := range lsResp.Body.GetSites() {
+ if ptr.Deref(site.GetSiteName()) == domain {
+ return ptr.Deref(site.GetSiteId()), nil
+ }
+ }
+ }
+
+ return 0, fmt.Errorf("site not found (fqdn: %q)", fqdn)
+}
diff --git a/providers/dns/aliesa/aliesa.toml b/providers/dns/aliesa/aliesa.toml
new file mode 100644
index 0000000000..aaf579768f
--- /dev/null
+++ b/providers/dns/aliesa/aliesa.toml
@@ -0,0 +1,33 @@
+Name = "Alibaba Cloud ESA"
+Description = ''''''
+URL = "https://www.alibabacloud.com/en/product/esa"
+Code = "aliesa"
+Since = "v4.29.0"
+
+Example = '''
+# Setup using instance RAM role
+ALIESA_RAM_ROLE=lego \
+lego --email you@example.com --dns aliesa -d '*.example.com' -d example.com run
+
+# Or, using credentials
+ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \
+ALIESA_SECRET_KEY=your-secret-key \
+ALIESA_SECURITY_TOKEN=your-sts-token \
+lego --email you@example.com --dns aliesa - -d '*.example.com' -d example.com run
+'''
+
+[Configuration]
+ [Configuration.Credentials]
+ ALIESA_RAM_ROLE = "Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)"
+ ALIESA_ACCESS_KEY = "Access key ID"
+ ALIESA_SECRET_KEY = "Access Key secret"
+ ALIESA_SECURITY_TOKEN = "STS Security Token (optional)"
+ [Configuration.Additional]
+ ALIESA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
+ ALIESA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
+ ALIESA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
+ ALIESA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
+
+[Links]
+ API = "https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-overview?spm=a2c63.p38356.help-menu-2673927.d_6_0_0.20b224c28PSZDc#:~:text=DNS-,DNS%20records,-DNS%20records"
+ GoClient = "https://github.com/alibabacloud-go/esa-20240910"
diff --git a/providers/dns/aliesa/aliesa_test.go b/providers/dns/aliesa/aliesa_test.go
new file mode 100644
index 0000000000..025529409b
--- /dev/null
+++ b/providers/dns/aliesa/aliesa_test.go
@@ -0,0 +1,151 @@
+package aliesa
+
+import (
+ "testing"
+
+ "github.com/go-acme/lego/v4/platform/tester"
+ "github.com/stretchr/testify/require"
+)
+
+const envDomain = envNamespace + "DOMAIN"
+
+var envTest = tester.NewEnvTest(
+ EnvAccessKey,
+ EnvSecretKey,
+ EnvRAMRole).
+ WithDomain(envDomain)
+
+func TestNewDNSProvider(t *testing.T) {
+ testCases := []struct {
+ desc string
+ envVars map[string]string
+ expected string
+ }{
+ {
+ desc: "success",
+ envVars: map[string]string{
+ EnvAccessKey: "123",
+ EnvSecretKey: "456",
+ },
+ },
+ {
+ desc: "success (RAM role)",
+ envVars: map[string]string{
+ EnvRAMRole: "LegoInstanceRole",
+ },
+ },
+ {
+ desc: "missing credentials",
+ envVars: map[string]string{
+ EnvAccessKey: "",
+ EnvSecretKey: "",
+ },
+ expected: "aliesa: some credentials information are missing: ALIESA_ACCESS_KEY,ALIESA_SECRET_KEY",
+ },
+ {
+ desc: "missing access key",
+ envVars: map[string]string{
+ EnvAccessKey: "",
+ EnvSecretKey: "456",
+ },
+ expected: "aliesa: some credentials information are missing: ALIESA_ACCESS_KEY",
+ },
+ {
+ desc: "missing secret key",
+ envVars: map[string]string{
+ EnvAccessKey: "123",
+ EnvSecretKey: "",
+ },
+ expected: "aliesa: some credentials information are missing: ALIESA_SECRET_KEY",
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.desc, func(t *testing.T) {
+ defer envTest.RestoreEnv()
+
+ envTest.ClearEnv()
+
+ envTest.Apply(test.envVars)
+
+ p, err := NewDNSProvider()
+
+ if test.expected == "" {
+ require.NoError(t, err)
+ require.NotNil(t, p)
+ require.NotNil(t, p.config)
+ require.NotNil(t, p.client)
+ } else {
+ require.EqualError(t, err, test.expected)
+ }
+ })
+ }
+}
+
+func TestNewDNSProviderConfig(t *testing.T) {
+ testCases := []struct {
+ desc string
+ ramRole string
+ apiKey string
+ secretKey string
+ expected string
+ }{
+ {
+ desc: "success",
+ apiKey: "123",
+ secretKey: "456",
+ },
+ {
+ desc: "success",
+ ramRole: "LegoInstanceRole",
+ },
+ {
+ desc: "missing credentials",
+ expected: "aliesa: ram role or credentials missing",
+ },
+ {
+ desc: "missing api key",
+ secretKey: "456",
+ expected: "aliesa: ram role or credentials missing",
+ },
+ {
+ desc: "missing secret key",
+ apiKey: "123",
+ expected: "aliesa: ram role or credentials missing",
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.desc, func(t *testing.T) {
+ config := NewDefaultConfig()
+ config.APIKey = test.apiKey
+ config.SecretKey = test.secretKey
+ config.RAMRole = test.ramRole
+
+ p, err := NewDNSProviderConfig(config)
+
+ if test.expected == "" {
+ require.NoError(t, err)
+ require.NotNil(t, p)
+ require.NotNil(t, p.config)
+ require.NotNil(t, p.client)
+ } else {
+ require.EqualError(t, err, test.expected)
+ }
+ })
+ }
+}
+
+func TestLivePresent(t *testing.T) {
+ if !envTest.IsLiveTest() {
+ t.Skip("skipping live test")
+ }
+
+ envTest.RestoreEnv()
+
+ provider, err := NewDNSProvider()
+ require.NoError(t, err)
+
+ err = provider.Present(envTest.GetDomain(), "", "123d==")
+ require.NoError(t, err)
+}
diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go
index 32de816a36..3b6ef4d42e 100644
--- a/providers/dns/zz_gen_dns_providers.go
+++ b/providers/dns/zz_gen_dns_providers.go
@@ -10,6 +10,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/acmedns"
"github.com/go-acme/lego/v4/providers/dns/active24"
"github.com/go-acme/lego/v4/providers/dns/alidns"
+ "github.com/go-acme/lego/v4/providers/dns/aliesa"
"github.com/go-acme/lego/v4/providers/dns/allinkl"
"github.com/go-acme/lego/v4/providers/dns/anexia"
"github.com/go-acme/lego/v4/providers/dns/arvancloud"
@@ -185,6 +186,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return active24.NewDNSProvider()
case "alidns":
return alidns.NewDNSProvider()
+ case "aliesa":
+ return aliesa.NewDNSProvider()
case "allinkl":
return allinkl.NewDNSProvider()
case "anexia":