diff --git a/Dockerfile b/Dockerfile index 1a479c6..b42c74f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,34 @@ -FROM nginx:1.8 +FROM nginx:1.8-alpine + +RUN apk update && apk add \ + python3 \ + openssl \ + && python3 -m ensurepip \ + && rm -r /usr/lib/python*/ensurepip \ + && pip3 install --upgrade pip setuptools \ + && rm -rf /var/cache/apk/* + +RUN pip3 install Jinja2==2.8 RUN rm /etc/nginx/nginx.conf RUN rm /etc/nginx/mime.types -COPY nginx.conf /etc/nginx/nginx.conf -COPY mime.types /etc/nginx/mime.types +COPY conf.d/nginx.conf /etc/nginx/nginx.conf +COPY conf.d/mime.types /etc/nginx/mime.types RUN rm /etc/nginx/conf.d/default.conf -COPY conf.d /etc/nginx/conf.d/ +COPY conf.d/default.conf /etc/nginx/conf.d/default.conf + +RUN mkdir /dockersible +COPY dockersible/ /dockersible +COPY backend.conf.j2 / +COPY install.py / + +RUN mkdir /certificates +VOLUME ["/certificates"] + +COPY entrypoint.sh / +RUN chmod 755 /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] -COPY ssl/lesspass.com.crt /etc/ssl/certs/lesspass.com.crt -COPY ssl/lesspass.com.key /etc/ssl/private/lesspass.com.key -COPY ssl/dhparam.pem /etc/ssl/certs/dhparam.pem -COPY ssl/AddTrustExternalCARoot.crt /etc/ssl/certs/AddTrustExternalCARoot.crt \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/README.md b/README.md index 160e53b..9dbcef1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # nginx nginx container for lesspass + diff --git a/conf.d/backend.conf b/backend.conf.j2 similarity index 83% rename from conf.d/backend.conf rename to backend.conf.j2 index fa3207f..8749bcc 100644 --- a/conf.d/backend.conf +++ b/backend.conf.j2 @@ -1,6 +1,6 @@ server { listen 80; - server_name localhost *.oslab.fr *.lesspass.com; + server_name {{ server_name }}; return 301 https://$server_name$request_uri; } @@ -10,12 +10,12 @@ server { listen [::]:443 ssl; listen 443 ssl; - server_name localhost *.oslab.fr *.lesspass.com; + server_name {{ server_name }}; charset utf-8; - ssl_certificate /etc/ssl/certs/lesspass.com.crt; - ssl_certificate_key /etc/ssl/private/lesspass.com.key; + ssl_certificate /etc/ssl/certs/certificate.crt; + ssl_certificate_key /etc/ssl/private/private.key; ssl_session_cache shared:SSL:20m; ssl_session_timeout 30m; @@ -26,8 +26,12 @@ server { ssl_stapling on; ssl_stapling_verify on; - ssl_dhparam /etc/ssl/certs/dhparam.pem; - ssl_trusted_certificate /etc/ssl/certs/AddTrustExternalCARoot.crt; +{% if dhparam %} + ssl_dhparam {{ dhparam_path }}; +{% endif %} +{% if ssl_trusted_certificate %} + ssl_trusted_certificate {{ ssl_trusted_certificate_path }}; +{% endif %} add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; diff --git a/mime.types b/conf.d/mime.types similarity index 100% rename from mime.types rename to conf.d/mime.types diff --git a/nginx.conf b/conf.d/nginx.conf similarity index 100% rename from nginx.conf rename to conf.d/nginx.conf diff --git a/docker.env b/docker.env new file mode 100644 index 0000000..9ff7907 --- /dev/null +++ b/docker.env @@ -0,0 +1,15 @@ +############################### +## self-signed certificates ## +############################### +domain=lesspass.com +server_name=localhost +############################### +## use custom certificate ## +############################### +#domain, server_name, private_key and certificate are mandatory +#domain=lesspass.com +#server_name=localhost *.lesspass.com +#private_key=lesspass.com.key +#certificate=lesspass.com.crt +#dhparam=dhparam.pem +#ssl_trusted_certificate=AddTrustExternalCARoot.crt diff --git a/dockersible/__init__.py b/dockersible/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dockersible/files.py b/dockersible/files.py new file mode 100644 index 0000000..0350b5d --- /dev/null +++ b/dockersible/files.py @@ -0,0 +1,44 @@ +import os +import shutil +import fnmatch + +from jinja2 import Template + + +def pattern_filter(file, patterns=None): + if patterns is None: + return True + + for p in patterns: + if fnmatch.fnmatch(file, p): + return True + + return False + + +def find(paths, patterns=None): + certificates = [] + for root, dirs, files in os.walk(paths): + for file in files: + if pattern_filter(file, patterns.split(',')): + certificates.append({'path': os.path.normpath(os.path.join(root, file))}) + return certificates + + +def copy(source, destination, basename=None, mode='0755'): + if not os.path.exists(destination): + os.makedirs(destination) + + shutil.copy2(src=source, dst=destination) + + file_path = os.path.join(destination, os.path.basename(source)) + os.chmod(file_path, int(mode, 8)) + + if basename: + os.rename(file_path, os.path.join(destination, basename)) + + +def template(source, context, destination): + jinja_template = Template(open(source).read()) + with open(destination, 'w') as f: + f.write(jinja_template.render(context)) diff --git a/dockersible/ssl.py b/dockersible/ssl.py new file mode 100644 index 0000000..b6c6c6a --- /dev/null +++ b/dockersible/ssl.py @@ -0,0 +1,18 @@ +import os +import shutil + + +def copy_certificates(certificates, destination='/etc/ssl', domain='example.org'): + private_key_folder = os.path.join(destination, 'private') + if not os.path.exists(private_key_folder): + os.makedirs(private_key_folder) + private_key = os.path.join(private_key_folder, domain + '.key') + shutil.copy2(certificates['key'], private_key) + os.chmod(private_key, 0o600) + + certificates_folder = os.path.join(destination, 'certs') + if not os.path.exists(certificates_folder): + os.makedirs(certificates_folder) + certificate = os.path.join(certificates_folder, domain + '.crt') + shutil.copy2(certificates['crt'], certificate) + os.chmod(certificate, 0o644) diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..b06ce4c --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +python3 install.py + +exec "$@" \ No newline at end of file diff --git a/install.py b/install.py new file mode 100644 index 0000000..4ff5320 --- /dev/null +++ b/install.py @@ -0,0 +1,59 @@ +import os + +import subprocess + +from dockersible.files import copy, template + + +def copy_certificates(): + copy(source=(os.path.join('/certificates', os.environ['private_key'])), + destination='/etc/ssl/private', + basename='private.key', + mode='0600') + copy(source=os.path.join('/certificates', os.environ['certificate']), + destination='/etc/ssl/certs', + basename='certificate.crt', + mode='0644') + context = { + 'server_name': os.environ['server_name'], + 'dhparam': False, + 'ssl_trusted_certificate': False + } + cert_folder = os.path.join('/etc/ssl/certs') + if 'dhparam' in os.environ: + dhparam = os.environ['dhparam'] + copy(source=os.path.join('/certificates', dhparam), destination=cert_folder, mode='0644') + context['dhparam'] = True + context['dhparam_path'] = os.path.join(cert_folder, dhparam) + if 'certificate' in os.environ: + certificate = os.environ['ssl_trusted_certificate'] + copy(source=os.path.join('/certificates', certificate), destination=cert_folder, mode='0644') + context['ssl_trusted_certificate'] = True + context['ssl_trusted_certificate_path'] = os.path.join(cert_folder, certificate) + return context + + +def create_certificates(): + cmd = """openssl req \ + -new \ + -newkey rsa:4096 \ + -days 365 \ + -nodes \ + -x509 \ + -subj "/C=US/ST=State/L=City/O=Company/CN=%s" \ + -keyout /etc/ssl/private/private.key \ + -out /etc/ssl/certs/certificate.crt""".format(os.environ['domain']) + subprocess.call(cmd, shell=True) + return { + 'server_name': os.environ['server_name'], + 'dhparam': False, + 'ssl_trusted_certificate': False + } + + +if __name__ == "__main__": + if 'private_key' in os.environ and 'certificate' in os.environ: + context = copy_certificates() + else: + context = create_certificates() + template('/backend.conf.j2', context, '/etc/nginx/conf.d/backend.conf') diff --git a/tests/templates/test.j2 b/tests/templates/test.j2 new file mode 100644 index 0000000..13c3927 --- /dev/null +++ b/tests/templates/test.j2 @@ -0,0 +1,3 @@ +{% if dhparam %} +ssl_dhparam {{ dhparam_path }}; +{% endif %} \ No newline at end of file diff --git a/tests/test_dockersible.py b/tests/test_dockersible.py new file mode 100644 index 0000000..07932d9 --- /dev/null +++ b/tests/test_dockersible.py @@ -0,0 +1,85 @@ +import os +import shutil +import tempfile +import unittest + +from dockersible.ssl import copy_certificates +from dockersible.files import find, copy, template + + +class DockersibleTestCase(unittest.TestCase): + def test_find(self): + parent_directory = os.path.dirname(os.path.realpath(__file__)) + ssl_directory = os.path.join(parent_directory, 'ssl') + certificates = find(paths=ssl_directory, patterns='*.key,*.crt') + for certificate in certificates: + expected_path = [os.path.join(ssl_directory, 'test.key'), os.path.join(ssl_directory, 'test.crt')] + self.assertTrue(certificate['path'] in expected_path) + + def test_copy_certificates(self): + temp_folder = tempfile.mkdtemp() + private_key_origin = os.path.join(temp_folder, 'test.key') + with open(private_key_origin, 'w') as f: f.write('') + certificate_origin = os.path.join(temp_folder, 'test.crt') + with open(certificate_origin, 'w') as f: f.write('') + certificates = { + 'key': private_key_origin, + 'crt': certificate_origin, + } + copy_certificates(certificates, temp_folder, 'oslab.fr') + private_key = os.path.join(temp_folder, 'private', 'oslab.fr.key') + self.assertTrue(os.path.exists(private_key)) + self.assertTrue((os.stat(private_key).st_mode & 0o777) == 0o600) + self.assertTrue(os.path.exists(private_key_origin)) + + certificate = os.path.join(temp_folder, 'certs', 'oslab.fr.crt') + self.assertTrue(os.path.exists(certificate)) + self.assertTrue((os.stat(certificate).st_mode & 0o777) == 0o644) + self.assertTrue(os.path.exists(certificate_origin)) + shutil.rmtree(temp_folder) + + def test_copy_file(self): + private_key_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ssl', 'test.key') + destination = tempfile.mkdtemp() + copy(source=private_key_path, destination=destination) + self.assertTrue(os.path.exists(os.path.join(destination, 'test.key'))) + shutil.rmtree(destination) + + def test_copy_file_change_basename(self): + private_key_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ssl', 'test.key') + destination = tempfile.mkdtemp() + copy(source=private_key_path, destination=destination, basename='lesspass.com.key', mode='0600') + self.assertTrue(os.path.exists(os.path.join(destination, 'lesspass.com.key'))) + shutil.rmtree(destination) + + def test_copy_file_change_mode(self): + private_key_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ssl', 'test.key') + destination = tempfile.mkdtemp() + + copy(source=private_key_path, destination=destination) + expected_private_key_path = os.path.join(destination, 'test.key') + self.assertTrue((os.stat(expected_private_key_path).st_mode & 0o777) == 0o755) + + copy(source=private_key_path, destination=destination, basename='lesspass.com.key', mode='0600') + expected_private_key_path = os.path.join(destination, 'lesspass.com.key') + self.assertTrue((os.stat(expected_private_key_path).st_mode & 0o777) == 0o600) + + shutil.rmtree(destination) + + def test_template_module_with_source_file(self): + template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'templates', 'test.j2') + destination = tempfile.mkdtemp() + context = { + 'dhparam': True, + 'dhparam_path': '/etc/ssl/certs/dhparam.pem' + } + destination_file = os.path.join(destination, 'test.txt') + + template(source=template_path, context=context, destination=destination_file) + + self.assertEqual('\nssl_dhparam /etc/ssl/certs/dhparam.pem;\n', open(destination_file).read()) + shutil.rmtree(destination) + + +if __name__ == '__main__': + unittest.main()