Repository: go-acme/lego
Pull request #2152 (2024-04-09 12:43:24)

Add DNS provider for Selectel v2
We, in Selectel, released v2 version of DNS API. But it fully independent service from v1. For a while(year or even more) we will support both versions, that why v1 ins not yet noted as deprecated and v2 porvider created as spearated package.  

Also we decided to do authorization with Openstack credentials instead of asking end user to obtain keystone token by himself as we think it is more convenient. If we will get feedback that users want to pass Keystone directly we will add env varbiale for this in the future.  

Didn't know that is correct way to mention version on .toml so decided to set to feature bump relative to current tag.  

- [x] add a description to your PR
- [x] have a homogeneous design with the other providers
- [x] add tests (units)
- [x] add tests ("live")
- [x] add a [provider descriptor](https://github.com/Archirk/lego/blob/master/providers/dns/selectelv2/selectelv2.toml)
- [x] generate CLI help, documentation, and readme.
- [x] do go mod tidy 
- [x] be able to do: (and put the output of this command to a comment)
```bash
make build
rm -rf .lego

SELECTELV2_USERNAME=***** \
SELECTELV2_PASSWORD=***** \
SELECTELV2_ACCOUNT_ID=****** \
SELECTELV2_PROJECT_ID=***** \
./dist/lego -m your@email.com --dns selectelv2 -d *.example.com -d example.com -s https://acme-staging-v02.api.letsencrypt.org/directory run
```
output:
```
./dist/lego --email digitalarchie@gmail.com --dns selectelv2 -d *.project-scribbler.art -d project-scribbler.art -s https://acme-staging-v02.api.letsencrypt.org/directory run
2024/04/09 15:23:33 No key found for account digitalarchie@gmail.com. Generating a P256 key.
2024/04/09 15:23:33 Saved key to /home/chirkov/Repositories/github/lego/.lego/accounts/acme-staging-v02.api.letsencrypt.org/digitalarchie@gmail.com/keys/digitalarchie@gmail.com.key
2024/04/09 15:23:33 Please review the TOS at https://letsencrypt.org/documents/LE-SA-v1.4-April-3-2024.pdf
Do you accept the TOS? Y/n
y
2024/04/09 15:23:34 [INFO] acme: Registering account for digitalarchie@gmail.com
!!!! HEADS UP !!!!

Your account credentials have been saved in your Let's Encrypt
configuration directory at "/home/chirkov/Repositories/github/lego/.lego/accounts".

You should make a secure backup of this folder now. This
configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal.
2024/04/09 15:23:35 [INFO] [*.project-scribbler.art, project-scribbler.art] acme: Obtaining bundled SAN certificate
2024/04/09 15:23:36 [INFO] [*.project-scribbler.art] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/11943318124
2024/04/09 15:23:36 [INFO] [project-scribbler.art] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/11943318134
2024/04/09 15:23:36 [INFO] [*.project-scribbler.art] acme: use dns-01 solver
2024/04/09 15:23:36 [INFO] [project-scribbler.art] acme: Could not find solver for: tls-alpn-01
2024/04/09 15:23:36 [INFO] [project-scribbler.art] acme: Could not find solver for: http-01
2024/04/09 15:23:36 [INFO] [project-scribbler.art] acme: use dns-01 solver
2024/04/09 15:23:36 [INFO] [*.project-scribbler.art] acme: Preparing to solve DNS-01
2024/04/09 15:23:38 [INFO] [project-scribbler.art] acme: Preparing to solve DNS-01
2024/04/09 15:23:38 [INFO] [*.project-scribbler.art] acme: Trying to solve DNS-01
2024/04/09 15:23:39 [INFO] [*.project-scribbler.art] acme: Checking DNS record propagation. [nameservers=127.0.0.53:53]
2024/04/09 15:23:44 [INFO] Wait for propagation [timeout: 2m0s, interval: 5s]
2024/04/09 15:23:44 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:23:49 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:23:54 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:23:59 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:24:04 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:24:09 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:24:14 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:24:19 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:24:24 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:24:29 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:24:34 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:24:39 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:24:44 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 15:25:08 [INFO] [*.project-scribbler.art] The server validated our request
2024/04/09 15:25:08 [INFO] [project-scribbler.art] acme: Trying to solve DNS-01
2024/04/09 15:25:08 [INFO] [project-scribbler.art] acme: Checking DNS record propagation. [nameservers=127.0.0.53:53]
2024/04/09 15:25:13 [INFO] Wait for propagation [timeout: 2m0s, interval: 5s]
2024/04/09 15:25:21 [INFO] [project-scribbler.art] The server validated our request
2024/04/09 15:25:21 [INFO] [*.project-scribbler.art] acme: Cleaning DNS-01 challenge
2024/04/09 15:25:24 [INFO] [project-scribbler.art] acme: Cleaning DNS-01 challenge
2024/04/09 15:25:25 [INFO] [*.project-scribbler.art, project-scribbler.art] acme: Validations succeeded; requesting certificates
2024/04/09 15:25:25 [INFO] Wait for certificate [timeout: 30s, interval: 500ms]
2024/04/09 15:25:27 [INFO] [*.project-scribbler.art] Server responded with a certificate.
```

Archirk: > [#2151 (comment)](https://github.com/go-acme/lego/pull/2151#issuecomment-2045098311)

Incorrect branch for merging
ldez: > Incorrect branch for merging

It's recommended to use a dedicated branch for a PR, and to avoid using `master`, so I don't understand.
Archirk: > > Incorrect branch for merging
> 
> It's recommended to use a dedicated branch for a PR, and to avoid using `master`, so I don't understand.

Is it required to update dedicated branch in my forked repository and reopen PR with dedicated branch or you could review this PR?
ldez: I can review this PR.
ldez: Hello, in order for a PR adding a DNS provider to be accepted, you have to:

- [ ] add a description to your PR
- [ ] be able to maintain this provider
- [ ] have a homogeneous design with the other providers
- [ ] add tests (units)
```shell
make test
```
- [ ] add tests ("live")
https://github.com/go-acme/lego/blob/c938de68f262c057542d44bcb67ef961cefcc60e/providers/dns/cloudflare/cloudflare_test.go#L125-L151

```shell
make test
```
- [ ] add a [provider descriptor](https://github.com/go-acme/lego/blob/master/providers/dns/cloudflare/cloudflare.toml)
- [ ] generate CLI help, documentation, and readme.
```bash
make generate-dns
```
- [ ] be able to do: _(and put the output of this command to a comment in your PR)_
```bash
rm -rf .lego

./lego -m your@email.com --dns YOUR_PROVIDER_NAME -d *.example.com -d example.com -s https://acme-staging-v02.api.letsencrypt.org/directory run
```
Note the wildcard domain is important.
- [ ] pass the linter ([golangci-lint](https://github.com/golangci/golangci-lint#install) must be installed):
```shell
make checks
```
- [ ] do `go mod tidy`
ldez: The code was far from having a homogeneous design with the other providers, so I fixed that.

The way to handle the token was not working with long-running instances (I guess the token has a lifespan).

Can you run again the following command and provide the output of the command?

```bash
rm -rf .lego

SELECTELV2_USERNAME=***** \
SELECTELV2_PASSWORD=***** \
SELECTELV2_ACCOUNT_ID=****** \
SELECTELV2_PROJECT_ID=***** \
./dist/lego -m your@email.com --dns selectelv2 -d *.example.com -d example.com -s https://acme-staging-v02.api.letsencrypt.org/directory run
```

Archirk: > The code was far from having a homogeneous design with the other providers, so I fixed that.
> 
> The way to handle the token was not working with long-running instances (I guess the token has a lifespan).
> 
> Can you run again the following command and provide the output of the command?
> 
> ```shell
> rm -rf .lego
> 
> SELECTELV2_USERNAME=***** \
> SELECTELV2_PASSWORD=***** \
> SELECTELV2_ACCOUNT_ID=****** \
> SELECTELV2_PROJECT_ID=***** \
> ./dist/lego -m your@email.com --dns selectelv2 -d *.example.com -d example.com -s https://acme-staging-v02.api.letsencrypt.org/directory run
> ```
> 
> @Archirk
Thank you.  

Yes token has lifespan but it is 24 hours.  

Seems to be working fine on my side.
```
chirkov@chirkov:~/Repositories/github/lego$ git log --pretty=format:'%h' -n 1
79a39c28
chirkov@chirkov:~/Repositories/github/lego$ SELECTELV2_USERNAME=*** SELECTELV2_PASSWORD=*** SELECTELV2_ACCOUNT_ID=*** SELECTELV2_PROJECT_ID=*** ./dist/lego --email digitalarchie@gmail.com --dns selectelv2 -d *.project-scribbler.art -d project-scribbler.art -s https://acme-staging-v02.api.letsencrypt.org/directory run
2024/04/09 17:58:08 No key found for account digitalarchie@gmail.com. Generating a P256 key.
2024/04/09 17:58:08 Saved key to /home/chirkov/Repositories/github/lego/.lego/accounts/acme-staging-v02.api.letsencrypt.org/digitalarchie@gmail.com/keys/digitalarchie@gmail.com.key
2024/04/09 17:58:09 Please review the TOS at https://letsencrypt.org/documents/LE-SA-v1.4-April-3-2024.pdf
Do you accept the TOS? Y/n
y
2024/04/09 17:58:11 [INFO] acme: Registering account for digitalarchie@gmail.com
!!!! HEADS UP !!!!

Your account credentials have been saved in your Let's Encrypt
configuration directory at "/home/chirkov/Repositories/github/lego/.lego/accounts".

You should make a secure backup of this folder now. This
configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal.
2024/04/09 17:58:11 [INFO] [*.project-scribbler.art, project-scribbler.art] acme: Obtaining bundled SAN certificate
2024/04/09 17:58:12 [INFO] [*.project-scribbler.art] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/11944399374
2024/04/09 17:58:12 [INFO] [project-scribbler.art] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/11944399384
2024/04/09 17:58:12 [INFO] [*.project-scribbler.art] acme: use dns-01 solver
2024/04/09 17:58:12 [INFO] [project-scribbler.art] acme: Could not find solver for: tls-alpn-01
2024/04/09 17:58:12 [INFO] [project-scribbler.art] acme: Could not find solver for: http-01
2024/04/09 17:58:12 [INFO] [project-scribbler.art] acme: use dns-01 solver
2024/04/09 17:58:12 [INFO] [*.project-scribbler.art] acme: Preparing to solve DNS-01
2024/04/09 17:58:14 [INFO] [project-scribbler.art] acme: Preparing to solve DNS-01
2024/04/09 17:58:16 [INFO] [*.project-scribbler.art] acme: Trying to solve DNS-01
2024/04/09 17:58:16 [INFO] [*.project-scribbler.art] acme: Checking DNS record propagation. [nameservers=127.0.0.53:53]
2024/04/09 17:58:21 [INFO] Wait for propagation [timeout: 2m0s, interval: 5s]
2024/04/09 17:58:22 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:58:27 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:58:32 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:58:37 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:58:42 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:58:47 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:58:52 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:58:57 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:59:02 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:59:07 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:59:12 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:59:17 [INFO] [*.project-scribbler.art] acme: Waiting for DNS record propagation.
2024/04/09 17:59:46 [INFO] [*.project-scribbler.art] The server validated our request
2024/04/09 17:59:46 [INFO] [project-scribbler.art] acme: Trying to solve DNS-01
2024/04/09 17:59:46 [INFO] [project-scribbler.art] acme: Checking DNS record propagation. [nameservers=127.0.0.53:53]
2024/04/09 17:59:51 [INFO] Wait for propagation [timeout: 2m0s, interval: 5s]
2024/04/09 18:00:09 [INFO] [project-scribbler.art] The server validated our request
2024/04/09 18:00:09 [INFO] [*.project-scribbler.art] acme: Cleaning DNS-01 challenge
2024/04/09 18:00:11 [INFO] [project-scribbler.art] acme: Cleaning DNS-01 challenge
2024/04/09 18:00:13 [INFO] [*.project-scribbler.art, project-scribbler.art] acme: Validations succeeded; requesting certificates
2024/04/09 18:00:13 [INFO] Wait for certificate [timeout: 30s, interval: 500ms]
2024/04/09 18:00:15 [INFO] [*.project-scribbler.art] Server responded with a certificate.

```
Archirk: @ldez 
Thank your for corrections. I am sorry that I understand **homogeneous** in different way so you had to make these changes.  

I assume from all passed checks and your thumb up that PR is ok.  Can you tell when I could expect PR to be merged into master so I could forward this information to my team?  
ldez: I always try waiting a bit after a review like that (when I make changes) just to be able to do a second review.
I did the "second" review (cf the latest commit), then I will merge the PR.
ldez: FYI your profile picture doesn't appear in the commit tree because the email you have used to commit is not linked to your GitHub account.
If you add it, your PP will appear.
Archirk: > FYI your profile picture doesn't appear in the commit tree because the email you have used to commit is not linked to your GitHub account. If you add it, your PP will appear.

Thank you for noticing. I know. Just do my work with work email and I have no habbit to specify local git config when do occasional work on public repos (probably should though)
Just hope it is not any security concerns for you. As for my public contributions stats and etc - I do not care that much. 

Patch:
diff --git a/README.md b/README.md
index c1e5b115..060497c2 100644
--- a/README.md
+++ b/README.md
@@ -79,13 +79,13 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
 | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/)                 | [OVH](https://go-acme.github.io/lego/dns/ovh/)                                  | [plesk.com](https://go-acme.github.io/lego/dns/plesk/)                          | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/)                          |
 | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/)                            | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/)                      | [RcodeZero](https://go-acme.github.io/lego/dns/rcodezero/)                      | [reg.ru](https://go-acme.github.io/lego/dns/regru/)                             |
 | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/)                          | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/)                  | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/)                 | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/)                        |
