Traefik wildcard certificate using azure dns
wordpress meta
title: 'Traefik Wildcard Certificate using Azure DNS'
date: '2020-05-20T11:47:52-05:00'
status: publish
permalink: /traefik-wildcard-certificate-using-azure-dns
author: admin
excerpt: ''
type: post
id: 1620
category:
- Azure
- Docker
- LetsEncrypt
- Traefik
tag: []
post_format: []
title: 'Traefik Wildcard Certificate using Azure DNS'
date: '2020-05-20T11:47:52-05:00'
status: publish
permalink: /traefik-wildcard-certificate-using-azure-dns
author: admin
excerpt: ''
type: post
id: 1620
category:
- Azure
- Docker
- LetsEncrypt
- Traefik
tag: []
post_format: []
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
```bash $ az account list | jq '.[] | .id' "masked..." ````
Create role
```bash $ 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
```bash $ 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
```bash $ 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 Succeeded ````
role assign
```bash $ 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
```bash $ 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
```bash $ 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: true
````
WARNING: 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
```bash $ docker-compose up -d --build whoami is up-to-date Recreating traefik ... db is up-to-date ... Recreating traefik ... done ````
Showing some log issues you may see
```bash $ 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.acme
````
In 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.