Skip to content

Commit ce92bb7

Browse files
committed
Release v0.3.0
- Add support of Cloudflare DNS - Reformat with Black - Use poetry for installation - Update ACMEv2 compatibility according to latest changes
1 parent fc5ccc8 commit ce92bb7

File tree

13 files changed

+903
-378
lines changed

13 files changed

+903
-378
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,4 @@ ENV/
8989
.ropeproject
9090

9191
.idea/
92+
*.swp

README.md

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ Simple way to get SSL certificates for free.
1616

1717
## Features
1818

19-
* Supports both Python 2 and Python 3
20-
* Works with both ACMEv1 and ACMEv2 protocols
19+
* Supports both Python 2 (deprecated) and Python 3
20+
* Works with both ACMEv1 (deprecated) and ACMEv2 protocols
2121
* Can issue [wildcard certificates](https://en.wikipedia.org/wiki/Wildcard_certificate)!
2222
* Easy to use and extend
2323

@@ -33,7 +33,7 @@ to send `SIGHUP` to it during challenge completion.
3333
As you may not trust this script feel free to check source code,
3434
it's under 700 lines of code.
3535

36-
Script should be run as root on host with running nginx server.
36+
Script should be run as root on host with running nginx server if you use http verification or if you use DNS verification as a regular user.
3737
Domain for which you request certificate should point to that host's IP and port
3838
80 should be available from outside if you use HTTP challenge.
3939
Script can generate all keys for you if you don't set them with command line arguments.
@@ -46,16 +46,20 @@ Should work with Python >= 2.6
4646

4747
## ACME v2
4848

49-
ACME v2 requires more logic so it's not as small as acme v1 script.
49+
ACME v2 requires more logic so it's not as small as ACME v1 script.
5050

5151
ACME v2 is supported partially: only `http-01` and `dns-01` challenges.
5252
Check https://tools.ietf.org/html/draft-ietf-acme-acme-07#section-9.7.6
5353

5454
New protocol is used by default.
5555

56-
`http-01` challenge is passed exactly as in v1 protocol realisation.
56+
`http-01` challenge is passed exactly as in v1 protocol realization.
5757

58-
`dns-01` currently supports only DigitalOcean, AWS Route53 DNS providers.
58+
`dns-01` currently supports following providers:
59+
60+
- DigitalOcean
61+
- AWS Route53
62+
- Cloudflare
5963

6064
Technically nginx is not needed for this type of challenge but script still calls nginx reload by default
6165
because it assumes that you store certificates on the same server where you issue
@@ -65,7 +69,7 @@ AWS Route53 uses `default` profile in session, specifying profile works with env
6569
Please check https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#environment-variable-configuration
6670

6771
In case you want to add support of different DNS providers your contribution is
68-
highly apprectiated.
72+
highly appreciated.
6973

7074
Wildcard certificates can not be issued with non-wildcard for the same domain.
7175
I.e. it's not possible to issue certificates for `*.example.com` and
@@ -78,21 +82,35 @@ Only HTTP challenge is supported at the moment.
7882

7983
## Installation
8084

81-
Please be informed that the quickiest and easiest way of installation is to use your OS
82-
installation way because Python way includes compilation of dependencies that
85+
Python 2 installation may require compilation of dependencies that
8386
may take much time and CPU resources and may require you to install all build
8487
dependencies.
8588

86-
### Fastest way
89+
### Preferred way
8790

88-
Just download executable compiled with [pyinstaller](https://github.com/pyinstaller/pyinstaller).
91+
Using [poetry](https://python-poetry.org/).
8992

90-
```
91-
wget https://github.com/kshcherban/acme-nginx/releases/download/v0.1.2/acme-nginx
92-
chmod +x acme-nginx
93-
```
93+
1. First [install](https://python-poetry.org/docs/) poetry:
94+
95+
```bash
96+
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 -
97+
source ~/.poetry/env
98+
```
99+
100+
2. Clone acme-nginx:
94101

95-
### Python way
102+
```bash
103+
git clone https://github.com/kshcherban/acme-nginx
104+
```
105+
106+
3. Install it:
107+
108+
```bash
109+
cd acme-nginx
110+
poetry install
111+
```
112+
113+
### Python pip way
96114

97115
Automatically
98116
```
@@ -124,8 +142,6 @@ docker cp acme:/usr/bin/acme-runner acme-nginx
124142
docker rm acme
125143
```
126144

127-
128-
129145
### Debian/Ubuntu way
130146

131147
```
@@ -173,13 +189,12 @@ Oct 12 23:42:23 Removing /etc/nginx/sites-enabled/letsencrypt and sending HUP to
173189
Certificate was generated into `/etc/ssl/private/letsencrypt-domain.pem`
174190

175191
You can now configure nginx to use it:
176-
```
192+
```nginx
177193
server {
178194
listen 443;
179195
ssl on;
180196
ssl_certificate /etc/ssl/private/letsencrypt-domain.pem;
181197
ssl_certificate_key /etc/ssl/private/letsencrypt-domain.key;
182-
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
183198
...
184199
```
185200

@@ -199,7 +214,7 @@ sudo acme-nginx \
199214
### Wildcard certificates
200215

201216
For wildcard certificate you need to have your domain managed by DNS provider
202-
with API. Currently only [DigitalOcean DNS](https://www.digitalocean.com/docs/networking/dns/) and
217+
with API. Currently only [DigitalOcean DNS](https://www.digitalocean.com/docs/networking/dns/), [Cloudflare](https://cloudflare.com) and
203218
[AWS Route53](https://aws.amazon.com/route53/) are supported.
204219

205220
Example how to get wildcard certificate without nginx
@@ -211,12 +226,25 @@ sudo acme-nginx --no-reload-nginx --dns-provider route53 -d "*.example.com"
211226

212227
Please create and export your DO API token as `API_TOKEN` env variable.
213228
Now you can generate wildcard certificate
214-
```
229+
230+
```bash
215231
sudo su -
216232
export API_TOKEN=yourDigitalOceanApiToken
217233
acme-nginx --dns-provider digitalocean -d '*.example.com'
218234
```
219235

236+
### Cloudflare
237+
238+
[Create API token](https://dash.cloudflare.com/profile/api-tokens) first. Then export it as `API_TOKEN` environment variable and use like this:
239+
240+
```bash
241+
sudo su -
242+
export API_TOKEN=yourCloudflareApiToken
243+
acme-nginx --dns-provider cloudflare -d '*.example.com'
244+
```
245+
246+
247+
220248
### Debug
221249

222250
To debug please use `--debug` flag. With debug enabled all intermediate files

acme-runner.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"""Convenience wrapper for running acme-nginx directly from source tree."""
55

66
from acme_nginx.client import main
7+
78
# uncomment this line for pyinstaller, this is boto3 dependency that pyinstaller ignores
8-
#import configparser
9+
# import configparser
910

10-
if __name__ == '__main__':
11+
if __name__ == "__main__":
1112
main()

acme_nginx/AWSRoute53.py

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
class AWSRoute53(object):
55
def __init__(self):
66
self.session = boto3.Session()
7-
self.client = self.session.client('route53')
7+
self.client = self.session.client("route53")
88

99
def determine_domain(self, domain):
1010
"""
@@ -14,15 +14,15 @@ def determine_domain(self, domain):
1414
Returns:
1515
zone_id, string, hosted zone id of matching domain
1616
"""
17-
if not domain.endswith('.'):
18-
domain = domain + '.'
17+
if not domain.endswith("."):
18+
domain = domain + "."
1919
# use paginator to iterate over all hosted zones
20-
paginator = self.client.get_paginator('list_hosted_zones')
20+
paginator = self.client.get_paginator("list_hosted_zones")
2121
# https://github.com/boto/botocore/issues/1535 result_key_iters is undocumented
2222
for page in paginator.paginate().result_key_iters():
2323
for result in page:
24-
if result['Name'] in domain:
25-
return result['Id']
24+
if result["Name"] in domain:
25+
return result["Id"]
2626

2727
def create_record(self, name, data, domain):
2828
"""
@@ -36,30 +36,26 @@ def create_record(self, name, data, domain):
3636
"""
3737
zone_id = self.determine_domain(domain)
3838
if not zone_id:
39-
raise Exception('Hosted zone for domain {0} not found'.format(domain))
39+
raise Exception("Hosted zone for domain {0} not found".format(domain))
4040
response = self.client.change_resource_record_sets(
4141
HostedZoneId=zone_id,
4242
ChangeBatch={
43-
'Changes': [
43+
"Changes": [
4444
{
45-
'Action': 'UPSERT',
46-
'ResourceRecordSet': {
47-
'Name': name,
48-
'Type': 'TXT',
49-
'TTL': 60,
50-
'ResourceRecords': [
51-
{
52-
'Value': '"{0}"'.format(data)
53-
}
54-
]
55-
}
45+
"Action": "UPSERT",
46+
"ResourceRecordSet": {
47+
"Name": name,
48+
"Type": "TXT",
49+
"TTL": 60,
50+
"ResourceRecords": [{"Value": '"{0}"'.format(data)}],
51+
},
5652
}
5753
]
58-
}
54+
},
5955
)
60-
waiter = self.client.get_waiter('resource_record_sets_changed')
61-
waiter.wait(Id=response['ChangeInfo']['Id'])
62-
return {'name': name, 'data': data}
56+
waiter = self.client.get_waiter("resource_record_sets_changed")
57+
waiter.wait(Id=response["ChangeInfo"]["Id"])
58+
return {"name": name, "data": data}
6359

6460
def delete_record(self, record, domain):
6561
"""
@@ -72,20 +68,18 @@ def delete_record(self, record, domain):
7268
self.client.change_resource_record_sets(
7369
HostedZoneId=zone_id,
7470
ChangeBatch={
75-
'Changes': [
71+
"Changes": [
7672
{
77-
'Action': 'DELETE',
78-
'ResourceRecordSet': {
79-
'Name': record['name'],
80-
'Type': 'TXT',
81-
'TTL': 60,
82-
'ResourceRecords': [
83-
{
84-
'Value': '"{0}"'.format(record['data'])
85-
}
86-
]
87-
}
73+
"Action": "DELETE",
74+
"ResourceRecordSet": {
75+
"Name": record["name"],
76+
"Type": "TXT",
77+
"TTL": 60,
78+
"ResourceRecords": [
79+
{"Value": '"{0}"'.format(record["data"])}
80+
],
81+
},
8882
}
8983
]
90-
}
84+
},
9185
)

0 commit comments

Comments
 (0)