dockersetup: add HTTPS support and use by default

If we want a minimum level of security we should enable HTTPS. However,
the only practical way we can do that without the user having to do further
infrastructure setup and/or pay a certification authority is to use a
self-signed certificate. Do this by default, and also provide an option
to specify a previously obtained certificate/key pair.

Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com>
This commit is contained in:
Paul Eggleton 2018-12-17 16:25:39 +13:00
parent c717a827d3
commit cb4955fe0b
3 changed files with 137 additions and 16 deletions

View File

@ -37,7 +37,7 @@ services:
# - "443:443" # - "443:443"
volumes: volumes:
- layersstatic:/usr/share/nginx/html - layersstatic:/usr/share/nginx/html
# - layerscerts:/etc/letsencrypt - ./docker/certs:/opt/cert
container_name: layersweb container_name: layersweb
layersrabbit: layersrabbit:
image: rabbitmq:alpine image: rabbitmq:alpine
@ -62,4 +62,3 @@ services:
volumes: volumes:
layersmeta: layersmeta:
layersstatic: layersstatic:
layerscerts:

7
docker/certs/README Normal file
View File

@ -0,0 +1,7 @@
This directory will be mounted as a volume in the Docker container setup to
contain SSL certificates for the web server.
If you run dockersetup.py and specify a certificate with --cert and
corresponding key with --key then they will be copied here; alternatively
let the setup script generate a self-signed certificate and key and they
will be written here as well.

View File

