Skip to content

Commit 7c1f9ac

Browse files
authored
Merge pull request #8 from michaelamattes/master
Add Azure dns provider challenge
2 parents 6b681c7 + b739f25 commit 7c1f9ac

11 files changed

Lines changed: 351 additions & 156 deletions

examples/SAN_example.com.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,30 @@
2727
letsencrypt_s3_config_secret_key: !vault |
2828
$ANSIBLE_VAULT;1.1;AES256
2929
...
30+
31+
# Azure DNS
32+
- name: create the certificate for example.com
33+
hosts: localhost
34+
roles:
35+
- letsencrypt
36+
vars:
37+
letsencrypt_create_private_keys: true
38+
letsencrypt_do_http_challenge: false
39+
letsencrypt_do_dns_challenge: true
40+
letsencrypt_dns_provider: azure
41+
letsencrypt_use_acme_live_directory: true
42+
account_email: "ssl-admin@example.com"
43+
azure_resource_group: "azure_resource_group"
44+
convert_cert_to: pfx
45+
domain:
46+
certificate_name: "example.com"
47+
common_name: "www.example.com"
48+
zone: "example.com"
49+
email_address: "ssl-admin@example.com"
50+
subject_alt_name:
51+
top_level:
52+
- example.com
53+
- domain1.example.com
54+
- domain2.example.com
55+
second_level:
56+
- domain1.example.co.uk"

examples/wildcard.example.com.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# AutoDNS
12
- name: create the certificate for *.example.com
23
hosts: localhost
34
roles:
@@ -13,6 +14,7 @@
1314
letsencrypt_create_private_keys: true
1415
letsencrypt_do_http_challenge: false
1516
letsencrypt_do_dns_challenge: true
17+
letsencrypt_dns_provider: autodns
1618
letsencrypt_use_acme_live_directory: false
1719
account_email: "ssl-admin@example.com"
1820
private_key_content: !vault |
@@ -22,3 +24,26 @@
2224
dns_password: !vault |
2325
$ANSIBLE_VAULT;1.1;AES256
2426
...
27+
28+
# Azure DNS
29+
- name: create the certificate for *.example.com
30+
hosts: localhost
31+
roles:
32+
- letsencrypt
33+
vars:
34+
letsencrypt_create_private_keys: true
35+
letsencrypt_do_http_challenge: false
36+
letsencrypt_do_dns_challenge: true
37+
letsencrypt_dns_provider: azure
38+
letsencrypt_use_acme_live_directory: true
39+
account_email: "ssl-admin@example.com"
40+
azure_resource_group: "azure_resource_group"
41+
convert_cert_to: pfx
42+
domain:
43+
email_address: "ssl-admin@example.com"
44+
certificate_name: "wildcard.example.com"
45+
common_name: "*.example.com"
46+
zone: "example.com"
47+
subject_alt_name:
48+
top_level:
49+
- "example.com"

