Introduction
I figured the second post I made should be about this blog in general, as it has been a long time coming. I’ve been planning this for months, but due to some life events, I’ve been quite busy and unable to pursue this. Now that I have some more free time, I was inspired to finally take the dive and start it up after SerHack reached out asking for some feedback on his own blog.
After chatting with him a bit I decided to dive into the tool he had used to build a clean, static, fast blog; a tool called Hugo. Hugo allows you to simply spin up and host a static blog using (primarily) MarkDown, a format that is familiar to many who have worked with Github in the past.
In this post I’ll detail how I spun up my blog specifically and detail some next steps I have planned as well.
For the entire source code behind this site, see the Github repo below:
https://github.com/sethforprivacy/sethforprivacy.com
NOTE: This blog post is a bit outdated now as I have moved to a full Docker + Traefik setup, but I hope to update it soon.
Hosting
For hosting my blog I decided to go with a well known cloud provider, Linode, and spin up a basic Debian VM to build on top of. A few of the things that I set up on a new host like this are:
- UFW for simple firewall setup
- Oh My ZSH for a better terminal experience
- Zabbix for operational host monitoring
- NGINX for a web server
- Tor for hosting hidden services/SOCKS proxy
Once I had these basics installed, it was time to dive into Hugo and learn my way around it.
Installing Hugo
While I could have installed Hugo via apt, I decided to compile from the source to get the latest features and improved theme support. Hugo made this simple with their clear and detailed docs, so I simply followed their “Installing” docs.
The first step was installing Go:
| |
Once I had Go installed and working (test with go version), it was time to build Hugo from source:
| |
The build takes a bit depending on resources, but installs the latest Hugo directly to a directory we just added to $PATH.
To update Hugo later on, simply run the following commands:
Now it was time to spin up a test site and play around with themes.
Starting my new Hugo site
Hugo’s docs again came in handy here, as they have a great quick start guide that makes it easy to get your test site up and running. Once I had simple site up, I tried out a few themes and eventually settled on “Terminal” by @panr. Not only was the theme very minimal and dark by default, but it had a great Monero-like color scheme to choose from.
One of the best features of Hugo is that it is extremely simple to test out changes to your site/posts as you go,
since you can simply run hugo server -D to serve a copy locally that auto-refreshes with each change you make.
All it took to start up a new blog post was:
| |
Then I just edited the MarkDown and alt+tabbed to FireFox to check my changes as I went!
Deploying the site publicly with NGINX
The above is all that is needed to see the site for yourself, but obviously the end goal is to share this with others. To do that I chose to deploy NGINX, which is a very simple and easy to use web server that is widely available.
This post by Gideon Wolfe came in handy, and gave me the building blocks I needed to customize my NGINX configuration to match the way Hugo works.
Installing NGINX from their apt repos
To install NGINX I chose to install the latest from their own repositories, as I wanted to be sure to have compatibility with the latest security standards. Their docs are a great guide here, and should be all you need to get started.
Creating a custom server configuration
I like to use Mozilla’s SSL config generator as the core of any NGINX configuration to make sure I have the proper security and SSL settings configured, and this
time was no different. Make sure to customize the certificate placement to match your own, and add in the unique lines
for your Hugo deployment and hostname to the new site config file (located at /etc/nginx/conf.d/name-me.conf for my NGINX version). My unique lines are similar to the below, and are the core of what I had to add to the generated config from Mozilla:
| |
I navigated to sethforprivacy.com and validated that the blog loaded up.
Note: I had SSL certificates for very cheap with my domain purchase and used those, but for most people getting certs from LetsEncrypt is a better choice all around. See “Using Free Let’s Encrypt SSL/TLS Certificates with NGINX” for more details on how to setup LetsEncrypt with NGINX.
Enabling native Tor support
I knew from the outset that I wanted to also host my blog as a native Hidden Service on Tor, so I’ll go over how I did that here. Thankfully, it’s very simple with NGINX and only took a few quick configuration edits.
I used this old but great blog post as a base to go off of, starting with installing Tor:
Then I just had to edit the Tor configuration file at /etc/tor/torrc and add in the following lines (replace “sethforprivacy.com” with your site name or other preferred name):
| |
Once that was added, I simply restarted tor (sudo systemctl restart tor) and cat the file to get the .onion domain (replace “sethforprivacy.com” with what you chose above as the directory name):
| |
The next (and last) step was to add a new server block to my NGINX configuration, telling NGINX to serve a copy of the blog on localhost so that Tor could share it as a hidden service:
| |
Note: To properly serve over Tor you need to set the baseURL to the .onion address while also setting a unique publishDir in a different configuration file for Hugo. You can see my full config.yaml files below.
clearnet_config.yml | |
| |
A simple nginx -t to verify the configuration was valid, a restart of NGINX via sudo systemctl restart nginx, and I
was up and running on both clearnet and Tor!
I also added in the appropriate header to prompt Tor users who navigate to the clearnet site to use the native Tor site instead. Simply add a similar line to the below to your NGINX configuration file, under the same server block as your clearnet HTTPS server:
| |
My full NGINX configuration file is below:
nginx.conf# generated 2020-11-29, Mozilla Guideline v5.6, nginx 1.18, OpenSSL 1.1.1d, intermediate configuration, no OCSP
# https://ssl-config.mozilla.org/#server=nginx&version=1.18&config=intermediate&openssl=1.1.1d&ocsp=false&guideline=5.6
# Setup client-side caching
map $sent_http_content_type $expires {
default off;
text/html epoch;
text/css max;
application/javascript max;
~image/ max;
}
server {
# Server block to redirect any non-HTTPS queries to HTTPS
listen 80 default_server;
listen [::]:80 default_server;
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 444;
}
return 301 https://$host$request_uri;
}
server {
# Server block for serving sethforprivacy.com as a Tor Hidden Service
listen 127.0.0.1:80;
server_name 6idyd6chquyis57aavk3nhqyu3x2xfrqelj4ay5atwrorfcpdqeuifid.onion;
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 444;
}
# Enable compression for all types of files
gzip_static always;
gzip_proxied expired no-cache no-store private auth;
gunzip on;
# Enable client-side caching
expires $expires;
root /var/www/sethforprivacy.com/tor/;
index index.html;
error_page 404 = /404.html;
# Block site from being framed with X-Frame-Options and CSP
add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; form-action 'none'; base-uri 'none'";
add_header X-Frame-Options "DENY";
# Security headers
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
# Privacy headers
add_header Referrer-Policy "no-referrer";
add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), speaker=(), usb=(), vibrate=(), sync-xhr=()";
location / {
try_files $uri $uri/ =404;
}
}
server {
# Server block for monitoring NGINX status via Zabbix
listen 127.0.0.1:8080;
location /basic_status {
stub_status;
allow 127.0.0.1; #only allow requests from localhost
deny all; #deny all other hosts
}
}
server {
# Server block for serving sethforprivacy.com as an HTTPS site
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name sethforprivacy.com;
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 444;
}
# Enable compression for all types of files
gzip_static always;
gzip_proxied expired no-cache no-store private auth;
gunzip on;
# Enable client-side caching
expires $expires;
root /var/www/sethforprivacy.com/public/; #Absolute path to where your hugo site is
index index.html; # Hugo generates HTML
ssl_certificate /etc/nginx/ssl/sethforprivacy.com.crt;
ssl_certificate_key /etc/nginx/ssl/sethforprivacy.com.key;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dhparam /etc/nginx/ssl/dhparam;
# intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
add_header Onion-Location http://6idyd6chquyis57aavk3nhqyu3x2xfrqelj4ay5atwrorfcpdqeuifid.onion$request_uri;
# Block site from being framed with X-Frame-Options and CSP
add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; form-action 'none'; base-uri 'none'";
add_header X-Frame-Options "DENY";
# Security headers
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
# Privacy headers
add_header Referrer-Policy "no-referrer";
add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), speaker=(), usb=(), vibrate=(), sync-xhr=()";
error_page 404 = 404.html;
location / {
try_files $uri $uri/ =404;
}
}
Writing workflow
A quick note on my workflow, as this was one of the primary drivers to choosing Hugo in the first place. Hugo enables
very quick and easy testing of changes via the native localhost server, and is easy to deploy with Github.
Adding a new blog post
Whenever I want to add a new blog post, I simply open up the directory where I keep my Github repo and do the following:
That creates the new post and starts a local server that displays draft pages (which all posts start out as). As I go, the changes are automatically refreshed in my local browser, so it’s easy to make sure formatting is what I want before I publish them.
Once I’m done with the post, I simply edit the top of it to draft=false, push the changes to Github, and then pull the
latest changes on my web server with the following script:
| |
All it takes are those three commands and my latest changes are live in seconds! It’s simple to work on the blog from any device, push the changes to Github, and then deploy in seconds.
Helpful Hugo Pages
Here are a few Hugo pages I found useful as I was customizing my site, and they may be interesting to you as well:
- Privacy settings
- Allows disabling excess features and using simple/no-JS versions of embedding etc.
- Tweet shortcodes
- Highlights Tweets cleanly within posts when used
- All shortcodes
- Useful ways to highlight or embed
- Using git info for page metadata
- Uses git info to set last update date among other data
- Page variables
- I used info from this page to set data like keywords, tags, and description (for a selected short description used on the homepage instead of the “first 70 word” default)
Next Steps
The biggest new features I’d like to add to my site are:
- Automatic deployments whenever a new commit is pushed to my repo
- Simple search
Thanks for reading, and please don’t hesitate to reach out via Signal, SimpleX, X, or Nostr if you have any questions or would like more detail!
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.