From 2c5dace56e395fe0842ac401db55aeae77bfa855 Mon Sep 17 00:00:00 2001 From: Shuai Lin Date: Tue, 15 Nov 2016 12:11:58 +0800 Subject: [PATCH] Support https with letsencrypt. TODO: add a cron job to renew the certs --- .dockerignore | 5 ++++ image/base/requirements.in | 1 + image/base/requirements.txt | 6 ++--- image/seafile/Dockerfile | 3 ++- image/seafile/seafile.nginx.conf | 30 ++++++++++++++++++++++ launcher | 13 +++++++--- samples/server.conf | 1 + scripts/bootstrap.py | 23 +++++++++++++++-- scripts/ssl.sh | 44 ++++++++++++++++++++++++++++++++ scripts/start.py | 21 ++++++++++++--- scripts/utils/__init__.py | 9 ++++++- 11 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 .dockerignore create mode 100755 scripts/ssl.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f54cb69 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +*~ +*# +*.swp +.DS_Store +*.pyc diff --git a/image/base/requirements.in b/image/base/requirements.in index acb8891..0704657 100644 --- a/image/base/requirements.in +++ b/image/base/requirements.in @@ -9,3 +9,4 @@ click==6.6 termcolor==1.1.0 prettytable==0.7.2 colorlog==2.7.0 +Jinja2==2.8 diff --git a/image/base/requirements.txt b/image/base/requirements.txt index 72595ac..3094155 100644 --- a/image/base/requirements.txt +++ b/image/base/requirements.txt @@ -6,12 +6,10 @@ # click==6.6 colorlog==2.7.0 +Jinja2==2.8 +MarkupSafe==0.23 # via jinja2 prettytable==0.7.2 python-memcached==1.58 six==1.10.0 # via python-memcached termcolor==1.1.0 urllib3==1.19 - -# The following packages are commented out because they are -# considered to be unsafe in a requirements file: -# setuptools # via python-ldap diff --git a/image/seafile/Dockerfile b/image/seafile/Dockerfile index add3cbb..98ccd2e 100644 --- a/image/seafile/Dockerfile +++ b/image/seafile/Dockerfile @@ -10,4 +10,5 @@ RUN mkdir -p /opt/seafile/ && \ RUN mkdir -p /etc/my_init.d ADD create_data_links.sh /etc/my_init.d/create_data_links.sh -ADD seafile.nginx.conf /etc/nginx/sites-enabled/seafile.nginx.conf +RUN mkdir -p /templates/ +ADD seafile.nginx.conf /templates/seafile.nginx.conf diff --git a/image/seafile/seafile.nginx.conf b/image/seafile/seafile.nginx.conf index 97368b8..ceac245 100644 --- a/image/seafile/seafile.nginx.conf +++ b/image/seafile/seafile.nginx.conf @@ -1,6 +1,30 @@ +{% if https -%} server { listen 80; server_name _ default_server; + rewrite ^ https://{{ domain }}$request_uri? permanent; +} +{% endif -%} + +server { +{% if https -%} + listen 443; + ssl on; + ssl_certificate /shared/ssl/{{ domain }}.crt; + ssl_certificate_key /shared/ssl/{{ domain }}.key; + + ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS; + + # TODO: More SSL security hardening: ssl_session_tickets & ssl_dhparam + # ssl_session_tickets on; + # ssl_session_ticket_key /etc/nginx/sessionticket.key; + # ssl_session_cache shared:SSL:10m; + # ssl_session_timeout 10m; +{% else -%} + listen 80; +{% endif -%} + + server_name {{ domain }}; client_max_body_size 10m; @@ -47,4 +71,10 @@ server { root /opt/seafile/seafile-server-latest/seahub; autoindex off; } + + # For letsencrypt + location /.well-known/acme-challenge/ { + alias /var/www/challenges/; + try_files $uri =404; + } } diff --git a/launcher b/launcher index 95ab25e..db453b7 100755 --- a/launcher +++ b/launcher @@ -35,6 +35,11 @@ init_shared() { fi } +set_ports() { + ports="-p 80:80 -p 443:443" + ports="" +} + set_volumes() { local mounts init_shared @@ -61,12 +66,14 @@ bootstrap() { err_and_quit "The file $bootstrap_conf doesn't exist. Have you run seafile-server-setup?" fi set_volumes - docker run --rm -it -e SEAFILE_BOOTSRAP=1 $volumes $image /sbin/my_init -- /scripts/bootstrap.py + set_ports + docker run --rm -it --name seafile-bootstrap -e SEAFILE_BOOTSRAP=1 $volumes $ports $image /sbin/my_init -- /scripts/bootstrap.py } start() { set_volumes - docker run --rm -it --name seafile $volumes $image \ + set_ports + docker run --rm -it --name seafile $volumes $ports $image \ /sbin/my_init -- /scripts/start.py # /sbin/my_init -- bash -l } @@ -75,7 +82,7 @@ enter() { err_and_quit "Not implemented yet" } -function main { +main() { local action while [[ $# -gt 0 ]] do diff --git a/samples/server.conf b/samples/server.conf index bf66497..00a21dc 100644 --- a/samples/server.conf +++ b/samples/server.conf @@ -1,4 +1,5 @@ [server] server.hostname = seafile.example.com +server.https = true admin.email = me@example.com admin.password = asecret \ No newline at end of file diff --git a/scripts/bootstrap.py b/scripts/bootstrap.py index 12287bc..e129191 100755 --- a/scripts/bootstrap.py +++ b/scripts/bootstrap.py @@ -13,15 +13,34 @@ import shutil import sys import uuid -from utils import call, get_conf, get_install_dir, get_script +from utils import call, get_conf, get_install_dir, get_script, render_nginx_conf installdir = get_install_dir() topdir = dirname(installdir) shared_seafiledir = '/shared/seafile' +ssl_dir = '/shared/ssl' + +def init_letsencryt(): + if not exists(ssl_dir): + os.mkdir(ssl_dir) + + domain = get_conf('server.hostname') + context = { + 'https': False, + 'domain': domain, + } + render_nginx_conf('/templates/seafile.nginx.conf', + '/etc/nginx/sites-enabled/seafile.nginx.conf', context) + call('nginx -s reload') + call('/scripts/ssl.sh {0} {1}'.format(ssl_dir, domain)) def main(): if not exists(shared_seafiledir): os.mkdir(shared_seafiledir) + + if get_conf('server.https', '').lower() == 'true': + init_letsencryt() + env = { 'SERVER_NAME': 'seafile', 'SERVER_IP': get_conf('server.hostname'), @@ -34,7 +53,7 @@ def main(): # Change the script to allow mysql root password to be empty call('''sed -i -e 's/if not mysql_root_passwd/if not mysql_root_passwd and "MYSQL_ROOT_PASSWD" not in os.environ/g' {}''' - .format(get_script('setup-seafile-mysql.py')), check_call=True) + .format(get_script('setup-seafile-mysql.py'))) setup_script = get_script('setup-seafile-mysql.sh') call('{} auto -n seafile'.format(setup_script), env=env) diff --git a/scripts/ssl.sh b/scripts/ssl.sh new file mode 100755 index 0000000..b4fdc67 --- /dev/null +++ b/scripts/ssl.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -e + +ssldir=${1:?"error params"} +domain=${2:?"error params"} + +letsencryptdir=$ssldir/letsencrypt +letsencrypt_script=$letsencryptdir/acme_tiny.py + +ssl_account_key=${domain}.account.key +ssl_csr=${domain}.csr +ssl_key=${domain}.key +ssl_crt=${domain}.crt + +mkdir -p /var/www/challenges && chmod -R 777 /var/www/challenges +mkdir -p ssldir + +if ! [[ -d $letsencryptdir ]]; then + git clone git://github.com/diafygi/acme-tiny.git $letsencryptdir +else + cd $letsencryptdir + git pull origin master:master +fi + +cd $ssldir + +if [[ ! -e ${ssl_account_key} ]]; then + openssl genrsa 4096 > ${ssl_account_key} +fi + +if [[ ! -e ${ssl_key} ]]; then + openssl genrsa 4096 > ${ssl_key} +fi + +if [[ ! -e ${ssl_csr} ]]; then + openssl req -new -sha256 -key ${ssl_key} -subj "/CN=$domain" > $ssl_csr +fi + +python $letsencrypt_script --account-key ${ssl_account_key} --csr $ssl_csr --acme-dir /var/www/challenges/ > ./signed.crt +curl -sSL -o intermediate.pem https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem +cat signed.crt intermediate.pem > ${ssl_crt} + +nginx -s reload diff --git a/scripts/start.py b/scripts/start.py index ad4fed0..3410b9b 100755 --- a/scripts/start.py +++ b/scripts/start.py @@ -14,7 +14,7 @@ import shutil import sys import time -from utils import call, get_conf, get_install_dir, get_script, get_command_output +from utils import call, get_conf, get_install_dir, get_script, get_command_output, render_nginx_conf installdir = get_install_dir() topdir = dirname(installdir) @@ -30,7 +30,18 @@ def watch_controller(): print 'seafile controller exited unexpectedly.' sys.exit(1) +def init_https(): + domain = get_conf('server.hostname') + context = { + 'https': True, + 'domain': domain, + } + render_nginx_conf('/templates/seafile.nginx.conf', + '/etc/nginx/sites-enabled/seafile.nginx.conf', context) + call('nginx -t && nginx -s reload') + def main(): + init_https() admin_pw = { 'email': get_conf('admin.email'), 'password': get_conf('admin.password'), @@ -39,9 +50,13 @@ def main(): with open(password_file, 'w') as fp: json.dump(admin_pw, fp) + while not exists('/var/run/mysqld/mysqld.sock'): + time.sleep(1) + print 'mysql server is ready' + try: - call('{} start'.format(get_script('seafile.sh')), check_call=True) - call('{} start'.format(get_script('seahub.sh')), check_call=True) + call('{} start'.format(get_script('seafile.sh'))) + call('{} start'.format(get_script('seahub.sh'))) finally: if exists(password_file): os.unlink(password_file) diff --git a/scripts/utils/__init__.py b/scripts/utils/__init__.py index 77f3150..57089f2 100644 --- a/scripts/utils/__init__.py +++ b/scripts/utils/__init__.py @@ -48,7 +48,7 @@ def call(*a, **kw): dry_run = kw.pop('dry_run', False) quiet = kw.pop('quiet', False) cwd = kw.get('cwd', os.getcwd()) - check_call = kw.pop('check_call', False) + check_call = kw.pop('check_call', True) reduct_args = kw.pop('reduct_args', []) if not quiet: toprint = a[0] @@ -220,3 +220,10 @@ def get_conf(key, default=None): _config.read("/bootstrap/bootstrap.conf") return _config.get("server", key) if _config.has_option("server", key) \ else default + +def render_nginx_conf(template, target, context): + from jinja2 import Environment, FileSystemLoader + env = Environment(loader=FileSystemLoader(dirname(template))) + content = env.get_template(basename(template)).render(**context) + with open(target, 'w') as fp: + fp.write(content)