roles/letsencrypt/README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ rewrite (\.well-known/acme-challenge.*) https://letsencrypt-challenge-bucket.s3.
4747
```
4848
4949
## dns-challenge
50-
Currently the role only supports the InternetX autodns API. Feel free to contribute with other DNS APIs.
50+
Currently the role supports the InternetX autodns API and the Azure DNS API. Feel free to contribute with other DNS APIs.
5151
5252
## Variables for DNS & HTTP challenge
5353
@@ -57,13 +57,17 @@ Currently the role only supports the InternetX autodns API. Feel free to contrib
5757
| certificate_name | yes | | name of the resulting certificate. Most useful for wildcard certificates to not have files named '*.example.com' on the filesystem
5858
| common_name | yes | | domain for which the certificate will be created, non wildcards are also possible
5959
| zone | yes | | zone in which the dns records should be created
60-
| subject_alt_name | yes | | if you want to use wildcard-certificates use base name again as otherwise DNS txt record creation could fail
60+
| subject_alt_name | yes | | if you want to use
61+
wildcard-certificates use base name again as otherwise DNS txt record creation could fail
62+
| subject_alt_name: top_level: | no | | list of top-level domains
63+
| subject_alt_name: second_level: | no | | list of second_level domains
6164
| email_address | yes | | mail address which is used for the certificate (reminder mails are sent here)
6265
| **configuration options** | | |
6366
| private_key_content | no | | content of the created private key for the certificate (allows reuse of keys)
6467
| letsencrypt_do_http_challenge | yes | false | use http challenge
6568
| letsencrypt_do_dns_challenge | yes | false | use dns challenge
6669
| letsencrypt_use_acme_live_directory | no | false | choose if production certificates should be created, the staging directory of LE will be used by default
70+
| azure_resource_group | no | | Azure Resource Group for zone_name
6771
6872
## Variables for HTTP challenge
6973
@@ -76,10 +80,11 @@ Currently the role only supports the InternetX autodns API. Feel free to contrib
7680
7781
## Variables for dns-challenge
7882
79-
| Variable | Required | Default | Description
80-
|--------------|----------|-----------|------------
81-
| dns_user | yes | | username to access the DNS api
82-
| dns_password | yes | | password to access the DNS api
83+
| Variable | Required | Default | Description
84+
|--------------------------|----------|-----------|------------
85+
| dns_user | yes | | username to access the DNS api
86+
| dns_password | yes | | password to access the DNS api
87+
| letsencrypt_dns_provider | no | | which DNS provider should be used: autodns, azure
8388
8489
## global role variables
8590
@@ -89,14 +94,16 @@ Currently the role only supports the InternetX autodns API. Feel free to contrib
8994
| letsencrypt_conf_dir_user | yes | $USER | you can overwrite letsencrypt_conf_dir_user with a user who should be used when a group readable directory is used
9095
| letsencrypt_conf_dir_group | yes | $GROUP | you can overwrite letsencrypt_conf_dir_group with a group which consists of multiple users if ansible role is used in subteams
9196
| letsencrypt_prerequisites_packagemanager | yes | yum | set the packagemanager which is used of the ansible_host. Possible values are all supported package managers from ansible package module
92-
| acme_directory | yes | acme-staging-v02.api.letsencrypt.org | acme directory which will be used for certificate challenge
97+
| acme_staging_directory | no | acme-staging-v02.api.letsencrypt.org | acme directory which will be used for certificate challenge
98+
| acme_live_directory | no | acme-v02.api.letsencrypt.org | acme directory which will be used for certificate challenge
9399
| account_key_path | yes | $letsencrypt_conf_dir | path for account key of letsencrypt
94100
| csr_path | yes | $letsencrypt_conf_dir/certs | path for csr which is created for challenge
95101
| cert_path | yes | $letsencrypt_conf_dir/certs | path for issued certificate
96102
| intermediate_path | yes | $letsencrypt_conf_dir/certs | path for intermediate chain
97103
| fullchain_path | yes | $letsencrypt_conf_dir/certs | path for full chain file (certificate + intermediate)
98104
| private_key_path | yes | $letsencrypt_conf_dir/certs | path for private key
99105
| remaining_days | yes | 30 | min days remaining before certificate will be renewed
106+
| convert_cert_to | no | | format to convert the certificate to: `pfx`
100107
101108
### Usage
102109

roles/letsencrypt/defaults/main.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ letsencrypt_prerequisites_packagemanager: "yum"
1818
letsencrypt_s3_config_region: "eu-west-1"
1919

2020
### http challenge / dns challenge
21-
acme_directory: "https://acme-staging-v02.api.letsencrypt.org/directory"
21+
acme_staging_directory: "https://acme-staging-v02.api.letsencrypt.org/directory"
22+
acme_live_directory: "https://acme-v02.api.letsencrypt.org/directory"
2223
account_key_path: "{{ letsencrypt_conf_dir }}/letsencrypt_account.pem"
2324
csr_path: "{{ letsencrypt_cert_dir }}/{{ domain.certificate_name }}.csr"
2425
cert_path: "{{ letsencrypt_cert_dir }}/{{ domain.certificate_name }}.pem"
26+
pfx_cert_path: "{{ letsencrypt_cert_dir }}/{{ domain.certificate_name }}.pfx"
2527
intermediate_path: "{{ letsencrypt_cert_dir }}/{{ domain.certificate_name }}_intermediate.pem"
2628
fullchain_path: "{{ letsencrypt_cert_dir }}/{{ domain.certificate_name }}_fullchain.pem"
2729
private_key_path: "{{ letsencrypt_cert_dir }}/{{ domain.certificate_name }}.key"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
- name: convert certificate to pfx format
3+
openssl_pkcs12:
4+
action: export
5+
path: "{{ pfx_cert_path }}"
6+
name: "{{ domain.certificate_name }}"
7+
privatekey_path: "{{ private_key_path }}"
8+
certificate_path: "{{ fullchain_path }}"
9+
when: convert_cert_to == "pfx"
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
### include/role 3 - validate challenge
3+
- name: validate challenge only if it is created or changed
4+
when: challenge is changed
5+
block:
6+
- name: Login to autodns
7+
uri:
8+
url: "https://api.autodns.com/v1/login"
9+
method: POST
10+
body_format: json
11+
body:
12+
{ "context": "4",
13+
"user": "{{ dns_user }}",
14+
"password": "{{ dns_password }}"
15+
}
16+
register: login
17+
18+
- name: add a new TXT record to the common domain
19+
uri:
20+
url: "https://api.autodns.com/v1/zone/{{ domain.zone }}/a.ns14.net"
21+
method: PATCH
22+
body_format: json
23+
body:
24+
{
25+
"origin": "{{ domain.zone }}",
26+
"resourceRecordsAdd": [
27+
{
28+
"name": "_acme-challenge",
29+
"ttl": 60,
30+
"type": "TXT",
31+
"value": "{{ challenge['challenge_data'][domain.common_name]['dns-01']['resource_value'] }}"
32+
}
33+
]
34+
}
35+
headers:
36+
Cookie: "{{ login.set_cookie }}"
37+
when:
38+
# only runs if the challenge is run the first time, because then there is challenge_data
39+
- challenge['challenge_data'][domain.common_name] is defined
40+
41+
- name: add a new TXT record to the SAN domains
42+
uri:
43+
url: "https://api.autodns.com/v1/zone/{{ domain.zone }}/a.ns14.net"
44+
method: PATCH
45+
body_format: json
46+
body:
47+
{
48+
"origin": "{{ item }}",
49+
"resourceRecordsAdd": [
50+
{
51+
"name": "_acme-challenge.{{ item }}",
52+
"ttl": 60,
53+
"type": "TXT",
54+
"value": "{{ challenge['challenge_data'][item]['dns-01']['resource_value'] }}"
55+
}
56+
]
57+
}
58+
headers:
59+
Cookie: "{{ login.set_cookie }}"
60+
loop: "{{ domain.subject_alt_name }}"
61+
when:
62+
- domain.subject_alt_name is defined
63+
# only runs if the challenge is run the first time, because then there is challenge_data
64+
- challenge['challenge_data'][item] is defined
65+
66+
- name: Let the challenge be validated and retrieve the cert and intermediate certificate
67+
acme_certificate:
68+
account_key_src: "{{ account_key_path }}"
69+
account_email: "{{ account_email }}"
70+
csr: "{{ csr_path }}"
71+
cert: "{{ cert_path }}"
72+
fullchain: "{{ fullchain_path }}"
73+
chain: "{{ intermediate_path }}"
74+
challenge: dns-01
75+
force: "{{ force_renewal | default(false) }}"
76+
acme_directory: "{{ acme_directory }}"
77+
acme_version: 2
78+
terms_agreed: true
79+
remaining_days: "{{ remaining_days }}"
80+
data: "{{ challenge }}"
81+
82+
- name: remove created TXT record to keep DNS zone clean
83+
uri:
84+
url: "https://api.autodns.com/v1/zone/{{ domain.zone }}/a.ns14.net"
85+
method: PATCH
86+
body_format: json
87+
body:
88+
{
89+
"origin": "{{ domain.zone }}",
90+
"resourceRecordsRem": [
91+
{
92+
"name": "_acme-challenge",
93+
"ttl": 60,
94+
"type": "TXT",
95+
"value": "{{ challenge['challenge_data'][domain.common_name]['dns-01']['resource_value'] }}"
96+
}
97+
]
98+
}
99+
headers:
100+
Cookie: "{{ login.set_cookie }}"
101+
when:
102+
# only runs if the challenge is run the first time, because then there is challenge_data
103+
- challenge['challenge_data'][domain.common_name] is defined
104+
105+
- name: remove created SAN TXT records to keep DNS zone clean
106+
uri:
107+
url: "https://api.autodns.com/v1/zone/{{ domain.zone }}/a.ns14.net"
108+
method: PATCH
109+
body_format: json
110+
body:
111+
{
112+
"origin": "{{ item }}",
113+
"resourceRecordsRem": [
114+
{
115+
"name": "_acme-challenge.{{ item }}",
116+
"ttl": 60,
117+
"type": "TXT",
118+
"value": "{{ challenge['challenge_data'][item]['dns-01']['resource_value'] }}"
119+
}
120+
]
121+
}
122+
headers:
123+
Cookie: "{{ login.set_cookie }}"
124+
loop: "{{ domain.subject_alt_name }}"
125+
when:
126+
- domain.subject_alt_name is defined
127+
# only runs if the challenge is run the first time, because then there is challenge_data
128+
- challenge['challenge_data'][item] is defined
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
- name: create TXT record in a new record set
3+
azure_rm_dnsrecordset:
4+
resource_group: "{{ azure_resource_group }}"
5+
relative_name: "_acme-challenge"
6+
zone_name: "{{ domain.zone }}"
7+
record_mode: append
8+
state: present
9+
record_type: TXT
10+
records:
11+
- entry: "{{ challenge['challenge_data'][domain.common_name]['dns-01']['resource_value'] }}"
12+
13+
# split top_level for zone_name and if subdomain is defined add subdomain to relative_name
14+
- name: add a new TXT record to the SAN top-level domains
15+
azure_rm_dnsrecordset:
16+
resource_group: "{{ azure_resource_group }}"
17+
relative_name: "{{ '_acme-challenge' if item == item.split('.')[-2:] | join('.') else '_acme-challenge.' + item.split('.')[:-2] | join('.') }}"
18+
zone_name: "{{ item.split('.')[-2:] | join('.') }}"
19+
record_mode: append
20+
state: present
21+
record_type: TXT
22+
records:
23+
- entry: "{{ challenge['challenge_data'][item]['dns-01']['resource_value'] }}"
24+
loop: "{{ domain.subject_alt_name.top_level }}"
25+
when: domain.subject_alt_name.top_level is defined
26+
27+
# split second_level for zone_name and if subdomain is defined add subdomain to relative_name
28+
- name: add a new TXT record to the SAN second-level domains
29+
azure_rm_dnsrecordset:
30+
resource_group: "{{ azure_resource_group }}"
31+
relative_name: "{{ '_acme-challenge' if item == item.split('.')[-3:] | join('.') else '_acme-challenge.' + item.split('.')[:-3] | join('.') }}"
32+
zone_name: "{{ item.split('.')[-3:] | join('.') }}"
33+
record_mode: append
34+
state: present
35+
record_type: TXT
36+
records:
37+
- entry: "{{ challenge['challenge_data'][item]['dns-01']['resource_value'] }}"
38+
loop: "{{ domain.subject_alt_name.second_level }}"
39+
when: domain.subject_alt_name.second_level is defined
40+
41+
- name: Let the challenge be validated and retrieve the cert and intermediate certificate
42+
acme_certificate:
43+
account_key_src: "{{ account_key_path }}"
44+
account_email: "{{ account_email }}"
45+
csr: "{{ csr_path }}"
46+
cert: "{{ cert_path }}"
47+
fullchain: "{{ fullchain_path }}"
48+
chain: "{{ intermediate_path }}"
49+
challenge: dns-01
50+
force: "{{ force_renewal | default(false) }}"
51+
acme_directory: "{{ acme_directory }}"
52+
acme_version: 2
53+
terms_agreed: true
54+
remaining_days: "{{ remaining_days }}"
55+
data: "{{ challenge }}"
56+
57+
- name: remove created TXT record to keep DNS zone clean
58+
azure_rm_dnsrecordset:
59+
resource_group: "{{ azure_resource_group }}"
60+
relative_name: "_acme-challenge"
61+
zone_name: "{{ domain.zone }}"
62+
state: absent
63+
record_type: TXT
64+
records:
65+
- entry: "{{ challenge['challenge_data'][domain.common_name]['dns-01']['resource_value'] }}"
66+
67+
- name: remove created SAN top-level TXT records to keep DNS zone clean
68+
azure_rm_dnsrecordset:
69+
resource_group: "{{ azure_resource_group }}"
70+
relative_name: "{{ '_acme-challenge' if item == item.split('.')[-2:] | join('.') else '_acme-challenge.' + item.split('.')[:-2] | join('.') }}"
71+
zone_name: "{{ item.split('.')[-2:] | join('.') }}"
72+
state: absent
73+
record_type: TXT
74+
records:
75+
- entry: "{{ challenge['challenge_data'][item]['dns-01']['resource_value'] }}"
76+
loop: "{{ domain.subject_alt_name.top_level }}"
77+
when: domain.subject_alt_name.top_level is defined
78+
79+
- name: remove created SAN second-level TXT records to keep DNS zone clean
80+
azure_rm_dnsrecordset:
81+
resource_group: "{{ azure_resource_group }}"
82+
relative_name: "{{ '_acme-challenge' if item == item.split('.')[-3:] | join('.') else '_acme-challenge.' + item.split('.')[:-3] | join('.') }}"
83+
zone_name: "{{ item.split('.')[-3:] | join('.') }}"
84+
state: absent
85+
record_type: TXT
86+
records:
87+
- entry: "{{ challenge['challenge_data'][item]['dns-01']['resource_value'] }}"
88+
loop: "{{ domain.subject_alt_name.second_level }}"
89+
when: domain.subject_alt_name.second_level is defined

0 commit comments

Comments
 (0)