Skip to content

Commit

Permalink
Release v0.3.0
Browse files Browse the repository at this point in the history
- Add support of Cloudflare DNS
- Reformat with Black
- Use poetry for installation
- Update ACMEv2 compatibility according to latest changes
  • Loading branch information
kshcherban committed Jul 14, 2021
1 parent fc5ccc8 commit ce92bb7
Show file tree
Hide file tree
Showing 13 changed files with 903 additions and 378 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ ENV/
.ropeproject

.idea/
*.swp
72 changes: 50 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Simple way to get SSL certificates for free.

## Features

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

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

Script should be run as root on host with running nginx server.
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.
Domain for which you request certificate should point to that host's IP and port
80 should be available from outside if you use HTTP challenge.
Script can generate all keys for you if you don't set them with command line arguments.
Expand All @@ -46,16 +46,20 @@ Should work with Python >= 2.6

## ACME v2

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

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

New protocol is used by default.

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

`dns-01` currently supports only DigitalOcean, AWS Route53 DNS providers.
`dns-01` currently supports following providers:

- DigitalOcean
- AWS Route53
- Cloudflare

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

In case you want to add support of different DNS providers your contribution is
highly apprectiated.
highly appreciated.

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

## Installation

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

### Fastest way
### Preferred way

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

```
wget https://github.com/kshcherban/acme-nginx/releases/download/v0.1.2/acme-nginx
chmod +x acme-nginx
```
1. First [install](https://python-poetry.org/docs/) poetry:

```bash
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 -
source ~/.poetry/env
```

2. Clone acme-nginx:

### Python way
```bash
git clone https://github.com/kshcherban/acme-nginx
```

3. Install it:

```bash
cd acme-nginx
poetry install
```

### Python pip way

Automatically
```
Expand Down Expand Up @@ -124,8 +142,6 @@ docker cp acme:/usr/bin/acme-runner acme-nginx
docker rm acme
```



### Debian/Ubuntu way

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

You can now configure nginx to use it:
```
```nginx
server {
listen 443;
ssl on;
ssl_certificate /etc/ssl/private/letsencrypt-domain.pem;
ssl_certificate_key /etc/ssl/private/letsencrypt-domain.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
...
```

Expand All @@ -199,7 +214,7 @@ sudo acme-nginx \
### Wildcard certificates

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

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

Please create and export your DO API token as `API_TOKEN` env variable.
Now you can generate wildcard certificate
```

```bash
sudo su -
export API_TOKEN=yourDigitalOceanApiToken
acme-nginx --dns-provider digitalocean -d '*.example.com'
```

### Cloudflare

[Create API token](https://dash.cloudflare.com/profile/api-tokens) first. Then export it as `API_TOKEN` environment variable and use like this:

```bash
sudo su -
export API_TOKEN=yourCloudflareApiToken
acme-nginx --dns-provider cloudflare -d '*.example.com'
```



### Debug

To debug please use `--debug` flag. With debug enabled all intermediate files
Expand Down
5 changes: 3 additions & 2 deletions acme-runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"""Convenience wrapper for running acme-nginx directly from source tree."""

from acme_nginx.client import main

# uncomment this line for pyinstaller, this is boto3 dependency that pyinstaller ignores
#import configparser
# import configparser

if __name__ == '__main__':
if __name__ == "__main__":
main()
66 changes: 30 additions & 36 deletions acme_nginx/AWSRoute53.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class AWSRoute53(object):
def __init__(self):
self.session = boto3.Session()
self.client = self.session.client('route53')
self.client = self.session.client("route53")

def determine_domain(self, domain):
"""
Expand All @@ -14,15 +14,15 @@ def determine_domain(self, domain):
Returns:
zone_id, string, hosted zone id of matching domain
"""
if not domain.endswith('.'):
domain = domain + '.'
if not domain.endswith("."):
domain = domain + "."
# use paginator to iterate over all hosted zones
paginator = self.client.get_paginator('list_hosted_zones')
paginator = self.client.get_paginator("list_hosted_zones")
# https://github.com/boto/botocore/issues/1535 result_key_iters is undocumented
for page in paginator.paginate().result_key_iters():
for result in page:
if result['Name'] in domain:
return result['Id']
if result["Name"] in domain:
return result["Id"]

def create_record(self, name, data, domain):
"""
Expand All @@ -36,30 +36,26 @@ def create_record(self, name, data, domain):
"""
zone_id = self.determine_domain(domain)
if not zone_id:
raise Exception('Hosted zone for domain {0} not found'.format(domain))
raise Exception("Hosted zone for domain {0} not found".format(domain))
response = self.client.change_resource_record_sets(
HostedZoneId=zone_id,
ChangeBatch={
'Changes': [
"Changes": [
{
'Action': 'UPSERT',
'ResourceRecordSet': {
'Name': name,
'Type': 'TXT',
'TTL': 60,
'ResourceRecords': [
{
'Value': '"{0}"'.format(data)
}
]
}
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": name,
"Type": "TXT",
"TTL": 60,
"ResourceRecords": [{"Value": '"{0}"'.format(data)}],
},
}
]
}
},
)
waiter = self.client.get_waiter('resource_record_sets_changed')
waiter.wait(Id=response['ChangeInfo']['Id'])
return {'name': name, 'data': data}
waiter = self.client.get_waiter("resource_record_sets_changed")
waiter.wait(Id=response["ChangeInfo"]["Id"])
return {"name": name, "data": data}

def delete_record(self, record, domain):
"""
Expand All @@ -72,20 +68,18 @@ def delete_record(self, record, domain):
self.client.change_resource_record_sets(
HostedZoneId=zone_id,
ChangeBatch={
'Changes': [
"Changes": [
{
'Action': 'DELETE',
'ResourceRecordSet': {
'Name': record['name'],
'Type': 'TXT',
'TTL': 60,
'ResourceRecords': [
{
'Value': '"{0}"'.format(record['data'])
}
]
}
"Action": "DELETE",
"ResourceRecordSet": {
"Name": record["name"],
"Type": "TXT",
"TTL": 60,
"ResourceRecords": [
{"Value": '"{0}"'.format(record["data"])}
],
},
}
]
}
},
)
Loading

0 comments on commit ce92bb7

Please sign in to comment.