@ -18,21 +18,28 @@
# It will build and run these containers and set up the database. # It will build and run these containers and set up the database.
import sys import sys
import os
import argparse import argparse
import re import re
import subprocess import subprocess
import time import time
import random import random
import shutil
def get_args(): def get_args():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description='Script sets up the Layer Index tool with Docker Containers.')
description='Script sets up the Layer Index tool with Docker Containers.')
parser.add_argument('-o', '--hostname', type=str, help='Hostname of your machine. Defaults to localhost if not set.', required=False, default = "localhost") parser.add_argument('-o', '--hostname', type=str, help='Hostname of your machine. Defaults to localhost if not set.', required=False, default = "localhost")
parser.add_argument('-p', '--http-proxy', type=str, help='http proxy in the format http://<myproxy:port>', required=False) parser.add_argument('-p', '--http-proxy', type=str, help='http proxy in the format http://<myproxy:port>', required=False)
parser.add_argument('-s', '--https-proxy', type=str, help='https proxy in the format http://<myproxy:port>', required=False) parser.add_argument('-s', '--https-proxy', type=str, help='https proxy in the format http://<myproxy:port>', required=False)
parser.add_argument('-d', '--databasefile', type=str, help='Location of your database file to import. Must be a .sql file.', required=False) parser.add_argument('-d', '--databasefile', type=str, help='Location of your database file to import. Must be a .sql file.', required=False)
parser.add_argument('-m', '--portmapping', type=str, help='Port mapping in the format HOST:CONTAINER. Default is %(default)s', required=False, default='8080:80') parser.add_argument('-m', '--portmapping', type=str, help='Port mapping in the format HOST:CONTAINER. Default is %(default)s', required=False, default='8080:80,8081:443')
parser.add_argument('--no-https', action="store_true", default=False, help='Disable HTTPS (HTTP only) for web server')
parser.add_argument('--cert', type=str, help='Existing SSL certificate to use for HTTPS web serving', required=False)
parser.add_argument('--cert-key', type=str, help='Existing SSL certificate key to use for HTTPS web serving', required=False)
args = parser.parse_args() args = parser.parse_args()
port = proxymod = "" port = proxymod = ""
try: try:
if args.http_proxy: if args.http_proxy:
@ -42,10 +49,26 @@ def get_args():
except IndexError: except IndexError:
raise argparse.ArgumentTypeError("http_proxy must be in format http://<myproxy:port>") raise argparse.ArgumentTypeError("http_proxy must be in format http://<myproxy:port>")
for entry in args.portmapping.split(','):
if len(entry.split(":")) != 2:
raise argparse.ArgumentTypeError("Port mapping must in the format HOST:CONTAINER. Ex: 8080:80. Multiple mappings should be separated by commas.")
if len(args.portmapping.split(":")) != 2: if args.no_https:
raise argparse.ArgumentTypeError("Port mapping must in the format HOST:CONTAINER. Ex: 8080:80") if args.cert or args.cert_key:
return args.hostname, args.http_proxy, args.https_proxy, args.databasefile, port, proxymod, args.portmapping raise argparse.ArgumentTypeError("--no-https and --cert/--cert-key options are mutually exclusive")
if args.cert and not os.path.exists(args.cert):
raise argparse.ArgumentTypeError("Specified certificate file %s does not exist" % args.cert)
if args.cert_key and not os.path.exists(args.cert_key):
raise argparse.ArgumentTypeError("Specified certificate key file %s does not exist" % args.cert_key)
if args.cert_key and not args.cert:
raise argparse.ArgumentTypeError("Certificate key file specified but not certificate")
cert_key = args.cert_key
if args.cert and not cert_key:
cert_key = os.path.splitext(args.cert)[0] + '.key'
if not os.path.exists(cert_key):
raise argparse.ArgumentTypeError("Could not find certificate key, please use --cert-key to specify it")
return args.hostname, args.http_proxy, args.https_proxy, args.databasefile, port, proxymod, args.portmapping, args.no_https, args.cert, cert_key
# Edit http_proxy and https_proxy in Dockerfile # Edit http_proxy and https_proxy in Dockerfile
def edit_dockerfile(http_proxy, https_proxy): def edit_dockerfile(http_proxy, https_proxy):
@ -86,15 +109,24 @@ def edit_dockercompose(hostname, dbpassword, secretkey, portmapping):
filedata= readfile("docker-compose.yml") filedata= readfile("docker-compose.yml")
in_layersweb = False in_layersweb = False
in_layersweb_ports = False in_layersweb_ports = False
in_layersweb_ports_format = None
newlines = [] newlines = []
lines = filedata.splitlines() lines = filedata.splitlines()
for line in lines: for line in lines:
if in_layersweb_ports: if in_layersweb_ports:
format = line[0:line.find("-")].replace("#", "") format = line[0:line.find("-")].replace("#", "")
newlines.append(format + '- "' + portmapping + '"' + "\n") if in_layersweb_ports_format:
if format != in_layersweb_ports_format:
in_layersweb_ports = False in_layersweb_ports = False
in_layersweb = False in_layersweb = False
elif "layersweb:" in line: else:
continue
else:
in_layersweb_ports_format = format
for portmap in portmapping.split(','):
newlines.append(format + '- "' + portmap + '"' + "\n")
continue
if "layersweb:" in line:
in_layersweb = True in_layersweb = True
newlines.append(line + "\n") newlines.append(line + "\n")
elif "hostname:" in line: elif "hostname:" in line:
@ -117,6 +149,46 @@ def edit_dockercompose(hostname, dbpassword, secretkey, portmapping):
newlines.append(line + "\n") newlines.append(line + "\n")
writefile("docker-compose.yml", ''.join(newlines)) writefile("docker-compose.yml", ''.join(newlines))
def edit_nginx_ssl_conf(hostname, https_port, certdir, certfile, keyfile):
filedata = readfile('docker/nginx-ssl.conf')
newlines = []
lines = filedata.splitlines()
for line in lines:
if 'ssl_certificate ' in line:
format = line[0:line.find('ssl_certificate')]
newlines.append(format + 'ssl_certificate ' + os.path.join(certdir, certfile) + ';\n')
elif 'ssl_certificate_key ' in line:
format = line[0:line.find('ssl_certificate_key')]
newlines.append(format + 'ssl_certificate_key ' + os.path.join(certdir, keyfile) + ';\n')
# Add a line for the dhparam file
newlines.append(format + 'ssl_dhparam ' + os.path.join(certdir, 'dhparam.pem') + ';\n')
elif 'https://layers.openembedded.org' in line:
line = line.replace('https://layers.openembedded.org', 'https://%s:%s' % (hostname, https_port))
newlines.append(line + "\n")
else:
line = line.replace('layers.openembedded.org', hostname)
newlines.append(line + "\n")
# Write to a different file so we can still replace the hostname next time
writefile("docker/nginx-ssl-edited.conf", ''.join(newlines))
def edit_dockerfile_web(hostname, no_https):
filedata = readfile('Dockerfile.web')
newlines = []
lines = filedata.splitlines()
for line in lines:
if line.startswith('COPY ') and line.endswith('/etc/nginx/nginx.conf'):
if no_https:
srcfile = 'docker/nginx.conf'
else:
srcfile = 'docker/nginx-ssl-edited.conf'
line = 'COPY %s /etc/nginx/nginx.conf' % srcfile
newlines.append(line + "\n")
writefile("Dockerfile.web", ''.join(newlines))
def generatepasswords(passwordlength): def generatepasswords(passwordlength):
return ''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789!@#%^&*-_=+') for i in range(passwordlength)]) return ''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789!@#%^&*-_=+') for i in range(passwordlength)])
@ -137,7 +209,22 @@ secretkey = generatepasswords(50)
dbpassword = generatepasswords(10) dbpassword = generatepasswords(10)
## Get user arguments and modify config files ## Get user arguments and modify config files
hostname, http_proxy, https_proxy, dbfile, port, proxymod, portmapping = get_args() hostname, http_proxy, https_proxy, dbfile, port, proxymod, portmapping, no_https, cert, cert_key = get_args()
https_port = None
http_port = None
for portmap in portmapping.split(','):
outport, inport = portmap.split(':', 1)
if inport == '443':
https_port = outport
elif inport == '80':
http_port = outport
if (not https_port) and (not no_https):
print("No HTTPS port mapping (to port 443 inside the container) was specified and --no-https was not specified")
sys.exit(1)
if not (http_port or https_port):
print("Port mapping must include a mapping to port 80 or 443 inside the container (or both)")
sys.exit(1)
if http_proxy: if http_proxy:
edit_gitproxy(proxymod, port) edit_gitproxy(proxymod, port)
@ -146,6 +233,33 @@ if http_proxy or https_proxy:
edit_dockercompose(hostname, dbpassword, secretkey, portmapping) edit_dockercompose(hostname, dbpassword, secretkey, portmapping)
edit_dockerfile_web(hostname, no_https)
if not no_https:
local_cert_dir = os.path.abspath('docker/certs')
if cert:
if os.path.abspath(os.path.dirname(cert)) != local_cert_dir:
shutil.copy(cert, local_cert_dir)
certfile = os.path.basename(cert)
if os.path.abspath(os.path.dirname(cert_key)) != local_cert_dir:
shutil.copy(cert_key, local_cert_dir)
keyfile = os.path.basename(cert_key)
else:
print('')
print('Generating self-signed SSL certificate. Please specify your hostname (%s) when prompted for the Common Name.' % hostname)
certfile = 'setup-selfsigned.crt'
keyfile = 'setup-selfsigned.key'
return_code = subprocess.call('openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout %s -out %s' % (os.path.join(local_cert_dir, keyfile), os.path.join(local_cert_dir, certfile)), shell=True)
if return_code != 0:
print("Self-signed certificate generation failed")
sys.exit(1)
return_code = subprocess.call('openssl dhparam -out %s 2048' % os.path.join(local_cert_dir, 'dhparam.pem'), shell=True)
if return_code != 0:
print("DH group generation failed")
sys.exit(1)
edit_nginx_ssl_conf(hostname, https_port, '/opt/cert', certfile, keyfile)
## Start up containers ## Start up containers
return_code = subprocess.call("docker-compose up -d", shell=True) return_code = subprocess.call("docker-compose up -d", shell=True)
if return_code != 0: if return_code != 0:
@ -190,9 +304,10 @@ if return_code != 0:
sys.exit(1) sys.exit(1)
print("") print("")
ports = portmapping.split(':') if https_port and not no_https:
if ports[1] == '443':
protocol = 'https' protocol = 'https'
port = https_port
else: else:
protocol = 'http' protocol = 'http'
print("The application should now be accessible at %s://%s:%s" % (protocol, hostname, ports[0])) port = http_port
print("The application should now be accessible at %s://%s:%s" % (protocol, hostname, port))