diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 919ce614..2d8080e1 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -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`) diff --git a/docs/content/dns/zz_gen_azuredns.md b/docs/content/dns/zz_gen_azuredns.md index 2acfcf98..da7f2a29 100644 --- a/docs/content/dns/zz_gen_azuredns.md +++ b/docs/content/dns/zz_gen_azuredns.md @@ -48,15 +48,12 @@ lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ -AZURE_SUBSCRIPTION_ID= \ AZURE_RESOURCE_GROUP= \ lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Managed Identity (Azure Arc) AZURE_TENANT_ID= \ -AZURE_SUBSCRIPTION_ID= \ -AZURE_RESOURCE_GROUP= \ 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 diff --git a/go.mod b/go.mod index b5750d8f..9a49153a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 4d193e34..3af4dd59 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/providers/dns/azuredns/azuredns.go b/providers/dns/azuredns/azuredns.go index 5978ce2d..05c1788b 100644 --- a/providers/dns/azuredns/azuredns.go +++ b/providers/dns/azuredns/azuredns.go @@ -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 diff --git a/providers/dns/azuredns/azuredns.toml b/providers/dns/azuredns/azuredns.toml index 745b4438..7cd1b581 100644 --- a/providers/dns/azuredns/azuredns.toml +++ b/providers/dns/azuredns/azuredns.toml @@ -27,15 +27,12 @@ lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ -AZURE_SUBSCRIPTION_ID= \ AZURE_RESOURCE_GROUP= \ lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Managed Identity (Azure Arc) AZURE_TENANT_ID= \ -AZURE_SUBSCRIPTION_ID= \ -AZURE_RESOURCE_GROUP= \ 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" diff --git a/providers/dns/azuredns/azuredns_test.go b/providers/dns/azuredns/azuredns_test.go index cf15055a..7ddb4de4 100644 --- a/providers/dns/azuredns/azuredns_test.go +++ b/providers/dns/azuredns/azuredns_test.go @@ -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") diff --git a/providers/dns/azuredns/private.go b/providers/dns/azuredns/private.go index 27370e99..8e77bf4c 100644 --- a/providers/dns/azuredns/private.go +++ b/providers/dns/azuredns/private.go @@ -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 } diff --git a/providers/dns/azuredns/public.go b/providers/dns/azuredns/public.go index c6d689b3..07c00d32 100644 --- a/providers/dns/azuredns/public.go +++ b/providers/dns/azuredns/public.go @@ -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 } diff --git a/providers/dns/azuredns/servicediscovery.go b/providers/dns/azuredns/servicediscovery.go new file mode 100644 index 00000000..62dfd662 --- /dev/null +++ b/providers/dns/azuredns/servicediscovery.go @@ -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() +} diff --git a/providers/dns/azuredns/servicediscovery_test.go b/providers/dns/azuredns/servicediscovery_test.go new file mode 100644 index 00000000..6bf8b033 --- /dev/null +++ b/providers/dns/azuredns/servicediscovery_test.go @@ -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) + }) + } +}