The existing documentation [1] for setting up SSL with Fossil is pretty thin. “Just set up an SSL web proxy,” it says. Yeah, “just.” :)
Other advice recently given on this list is to use stunnel, but that’s only of use when Fossil hosts the whole web site, so you just need to proxy Fossil. If Fossil is just a *part* of an existing web site — e.g. a typical open source project site, with docs, downloads, a forum, a blog, etc. all hosted outside Fossil, such as static files, a CMS, etc. — you’re probably using a full web server or a proxy server of some kind, and need to enable SSL/TLS in *that* layer instead. Another thing about the existing documentation is that it’s focused on self-signed certs, which requires messing with the platform’s certificate trust store to allow Fossil to trust your certificate. Here in 2016, we don’t need to mess with self-signed certs any more. I’ve recently worked out solutions to all of the above problems, so I thought I’d document the process here. The main thing that makes all of this relatively painless is Let’s Encrypt [2], which provides free globally-trusted TLS certificates for you, on demand. It’s in a beta state right now, a fact which will cause us a bit of grief below, but even in its current state it’s still better than the bad old manual way, involving a lot of openssl command line gymnastics. If you’re using Apache as your front end proxy (e.g. mod_proxy) you can use the letsencrypt-auto helper, which will let you skip a few of the steps below as they’re taken care of by the helper program. I prefer nginx for simple proxying, however, because its takes a lot less RAM than Apache, which directly affects how much I need to spend per month on hosting fees: VPS and cloud hosting providers charge for the RAM you use, so the less RAM you need, the lower your hosting fees. Unfortunately, the automatic Let’s Encrypt certificate installer doesn’t yet know how to safely modify your nginx configuration files, so you have to do it by hand. The rest of this article will assume you’re going down this semi-manual path. STEP 1: Split your “server” configurations ------ Throughout this article, we’re going to assume that we’re setting up HTTPS as a simple alternative for most of the site, which will continue to be accessible over HTTP. The only exception will be the Fossil sub-section, which we’ll force to HTTPS for security reasons. You probably have your site’s nginx configuration written as a single server { } block at the moment, because it is only serving on port 80/HTTP. The way nginx works, you need a separate block to serve the same content on port 443/HTTPS. Since most of the port 80 configuration will be the same as the port 443 configuration, we’ll follow the DRY principle and extract the common bits to a separate file. Thus, this: server { listen 80; server_name .example.com 1.2.3.4 “”; location / { root /var/www/example.com; ... } ... } …needs to become this: server { listen 80; include local/letsencrypt-challenge; include local/site; } …plus a separate per-site file, which we’re calling local/site stored relative to your nginx configuration directory: server_name .example.com 1.2.3.4 “”; location / { root /var/www/example.com; ... } ... We’ll write the second server { } block for the HTTPS case later, after we’ve generated the TLS keys. If your nginx server has multiple name-based virtual hosts and you want this TLS cert to cover all of them, split those, too. Each one needs to include the letsencrypt-challenge file, which we’ll create in the next step. STEP 2: Prepare for the Let’s Encrypt challenge/response sequence ------ The server { } block above includes a file called local/letsencrypt-challenge, which contains this: location '/.well-known/acme-challenge' { default_type "text/plain"; root /var/www/letsencrypt; } This simply declares that any URL beginning with the Let’s Encrypt ACME protocol challenge prefix is served from a directory somewhere under your OS’s web root. The letsencrypt program writes temporary challenge/response files in that directory for remote access by the Let’s Encrypt ACME service, so it needs to be a) writeable by the one who runs the wrapper script in a later step; and b) readable by the web server, which on SELinux-protected machines often means a specific sub-tree of the filesystem, like /var/www. (Because these challenge/response files are ephemeral, served only during the ACME negotiation, some tutorials you’ll find online will recommend using something in /tmp instead, but that doesn’t work under MAC systems like SELinux that restrict the web server to reading from only certain directories. That’s why I recommend using /var/www/letsencrypt above. If your host OS has a MAC system but it’s configured differently, you may need to adjust this path to a directory that the web server is allowed to read from.) STEP 3: Write the wrapper script ------ Save the following to a file called letsencrypt-wrapper: #!/bin/sh sudo ~/.local/share/letsencrypt/bin/letsencrypt certonly \ --webroot-path=/var/www/letsencrypt -a webroot \ --server https://acme-v01.api.letsencrypt.org/directory \ -d example.com -d www.example.com You will need to make certain changes: 1. If you chose a different webroot path in the nginx configuration fragment above, adjust the --webroot-path option’s value here to match. The script won’t create this directory for you, because your OS probably needs it to have a particular set of permissions to allow nginx to read from it: readable by the nginx user, appropriate SELinux labels set for it, etc. 2. The path to the letsencrypt program may not be correct for your system. It might be in /usr/bin if you installed it via your OS’s package manager, or it might be in /usr/local/bin if you installed it from source as root. I installed it from source as a normal user, so it landed under my home directory, as you see here. 3. Adjust the -d flag values to your site’s domain name, not forgetting all of the CNAMEs and other aliases you want this cert to be valid for. At minimum, you probably need both the www. and “bare” versions of your domain name. Maybe you have multiple domains aliased to the same server, such as the .com, .net, and .org variants; include all of those, with and without www. prefixes. You can list up to 100 -d flags per certificate at this time, so don’t be shy about tossing all of the possibilities you might need in here. There is a low limit on the number of times you can change the set of domains covered by your cert,[5] so it behooves you to think this through carefully up front. 4. If your system doesn’t use sudo, remove that, but remember to run the script as root in the next step. (On success, the letsencrypt program will write some files in locations that only root typically has write access to.) STEP 4: Restart ngnix and run the wrapper ------ We’re going to reload the nginx server configuration now, even though it is not complete, because part of the Let’s Encrypt process is a challenge/response sequence negotiated by the letsencrypt helper program over your site’s HTTP connection. This sequence proves to the Let’s Encrypt ACME server that you control the web server for each domain you named with -d flags in the wrapper script. With ngnix running with the new configuration above, run the wrapper script. It should percolate for a few seconds, then report that it wrote your certificates out underneath /etc/letsencrypt. (That’s why it runs the helper under sudo.) STEP 5: Create the base SSL/TLS configuration ------ We extracted the site configuration from our server { } blocks above because we now need to create a second such block for each site that nginx serves. server { include local/ssl; include local/site; } That is, instead of including the letsencrypt-challenge file — since we only serve the Let’s Encrypt challenge/response sequence via HTTP — we include the following SSL configuration file: listen 443 ssl; ssl on; ssl_stapling on; ssl_stapling_verify on; ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; ssl_dhparam /etc/ssl/private/dhparams.pem; ssl_prefer_server_ciphers on; ssl_protocols TLSv1.2 TLSv1.1 TLSv1; ssl_session_timeout 5m; ssl_certificate /etc/letsencrypt/live/example.com/cert.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/example.com/fullchain.pem; This file is purposely semi-generic, so you can include it into as many server { } blocks as you need to. It is possible to write a much shorter local/ssl file, but that will give you a C grade or worse in online SSL certificate tests.[3][4] The stock nginx SSL configuration is vulnerable to a number of attacks, which we must fix by configuration. The above configuration addresses several of these: 1. We enable OCSP stapling, which works around a weakness in the way certificate revocation was originally designed to work, which in practice resulted in revocations often being ignored. If we ever have to revoke one of these keys, we don’t want browsers to continue to accept it merely because it isn’t yet expired because they’re being lazy in their revocation checks. (Yes, browsers really do do that, trading security for speed!) 2. We restrict the default wide-open cipher suite set, disabling a bunch that are either known to be weak (e.g. RC4) or that allow downgrade attacks (e.g. export-grade encryption and null encryption). This unfortunately means certain older browsers (e.g. IE6) won’t be able to connect to us, but scraping them off gives better security to the majority remaining. You may want to pare the list down even further than this, depending on the client types you need to support. One of the SSL testing services linked below tests a long list of browser profiles against your configuration and reports which ones will connect and which won’t. 3. The dhparams.pem file prevents the Logjam attack. You need to generate it on the server with this command: openssl dhparam -out dhparams.pem 2048 Beware, this will take a few minutes of CPU time. 4. We restrict the protocol suite to drop SSL 3.0 and older, as they’ve got known vulnerabilities. (POODLE and such.) 5. The *.pem files mentioned at the end will be named differently on your system, since you aren’t doing this for example.com, but for your own domain name(s). If you gave several domains with -d in the letsencrypt-wrapper script, you still only get one set of certificate files, named after the first -d flag you gave. That’s why the *.pem files are named in the generic local/ssl file, and not repeated in each per-site server { } block. One things I don’t do here, which will result in a marginally higher grade from the SSL testing services (e.g. “A” to “A+”) is enable HSTS: HTTP Strict Transport Security. This lets a site declare that it must always be accessed via TLS. Once a browser has accessed a server with HSTS enabled, it will forever more refuse to use HTTP with that site in order to avoid MITM attacks, even if you explicitly type “http://“ in the browser’s location bar. I don’t enable HSTS on my sites because it means if I ever screw up and let a TLS cert lapse, all my users will be locked out of the site. If you trust yourself to be more on-the-ball about keeping your cert up-to-date than I trust myself, you should enable HSTS because it closes the redirect window; a MITM could sit on the HTTP port and instead of passing the redirect to the HTTPS side, continue to talk to the victim over HTTP, thus extracting all of its secrets. Let’s Encrypt certs only last for 90 days, which means it’s an ongoing task to keep this up-to-date. Until Let’s Encrypt learns about safe nginx configuration file modification, it’s a manual process. (With Apache, letsencrypt-auto sets up a background auto-renewal process so you can’t forget to renew. You could script this manually for nginx, if you wanted.) Another thing I don’t do here is make the HTTP site simply redirect to the HTTPS site. That goes against the major premise of this article, however, which is that the Fossil repo service is only part of the overall web service from your site. Presumably you, like me, have parts of the site you don’t need to protect, which are fine if served insecurely. Serving everything via HTTPS also makes bootstrapping Let’s Encrypt impossible, because you can’t redirect all HTTP traffic to HTTPS until after you’ve got your cert set up. STEP 6: Restart and test ------ At this point, restarting nginx again should bring up the same content on both ports 80 and 443. You might want to run SSL tests against it at this point.[3][4] STEP 7: Proxy Fossil ------ Now that you have SSL up and running, you can add the following to the server { } blocks to proxy access to Fossil via TLS. In the HTTP block, add this: location /code { rewrite ^ https://$host$request_uri permanent; } That forces all accesses to http://mydomain.com/code/* to be redirected to HTTPS, so that no sensitive data ever gets sent over HTTP. You can change the /code part to anything else you prefer, like /repo. Then in the HTTPS block, put this: location /code { include scgi_params; scgi_pass 127.0.0.1:48325; scgi_param SCRIPT_NAME "/code"; } If you changed /code above, make the same changes here. Finally, start Fossil on the server with this script, which I call fslsrv: #!/bin/sh OLDPID=`pgrep fossil` if [ -n "$OLDPID" ] then echo "Killing old Fossil instance (PID $OLDPID) first..." kill $OLDPID fi fossil server --localhost --port 48325 --scgi \ --baseurl https://example.com/code \ /path/to/museum/repo.fossil > /dev/null & echo Fossil server running, PID $!. Because it’s binding to a port > 1023 and it’s serving from a repo file owned by your user, it doesn’t have to (and shouldn’t!) run as root. You might even want to run this under a purpose-created unprivileged user, and even put it in a chroot or jail. Principle of least privilege and all that. You will need to change the --baseurl script to match your domain name. If you changed /code in the nginx configuration above to something else, match the change here, too. And of course, you need to give the path to your Fossil repository file/directory. The port number is random. Feel free to choose a different value, and make the matching change in the nginx configuration above. The important thing is that Fossil is listening only on localhost, so that an outsider cannot access it except via the HTTPS front-end proxy service provided by nginx. STEP 8: Re-sync ------ Now that you’re serving Fossil over HTTPS, you may need to change existing Fossil sync URLs. The redirect we added above means you don’t absolutely need to do this, but it saves an HTTP round-trip if you do: $ cd ~/path/to/checkout $ f sync https://example.com/code Above, I’ve assumed that you’re serving Fossil underneath /code within your site’s existing URL structure. Another way to go would be to add a CNAME record for your web host server’s IP like fossil.example.com. In that case, you would write two separate server { } blocks for that subdomain, one redirecting unconditionally to the other. You wouldn’t need the scgi_param SCRIPT_NAME "/code”; bit in the configuration in that case, either, since the top-level URL as seen by Fossil would be the same as the top-level URL as seen by nginx. [1]: http://fossil-scm.org/index.html/doc/trunk/www/ssl.wiki [2]: https://letsencrypt.org/ [3]: https://www.ssllabs.com/ssltest/ [4]: https://www.htbridge.com/ssl/ [5]: https://community.letsencrypt.org/t/rate-limits-for-lets-encrypt/ _______________________________________________ fossil-users mailing list fossil-users@lists.fossil-scm.org http://lists.fossil-scm.org:8080/cgi-bin/mailman/listinfo/fossil-users