In this article, I will present you how I configured the deployment of my blog that use the static site generator Hugo

To do so, I used the following tools:

This article will go thought all the ansible tasks I had to put in place in order to install nginx, configure my blog virtualhost, add TLS/SSL encryption to the blog, and build the blog with Hugo

Prerequisites

At the start of my journey, I had:

  • a server on Debian 10 Buster

  • an Hugo blog (not built) available in a private git repository (on gitlab)

  • the public ssh key of my server user registered in this public git repository

  • the public git repository added to ssh known hosts of my server user

  • an ansible playbook I used to configure my server

What I had to do is to add an Ansible role to configure nginx, TLS/SSL certificates and Hugo site built. The ansible role , called blog, would be run with admin priviledges. It will be organized as follow:

- blog
  - tasks
    - main.yml
    - nginx.yml
    - ssl.yml
    - hugo.yml
    - blog.yml
  - handler
    - main.yml
  - files
    - vincent-technical-blog.conf

We will first explore the steps for installing and configuring nginx, then the steps to add TLS/SSL encryption to my nginx confiugration, then concluding with Hugo installation on server and run.

Those steps are mirroring how I organized my ansible role. So, the blog/tasks/main.yml will contain the following content:

- include: nginx.yml
- include: ssl.yml
- include: hugo.yml
- include: blog.yml

Nginx installation and configuration

In this part, I will describe task per task the configuration of nginx. The tasks are in the file blog/tasks/nginx.yml

The first taks is rather easy with the installation of nginx.

- name: "Install nginx webserver"
  apt:
    name: nginx
    state: latest

Next we remove the default nginx website

- name: Remove default nginx site
  file:
    name: "/etc/nginx/sites-enabled/default"
    state: absent
  notify: Restart nginx

We remove the display of nginx version on error pages in order to avoid giving too much information too easely to a potential attacker

- name: "Do not display version in nginx error page"
  lineinfile:
    path: /etc/nginx/nginx.conf
    regexp: "# server_tokens off;"
    line: "  server_tokens off;"
  notify: Restart nginx

Then we add our website to available site

- name: Add blog site to nginx
  copy:
    src: vincent-technical-blog.conf
    dest: "/etc/nginx/sites-available/vincent-technical-blog.conf"
    owner: root
    group: root
    mode: 0644

The configuration of the site (file blog/files/vincent-technical-blog.conf) a is simple http site. It will be automatically enriched latter when we install a let’s encrypt certificate:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    root /var/www/vincent-technical-blog;
    server_name vincent.doba.fr doba.fr;
}

Then we enable this website:

- name: Enable blog site
  file:
    src: "/etc/nginx/sites-available/vincent-technical-blog.conf"
    dest: "/etc/nginx/sites-enabled/vincent-technical-blog.conf"
    state: link
  notify: Restart nginx

And we don’t forget to restart the nginx server, using the following handler (in file blog/handlers/main.yml)

- name: Restart nginx
  service:
    name: nginx
    state: restarted

So, at the end of all those tasks, we have a running website on port 80, without TLS/SSL encryption. Let’s add it in the next file, blog/tasks/ssl.yml

SSL with let’s encrypt

In this part, we will add the TLS/SSL encryption to our basic http website. To do so, we will use certbot and let’s encrypt. The following tasks will be defined in file blog/tasks/ssl.yml. Those tasks must be executed after the setup of nginx server seen in previous part as certbot will update some nginx configuration in order to add TLS/SSL.

The first task is to install certbot and the nginx module for certbot

- name: Install certbot
  apt:
    name:
      - certbot
      - python-certbot-nginx
    state: latest

The next task is to generate the certificates:

- name: Generate Certificate for Domains
  shell: certbot --nginx -d 'doba.fr,vincent.doba.fr' -m vincent.doba@example.com --agree-tos -n --nginx-ctl '/usr/sbin/nginx' --redirect

Let’s describe a bit what are the different options used above:

parameter description

--nginx

Will update website conf for nginx

-d 'doba.fr,vincent.doba.fr'

The list of domains for which you will generate certificates

-m vincent.doba@example.com

Contact email

--agree-tos

Say that you agree the let’s encrypt term of services

-n

Not interactive mode

--nginx-ctl '/usr/sbin/nginx'

The path to the nginx executable

--redirect

Force redirection to https when updating nginx website configurations

The argument --nginx-ctl is not supposed to be necessary. However, there is a kind of bug for ansible on debian that prevents nginx to be found in path of an ansible task, resulting with the following error message when running this task:

fatal: [127.0.0.1]: FAILED! =>
{
  "changed": true,
  "cmd": "certbot --nginx -d 'doba.fr,vincent.doba.fr' -m vincent.doba@example.com --agree-tos -n --redirect",
  "delta": "0:00:01.982115",
  "end": "2020-07-19 21:31:23.860112",
  "msg": "non-zero return code",
  "rc": 1,
  "start": "2020-07-19 21:31:21.877997",
  "stderr": "Saving debug log to /var/log/letsencrypt/letsencrypt.log\nThe nginx plugin is not working; there may be problems with your existing configuration.\nThe error was: NoInstallationError(\"Could not find a usable 'nginx' binary. Ensure nginx exists, the binary is executable, and your PATH is set correctly.\")",
  "stderr_lines": ["Saving debug log to /var/log/letsencrypt/letsencrypt.log", "The nginx plugin is not working; there may be problems with your existing configuration.", "The error was: NoInstallationError(\"Could not find a usable 'nginx' binary. Ensure nginx exists, the binary is executable, and your PATH is set correctly.\")"],
  "stdout": "",
  "stdout_lines": []
}

