24 Mar 2019

Build your blog with docker-compose: Nginx and Jekyll

Deploy Blog

In this tutorial I will describe how I deployed this blog using Docker, Nginx and Jekyll on Debian 9.

  1. To build you blog you need a public IP linked to a Debian VM / server and a domain name. An easy way to get this is by using a VPS, for example Digital Ocean or Vultr. For your blog you can get a VPS with 1CPU and 1GB RAM for about 5$/month. To get a domain name you can use a domain registrar like Hostinger or GoDaddy, first year can be as cheap as 1$.

  2. In order to acces easily our VPS we can use SSH, but for security measures I recommend changing the default SSH port - 22, disable password login and using SSH Public Key Authentication. Also I recommend setting up a firewall like ufw.

  3. Once everything is set up, SSH into your Debian 9 instance and install docker and docker-compose.

  4. In the DNS zone of your domain, from your Domain Name Registrer account, add a new A record name www that will point to the IP address of your VPS, with a TTL of 1 day, this will look like:
    www.example.com.		14400	IN	A	[ip_address]
  5. In this point you should have added your user to docker group and use that user for deploy. First we clone Jalpc-docker-compose repository.
    git clone https://github.com/serbanb11/Jalpc-docker-compose.git
  6. In Jalpc-docker-compose folder clone Jalpc github repo:
    cd Jalpc-docker-compose
    git clone https://github.com/jarrekk/Jalpc.git
  7. We need to add in nginx.conf, CNAME and .config files the domain name of the blog. To do this easier I put together a short script. We can call the script like:
    ./change_name.sh www.example.com
  8. Next step is to obtain a Let’s Encrypt Certificate. First we will build our containers and start them, we can do this using the following commands:
    docker-compose up -d --build
    docker exec -it nginx bash
  9. Last command will open a shell inside our jekyll docker container. Here we can use the register bash script to request the certificate.
    ./register www.example.com
  10. You’ll be taken through a dialog asking a few questions, including your e-mail address. Success is indicated by:
     - Congratulations! Your certificate and chain have been saved at:
  11. We can check the certificates using the following command:
    ./certbot-auto certificates
  12. Once we have the certificate we can start serving our website with the new certificate. We can do this by deleting the # from lines 39, 51 and 52 from nginx-lets-encrypt/nginx.conf file. Also we need to generate our DH parameters, to do this we will exit our container shell and run the following commands as root:
    cd lets-encrypt-data/
    openssl dhparam -out dhparams.pem 2048
  13. We can now restart our docker-compose in order for nginx to process the changes in config file.
    docker-compose stop
    docker-compose up -d
  14. Now we should be able to browse to the url and see our website up and running.
  15. Disable directory listing
    If you browse to <web_url>/static you are able to view the directory listing, this is actually a vulnerability, classified by MITRE as CWE-548: Information Exposure Through Directory Listing. In order to disable directory listing we can use this Jekyll gem JekyllRedirectFrom. First edit the content of 404.html file to start with:
    layout: null
    title: 404
    permalink: /404.html
    - /static/
    - /static/css/
    - /static/js/
    - /static/locales/
    - /static/slick/
    - /static/fonts/
    - /static/assets/
    - /static/assets/fonts/
    - /static/assets/img/

    Add the gem in config file, install it and rebuild the website:

    echo -e "gems:\n  - jekyll-redirect-from" >> ./_config.xml
    docker exec -it -d jekyll sh -c "gem install jekyll-redirect-from"
    docker exec -it -d jekyll sh -c "jekyll build"

    Security considerations

