azuredns: servicediscovery for zones (#2140)

Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
Markus Blaschke 2024-03-20 04:36:35 +01:00 committed by GitHub
parent 27fd142ca1
commit 874e3ea023
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 540 additions and 248 deletions

View file

@ -314,8 +314,6 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln(` - "AZURE_CLIENT_CERTIFICATE_PATH": Client certificate path`)
ew.writeln(` - "AZURE_CLIENT_ID": Client ID`)
ew.writeln(` - "AZURE_CLIENT_SECRET": Client secret`)
ew.writeln(` - "AZURE_RESOURCE_GROUP": DNS zone resource group`)
ew.writeln(` - "AZURE_SUBSCRIPTION_ID": DNS zone subscription ID`)
ew.writeln(` - "AZURE_TENANT_ID": Tenant ID`)
ew.writeln()
@ -326,6 +324,9 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln(` - "AZURE_POLLING_INTERVAL": Time between DNS propagation check`)
ew.writeln(` - "AZURE_PRIVATE_ZONE": Set to true to use Azure Private DNS Zones and not public`)
ew.writeln(` - "AZURE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
ew.writeln(` - "AZURE_RESOURCE_GROUP": DNS zone resource group`)
ew.writeln(` - "AZURE_SERVICEDISCOVERY_FILTER": Advanced ServiceDiscovery filter using Kusto query condition`)
ew.writeln(` - "AZURE_SUBSCRIPTION_ID": DNS zone subscription ID`)
ew.writeln(` - "AZURE_TTL": The TTL of the TXT record used for the DNS challenge`)
ew.writeln(` - "AZURE_ZONE_NAME": Zone name to use inside Azure DNS service to add the TXT record in`)

View file

@ -48,15 +48,12 @@ lego --domains example.com --email your_example@email.com --dns azuredns run
### Using Managed Identity (Azure VM)
AZURE_TENANT_ID=<your service principal tenant ID> \
AZURE_SUBSCRIPTION_ID=<your target zone subscription ID> \
AZURE_RESOURCE_GROUP=<your target zone resource group name> \
lego --domains example.com --email your_example@email.com --dns azuredns run
### Using Managed Identity (Azure Arc)
AZURE_TENANT_ID=<your service principal tenant ID> \
AZURE_SUBSCRIPTION_ID=<your target zone subscription ID> \
AZURE_RESOURCE_GROUP=<your target zone resource group name> \
IMDS_ENDPOINT=http://localhost:40342 \
IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \
lego --domains example.com --email your_example@email.com --dns azuredns run
@ -73,8 +70,6 @@ lego --domains example.com --email your_example@email.com --dns azuredns run
| `AZURE_CLIENT_CERTIFICATE_PATH` | Client certificate path |
| `AZURE_CLIENT_ID` | Client ID |
| `AZURE_CLIENT_SECRET` | Client secret |
| `AZURE_RESOURCE_GROUP` | DNS zone resource group |
| `AZURE_SUBSCRIPTION_ID` | DNS zone subscription ID |
| `AZURE_TENANT_ID` | Tenant ID |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
@ -91,6 +86,9 @@ More information [here]({{< ref "dns#configuration-and-credentials" >}}).
| `AZURE_POLLING_INTERVAL` | Time between DNS propagation check |
| `AZURE_PRIVATE_ZONE` | Set to true to use Azure Private DNS Zones and not public |
| `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
| `AZURE_RESOURCE_GROUP` | DNS zone resource group |
| `AZURE_SERVICEDISCOVERY_FILTER` | Advanced ServiceDiscovery filter using Kusto query condition |
| `AZURE_SUBSCRIPTION_ID` | DNS zone subscription ID |
| `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge |
| `AZURE_ZONE_NAME` | Zone name to use inside Azure DNS service to add the TXT record in |
@ -115,6 +113,22 @@ Link:
### Environment variables
#### Service Discovery
Lego automatically finds all visible Azure (private) DNS zones using [Azure ResourceGraph query](https://learn.microsoft.com/en-us/azure/governance/resource-graph/).
This can be limited by specifying environment variable `AZURE_SUBSCRIPTION_ID` and/or `AZURE_RESOURCE_GROUP` which limits the
DNS zones to only a subscription or to one resourceGroup.
Additionally environment variable `AZURE_SERVICEDISCOVERY_FILTER` can be used to filter DNS zones with an addition Kusto filter eg:
```
resources
| where type =~ "microsoft.network/dnszones"
| ${AZURE_SERVICEDISCOVERY_FILTER}
| project subscriptionId, resourceGroup, name
```
#### Client secret
The Azure Credentials can be configured using the following environment variables:
@ -122,7 +136,7 @@ The Azure Credentials can be configured using the following environment variable
* AZURE_CLIENT_SECRET = "Client secret"
* AZURE_TENANT_ID = "Tenant ID"
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.
#### Client certificate
@ -131,7 +145,7 @@ The Azure Credentials can be configured using the following environment variable
* AZURE_CLIENT_CERTIFICATE_PATH = "Client certificate path"
* AZURE_TENANT_ID = "Tenant ID"
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.
### Workload identity
@ -142,12 +156,12 @@ This must be configured in kubernetes workload deployment in one hand and on the
Here is a summary of the steps to follow to use it :
* create a `ServiceAccount` resource, add following annotations to reference the targeted Azure AD application registration : `azure.workload.identity/client-id` and `azure.workload.identity/tenant-id`.
* on the `Deployment` resource you must reference the previous `ServiceAccount` and add the following label : `azure.workload.identity/use: "true"`.
* create a fedreated credentials of type `Kubernetes accessing Azure resources`, add the cluster issuer URL and add the namespace and name of your kubernetes service account.
* create a federated credentials of type `Kubernetes accessing Azure resources`, add the cluster issuer URL and add the namespace and name of your kubernetes service account.
Link :
- [Azure AD Workload identity](https://azure.github.io/azure-workload-identity/docs/topics/service-account-labels-and-annotations.html)
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `wli`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `wli`.
### Azure Managed Identity
@ -182,9 +196,9 @@ az role assignment create \
```
A timeout wrapper is configured for this authentication method.
The duraction can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.
The duration can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.
The default timeout is 2 seconds.
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.
#### Azure Managed Identity (with Azure Arc)
@ -198,9 +212,9 @@ you may need to set the environment variables:
* `IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token`
A timeout wrapper is configured for this authentication method.
The duraction can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.
The duration can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.
The default timeout is 2 seconds.
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.
### Azure CLI
@ -208,7 +222,7 @@ The Azure CLI is a command-line tool provided by Microsoft to interact with Azur
It provides an easy way to authenticate by simply running `az login` command.
The generated token will be cached by default in the `~/.azure` folder.
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.
### Open ID Connect

14
go.mod
View file

@ -7,10 +7,11 @@ go 1.21
require (
cloud.google.com/go/compute/metadata v0.2.3
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0
github.com/Azure/go-autorest/autorest v0.11.29
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12
github.com/Azure/go-autorest/autorest/to v0.4.0
@ -88,14 +89,14 @@ require (
require (
cloud.google.com/go/compute v1.20.1 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
@ -123,10 +124,11 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
@ -144,7 +146,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect

30
go.sum
View file

@ -19,18 +19,20 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 h1:8kDqDngH+DmVBiCtIjCFTGa7MBnsIOkF9IccInFEbjk=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 h1:8iR6OLffWWorFdzL2JFCab5xpD8VKEE2DUBBl+HNTDY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0/go.mod h1:copqlcjMWc/wgQ1N2fzsJFQxDdqKGg1EQt8T5wJMOGE=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 h1:rR8ZW79lE/ppfXTfiYSnMFv5EzmVuY4pfZWIkscIJ64=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0/go.mod h1:y2zXtLSMM/X5Mfawq0lOftpWn3f4V6OCsRdINsvWBPI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
@ -56,8 +58,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
@ -238,6 +240,8 @@ github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -291,8 +295,8 @@ github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkj
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@ -514,8 +518,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -800,7 +804,6 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -810,6 +813,7 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View file

@ -15,6 +15,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
)
@ -40,6 +41,8 @@ const (
EnvAuthMethod = envNamespace + "AUTH_METHOD"
EnvAuthMSITimeout = envNamespace + "AUTH_MSI_TIMEOUT"
EnvServiceDiscoveryFilter = envNamespace + "SERVICEDISCOVERY_FILTER"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
@ -73,6 +76,8 @@ type Config struct {
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
ServiceDiscoveryFilter string
}
// NewDefaultConfig returns a default configuration for the DNSProvider.
@ -121,6 +126,8 @@ func NewDNSProvider() (*DNSProvider, error) {
config.OIDCToken = env.GetOrFile(EnvOIDCToken)
config.OIDCTokenFilePath = env.GetOrFile(EnvOIDCTokenFilePath)
config.ServiceDiscoveryFilter = env.GetOrFile(EnvServiceDiscoveryFilter)
oidcValues, _ := env.GetWithFallback(
[]string{EnvOIDCRequestURL, EnvGitHubOIDCRequestURL},
[]string{EnvOIDCRequestToken, EnvGitHubOIDCRequestToken},
@ -150,14 +157,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return nil, fmt.Errorf("azuredns: Unable to retrieve valid credentials: %w", err)
}
if config.SubscriptionID == "" {
return nil, errors.New("azuredns: SubscriptionID is missing")
}
if config.ResourceGroup == "" {
return nil, errors.New("azuredns: ResourceGroup is missing")
}
var dnsProvider challenge.ProviderTimeout
if config.PrivateZone {
dnsProvider, err = NewDNSProviderPrivate(config, credentials)
@ -254,7 +253,21 @@ func (w *timeoutTokenCredential) GetToken(ctx context.Context, opts policy.Token
return tk, err
}
func deref[T string | int | int32 | int64](v *T) T {
func getAuthZone(fqdn string) (string, error) {
authZone := env.GetOrFile(EnvZoneName)
if authZone != "" {
return authZone, nil
}
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return "", fmt.Errorf("could not find zone: %w", err)
}
return authZone, nil
}
func deref[T any](v *T) T {
if v == nil {
var zero T
return zero

View file

@ -27,15 +27,12 @@ lego --domains example.com --email your_example@email.com --dns azuredns run
### Using Managed Identity (Azure VM)
AZURE_TENANT_ID=<your service principal tenant ID> \
AZURE_SUBSCRIPTION_ID=<your target zone subscription ID> \
AZURE_RESOURCE_GROUP=<your target zone resource group name> \
lego --domains example.com --email your_example@email.com --dns azuredns run
### Using Managed Identity (Azure Arc)
AZURE_TENANT_ID=<your service principal tenant ID> \
AZURE_SUBSCRIPTION_ID=<your target zone subscription ID> \
AZURE_RESOURCE_GROUP=<your target zone resource group name> \
IMDS_ENDPOINT=http://localhost:40342 \
IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \
lego --domains example.com --email your_example@email.com --dns azuredns run
@ -61,6 +58,22 @@ Link:
### Environment variables
#### Service Discovery
Lego automatically finds all visible Azure (private) DNS zones using [Azure ResourceGraph query](https://learn.microsoft.com/en-us/azure/governance/resource-graph/).
This can be limited by specifying environment variable `AZURE_SUBSCRIPTION_ID` and/or `AZURE_RESOURCE_GROUP` which limits the
DNS zones to only a subscription or to one resourceGroup.
Additionally environment variable `AZURE_SERVICEDISCOVERY_FILTER` can be used to filter DNS zones with an addition Kusto filter eg:
```
resources
| where type =~ "microsoft.network/dnszones"
| ${AZURE_SERVICEDISCOVERY_FILTER}
| project subscriptionId, resourceGroup, name
```
#### Client secret
The Azure Credentials can be configured using the following environment variables:
@ -68,7 +81,7 @@ The Azure Credentials can be configured using the following environment variable
* AZURE_CLIENT_SECRET = "Client secret"
* AZURE_TENANT_ID = "Tenant ID"
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.
#### Client certificate
@ -77,7 +90,7 @@ The Azure Credentials can be configured using the following environment variable
* AZURE_CLIENT_CERTIFICATE_PATH = "Client certificate path"
* AZURE_TENANT_ID = "Tenant ID"
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.
### Workload identity
@ -88,12 +101,12 @@ This must be configured in kubernetes workload deployment in one hand and on the
Here is a summary of the steps to follow to use it :
* create a `ServiceAccount` resource, add following annotations to reference the targeted Azure AD application registration : `azure.workload.identity/client-id` and `azure.workload.identity/tenant-id`.
* on the `Deployment` resource you must reference the previous `ServiceAccount` and add the following label : `azure.workload.identity/use: "true"`.
* create a fedreated credentials of type `Kubernetes accessing Azure resources`, add the cluster issuer URL and add the namespace and name of your kubernetes service account.
* create a federated credentials of type `Kubernetes accessing Azure resources`, add the cluster issuer URL and add the namespace and name of your kubernetes service account.
Link :
- [Azure AD Workload identity](https://azure.github.io/azure-workload-identity/docs/topics/service-account-labels-and-annotations.html)
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `wli`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `wli`.
### Azure Managed Identity
@ -128,9 +141,9 @@ az role assignment create \
```
A timeout wrapper is configured for this authentication method.
The duraction can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.
The duration can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.
The default timeout is 2 seconds.
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.
#### Azure Managed Identity (with Azure Arc)
@ -144,9 +157,9 @@ you may need to set the environment variables:
* `IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token`
A timeout wrapper is configured for this authentication method.
The duraction can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.
The duration can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.
The default timeout is 2 seconds.
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.
### Azure CLI
@ -154,7 +167,7 @@ The Azure CLI is a command-line tool provided by Microsoft to interact with Azur
It provides an easy way to authenticate by simply running `az login` command.
The generated token will be cached by default in the `~/.azure` folder.
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.
This authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.
### Open ID Connect
@ -169,10 +182,11 @@ It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oi
AZURE_CLIENT_SECRET = "Client secret"
AZURE_TENANT_ID = "Tenant ID"
AZURE_CLIENT_CERTIFICATE_PATH = "Client certificate path"
AZURE_SUBSCRIPTION_ID = "DNS zone subscription ID"
AZURE_RESOURCE_GROUP = "DNS zone resource group"
[Configuration.Additional]
AZURE_ENVIRONMENT = "Azure environment, one of: public, usgovernment, and china"
AZURE_SUBSCRIPTION_ID = "DNS zone subscription ID"
AZURE_RESOURCE_GROUP = "DNS zone resource group"
AZURE_SERVICEDISCOVERY_FILTER = "Advanced ServiceDiscovery filter using Kusto query condition"
AZURE_PRIVATE_ZONE = "Set to true to use Azure Private DNS Zones and not public"
AZURE_ZONE_NAME = "Zone name to use inside Azure DNS service to add the TXT record in"
AZURE_AUTH_METHOD = "Specify which authentication method to use"

View file

@ -1,8 +1,6 @@
package azuredns
import (
"net/http"
"net/http/httptest"
"testing"
"time"
@ -25,20 +23,10 @@ func TestNewDNSProvider(t *testing.T) {
envVars map[string]string
expected string
}{
{
desc: "success",
envVars: map[string]string{
EnvEnvironment: "",
EnvSubscriptionID: "A",
EnvResourceGroup: "B",
},
},
{
desc: "unknown environment",
envVars: map[string]string{
EnvEnvironment: "test",
EnvSubscriptionID: "A",
EnvResourceGroup: "B",
EnvEnvironment: "test",
},
expected: "azuredns: unknown environment test",
},
@ -67,78 +55,6 @@ func TestNewDNSProvider(t *testing.T) {
}
}
func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct {
desc string
subscriptionID string
resourceGroup string
privateZone bool
handler func(w http.ResponseWriter, r *http.Request)
expected string
}{
{
desc: "success (public)",
subscriptionID: "A",
resourceGroup: "B",
privateZone: false,
},
{
desc: "success (private)",
subscriptionID: "A",
resourceGroup: "B",
privateZone: true,
},
{
desc: "SubscriptionID missing",
subscriptionID: "",
resourceGroup: "",
expected: "azuredns: SubscriptionID is missing",
},
{
desc: "ResourceGroup missing",
subscriptionID: "A",
resourceGroup: "",
expected: "azuredns: ResourceGroup is missing",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.SubscriptionID = test.subscriptionID
config.ResourceGroup = test.resourceGroup
config.PrivateZone = test.privateZone
mux := http.NewServeMux()
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
if test.handler == nil {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
} else {
mux.HandleFunc("/", test.handler)
}
p, err := NewDNSProviderConfig(config)
if test.expected != "" {
require.EqualError(t, err, test.expected)
return
}
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.provider)
if test.privateZone {
assert.IsType(t, p.provider, new(DNSProviderPrivate))
} else {
assert.IsType(t, p.provider, new(DNSProviderPublic))
}
})
}
}
func TestLivePresent(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")

View file

@ -9,40 +9,30 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
)
// DNSProviderPrivate implements the challenge.Provider interface for Azure Private Zone DNS.
type DNSProviderPrivate struct {
config *Config
zoneClient *armprivatedns.PrivateZonesClient
recordClient *armprivatedns.RecordSetsClient
config *Config
credentials azcore.TokenCredential
serviceDiscoveryZones map[string]ServiceDiscoveryZone
}
// NewDNSProviderPrivate creates a DNSProviderPrivate structure with initialized Azure clients.
// NewDNSProviderPrivate creates a DNSProviderPrivate structure.
func NewDNSProviderPrivate(config *Config, credentials azcore.TokenCredential) (*DNSProviderPrivate, error) {
options := arm.ClientOptions{
ClientOptions: azcore.ClientOptions{
Cloud: config.Environment,
},
}
zoneClient, err := armprivatedns.NewPrivateZonesClient(config.SubscriptionID, credentials, &options)
zones, err := discoverDNSZones(context.Background(), config, credentials)
if err != nil {
return nil, err
}
recordClient, err := armprivatedns.NewRecordSetsClient(config.SubscriptionID, credentials, &options)
if err != nil {
return nil, err
return nil, fmt.Errorf("discover DNS zones: %w", err)
}
return &DNSProviderPrivate{
config: config,
zoneClient: zoneClient,
recordClient: recordClient,
config: config,
credentials: credentials,
serviceDiscoveryZones: zones,
}, nil
}
@ -57,18 +47,23 @@ func (d *DNSProviderPrivate) Present(domain, _, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
zone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)
zone, err := d.getHostedZone(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
client, err := newPrivateZoneClient(zone, d.credentials, d.config.Environment)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
// Get existing record set
rset, err := d.recordClient.Get(ctx, d.config.ResourceGroup, zone, armprivatedns.RecordTypeTXT, subDomain, nil)
resp, err := client.Get(ctx, subDomain)
if err != nil {
var respErr *azcore.ResponseError
if !errors.As(err, &respErr) || respErr.StatusCode != http.StatusNotFound {
@ -77,32 +72,23 @@ func (d *DNSProviderPrivate) Present(domain, _, keyAuth string) error {
}
// Construct unique TXT records using map
uniqRecords := map[string]struct{}{info.Value: {}}
if rset.RecordSet.Properties != nil && rset.RecordSet.Properties.TxtRecords != nil {
for _, txtRecord := range rset.RecordSet.Properties.TxtRecords {
// Assume Value doesn't contain multiple strings
if len(txtRecord.Value) > 0 {
uniqRecords[deref(txtRecord.Value[0])] = struct{}{}
}
}
}
uniqRecords := privateUniqueRecords(resp.RecordSet, info.Value)
var txtRecords []*armprivatedns.TxtRecord
for txt := range uniqRecords {
txtRecord := txt
txtRecords = append(txtRecords, &armprivatedns.TxtRecord{Value: []*string{&txtRecord}})
txtRecords = append(txtRecords, &armprivatedns.TxtRecord{Value: to.SliceOfPtrs(txtRecord)})
}
ttlInt64 := int64(d.config.TTL)
rec := armprivatedns.RecordSet{
Name: &subDomain,
Properties: &armprivatedns.RecordSetProperties{
TTL: &ttlInt64,
TTL: to.Ptr(int64(d.config.TTL)),
TxtRecords: txtRecords,
},
}
_, err = d.recordClient.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, armprivatedns.RecordTypeTXT, subDomain, rec, nil)
_, err = client.CreateOrUpdate(ctx, subDomain, rec)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
@ -115,17 +101,22 @@ func (d *DNSProviderPrivate) CleanUp(domain, _, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
zone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)
zone, err := d.getHostedZone(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
client, err := newPrivateZoneClient(zone, d.credentials, d.config.Environment)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
_, err = d.recordClient.Delete(ctx, d.config.ResourceGroup, zone, armprivatedns.RecordTypeTXT, subDomain, nil)
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
_, err = client.Delete(ctx, subDomain)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
@ -134,21 +125,67 @@ func (d *DNSProviderPrivate) CleanUp(domain, _, keyAuth string) error {
}
// Checks that azure has a zone for this domain name.
func (d *DNSProviderPrivate) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {
if zone := env.GetOrFile(EnvZoneName); zone != "" {
return zone, nil
}
authZone, err := dns01.FindZoneByFqdn(fqdn)
func (d *DNSProviderPrivate) getHostedZone(fqdn string) (ServiceDiscoveryZone, error) {
authZone, err := getAuthZone(fqdn)
if err != nil {
return "", fmt.Errorf("could not find zone: %w", err)
return ServiceDiscoveryZone{}, err
}
zone, err := d.zoneClient.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone), nil)
if err != nil {
return "", err
azureZone, exists := d.serviceDiscoveryZones[dns01.UnFqdn(authZone)]
if !exists {
return ServiceDiscoveryZone{}, fmt.Errorf("could not find zone (from discovery): %s", authZone)
}
// zone.Name shouldn't have a trailing dot(.)
return dns01.UnFqdn(deref(zone.Name)), nil
return azureZone, nil
}
// privateZoneClient provides Azure client for one DNS zone.
type privateZoneClient struct {
zone ServiceDiscoveryZone
recordClient *armprivatedns.RecordSetsClient
}
// newPrivateZoneClient creates privateZoneClient structure with initialized Azure client.
func newPrivateZoneClient(zone ServiceDiscoveryZone, credential azcore.TokenCredential, environment cloud.Configuration) (*privateZoneClient, error) {
options := &arm.ClientOptions{
ClientOptions: azcore.ClientOptions{
Cloud: environment,
},
}
recordClient, err := armprivatedns.NewRecordSetsClient(zone.SubscriptionID, credential, options)
if err != nil {
return nil, err
}
return &privateZoneClient{
zone: zone,
recordClient: recordClient,
}, nil
}
func (c privateZoneClient) Get(ctx context.Context, subDomain string) (armprivatedns.RecordSetsClientGetResponse, error) {
return c.recordClient.Get(ctx, c.zone.ResourceGroup, c.zone.Name, armprivatedns.RecordTypeTXT, subDomain, nil)
}
func (c privateZoneClient) CreateOrUpdate(ctx context.Context, subDomain string, rec armprivatedns.RecordSet) (armprivatedns.RecordSetsClientCreateOrUpdateResponse, error) {
return c.recordClient.CreateOrUpdate(ctx, c.zone.ResourceGroup, c.zone.Name, armprivatedns.RecordTypeTXT, subDomain, rec, nil)
}
func (c privateZoneClient) Delete(ctx context.Context, subDomain string) (armprivatedns.RecordSetsClientDeleteResponse, error) {
return c.recordClient.Delete(ctx, c.zone.ResourceGroup, c.zone.Name, armprivatedns.RecordTypeTXT, subDomain, nil)
}
func privateUniqueRecords(recordSet armprivatedns.RecordSet, value string) map[string]struct{} {
uniqRecords := map[string]struct{}{value: {}}
if recordSet.Properties != nil && recordSet.Properties.TxtRecords != nil {
for _, txtRecord := range recordSet.Properties.TxtRecords {
// Assume Value doesn't contain multiple strings
if len(txtRecord.Value) > 0 {
uniqRecords[deref(txtRecord.Value[0])] = struct{}{}
}
}
}
return uniqRecords
}

View file

@ -9,40 +9,30 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
)
// DNSProviderPublic implements the challenge.Provider interface for Azure Public Zone DNS.
type DNSProviderPublic struct {
config *Config
zoneClient *armdns.ZonesClient
recordClient *armdns.RecordSetsClient
config *Config
credentials azcore.TokenCredential
serviceDiscoveryZones map[string]ServiceDiscoveryZone
}
// NewDNSProviderPublic creates a DNSProviderPublic structure with intialised Azure clients.
// NewDNSProviderPublic creates a DNSProviderPublic structure.
func NewDNSProviderPublic(config *Config, credentials azcore.TokenCredential) (*DNSProviderPublic, error) {
options := arm.ClientOptions{
ClientOptions: azcore.ClientOptions{
Cloud: config.Environment,
},
}
zoneClient, err := armdns.NewZonesClient(config.SubscriptionID, credentials, &options)
zones, err := discoverDNSZones(context.Background(), config, credentials)
if err != nil {
return nil, err
}
recordClient, err := armdns.NewRecordSetsClient(config.SubscriptionID, credentials, &options)
if err != nil {
return nil, err
return nil, fmt.Errorf("discover DNS zones: %w", err)
}
return &DNSProviderPublic{
config: config,
zoneClient: zoneClient,
recordClient: recordClient,
config: config,
credentials: credentials,
serviceDiscoveryZones: zones,
}, nil
}
@ -57,18 +47,23 @@ func (d *DNSProviderPublic) Present(domain, _, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
zone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)
zone, err := d.getHostedZone(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
client, err := newPublicZoneClient(zone, d.credentials, d.config.Environment)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
// Get existing record set
rset, err := d.recordClient.Get(ctx, d.config.ResourceGroup, zone, subDomain, armdns.RecordTypeTXT, nil)
resp, err := client.Get(ctx, subDomain)
if err != nil {
var respErr *azcore.ResponseError
if !errors.As(err, &respErr) || respErr.StatusCode != http.StatusNotFound {
@ -76,33 +71,23 @@ func (d *DNSProviderPublic) Present(domain, _, keyAuth string) error {
}
}
// Construct unique TXT records using map
uniqRecords := map[string]struct{}{info.Value: {}}
if rset.RecordSet.Properties != nil && rset.RecordSet.Properties.TxtRecords != nil {
for _, txtRecord := range rset.RecordSet.Properties.TxtRecords {
// Assume Value doesn't contain multiple strings
if len(txtRecord.Value) > 0 {
uniqRecords[deref(txtRecord.Value[0])] = struct{}{}
}
}
}
uniqRecords := publicUniqueRecords(resp.RecordSet, info.Value)
var txtRecords []*armdns.TxtRecord
for txt := range uniqRecords {
txtRecord := txt
txtRecords = append(txtRecords, &armdns.TxtRecord{Value: []*string{&txtRecord}})
txtRecords = append(txtRecords, &armdns.TxtRecord{Value: to.SliceOfPtrs(txtRecord)})
}
ttlInt64 := int64(d.config.TTL)
rec := armdns.RecordSet{
Name: &subDomain,
Properties: &armdns.RecordSetProperties{
TTL: &ttlInt64,
TTL: to.Ptr(int64(d.config.TTL)),
TxtRecords: txtRecords,
},
}
_, err = d.recordClient.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, subDomain, armdns.RecordTypeTXT, rec, nil)
_, err = client.CreateOrUpdate(ctx, subDomain, rec)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
@ -115,17 +100,22 @@ func (d *DNSProviderPublic) CleanUp(domain, _, keyAuth string) error {
ctx := context.Background()
info := dns01.GetChallengeInfo(domain, keyAuth)
zone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)
zone, err := d.getHostedZone(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
client, err := newPublicZoneClient(zone, d.credentials, d.config.Environment)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
_, err = d.recordClient.Delete(ctx, d.config.ResourceGroup, zone, subDomain, armdns.RecordTypeTXT, nil)
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
_, err = client.Delete(ctx, subDomain)
if err != nil {
return fmt.Errorf("azuredns: %w", err)
}
@ -134,21 +124,66 @@ func (d *DNSProviderPublic) CleanUp(domain, _, keyAuth string) error {
}
// Checks that azure has a zone for this domain name.
func (d *DNSProviderPublic) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {
if zone := env.GetOrFile(EnvZoneName); zone != "" {
return zone, nil
}
authZone, err := dns01.FindZoneByFqdn(fqdn)
func (d *DNSProviderPublic) getHostedZone(fqdn string) (ServiceDiscoveryZone, error) {
authZone, err := getAuthZone(fqdn)
if err != nil {
return "", fmt.Errorf("could not find zone: %w", err)
return ServiceDiscoveryZone{}, err
}
zone, err := d.zoneClient.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone), nil)
if err != nil {
return "", err
azureZone, exists := d.serviceDiscoveryZones[dns01.UnFqdn(authZone)]
if !exists {
return ServiceDiscoveryZone{}, fmt.Errorf("could not find zone (from discovery): %s", authZone)
}
// zone.Name shouldn't have a trailing dot(.)
return dns01.UnFqdn(deref(zone.Name)), nil
return azureZone, nil
}
type publicZoneClient struct {
zone ServiceDiscoveryZone
recordClient *armdns.RecordSetsClient
}
// newPublicZoneClient creates publicZoneClient structure with initialized Azure client.
func newPublicZoneClient(zone ServiceDiscoveryZone, credential azcore.TokenCredential, environment cloud.Configuration) (*publicZoneClient, error) {
options := &arm.ClientOptions{
ClientOptions: azcore.ClientOptions{
Cloud: environment,
},
}
recordClient, err := armdns.NewRecordSetsClient(zone.SubscriptionID, credential, options)
if err != nil {
return nil, err
}
return &publicZoneClient{
zone: zone,
recordClient: recordClient,
}, nil
}
func (c publicZoneClient) Get(ctx context.Context, subDomain string) (armdns.RecordSetsClientGetResponse, error) {
return c.recordClient.Get(ctx, c.zone.ResourceGroup, c.zone.Name, subDomain, armdns.RecordTypeTXT, nil)
}
func (c publicZoneClient) CreateOrUpdate(ctx context.Context, subDomain string, rec armdns.RecordSet) (armdns.RecordSetsClientCreateOrUpdateResponse, error) {
return c.recordClient.CreateOrUpdate(ctx, c.zone.ResourceGroup, c.zone.Name, subDomain, armdns.RecordTypeTXT, rec, nil)
}
func (c publicZoneClient) Delete(ctx context.Context, subDomain string) (armdns.RecordSetsClientDeleteResponse, error) {
return c.recordClient.Delete(ctx, c.zone.ResourceGroup, c.zone.Name, subDomain, armdns.RecordTypeTXT, nil)
}
func publicUniqueRecords(recordSet armdns.RecordSet, value string) map[string]struct{} {
uniqRecords := map[string]struct{}{value: {}}
if recordSet.Properties != nil && recordSet.Properties.TxtRecords != nil {
for _, txtRecord := range recordSet.Properties.TxtRecords {
// Assume Value doesn't contain multiple strings
if len(txtRecord.Value) > 0 {
uniqRecords[deref(txtRecord.Value[0])] = struct{}{}
}
}
}
return uniqRecords
}

View file

@ -0,0 +1,126 @@
package azuredns
import (
"bytes"
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph"
)
type ServiceDiscoveryZone struct {
Name string
SubscriptionID string
ResourceGroup string
}
const (
ResourceGraphTypePublicDNSZone = "microsoft.network/dnszones"
ResourceGraphTypePrivateDNSZone = "microsoft.network/privatednszones"
)
const ResourceGraphQueryOptionsTop int32 = 1000
// discoverDNSZones finds all visible Azure DNS zones based on optional subscriptionID, resourceGroup and serviceDiscovery filter using Kusto query.
func discoverDNSZones(ctx context.Context, config *Config, credentials azcore.TokenCredential) (map[string]ServiceDiscoveryZone, error) {
options := &arm.ClientOptions{
ClientOptions: azcore.ClientOptions{
Cloud: config.Environment,
},
}
client, err := armresourcegraph.NewClient(credentials, options)
if err != nil {
return nil, err
}
// Set options
requestOptions := &armresourcegraph.QueryRequestOptions{
ResultFormat: to.Ptr(armresourcegraph.ResultFormatObjectArray),
Top: to.Ptr(ResourceGraphQueryOptionsTop),
Skip: to.Ptr[int32](0),
}
zones := map[string]ServiceDiscoveryZone{}
for {
// create the query request
request := armresourcegraph.QueryRequest{
Query: to.Ptr(createGraphQuery(config)),
Options: requestOptions,
}
result, err := client.Resources(ctx, request, nil)
if err != nil {
return zones, err
}
resultList, ok := result.Data.([]any)
if !ok {
// got invalid or empty data, skipping
break
}
for _, row := range resultList {
rowData, ok := row.(map[string]any)
if !ok {
continue
}
zoneName, ok := rowData["name"].(string)
if !ok {
continue
}
if _, exists := zones[zoneName]; exists {
return zones, fmt.Errorf(`found duplicate dns zone "%s"`, zoneName)
}
zones[zoneName] = ServiceDiscoveryZone{
Name: zoneName,
ResourceGroup: rowData["resourceGroup"].(string),
SubscriptionID: rowData["subscriptionId"].(string),
}
}
*requestOptions.Skip += ResourceGraphQueryOptionsTop
if result.TotalRecords != nil {
if int64(deref(requestOptions.Skip)) >= deref(result.TotalRecords) {
break
}
}
}
return zones, nil
}
func createGraphQuery(config *Config) string {
buf := new(bytes.Buffer)
buf.WriteString("\nresources\n")
resourceType := ResourceGraphTypePublicDNSZone
if config.PrivateZone {
resourceType = ResourceGraphTypePrivateDNSZone
}
_, _ = fmt.Fprintf(buf, "| where type =~ %q\n", resourceType)
if config.SubscriptionID != "" {
_, _ = fmt.Fprintf(buf, "| where subscriptionId =~ %q\n", config.SubscriptionID)
}
if config.ResourceGroup != "" {
_, _ = fmt.Fprintf(buf, "| where resourceGroup =~ %q\n", config.ResourceGroup)
}
if config.ServiceDiscoveryFilter != "" {
_, _ = fmt.Fprintf(buf, "| %s\n", config.ServiceDiscoveryFilter)
}
buf.WriteString("| project subscriptionId, resourceGroup, name")
return buf.String()
}

View file

@ -0,0 +1,130 @@
package azuredns
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_createGraphQuery(t *testing.T) {
testCases := []struct {
desc string
cfg *Config
expected string
}{
{
desc: "empty configuration (public)",
cfg: &Config{},
expected: `
resources
| where type =~ "microsoft.network/dnszones"
| project subscriptionId, resourceGroup, name`,
},
{
desc: "SubscriptionID (public)",
cfg: &Config{
SubscriptionID: "123",
},
expected: `
resources
| where type =~ "microsoft.network/dnszones"
| where subscriptionId =~ "123"
| project subscriptionId, resourceGroup, name`,
},
{
desc: "ResourceGroup (public)",
cfg: &Config{
ResourceGroup: "123",
},
expected: `
resources
| where type =~ "microsoft.network/dnszones"
| where resourceGroup =~ "123"
| project subscriptionId, resourceGroup, name`,
},
{
desc: "ServiceDiscoveryFilter (public)",
cfg: &Config{
ServiceDiscoveryFilter: "123",
},
expected: `
resources
| where type =~ "microsoft.network/dnszones"
| 123
| project subscriptionId, resourceGroup, name`,
},
{
desc: "empty configuration (private)",
cfg: &Config{
PrivateZone: true,
},
expected: `
resources
| where type =~ "microsoft.network/privatednszones"
| project subscriptionId, resourceGroup, name`,
},
{
desc: "SubscriptionID (private)",
cfg: &Config{
SubscriptionID: "123",
PrivateZone: true,
},
expected: `
resources
| where type =~ "microsoft.network/privatednszones"
| where subscriptionId =~ "123"
| project subscriptionId, resourceGroup, name`,
},
{
desc: "ResourceGroup (private)",
cfg: &Config{
ResourceGroup: "123",
PrivateZone: true,
},
expected: `
resources
| where type =~ "microsoft.network/privatednszones"
| where resourceGroup =~ "123"
| project subscriptionId, resourceGroup, name`,
},
{
desc: "ServiceDiscoveryFilter (private)",
cfg: &Config{
ServiceDiscoveryFilter: "123",
PrivateZone: true,
},
expected: `
resources
| where type =~ "microsoft.network/privatednszones"
| 123
| project subscriptionId, resourceGroup, name`,
},
{
desc: "all (private)",
cfg: &Config{
SubscriptionID: "123",
ResourceGroup: "456",
ServiceDiscoveryFilter: "789",
PrivateZone: true,
},
expected: `
resources
| where type =~ "microsoft.network/privatednszones"
| where subscriptionId =~ "123"
| where resourceGroup =~ "456"
| 789
| project subscriptionId, resourceGroup, name`,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
query := createGraphQuery(test.cfg)
assert.Equal(t, strings.ReplaceAll(test.expected, "\r", ""), query)
})
}
}