-| [Selectel](https://go-acme.github.io/lego/dns/selectel/)                        | [Servercow](https://go-acme.github.io/lego/dns/servercow/)                      | [Shellrent](https://go-acme.github.io/lego/dns/shellrent/)                      | [Simply.com](https://go-acme.github.io/lego/dns/simply/)                        |
-| [Sonic](https://go-acme.github.io/lego/dns/sonic/)                              | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/)                      | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/)           | [TransIP](https://go-acme.github.io/lego/dns/transip/)                          |
-| [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/)                   | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/)                        | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/)                    | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/)                          |
-| [Vercel](https://go-acme.github.io/lego/dns/vercel/)                            | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/)                 | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/)                        | [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/)                         |
-| [Vscale](https://go-acme.github.io/lego/dns/vscale/)                            | [Vultr](https://go-acme.github.io/lego/dns/vultr/)                              | [Webnames](https://go-acme.github.io/lego/dns/webnames/)                        | [Websupport](https://go-acme.github.io/lego/dns/websupport/)                    |
-| [WEDOS](https://go-acme.github.io/lego/dns/wedos/)                              | [Yandex 360](https://go-acme.github.io/lego/dns/yandex360/)                     | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/)                 | [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/)                        |
-| [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/)                           | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/)                            |                                                                                 |                                                                                 |
+| [Selectel v2](https://go-acme.github.io/lego/dns/selectelv2/)                   | [Selectel](https://go-acme.github.io/lego/dns/selectel/)                        | [Servercow](https://go-acme.github.io/lego/dns/servercow/)                      | [Shellrent](https://go-acme.github.io/lego/dns/shellrent/)                      |
+| [Simply.com](https://go-acme.github.io/lego/dns/simply/)                        | [Sonic](https://go-acme.github.io/lego/dns/sonic/)                              | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/)                      | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/)           |
+| [TransIP](https://go-acme.github.io/lego/dns/transip/)                          | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/)                   | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/)                        | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/)                    |
+| [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/)                          | [Vercel](https://go-acme.github.io/lego/dns/vercel/)                            | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/)                 | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/)                        |
+| [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/)                         | [Vscale](https://go-acme.github.io/lego/dns/vscale/)                            | [Vultr](https://go-acme.github.io/lego/dns/vultr/)                              | [Webnames](https://go-acme.github.io/lego/dns/webnames/)                        |
+| [Websupport](https://go-acme.github.io/lego/dns/websupport/)                    | [WEDOS](https://go-acme.github.io/lego/dns/wedos/)                              | [Yandex 360](https://go-acme.github.io/lego/dns/yandex360/)                     | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/)                 |
+| [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/)                        | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/)                           | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/)                            |                                                                                 |
 
 <!-- END DNS PROVIDERS LIST -->
 
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go
index 2d8080e1..d28400fd 100644
--- a/cmd/zz_gen_cmd_dnshelp.go
+++ b/cmd/zz_gen_cmd_dnshelp.go
@@ -119,6 +119,7 @@ func allDNSCodes() string {
 		"sakuracloud",
 		"scaleway",
 		"selectel",
+		"selectelv2",
 		"servercow",
 		"shellrent",
 		"simply",
@@ -2398,6 +2399,30 @@ func displayDNSHelp(w io.Writer, name string) error {
 		ew.writeln()
 		ew.writeln(`More information: https://go-acme.github.io/lego/dns/selectel`)
 
+	case "selectelv2":
+		// generated from: providers/dns/selectelv2/selectelv2.toml
+		ew.writeln(`Configuration for Selectel v2.`)
+		ew.writeln(`Code:	'selectelv2'`)
+		ew.writeln(`Since:	'v4.17.0'`)
+		ew.writeln()
+
+		ew.writeln(`Credentials:`)
+		ew.writeln(`	- "SELECTELV2_ACCOUNT_ID":	Selectel account ID (INT)`)
+		ew.writeln(`	- "SELECTELV2_PASSWORD":	Openstack username's password`)
+		ew.writeln(`	- "SELECTELV2_PROJECT_ID":	Cloud project ID (UUID)`)
+		ew.writeln(`	- "SELECTELV2_USERNAME":	Openstack username`)
+		ew.writeln()
+
+		ew.writeln(`Additional Configuration:`)
+		ew.writeln(`	- "SELECTELV2_BASE_URL":	API endpoint URL`)
+		ew.writeln(`	- "SELECTELV2_HTTP_TIMEOUT":	API request timeout`)
+		ew.writeln(`	- "SELECTELV2_POLLING_INTERVAL":	Time between DNS propagation check`)
+		ew.writeln(`	- "SELECTELV2_PROPAGATION_TIMEOUT":	Maximum waiting time for DNS propagation`)
+		ew.writeln(`	- "SELECTELV2_TTL":	The TTL of the TXT record used for the DNS challenge`)
+
+		ew.writeln()
+		ew.writeln(`More information: https://go-acme.github.io/lego/dns/selectelv2`)
+
 	case "servercow":
 		// generated from: providers/dns/servercow/servercow.toml
 		ew.writeln(`Configuration for Servercow.`)
diff --git a/docs/content/dns/zz_gen_selectelv2.md b/docs/content/dns/zz_gen_selectelv2.md
new file mode 100644
index 00000000..8676ef39
--- /dev/null
+++ b/docs/content/dns/zz_gen_selectelv2.md
@@ -0,0 +1,75 @@
+---
+title: "Selectel v2"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: selectelv2
+dnsprovider:
+  since:    "v4.17.0"
+  code:     "selectelv2"
+  url:      "https://selectel.ru"
+---
+
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+<!-- providers/dns/selectelv2/selectelv2.toml -->
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+
+
+Configuration for [Selectel v2](https://selectel.ru).
+
+
+<!--more-->
+
+- Code: `selectelv2`
+- Since: v4.17.0
+
+
+Here is an example bash command using the Selectel v2 provider:
+
+```bash
+SELECTEL_USERNAME=trex \
+SELECTEL_PASSWORD=xxxxx \
+SELECTEL_ACCOUNT_ID=1234567 \
+SELECTEL_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \
+lego --email you@example.com --dns selectelv2 --domains my.example.org run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `SELECTELV2_ACCOUNT_ID` | Selectel account ID (INT) |
+| `SELECTELV2_PASSWORD` | Openstack username's password |
+| `SELECTELV2_PROJECT_ID` | Cloud project ID (UUID) |
+| `SELECTELV2_USERNAME` | Openstack username |
+
+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 |
+|--------------------------------|-------------|
+| `SELECTELV2_BASE_URL` | API endpoint URL |
+| `SELECTELV2_HTTP_TIMEOUT` | API request timeout |
+| `SELECTELV2_POLLING_INTERVAL` | Time between DNS propagation check |
+| `SELECTELV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
+| `SELECTELV2_TTL` | The TTL of the TXT record used for the DNS challenge |
+
+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://developers.selectel.ru/docs/cloud-services/dns_api/dns_api_actual/)
+- [Go client](https://github.com/selectel/domains-go)
+
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+<!-- providers/dns/selectelv2/selectelv2.toml -->
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml
index dfa3587e..bb57a56b 100644
--- a/docs/data/zz_cli_help.toml
+++ b/docs/data/zz_cli_help.toml
@@ -137,7 +137,7 @@ To display the documentation for a specific DNS provider, run:
   $ lego dnshelp -c code
 
 Supported DNS providers:
-  acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, azuredns, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, constellix, cpanel, derak, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, efficientip, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, mailinabox, manual, metaname, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rcodezero, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, shellrent, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, webnames, websupport, wedos, yandex, yandex360, yandexcloud, zoneee, zonomi
+  acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, azuredns, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, constellix, cpanel, derak, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, efficientip, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, mailinabox, manual, metaname, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rcodezero, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, servercow, shellrent, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, webnames, websupport, wedos, yandex, yandex360, yandexcloud, zoneee, zonomi
 
 More information: https://go-acme.github.io/lego/dns
 """
diff --git a/go.mod b/go.mod
index 4f8ebb70..0edbd856 100644
--- a/go.mod
+++ b/go.mod
@@ -35,7 +35,7 @@ require (
 	github.com/go-jose/go-jose/v4 v4.0.1
 	github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1
 	github.com/google/go-querystring v1.1.0
-	github.com/gophercloud/gophercloud v1.0.0
+	github.com/gophercloud/gophercloud v1.5.0
 	github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae
 	github.com/hashicorp/go-retryablehttp v0.7.5
 	github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df
@@ -65,6 +65,8 @@ require (
 	github.com/sacloud/api-client-go v0.2.8
 	github.com/sacloud/iaas-api-go v1.11.1
 	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22
+	github.com/selectel/domains-go v1.0.2
+	github.com/selectel/go-selvpcclient/v3 v3.1.1
 	github.com/softlayer/softlayer-go v1.1.3
 	github.com/stretchr/testify v1.8.4
 	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490
diff --git a/go.sum b/go.sum
index 3af4dd59..8e507d13 100644
--- a/go.sum
+++ b/go.sum
@@ -304,8 +304,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
 github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
 github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
 github.com/gophercloud/gophercloud v0.15.1-0.20210202035223-633d73521055/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075O6i+LY+pXsKCBsb4=
-github.com/gophercloud/gophercloud v1.0.0 h1:9nTGx0jizmHxDobe4mck89FyQHVyA3CaXLIUSGJjP9k=
-github.com/gophercloud/gophercloud v1.0.0/go.mod h1:Q8fZtyi5zZxPS/j9aj3sSxtvj41AdQMDwyo1myduD5c=
+github.com/gophercloud/gophercloud v1.5.0 h1:cDN6XFCLKiiqvYpjQLq9AiM7RDRbIC9450WpPH+yvXo=
+github.com/gophercloud/gophercloud v1.5.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
 github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae h1:Hi3IgB9RQDE15Kfovd8MTZrcana+UlQqNbOif8dLpA0=
 github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae/go.mod h1:wx8HMD8oQD0Ryhz6+6ykq75PJ79iPyEqYHfwZ4l7OsA=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -357,8 +357,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU=
 github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
 github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
-github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
-github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
+github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
+github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
@@ -572,6 +572,10 @@ github.com/sacloud/packages-go v0.0.9/go.mod h1:k+EEUMF2LlncjbNIJNOqLyZ9wjTESPIW
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 h1:wJrcTdddKOI8TFxs8cemnhKP2EmKy3yfUKHj3ZdfzYo=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/selectel/domains-go v1.0.2 h1:Si6iGaMnTFJxwiJVI50DOdZnwcxc87kqaWrVQYW0a4U=
+github.com/selectel/domains-go v1.0.2/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA=
+github.com/selectel/go-selvpcclient/v3 v3.1.1 h1:C1q2LqqosiapoLpnGITGmysg0YCSQYDo2Gh69CioevM=
+github.com/selectel/go-selvpcclient/v3 v3.1.1/go.mod h1:NM7IXhh1IzqZ88DOw1Qc5Ez3tULLViXo95l5+rKPuyQ=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -683,10 +687,10 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go
index 1a1da7c8..ff26a492 100644
--- a/providers/dns/dns_providers.go
+++ b/providers/dns/dns_providers.go
@@ -110,6 +110,7 @@ import (
 	"github.com/go-acme/lego/v4/providers/dns/sakuracloud"
 	"github.com/go-acme/lego/v4/providers/dns/scaleway"
 	"github.com/go-acme/lego/v4/providers/dns/selectel"
+	"github.com/go-acme/lego/v4/providers/dns/selectelv2"
 	"github.com/go-acme/lego/v4/providers/dns/servercow"
 	"github.com/go-acme/lego/v4/providers/dns/shellrent"
 	"github.com/go-acme/lego/v4/providers/dns/simply"
@@ -351,6 +352,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
 		return scaleway.NewDNSProvider()
 	case "selectel":
 		return selectel.NewDNSProvider()
+	case "selectelv2":
+		return selectelv2.NewDNSProvider()
 	case "servercow":
 		return servercow.NewDNSProvider()
 	case "shellrent":
diff --git a/providers/dns/selectelv2/selectelv2.go b/providers/dns/selectelv2/selectelv2.go
new file mode 100644
index 00000000..a6f1d730
--- /dev/null
+++ b/providers/dns/selectelv2/selectelv2.go
@@ -0,0 +1,293 @@
+// Package selectelv2 implements a DNS provider for solving the DNS-01 challenge using Selectel Domains APIv2.
+package selectelv2
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"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/selectel"
+	selectelapi "github.com/selectel/domains-go/pkg/v2"
+	"github.com/selectel/go-selvpcclient/v3/selvpcclient"
+)
+
+const tokenHeader = "X-Auth-Token"
+
+const (
+	defaultBaseURL            = "https://api.selectel.ru/domains/v2"
+	defaultTTL                = 60
+	defaultPropagationTimeout = 120 * time.Second
+	defaultPollingInterval    = 5 * time.Second
+	defaultHTTPTimeout        = 30 * time.Second
+)
+
+const (
+	envNamespace = "SELECTELV2_"
+
+	EnvBaseURL    = envNamespace + "BASE_URL"
+	EnvUsernameOS = envNamespace + "USERNAME"
+	EnvPasswordOS = envNamespace + "PASSWORD"
+	EnvAccount    = envNamespace + "ACCOUNT_ID"
+	EnvProjectID  = envNamespace + "PROJECT_ID"
+
+	EnvTTL                = envNamespace + "TTL"
+	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
+	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
+	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
+)
+
+var errNotFound = errors.New("rrset not found")
+
+// Config is used to configure the creation of the DNSProvider.
+type Config struct {
+	BaseURL            string
+	Username           string
+	Password           string
+	Account            string
+	ProjectID          string
+	TTL                int
+	PropagationTimeout time.Duration
+	PollingInterval    time.Duration
+	HTTPClient         *http.Client
+}
+
+// NewDefaultConfig returns a default configuration for the DNSProvider.
+func NewDefaultConfig() *Config {
+	return &Config{
+		BaseURL:            env.GetOrDefaultString(EnvBaseURL, selectel.DefaultSelectelBaseURL),
+		TTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),
+		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout),
+		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval),
+		HTTPClient: &http.Client{
+			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, defaultHTTPTimeout),
+		},
+	}
+}
+
+type DNSProvider struct {
+	baseClient selectelapi.DNSClient[selectelapi.Zone, selectelapi.RRSet]
+	config     *Config
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Selectel Domains APIv2.
+func NewDNSProvider() (*DNSProvider, error) {
+	values, err := env.Get(EnvUsernameOS, EnvPasswordOS, EnvAccount, EnvProjectID)
+	if err != nil {
+		return nil, fmt.Errorf("selectelv2: %w", err)
+	}
+
+	config := NewDefaultConfig()
+	config.Username = values[EnvUsernameOS]
+	config.Password = values[EnvPasswordOS]
+	config.Account = values[EnvAccount]
+	config.ProjectID = values[EnvProjectID]
+
+	return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for selectel.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+	if config == nil {
+		return nil, errors.New("selectelv2: the configuration of the DNS provider is nil")
+	}
+
+	if config.Username == "" {
+		return nil, errors.New("selectelv2: missing username")
+	}
+
+	if config.Password == "" {
+		return nil, errors.New("selectelv2: missing password")
+	}
+
+	if config.Account == "" {
+		return nil, errors.New("selectelv2: missing account")
+	}
+
+	if config.ProjectID == "" {
+		return nil, errors.New("selectelv2: missing project ID")
+	}
+
+	headers := http.Header{}
+	headers.Set("User-Agent", "lego/selectelv2")
+
+	return &DNSProvider{
+		baseClient: selectelapi.NewClient(defaultBaseURL, config.HTTPClient, headers),
+		config:     config,
+	}, nil
+}
+
+// Timeout returns the Timeout and interval to use when checking for DNS propagation.
+// Adjusting here to cope with spikes in propagation times.
+func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
+	return p.config.PropagationTimeout, p.config.PollingInterval
+}
+
+// Present creates a TXT record to fulfill DNS-01 challenge.
+func (p *DNSProvider) Present(domain, _, keyAuth string) error {
+	ctx := context.Background()
+
+	client, err := p.authorize()
+	if err != nil {
+		return fmt.Errorf("selectelv2: authorize: %w", err)
+	}
+
+	info := dns01.GetChallengeInfo(domain, keyAuth)
+
+	zone, err := client.getZone(ctx, domain)
+	if err != nil {
+		return fmt.Errorf("selectelv2: get zone: %w", err)
+	}
+
+	rrset, err := client.getRRset(ctx, dns01.UnFqdn(info.EffectiveFQDN), zone.ID)
+	if err != nil {
+		if !errors.Is(err, errNotFound) {
+			return fmt.Errorf("selectelv2: get RRSet: %w", err)
+		}
+
+		newRRSet := &selectelapi.RRSet{
+			Name:    info.EffectiveFQDN,
+			Type:    selectelapi.TXT,
+			TTL:     p.config.TTL,
+			Records: []selectelapi.RecordItem{{Content: fmt.Sprintf("%q", info.Value)}},
+		}
+
+		_, err = client.CreateRRSet(ctx, zone.ID, newRRSet)
+		if err != nil {
+			return fmt.Errorf("selectelv2: create RRSet: %w", err)
+		}
+
+		return nil
+	}
+
+	rrset.Records = append(rrset.Records, selectelapi.RecordItem{Content: fmt.Sprintf("%q", info.Value)})
+
+	err = client.UpdateRRSet(ctx, zone.ID, rrset.ID, rrset)
+	if err != nil {
+		return fmt.Errorf("selectelv2: update RRSet: %w", err)
+	}
+
+	return nil
+}
+
+// CleanUp removes a TXT record used for DNS-01 challenge.
+func (p *DNSProvider) CleanUp(domain, _, keyAuth string) error {
+	ctx := context.Background()
+
+	client, err := p.authorize()
+	if err != nil {
+		return fmt.Errorf("selectelv2: authorize: %w", err)
+	}
+
+	info := dns01.GetChallengeInfo(domain, keyAuth)
+
+	zone, err := client.getZone(ctx, domain)
+	if err != nil {
+		return fmt.Errorf("selectelv2: get zone: %w", err)
+	}
+
+	rrset, err := client.getRRset(ctx, dns01.UnFqdn(info.EffectiveFQDN), zone.ID)
+	if err != nil {
+		return fmt.Errorf("selectelv2: get RRSet: %w", err)
+	}
+
+	if len(rrset.Records) <= 1 {
+		err = client.DeleteRRSet(ctx, zone.ID, rrset.ID)
+		if err != nil {
+			return fmt.Errorf("selectelv2: %w", err)
+		}
+
+		return nil
+	}
+
+	for i, item := range rrset.Records {
+		if strings.Trim(item.Content, `"`) == info.Value {
+			rrset.Records = append(rrset.Records[:i], rrset.Records[i+1:]...)
+			break
+		}
+	}
+
+	err = client.UpdateRRSet(ctx, zone.ID, rrset.ID, rrset)
+	if err != nil {
+		return fmt.Errorf("selectelv2: update RRSet: %w", err)
+	}
+
+	return nil
+}
+
+func (p *DNSProvider) authorize() (*clientWrapper, error) {
+	token, err := obtainOpenstackToken(p.config)
+	if err != nil {
+		return nil, err
+	}
+
+	extraHeaders := http.Header{}
+	extraHeaders.Set(tokenHeader, token)
+
+	return &clientWrapper{
+		DNSClient: p.baseClient.WithHeaders(extraHeaders),
+	}, nil
+}
+
+func obtainOpenstackToken(config *Config) (string, error) {
+	vpcClient, err := selvpcclient.NewClient(&selvpcclient.ClientOptions{
+		Username:       config.Username,
+		Password:       config.Password,
+		UserDomainName: config.Account,
+		ProjectID:      config.ProjectID,
+	})
+	if err != nil {
+		return "", fmt.Errorf("new VPC client: %w", err)
+	}
+
+	return vpcClient.GetXAuthToken(), nil
+}
+
+type clientWrapper struct {
+	selectelapi.DNSClient[selectelapi.Zone, selectelapi.RRSet]
+}
+
+func (w *clientWrapper) getZone(ctx context.Context, name string) (*selectelapi.Zone, error) {
+	params := &map[string]string{"filter": name}
+
+	zones, err := w.ListZones(ctx, params)
+	if err != nil {
+		return nil, fmt.Errorf("list zone: %w", err)
+	}
+
+	for _, zone := range zones.GetItems() {
+		if zone.Name == dns01.ToFqdn(name) {
+			return zone, nil
+		}
+	}
+
+	if len(strings.Split(dns01.UnFqdn(name), ".")) == 1 {
+		return nil, errors.New("zone for challenge has not been found")
+	}
+
+	// -1 can not be returned since if no dots present we exit above
+	i := strings.Index(name, ".")
+
+	return w.getZone(ctx, name[i+1:])
+}
+
+func (w *clientWrapper) getRRset(ctx context.Context, name, zoneID string) (*selectelapi.RRSet, error) {
+	params := &map[string]string{"name": name, "rrset_types": string(selectelapi.TXT)}
+
+	resp, err := w.ListRRSets(ctx, zoneID, params)
+	if err != nil {
+		return nil, fmt.Errorf("list rrset: %w", err)
+	}
+
+	for _, rrset := range resp.GetItems() {
+		if rrset.Name == dns01.ToFqdn(name) {
+			return rrset, nil
+		}
+	}
+
+	return nil, errNotFound
+}
diff --git a/providers/dns/selectelv2/selectelv2.toml b/providers/dns/selectelv2/selectelv2.toml
new file mode 100644
index 00000000..7870688b
--- /dev/null
+++ b/providers/dns/selectelv2/selectelv2.toml
@@ -0,0 +1,30 @@
+Name = "Selectel v2"
+Description = ''''''
+URL = "https://selectel.ru"
+Code = "selectelv2"
+Since = "v4.17.0"
+
+Example = '''
+SELECTEL_USERNAME=trex \
+SELECTEL_PASSWORD=xxxxx \
+SELECTEL_ACCOUNT_ID=1234567 \
+SELECTEL_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \
+lego --email you@example.com --dns selectelv2 --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    SELECTELV2_USERNAME = "Openstack username"
+    SELECTELV2_PASSWORD = "Openstack username's password"
+    SELECTELV2_ACCOUNT_ID = "Selectel account ID (INT)"
+    SELECTELV2_PROJECT_ID = "Cloud project ID (UUID)"
+  [Configuration.Additional]
+    SELECTELV2_BASE_URL = "API endpoint URL"
+    SELECTELV2_POLLING_INTERVAL = "Time between DNS propagation check"
+    SELECTELV2_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    SELECTELV2_TTL = "The TTL of the TXT record used for the DNS challenge"
+    SELECTELV2_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://developers.selectel.ru/docs/cloud-services/dns_api/dns_api_actual/"
+  GoClient = "https://github.com/selectel/domains-go"


Test patch:
diff --git a/providers/dns/selectelv2/selectelv2_test.go b/providers/dns/selectelv2/selectelv2_test.go
new file mode 100644
index 00000000..4859b993
--- /dev/null
+++ b/providers/dns/selectelv2/selectelv2_test.go
@@ -0,0 +1,185 @@
+package selectelv2
+
+import (
+	"testing"
+	"time"
+
+	"github.com/go-acme/lego/v4/platform/tester"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const envDomain = envNamespace + "DOMAIN"
+
+var envTest = tester.NewEnvTest(EnvUsernameOS, EnvPasswordOS, EnvAccount, EnvProjectID).
+	WithDomain(envDomain)
+
+func TestNewDNSProvider(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		envVars  map[string]string
+		expected string
+	}{
+		{
+			desc: "success",
+			envVars: map[string]string{
+				EnvUsernameOS: "someName",
+				EnvPasswordOS: "qwerty",
+				EnvAccount:    "1",
+				EnvProjectID:  "111a11111aaa11aa1a11aaa11111aa1a",
+			},
+		},
+		{
+			desc: "missing username",
+			envVars: map[string]string{
+				EnvPasswordOS: "qwerty",
+				EnvAccount:    "1",
+				EnvProjectID:  "111a11111aaa11aa1a11aaa11111aa1a",
+			},
+			expected: "selectelv2: some credentials information are missing: SELECTELV2_USERNAME",
+		},
+		{
+			desc: "missing password",
+			envVars: map[string]string{
+				EnvUsernameOS: "someName",
+				EnvAccount:    "1",
+				EnvProjectID:  "111a11111aaa11aa1a11aaa11111aa1a",
+			},
+			expected: "selectelv2: some credentials information are missing: SELECTELV2_PASSWORD",
+		},
+		{
+			desc: "missing account",
+			envVars: map[string]string{
+				EnvUsernameOS: "someName",
+				EnvPasswordOS: "qwerty",
+				EnvProjectID:  "111a11111aaa11aa1a11aaa11111aa1a",
+			},
+			expected: "selectelv2: some credentials information are missing: SELECTELV2_ACCOUNT_ID",
+		},
+		{
+			desc: "missing project",
+			envVars: map[string]string{
+				EnvUsernameOS: "someName",
+				EnvPasswordOS: "qwerty",
+				EnvAccount:    "1",
+			},
+			expected: "selectelv2: some credentials information are missing: SELECTELV2_PROJECT_ID",
+		},
+	}
+
+	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)
+				assert.NotNil(t, p.config)
+				assert.NotNil(t, p.baseClient)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
+}
+
+func TestNewDNSProviderConfig(t *testing.T) {
+	testCases := []struct {
+		desc      string
+		username  string
+		password  string
+		account   string
+		projectID string
+		expected  string
+	}{
+		{
+			desc:      "success",
+			username:  "user",
+			password:  "secret",
+			account:   "1",
+			projectID: "111a11111aaa11aa1a11aaa11111aa1a",
+		},
+		{
+			desc:      "missing username",
+			password:  "secret",
+			account:   "1",
+			projectID: "111a11111aaa11aa1a11aaa11111aa1a",
+			expected:  "selectelv2: missing username",
+		},
+		{
+			desc:      "missing password",
+			username:  "user",
+			account:   "1",
+			projectID: "111a11111aaa11aa1a11aaa11111aa1a",
+			expected:  "selectelv2: missing password",
+		},
+		{
+			desc:      "missing account",
+			username:  "user",
+			password:  "secret",
+			projectID: "111a11111aaa11aa1a11aaa11111aa1a",
+			expected:  "selectelv2: missing account",
+		},
+		{
+			desc:     "missing projectID",
+			username: "user",
+			password: "secret",
+			account:  "1",
+			expected: "selectelv2: missing project ID",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			config := NewDefaultConfig()
+			config.Username = test.username
+			config.Password = test.password
+			config.Account = test.account
+			config.ProjectID = test.projectID
+
+			p, err := NewDNSProviderConfig(config)
+
+			if test.expected == "" {
+				require.NoError(t, err)
+				require.NotNil(t, p)
+				assert.NotNil(t, p.config)
+				assert.NotNil(t, p.baseClient)
+			} 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)
+}
+
+func TestLiveCleanUp(t *testing.T) {
+	if !envTest.IsLiveTest() {
+		t.Skip("skipping live test")
+	}
+
+	envTest.RestoreEnv()
+	provider, err := NewDNSProvider()
+	require.NoError(t, err)
+
+	time.Sleep(2 * time.Second)
+
+	err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
+	require.NoError(t, err)
+}