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:
-
Ansible (version 2.9.10) for configuration managment system
-
Nginx for webserver
-
Let’s encrypt and certbot for TLS/SSL certificates
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 |
---|---|
|
Will update website conf for nginx |
|
The list of domains for which you will generate certificates |
Contact email |
|
|
Say that you agree the let’s encrypt term of services |
|
Not interactive mode |
|
The path to the nginx executable |
|
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:
The complete role for ansible is available here: https://github.com/vincentdoba/blog-examples/tree/master/20200719-blog-installation