To avoid this issue, we explicitly set the path to nginx executable.

After executing this task, the website configuration file vincent-technical-blog.conf in /etc/nginx/sites-available now contains the following configuration:

server {
    root /var/www/vincent-technical-blog;
    server_name vincent.doba.fr doba.fr;

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/doba.fr/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/doba.fr/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot


}server {
    if ($host = vincent.doba.fr) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    if ($host = doba.fr) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80 default_server;
    listen [::]:80 default_server;
    server_name vincent.doba.fr doba.fr;
    return 404; # managed by Certbot
}

As you can see, certbot updated the configuration to enable TLS/SSL on website

the previous task is idempotent: if the certificates have already been generated, certbot will not regenerate them. However it will still update the nginx website configuration.

As currently certbot provided with debian buster doesn’t disable outdated TLS 1.0 and 1.1, we set the following tasks to disable it in nginx let’s encrypt configuration

- name: "Disable TLS 1.0 and 1.1"
  lineinfile:
    path: /etc/letsencrypt/options-ssl-nginx.conf
    regexp: '^ssl_protocols TLSv1 TLSv1\.1 TLSv1\.2;'
    line: "ssl_protocols TLSv1.2 TLSv1.3;"
  notify: Restart nginx

And to avoid forgetting the renewal of our certificates, we add a crontab, running every week (Tuesday at 3:30 AM) that renew the TLS/SSL certificates if needed:

- name: Add automatic certificate renewal
  cron:
    name: Certbot automatic renewal
    job: "certbot renew --quiet --no-self-upgrade"
    minute: "30"
    hour: "3"
    weekday: "2"

So now we have our website configuration done. Now we just need to add content in our website root repository to have a fully functionnal website.

Install Hugo

In this part, we will only install Hugo static website generator. The corresponding file is blog/tasks/hugo.yml:

- name: "Install hugo static blog generator"
  apt:
    deb: https://github.com/gohugoio/hugo/releases/download/v0.74.2/hugo_0.74.2_Linux-64bit.deb
  environment:
    PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Here there is two issues that we needed to handle:

First, the Hugo version available in debian buster repositories is the 0.54, which is clearly outdated, preventing modern Hugo themes to be built. If I try to build my site using Hugo 0.54, I have the following error:

Error: "/home/vincent/vincent-technical-blog/themes/ananke/layouts/partials/func/GetFeaturedImage.html:35:1": parse failed: template: partials/func/GetFeaturedImage.html:35: function "return" not defined

I didn’t find any up-to-date debian repositories thus we had to use the dpkg method to get recent version of Hugo.

Second, we ran into the same issue that when we tried to generate TLS/SSL certificates: as the path used by ansible tasks is incomplete on debian, we ran into the following error:

fatal: [127.0.0.1]: FAILED! =>
{
  "changed": false,
  "msg": "dpkg --force-confdef --force-confold -i /home/vincent/.ansible/tmp/ansible-tmp-1595175765.6423273-12568-19963344095115/hugo_0.74.2_Linux-64bitfIKo2g.deb failed",
  "stderr": "dpkg: warning: 'ldconfig' not found in PATH or not executable\ndpkg: warning: 'start-stop-daemon' not found in PATH or not executable\ndpkg: error: 2 expected programs not found in PATH or not executable\nNote: root's PATH should usually contain /usr/local/sbin, /usr/sbin and /sbin\n",
  "stderr_lines": ["dpkg: warning: 'ldconfig' not found in PATH or not executable", "dpkg: warning: 'start-stop-daemon' not found in PATH or not executable", "dpkg: error: 2 expected programs not found in PATH or not executable", "Note: root's PATH should usually contain /usr/local/sbin, /usr/sbin and /sbin"],
  "stdout": "",
  "stdout_lines": []
}

To avoid this, we add the path as an environment variable in task

So now we have a recent Hugo version installed, the last step will be to retrieve our content from our public git repository and build the website.

Build the website

The two last tasks will be about retrieving data from git public repository and build website. The following tasks are set in the file blog/tasks/blog.yml

First we retrieve date from our git public repository (here gitlab):

- name: "Checkout blog repository"
  git:
    repo: git@gitlab.com:vincentdoba/vincent-technical-blog.git
    dest: /home/vincent/vincent-technical-blog
    force: yes
  become: yes
  become_user: vincent

Here the only point is to use the server user instead of root to retrieve the git repository, as the SSH key is registered with the server user and not the admin user

We remove the previous blog generation

- name: "Delete previous blog data"
  file:
    path: /var/www/vincent-technical-blog
    state: absent

And then we build the website using Hugo:

- name: "Build blog"
  shell: hugo -s /home/vincent/vincent-technical-blog -d /var/www/vincent-technical-blog

And we’re done!

Conclusion

We saw here how to install a static website generated with Hugo using the nginx webserver, and secure it with let’s encrypt.

By doing so, we were able to have a working website rated A in Qualys SSL server test:

vincent.doba.fr SSL report