As this is a security blog let’s make some tests to view some security aspects of our deployment.

  1. Test SSL configuration
    The Nginx configuration is inspired from this StackOverflow reponse. We can test our configration using either SSLlabs or testssl.sh bash script. Using the default configuration we get a SSLlabs score of A+ but we have 90 for Key Exchange and Cipher Strength and 95 for Protocol Support. We can get a 100 for all categories by editing nginx.conf according to some of the comments in the file, however I recommend using the default configuration for compatibility reasons. When testing with SSLlabs, one thing that might catch your eye is the DNS CAA no field. You can read more about what this means on this Qualys blog post.

    We can resolve this issue pretty easy by adding the following record in our Domain Name Registrer account:
    www.example.com.                14400   CAA     0	issue "letsencrypt.org" 

    After adding this record we can run again SSLlabs test and view the change

    We can also test our SSL setup using testssl.sh script, let’s test our blog. We can run testssl directly from docker, changing www.example.com with our CNAME:

    docker run -ti drwetter/testssl.sh https://www.example.com/

  2. Let’s take a look at our docker deployment, we can status of CIS Docker Community Edition Benchmark v1.1.0 using Docker Bench for Security. Out of the box this script will report a bunch of warnings, however this tutorial can help us solve most of them, I recomment following it and understand what it does. Two great resources for this comes from OWASP, this cheatsheet and OWASP/Docker-Security which, at the time of writing this post, is still under development, missing some sections.
    I will summerize the commands I used on my Debian 9 box. For starters I stoped the service and remove everything.
    docker-compose stop
    docker system prune
    docker rmi $(docker images -q)
    systemctl stop docker
    • 1.x Host Configuration
      We can pass all the checks using the following:
      First install auditd
      sudo apt-get install auditd

      Add the following rules to /etc/audit/rules.d/audit.rules

      -w /usr/bin/docker -p wa
      -w /var/lib/docker -p wa
      -w /etc/docker -p wa
      -w /lib/systemd/system/docker.service -p wa
      -w /lib/systemd/system/docker.socket -p wa
      -w /etc/default/docker -p wa
      -w /etc/docker/daemon.json -p wa
      -w /usr/bin/docker-containerd -p wa
      -w /usr/bin/docker-runc -p wa

      Next we restart auditd service and list our new rules

      systemctl restart auditd
      auditctl -l
    • 2.x Docker daemon configuration
      Using the following commands we will pass all checks except 2.11 that it’s a little harder to implement.
      Add the following lines to /etc/docker/daemon.json
       "icc": false,
       "userns-remap": "default",
       "log-driver": "syslog",
       "disable-legacy-registry": true,
       "live-restore": true,
       "userland-proxy": false,
       "no-new-privileges": true
    • 3.x Docker daemon configuration files
      All checks should be passed.

    • 4.x Container Images and Build File
      It’s a little harder to solve because these warnings address the images we are using and we are using standard containers from nginx and jekyll.
      We get warnings from 4.1 that our container are running as root, however we should change the images to run as user, for example for nginx this has been done in docker-nginx-unprivileged
      To solve 4.5 we can enable content trust by running the following command:
      echo "DOCKER_CONTENT_TRUST=1" | sudo tee -a /etc/environment

      We also get warnings from 4.6 that our images doesn’t has HEALTHCHECK instructions, however we implemented healthcheck in our docker-compose file so we shouls be fine.

    • 5.x Container Runtime
      We get warning from 5.7 about using port 80, 443 however this should be an exception because we want to use standard http and https ports.
      Also warnings from 5.10, 5.11, 5.12 and 5.28 can’t be solved from docker-compose.yml because use some deploy directives. We need to deploy our containers using swarm in order to pass these checks.
      To pass 5.13 check we need to replace wildcard IP from docker-compose.yml with our public IP address.

    • 7.x Docker Swarm Configuration. Every check should be passed.

  3. After we’ve made the changes described above we can redeploy our blog. Because with the changes we isolated container with a user namespace we need to give new rights on _site and lets-encrypt-data folders from Jalpc. First we need to find UID our container uses, we can do this by running the following commands.
    cat /etc/subuid

    We need to change the rights:

    sudo chown -R 165536:165536 Jalpc/_site
    sudo chown -R 165536:165536 lets-encrypt-data
    sudo chown -R 165536:165536 Jalpc/.sass-cache
    systemctl start docker
    docker-compose up -d --build
  4. Let’s run docker-bench-security to see status of our deployment:
    cd /opt
    git clone https://github.com/docker/docker-bench-security.git
    cd docker-bench-security
    sudo sh docker-bench-security.sh

    We should get a score of 54

Blog Customization

  1. Now we can customize our blog, first by editing Jalpc/_config.yml. For more$ I did the following changes, besides obvious configurations:
    img_path: /static/assets/img/blog
  2. On every change made, to reload and view the changes we need to run:
    docker exec -it -d jekyll sh -c "jekyll build"

Now everything should be good to do, most things are easily customizable, for example I changed the code block syle, I’ve made the landing page the blog instead of about me page and commented some sections. Posts are easy to write with a markdown cheat-sheet handy. Good luck at blogging quality content!