dns challenge letsencrypt Azure DNS
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”t interrupt your web server and it works even if your server is unreachable from the outside world. Our DNS provider is Azure DNS.
Azure Configuration
Pre-req
- azure cli setup
- Wildcard DNS entry *.my.domain
Get subscription id
$ az account list | jq '.[] | .id'"masked..."Create role
$ az role definition create --role-definition role.json { "assignableScopes": [ "/subscriptions/masked..." ], "description": "Can manage DNS TXT records only.", "id": "/subscriptions/masked.../providers/Microsoft.Authorization/roleDefinitions/masked...", "name": "masked...", "permissions": [ { "actions": [ "Microsoft.Network/dnsZones/TXT/*", "Microsoft.Network/dnsZones/read", "Microsoft.Authorization/*/read", "Microsoft.Insights/alertRules/*", "Microsoft.ResourceHealth/availabilityStatuses/read", "Microsoft.Resources/deployments/read", "Microsoft.Resources/subscriptions/resourceGroups/read" ], "dataActions": [], "notActions": [], "notDataActions": [] } ], "roleName": "DNS TXT Contributor", "roleType": "CustomRole", "type": "Microsoft.Authorization/roleDefinitions" }NOTE: If you screwed up and need to delete do like like this: az role definition delete —name “DNS TXT Contributor”
Create json file with correct subscription and create role definition
$ cat role.json { "Name":"DNS TXT Contributor", "Id":"", "IsCustom":true, "Description":"Can manage DNS TXT records only.", "Actions":[ "Microsoft.Network/dnsZones/TXT/*", "Microsoft.Network/dnsZones/read", "Microsoft.Authorization/*/read", "Microsoft.Insights/alertRules/*", "Microsoft.ResourceHealth/availabilityStatuses/read", "Microsoft.Resources/deployments/read", "Microsoft.Resources/subscriptions/resourceGroups/read" ], "NotActions":[
], "AssignableScopes":[ "/subscriptions/masked..." ] }
$ az role definition create --role-definition role.json { "assignableScopes": [ "/subscriptions/masked..." ], "description": "Can manage DNS TXT records only.", "id": "/subscriptions/masked.../providers/Microsoft.Authorization/roleDefinitions/masked...", "name": "masked...", "permissions": [ { "actions": [ "Microsoft.Network/dnsZones/TXT/*", "Microsoft.Network/dnsZones/read", "Microsoft.Authorization/*/read", "Microsoft.Insights/alertRules/*", "Microsoft.ResourceHealth/availabilityStatuses/read", "Microsoft.Resources/deployments/read", "Microsoft.Resources/subscriptions/resourceGroups/read" ], "dataActions": [], "notActions": [], "notDataActions": [] } ], "roleName": "DNS TXT Contributor", "roleType": "CustomRole", "type": "Microsoft.Authorization/roleDefinitions" }Checking DNS and resource group
$ az network dns zone list [ { "etag": "masked...", "id": "/subscriptions/masked.../resourceGroups/sites/providers/Microsoft.Network/dnszones/iqonda.net", "location": "global", "maxNumberOfRecordSets": 10000, "name": "masked...", "nameServers": [ "ns1-09.azure-dns.com.", "ns2-09.azure-dns.net.", "ns3-09.azure-dns.org.", "ns4-09.azure-dns.info." ], "numberOfRecordSets": 14, "registrationVirtualNetworks": null, "resolutionVirtualNetworks": null, "resourceGroup": "masked...", "tags": {}, "type": "Microsoft.Network/dnszones", "zoneType": "Public" } ]
$ az network dns zone list --output table ZoneName ResourceGroup RecordSets MaxRecordSets ---------- --------------- ------------ --------------- masked... masked... 14 10000
$ az group list --output table Name Location Status ---------------------------------- -------------- --------- cloud-shell-storage-southcentralus southcentralus Succeeded masked... eastus Succeeded masked... eastus Succeeded masked... eastus Succeededrole assign
$ az ad sp create-for-rbac --name "Acme2DnsValidator" --role "DNS TXT Contributor" --scopes "/subscriptions/masked.../resourceGroups/sites/providers/Microsoft.Network/dnszones/masked..." Changing "Acme2DnsValidator" to a valid URI of "http://Acme2DnsValidator", which is the required format used for service principal names Found an existing application instance of "masked...". We will patch it Creating a role assignment under the scope of "/subscriptions/masked.../resourceGroups/sites/providers/Microsoft.Network/dnszones/masked..." { "appId": "masked...", "displayName": "Acme2DnsValidator", "name": "http://Acme2DnsValidator", "password": "masked...", "tenant": "masked..." }
$ az ad sp create-for-rbac --name "Acme2DnsValidator" --role "DNS TXT Contributor" --scopes "/subscriptions/masked.../resourceGroups/masked..." Changing "Acme2DnsValidator" to a valid URI of "http://Acme2DnsValidator", which is the required format used for service principal names Found an existing application instance of "masked...". We will patch it Creating a role assignment under the scope of "/subscriptions/masked.../resourceGroups/masked..." { "appId": "masked...", "displayName": "Acme2DnsValidator", "name": "http://Acme2DnsValidator", "password": "masked...", "tenant": "masked..." }
$ az role assignment list --all | jq -r '.[] | [.principalName,.roleDefinitionName,.scope]' [ "http://Acme2DnsValidator", "DNS TXT Contributor", "/subscriptions/masked.../resourceGroups/masked..." ] [ "masked...", "Owner", "/subscriptions/masked.../resourcegroups/masked.../providers/Microsoft.Storage/storageAccounts/masked..." ] [ "http://Acme2DnsValidator", "DNS TXT Contributor", "/subscriptions/masked.../resourceGroups/masked.../providers/Microsoft.Network/dnszones/masked..." ]
$ az ad sp list | jq -r '.[] | [.displayName,.appId]' 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...
[ "AzureDnsFrontendApp", "masked..." ]
[ "Azure DNS", "masked..." ]Traefik Configuration
reference
- Traefik use Lego the Let”s Encrypt client and ACME library written in Go
- Lego Azure section
- ACME
- Free Wildcard Certificates using Azure DNS, Let”s Encrypt and acme.sh
- DoTheEvo / Traefik-v2-examples
Azure Credentials in environment file
$ cat .env AZURE_CLIENT_ID=masked... AZURE_CLIENT_SECRET=masked... AZURE_SUBSCRIPTION_ID=masked... AZURE_TENANT_ID=masked... AZURE_RESOURCE_GROUP=masked... #AZURE_METADATA_ENDPOINT=Traefik Files
$ cat traefik.yml ## STATIC CONFIGURATION log: level: INFO
api: insecure: true dashboard: true
entryPoints: web: address: ":80" websecure: address: ":443"
providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false
certificatesResolvers: lets-encr: acme: #caServer: https://acme-staging-v02.api.letsencrypt.org/directory storage: acme.json email: admin@my.doman dnsChallenge: provider: azure
$ cat docker-compose.yml version: "3.3"
services:
traefik: image: "traefik:v2.2" container_name: "traefik" restart: always env_file: - .env command: #- "--log.level=DEBUG" - "--api.insecure=true" - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" labels: ## DNS CHALLENGE - "traefik.http.routers.traefik.tls.certresolver=lets-encr" - "traefik.http.routers.traefik.tls.domains[0].main=*.iqonda.net" - "traefik.http.routers.traefik.tls.domains[0].sans=iqonda.net" ## HTTP REDIRECT #- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" #- "traefik.http.routers.redirect-https.rule=hostregexp(`{host:.+}`)" #- "traefik.http.routers.redirect-https.entrypoints=web" #- "traefik.http.routers.redirect-https.middlewares=redirect-to-https" ports: - "80:80" - "8080:8080" #Web UI - "443:443" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./traefik.yml:/traefik.yml:ro" - "./acme.json:/acme.json" networks: - external_network
whoami: image: "containous/whoami" container_name: "whoami" restart: always labels: - "traefik.enable=true" - "traefik.http.routers.whoami.entrypoints=web" - "traefik.http.routers.whoami.rule=Host(`whoami.iqonda.net`)" #- "traefik.http.routers.whoami.tls.certresolver=lets-encr" #- "traefik.http.routers.whoami.tls=true" networks: - external_network
db: image: mariadb container_name: "db" volumes: - db_data:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: somewordpress MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress networks: - internal_network
wpsites: depends_on: - db ports: - 8002:80 image: wordpress:latest container_name: "wpsites" volumes: - /d01/html/wpsites.my.domain:/var/www/html restart: always environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wpsites WORDPRESS_DB_NAME: wpsites labels: - "traefik.enable=true" - "traefik.http.routers.wpsites.rule=Host(`wpsites.my.domain`)" - "traefik.http.routers.wpsites.entrypoints=websecure" - "traefik.http.routers.wpsites.tls.certresolver=lets-encr" - "traefik.http.routers.wpsites.service=wpsites-svc" - "traefik.http.services.wpsites-svc.loadbalancer.server.port=80" networks: - external_network - internal_network
volumes: db_data: {}
networks: external_network: internal_network: internal: trueWARNING: 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.
Start Containers
$ docker-compose up -d --buildwhoami is up-to-dateRecreating traefik ...db is up-to-date...Recreating traefik ... doneShowing some log issues you may see
$ docker logs traefik -f ... time="2020-05-17T21:17:40Z" level=info msg="Testing certificate renew..." providerName=lets-encr.acme ... time="2020-05-17T21:17:51Z" level=error msg="Unable to obtain ACME certificate for domains ..."AADSTS7000215: Invalid client secret is provided.
$ docker logs traefik -f ... \"keyType\":\"RSA4096\",\"dnsChallenge\":{\"provider\":\"azure\"},\"ResolverName\":\"lets-encr\",\"store\":{},\"ChallengeStore\":{}}" acme: error presenting token: azure: dns.ZonesClient#Get: Invalid input: autorest/validation: validation failed: parameter=resourceGroupName constraint=Pattern value=\"\\\"sites\\\"\" details: value
$ docker logs traefik -f ... 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\":{}}" time="2020-05-17T22:23:38Z" level=info msg="Testing certificate renew..." providerName=lets-encr.acme time="2020-05-17T22:23:38Z" level=info msg="Starting provider *traefik.Provider {}" 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 }}`)\",\"swarmModeRefreshSeconds\":15000000000}" time="2020-05-17T22:23:48Z" level=info msg=Register... providerName=lets-encr.acmeIn a browser looking at cert this means working but still stage url: CN=Fake LE Intermediate X1
NOTE: In Azure DNS activity log i can see TXT record was created and deleted. Record will be something like this: _acme-challenge.my.domain
Browser still not showing lock. Test with https://www.whynopadlock.com and in my case was just a hardcoded image on the page making it insecure.