Initial commit

master
Thor 4 years ago
commit 63f18972f9
  1. 2
      .gitignore
  2. 20
      LICENSE
  3. 104
      README.md
  4. 109
      certbot.py
  5. 30
      certbot_config.example.py

2
.gitignore vendored

@ -0,0 +1,2 @@
__pycache__
certbot_config.py

@ -0,0 +1,20 @@
Copyright (C) 2020 T.H.J. <thj@thj.no>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,104 @@
# Docker Nginx Certbot script
This is a Python 3 script that configures Let's Encrypt for an Nginx
server running inside of a Docker container.
## certbot.py
The main script. It expects a config file, located in the same directory,
named `certbot_config.py`. Make a copy of the included example configuration
and edit to taste.
## Dependencies
```
pip3 install crossplane
```
## Setup
`certbot` expects a file in the `.well-known` directory on the webroot when
verifying domains. Certbot typically modifies the Nginx configuration files
and reloads them underway in order to serve this directory via plain HTTP
before SSL is configured. Since the script uses the `webroot` plugin, we
need to take a different approach:
The script assumes that the SSL configuration directives for each Nginx host
is kept in separate files under `/etc/nginx/conf.d` per host. Before SSH is
enabled for a new host, its HTTPS configuration file should be a dotfile
in order to prevent Nginx from attempting to load it.
The plain HTTP configuration file for a host should look roughly like this:
```
server {
listen 80;
server_name host.tld www.host.tld;
location ~ /.well-known {
root /usr/share/nginx/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
```
It resembles the HTTP half of an Nginx host configuration file generated by
Certbot's Nginx plugin; the difference being that it serves `.well-known`
if requested, instead of unconditionally redirecting to HTTPS. The path
`/usr/share/nginx/certbot` on the container, mounted somewhere on the host,
is used by Certbot to configure all domains for SSL.
For `certbot.py` to work, it's required that all `include` statements in
your Nginx configuration files use relative paths, or Crossplane won't be
able to parse them properly.
For convenience, create a file `/etc/nginx/ssl.conf` looking roughly like this:
```
ssl_certificate /etc/letsencrypt/live/$PRIMARY_DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$PRIMARY_DOMAIN/privkey.pem;
ssl_dhparam ssl-dhparams.pem;
ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_timeout 1440m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS";
```
Be sure to substitute `$PRIMARY_DOMAIN` for the primary domain in your
`certbot_config.py` file, as this is the location Certbot will place your
certificates in.
The SSL version of your Nginx host configuration file would look roughly like this:
```
server {
listen 443 ssl;
server_name host.tld www.host.tld;
include ssl.conf;
location / {
root /usr/share/nginx/thj.no;
index index.html index.htm;
}
}
```
With these files in place, you can reload Nginx and run `./certbot.py`.
Certificate configuration should then commence in the usual fashion.
Once completed, rename all your SSL dotfiles to regular dotfiles and
reload Nginx again. You should now have a working SSL configuration
for all your domains.
To add new domains, simply follow the same procedure as above, while
leaving the other configuration files in place, and `certbot` will
generate a new certificate containing the new domains.

@ -0,0 +1,109 @@
#!/usr/bin/python3
# Dependencies:
# pip3 install crossplane
# https://github.com/nginxinc/crossplane
import sys
import crossplane
import pprint
import shlex
from subprocess import run
import certbot_config as config
# Resolve include directive (non-recursive)
def resolve_include(response, directive):
result = []
for index in directive['includes']:
result.extend(response['config'][index]['parsed'])
return result
# Resolve all include directives (recursive)
def resolve_includes(response, directives=None):
# Assume resolve of first file in response if not specified
if directives is None:
directives = response['config'][0]['parsed']
# Rebuild directive tree with includes in-place
result = []
for directive in directives:
if 'includes' in directive:
result.extend(resolve_includes(response, resolve_include(response, directive)))
continue
if 'block' in directive:
directive['block'] = resolve_includes(response, directive['block'])
result.append(directive)
return result
# Search in directives using matching function
def search_directives(directives, fn, recursive=False):
result = []
for directive in directives:
if fn(directive):
result.append(directive)
elif recursive and 'block' in directive:
result.extend(search_directives(directive['block'], fn, True))
return result
# Recursive search in directives, steered by matching functions
def resolve_path(directives, fns):
results = search_directives(directives, fns[0])
if len(results) > 0 and len(fns) > 1:
return resolve_path(results[0]['block'], fns[1:])
else:
return results
# Get all HTTP server directives
def get_servers(directives):
return resolve_path(directives, [
lambda d: ('directive', 'http') in d.items(),
lambda d: ('directive', 'server') in d.items()])
# Generate { server_name: root } dictionary of virtual hosts
def get_vhosts(directives):
servers = get_servers(directives)
vhosts = []
for server in servers:
server_name = search_directives(server['block'], lambda d: ('directive', 'server_name') in d.items())
if len(server_name) == 0:
continue
for name in server_name[0]['args']:
vhosts.append(name)
return vhosts
response = crossplane.parse(config.nginx_config_file)
if response['errors']:
pprint.pprint(response['errors'])
directives = resolve_includes(response)
vhosts = [ name for name in get_vhosts(directives) if not name in config.ignored_domains ]
if config.primary_domain in vhosts:
vhosts.remove(config.primary_domain)
vhosts.insert(0, config.primary_domain)
else:
print('Primary domain is missing from configuration')
sys.exit(1)
print()
print('Found virtual hosts:')
for name in vhosts:
print(' {}'.format(name))
print()
cmd = config.certbot_command
for name in vhosts:
cmd.extend(['-d', name])
print('Running', ' '.join(cmd))
print()
run(cmd)

@ -0,0 +1,30 @@
import sys
import os
# Docker project directory
project_dir = os.path.dirname(sys.argv[0])
# Email address for Let's Encrypt notices
email = 'user@host.tld'
# First domain in Certbot command line
primary_domain = 'domain.tld'
# Domains to ignore
ignored_domains = ['localhost']
# Location of Nginx configuration file
nginx_config_file = os.path.join(project_dir, 'data/etc/nginx/nginx.conf')
# Default arguments for Certbot
certbot_command = ['certbot',
'certonly',
'--config-dir', os.path.join(project_dir, 'data/etc/letsencrypt'),
# '--post-hook', 'docker-compose -f {} restart'.format(shlex.quote(os.path.join(PROJECT_DIR, 'docker-compose.yml'))),
'--webroot',
# '--staging',
'--agree-tos',
'--expand',
'-n',
'-m', email,
'-w', os.path.join(project_dir, 'data/usr/share/nginx/certbot')]
Loading…
Cancel
Save