{"id":1620,"date":"2020-05-20T11:47:52","date_gmt":"2020-05-20T16:47:52","guid":{"rendered":"https:\/\/blog.iqonda.net\/?p=1620"},"modified":"2020-05-20T12:12:55","modified_gmt":"2020-05-20T17:12:55","slug":"traefik-wildcard-certificate-using-azure-dns","status":"publish","type":"post","link":"https:\/\/blog.ls-al.com\/traefik-wildcard-certificate-using-azure-dns\/","title":{"rendered":"Traefik Wildcard Certificate using Azure DNS"},"content":{"rendered":"
Using Traefik as edge router(reverse proxy) to http sites and enabling a Lets Encrypt ACME v2 wildcard certificate on the docker Traefik container. Verify ourselves using DNS, specifically the dns-01 method, because DNS verification doesn\u2019t interrupt your web server and it works even if your server is unreachable from the outside world. Our DNS provider is Azure DNS.<\/p>\n
$\u00a0az account list | jq '.[] | .id'\n\"masked...\"<\/code><\/pre>\nCreate role<\/h2>\n$\u00a0az role definition create --role-definition role.json \n {\n \"assignableScopes\": [\n \"\/subscriptions\/masked...\"\n ],\n \"description\": \"Can manage DNS TXT records only.\",\n \"id\": \"\/subscriptions\/masked...\/providers\/Microsoft.Authorization\/roleDefinitions\/masked...\",\n \"name\": \"masked...\",\n \"permissions\": [\n {\n \"actions\": [\n \"Microsoft.Network\/dnsZones\/TXT\/*\",\n \"Microsoft.Network\/dnsZones\/read\",\n \"Microsoft.Authorization\/*\/read\",\n \"Microsoft.Insights\/alertRules\/*\",\n \"Microsoft.ResourceHealth\/availabilityStatuses\/read\",\n \"Microsoft.Resources\/deployments\/read\",\n \"Microsoft.Resources\/subscriptions\/resourceGroups\/read\"\n ],\n \"dataActions\": [],\n \"notActions\": [],\n \"notDataActions\": []\n }\n ],\n \"roleName\": \"DNS TXT Contributor\",\n \"roleType\": \"CustomRole\",\n \"type\": \"Microsoft.Authorization\/roleDefinitions\"\n }<\/code><\/pre>\nNOTE: If you screwed up and need to delete do like like this:
\naz role definition delete --name "DNS TXT Contributor"<\/em><\/p>\nCreate json file with correct subscription and create role definition<\/h2>\n$ cat role.json\n {\n \"Name\":\"DNS TXT Contributor\",\n \"Id\":\"\",\n \"IsCustom\":true,\n \"Description\":\"Can manage DNS TXT records only.\",\n \"Actions\":[\n \"Microsoft.Network\/dnsZones\/TXT\/*\",\n \"Microsoft.Network\/dnsZones\/read\",\n \"Microsoft.Authorization\/*\/read\",\n \"Microsoft.Insights\/alertRules\/*\",\n \"Microsoft.ResourceHealth\/availabilityStatuses\/read\",\n \"Microsoft.Resources\/deployments\/read\",\n \"Microsoft.Resources\/subscriptions\/resourceGroups\/read\"\n ],\n \"NotActions\":[\n\n ],\n \"AssignableScopes\":[\n \"\/subscriptions\/masked...\"\n ]\n }\n\n $\u00a0az role definition create --role-definition role.json \n {\n \"assignableScopes\": [\n \"\/subscriptions\/masked...\"\n ],\n \"description\": \"Can manage DNS TXT records only.\",\n \"id\": \"\/subscriptions\/masked...\/providers\/Microsoft.Authorization\/roleDefinitions\/masked...\",\n \"name\": \"masked...\",\n \"permissions\": [\n {\n \"actions\": [\n \"Microsoft.Network\/dnsZones\/TXT\/*\",\n \"Microsoft.Network\/dnsZones\/read\",\n \"Microsoft.Authorization\/*\/read\",\n \"Microsoft.Insights\/alertRules\/*\",\n \"Microsoft.ResourceHealth\/availabilityStatuses\/read\",\n \"Microsoft.Resources\/deployments\/read\",\n \"Microsoft.Resources\/subscriptions\/resourceGroups\/read\"\n ],\n \"dataActions\": [],\n \"notActions\": [],\n \"notDataActions\": []\n }\n ],\n \"roleName\": \"DNS TXT Contributor\",\n \"roleType\": \"CustomRole\",\n \"type\": \"Microsoft.Authorization\/roleDefinitions\"\n }<\/code><\/pre>\nChecking DNS and resource group<\/h2>\n$\u00a0az network dns zone list\n [\n {\n \"etag\": \"masked...\",\n \"id\": \"\/subscriptions\/masked...\/resourceGroups\/sites\/providers\/Microsoft.Network\/dnszones\/iqonda.net\",\n \"location\": \"global\",\n \"maxNumberOfRecordSets\": 10000,\n \"name\": \"masked...\",\n \"nameServers\": [\n \"ns1-09.azure-dns.com.\",\n \"ns2-09.azure-dns.net.\",\n \"ns3-09.azure-dns.org.\",\n \"ns4-09.azure-dns.info.\"\n ],\n \"numberOfRecordSets\": 14,\n \"registrationVirtualNetworks\": null,\n \"resolutionVirtualNetworks\": null,\n \"resourceGroup\": \"masked...\",\n \"tags\": {},\n \"type\": \"Microsoft.Network\/dnszones\",\n \"zoneType\": \"Public\"\n }\n ]\n\n$\u00a0az network dns zone list --output table\n ZoneName ResourceGroup RecordSets MaxRecordSets\n ---------- --------------- ------------ ---------------\n masked... masked... 14 10000\n\n$\u00a0az group list --output table\n Name Location Status\n ---------------------------------- -------------- ---------\n cloud-shell-storage-southcentralus southcentralus Succeeded\n masked... eastus Succeeded\n masked... eastus Succeeded\n masked... eastus Succeeded<\/code><\/pre>\nrole assign<\/h2>\n $\u00a0az ad sp create-for-rbac --name \"Acme2DnsValidator\" --role \"DNS TXT Contributor\" --scopes \"\/subscriptions\/masked...\/resourceGroups\/sites\/providers\/Microsoft.Network\/dnszones\/masked...\"\n Changing \"Acme2DnsValidator\" to a valid URI of \"http:\/\/Acme2DnsValidator\", which is the required format used for service principal names\n Found an existing application instance of \"masked...\". We will patch it\n Creating a role assignment under the scope of \"\/subscriptions\/masked...\/resourceGroups\/sites\/providers\/Microsoft.Network\/dnszones\/masked...\"\n {\n \"appId\": \"masked...\",\n \"displayName\": \"Acme2DnsValidator\",\n \"name\": \"http:\/\/Acme2DnsValidator\",\n \"password\": \"masked...\",\n \"tenant\": \"masked...\"\n }\n\n $\u00a0az ad sp create-for-rbac --name \"Acme2DnsValidator\" --role \"DNS TXT Contributor\" --scopes \"\/subscriptions\/masked...\/resourceGroups\/masked...\"\n Changing \"Acme2DnsValidator\" to a valid URI of \"http:\/\/Acme2DnsValidator\", which is the required format used for service principal names\n Found an existing application instance of \"masked...\". We will patch it\n Creating a role assignment under the scope of \"\/subscriptions\/masked...\/resourceGroups\/masked...\"\n {\n \"appId\": \"masked...\",\n \"displayName\": \"Acme2DnsValidator\",\n \"name\": \"http:\/\/Acme2DnsValidator\",\n \"password\": \"masked...\",\n \"tenant\": \"masked...\"\n }\n\n $\u00a0az role assignment list --all | jq -r '.[] | [.principalName,.roleDefinitionName,.scope]'\n [\n \"http:\/\/Acme2DnsValidator\",\n \"DNS TXT Contributor\",\n \"\/subscriptions\/masked...\/resourceGroups\/masked...\"\n ]\n [\n \"masked...\",\n \"Owner\",\n \"\/subscriptions\/masked...\/resourcegroups\/masked...\/providers\/Microsoft.Storage\/storageAccounts\/masked...\"\n ]\n [\n \"http:\/\/Acme2DnsValidator\",\n \"DNS TXT Contributor\",\n \"\/subscriptions\/masked...\/resourceGroups\/masked...\/providers\/Microsoft.Network\/dnszones\/masked...\"\n ]\n\n$\u00a0az ad sp list | jq -r '.[] | [.displayName,.appId]'\n The result is not complete. You can still use '--all' to get all of them with long latency expected, or provide a filter through command arguments\n...\n\n [\n \"AzureDnsFrontendApp\",\n \"masked...\"\n ]\n\n [\n \"Azure DNS\",\n \"masked...\"\n ]<\/code><\/pre>\nTraefik Configuration<\/h3>\nreference<\/h2>\n\n- Traefik use Lego the Let\u2019s Encrypt client and ACME library written in Go<\/a><\/li>\n
- Lego Azure section<\/a><\/li>\n
- ACME<\/a><\/li>\n
- Free Wildcard Certificates using Azure DNS, Let\u2019s Encrypt and acme.sh<\/a><\/li>\n
- DoTheEvo \/ Traefik-v2-examples<\/a><\/li>\n<\/ul>\n
Azure Credentials in environment file<\/h2>\n$ cat .env\n AZURE_CLIENT_ID=masked...\n AZURE_CLIENT_SECRET=masked...\n AZURE_SUBSCRIPTION_ID=masked...\n AZURE_TENANT_ID=masked...\n AZURE_RESOURCE_GROUP=masked...\n #AZURE_METADATA_ENDPOINT=<\/code><\/pre>\nTraefik Files<\/h2>\n $ cat traefik.yml \n ## STATIC CONFIGURATION\n log:\n level: INFO\n\n api:\n insecure: true\n dashboard: true\n\n entryPoints:\n web:\n address: \":80\"\n websecure:\n address: \":443\"\n\n providers:\n docker:\n endpoint: \"unix:\/\/\/var\/run\/docker.sock\"\n exposedByDefault: false\n\n certificatesResolvers:\n lets-encr:\n acme:\n #caServer: https:\/\/acme-staging-v02.api.letsencrypt.org\/directory\n storage: acme.json\n email: admin@my.doman\n dnsChallenge:\n provider: azure\n\n $ cat docker-compose.yml \n version: \"3.3\"\n\n services:\n\n traefik:\n image: \"traefik:v2.2\"\n container_name: \"traefik\"\n restart: always\n env_file:\n - .env\n command:\n #- \"--log.level=DEBUG\"\n - \"--api.insecure=true\"\n - \"--providers.docker=true\"\n - \"--providers.docker.exposedbydefault=false\"\n labels:\n ## DNS CHALLENGE\n - \"traefik.http.routers.traefik.tls.certresolver=lets-encr\"\n - \"traefik.http.routers.traefik.tls.domains[0].main=*.iqonda.net\"\n - \"traefik.http.routers.traefik.tls.domains[0].sans=iqonda.net\"\n ## HTTP REDIRECT\n #- \"traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https\"\n #- \"traefik.http.routers.redirect-https.rule=hostregexp({host:.+}<\/code>)\"\n #- \"traefik.http.routers.redirect-https.entrypoints=web\"\n #- \"traefik.http.routers.redirect-https.middlewares=redirect-to-https\"\n ports:\n - \"80:80\"\n - \"8080:8080\" #Web UI\n - \"443:443\"\n volumes:\n - \"\/var\/run\/docker.sock:\/var\/run\/docker.sock:ro\"\n - \".\/traefik.yml:\/traefik.yml:ro\"\n - \".\/acme.json:\/acme.json\"\n networks:\n - external_network\n\n whoami:\n image: \"containous\/whoami\"\n container_name: \"whoami\"\n restart: always\n labels:\n - \"traefik.enable=true\"\n - \"traefik.http.routers.whoami.entrypoints=web\"\n - \"traefik.http.routers.whoami.rule=Host(whoami.iqonda.net<\/code>)\"\n #- \"traefik.http.routers.whoami.tls.certresolver=lets-encr\"\n #- \"traefik.http.routers.whoami.tls=true\"\n networks:\n - external_network\n\n db:\n image: mariadb\n container_name: \"db\"\n volumes:\n - db_data:\/var\/lib\/mysql\n restart: always\n environment:\n MYSQL_ROOT_PASSWORD: somewordpress\n MYSQL_DATABASE: wordpress\n MYSQL_USER: wordpress\n MYSQL_PASSWORD: wordpress\n networks:\n - internal_network\n\n wpsites:\n depends_on:\n - db\n ports:\n - 8002:80\n image: wordpress:latest\n container_name: \"wpsites\"\n volumes:\n - \/d01\/html\/wpsites.my.domain:\/var\/www\/html\n restart: always\n environment:\n WORDPRESS_DB_HOST: db:3306\n WORDPRESS_DB_USER: wpsites\n WORDPRESS_DB_NAME: wpsites\n labels:\n - \"traefik.enable=true\"\n - \"traefik.http.routers.wpsites.rule=Host(wpsites.my.domain<\/code>)\"\n - \"traefik.http.routers.wpsites.entrypoints=websecure\"\n - \"traefik.http.routers.wpsites.tls.certresolver=lets-encr\"\n - \"traefik.http.routers.wpsites.service=wpsites-svc\"\n - \"traefik.http.services.wpsites-svc.loadbalancer.server.port=80\"\n networks:\n - external_network\n - internal_network\n\n volumes:\n db_data: {}\n\n networks:\n external_network:\n internal_network:\n internal: true<\/code><\/pre>\nWARNING: If you are not using the staging endpoint for LetsEncrypt strongly reconside doing that while working on this. You can get blocked for a week.<\/p>\n
Start Containers<\/h2>\n$ docker-compose up -d --build\nwhoami is up-to-date\nRecreating traefik ... \ndb is up-to-date\n...\nRecreating traefik ... done<\/code><\/pre>\nShowing some log issues you may see<\/h2>\n$ docker logs traefik -f\n ...\n time=\"2020-05-17T21:17:40Z\" level=info msg=\"Testing certificate renew...\" providerName=lets-encr.acme\n ...\n time=\"2020-05-17T21:17:51Z\" level=error msg=\"Unable to obtain ACME certificate for domains\n ...\"AADSTS7000215: Invalid client secret is provided.\n\n$ docker logs traefik -f\n ...\n \\\"keyType\\\":\\\"RSA4096\\\",\\\"dnsChallenge\\\":{\\\"provider\\\":\\\"azure\\\"},\\\"ResolverName\\\":\\\"lets-encr\\\",\\\"store\\\":{},\\\"ChallengeStore\\\":{}}\"\n acme: error presenting token: azure: dns.ZonesClient#Get: Invalid input: autorest\/validation: validation failed: parameter=resourceGroupName constraint=Pattern value=\\\"\\\\\\\"sites\\\\\\\"\\\" details: value \n\n$ docker logs traefik -f\n ...\n time=\"2020-05-17T22:23:38Z\" level=info msg=\"Starting provider *acme.Provider {\\\"email\\\":\\\"admin@iqonda.com\\\",\\\"caServer\\\":\\\"https:\/\/acme-staging-v02.api.letsencrypt.org\/ directory\\\",\\\"storage\\\":\\\"acme.json\\\",\\\"keyType\\\":\\\"RSA4096\\\",\\\"dnsChallenge\\\":{\\\"provider\\\":\\\"azure\\\"},\\\"ResolverName\\\":\\\"lets-encr\\\",\\\"store\\\":{},\\\"ChallengeStore\\\":{}}\"\n time=\"2020-05-17T22:23:38Z\" level=info msg=\"Testing certificate renew...\" providerName=lets-encr.acme\n time=\"2020-05-17T22:23:38Z\" level=info msg=\"Starting provider *traefik.Provider {}\"\n time=\"2020-05-17T22:23:38Z\" level=info msg=\"Starting provider *docker.Provider {\\\"watch\\\":true,\\\"endpoint\\\":\\\"unix:\/\/\/var\/run\/docker.sock\\\",\\\"defaultRule\\\":\\\"Host({{ normalize .Name }}<\/code>)\\\",\\\"swarmModeRefreshSeconds\\\":15000000000}\"\n time=\"2020-05-17T22:23:48Z\" level=info msg=Register... providerName=lets-encr.acme<\/code><\/pre>\n\nIn a browser looking at cert this means working but still stage url: CN=Fake LE Intermediate X1<\/em><\/p>\n<\/blockquote>\nNOTE: In Azure DNS activity log i can see TXT record was created and deleted. Record will be something like this: _acme-challenge.my.domain<\/em><\/p>\n