mysite/static/sources/posts/tor_clearnet_blog.md

290 lines
9.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "Running my blog on Tor (.onion) and the Clearnet with Hugo + PaperMod"
date: 2025-09-19
tags: ["tor", "nginx", "hugo", "papermod", "cloudflare", "letsencrypt"]
categories: ["DevOps", "Notes"]
description: "How I hosted a Hugo + PaperMod site both as a Tor onion service and a normal HTTPS site behind Cloudflare, with clean configs, dual builds, and a one-command deploy."
draft: false
ShowToc: true
TocOpen: true
---
> TL;DR — The site is built once (Hugo project), **published twice**:
> - **Onion**: `http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/` via Tor, no TLS/HSTS, bound to `127.0.0.1:3301`.
> - **Clearnet**: `https://blog.alipourimjourneys.ir` behind Cloudflare, Lets Encrypt cert, `Onion-Location` header pointing to the onion mirror.
---
## 1) Tor hidden service (onion) basics
I used Tors v3 onion services and mapped onion port 80 → my local web server on `127.0.0.1:3301`.
**Install & configure Tor (Debian/Ubuntu):**
```bash
sudo apt update && sudo apt install -y tor
sudoedit /etc/tor/torrc
```
Add:
```
HiddenServiceDir /var/lib/tor/hidden_site/
HiddenServicePort 80 127.0.0.1:3301
```
Make sure the directory is owned by Tors user and private:
```bash
sudo mkdir -p /var/lib/tor/hidden_site
sudo chown -R debian-tor:debian-tor /var/lib/tor/hidden_site
sudo chmod 700 /var/lib/tor/hidden_site
```
**Important:** use the right systemd unit:
```bash
# validate as the Tor user
sudo -u debian-tor tor -f /etc/tor/torrc --verify-config
# (re)start the real service
sudo systemctl enable --now tor@default
sudo systemctl restart tor@default
```
Get the onion address:
```bash
sudo cat /var/lib/tor/hidden_site/hostname
```
Quick check (do remote DNS via SOCKS):
```bash
curl -I --socks5-hostname 127.0.0.1:9050 "http://$(sudo cat /var/lib/tor/hidden_site/hostname)"
```
---
## 2) Nginx for the onion (localhost-only)
I keep the onion site strictly on localhost: **no HTTPS, no redirects, no HSTS**. Tor already provides e2e encryption and authenticity.
`/etc/nginx/sites-available/onion-blog`:
```nginx
server {
listen 127.0.0.1:3301 default_server;
server_name <your-56-char>.onion 127.0.0.1 localhost;
# keep onion simple; no HSTS/redirects here
add_header Referrer-Policy no-referrer always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
# (optional) strict CSP so nothing leaks to clearnet
# add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'" always;
root /srv/hugo/mysite/public-onion;
index index.html;
location / { try_files $uri $uri/ /index.html; }
location ~* \.(css|js|ico|png|jpg|jpeg|gif|svg|webp|txt|xml)$ {
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
}
```
Enable/reload:
```bash
sudo ln -sf /etc/nginx/sites-available/onion-blog /etc/nginx/sites-enabled/onion-blog
sudo nginx -t && sudo systemctl reload nginx
```
> Gotcha I hit: I had **two** server blocks on `127.0.0.1:3301`, which caused 404s via onion. Make sure only the intended vhost listens there (or mark it `default_server`). Also, if you ever use a regex in `server_name`, the syntax is `server_name ~* \.onion$` (note the space after `~*`).
---
## 3) Hugo + PaperMod setup (and version bumps)
PaperMod now requires **Hugo Extended ≥ 0.146.0**. I installed the extended binary from the official tarball to avoid Snaps sandbox limitations (Snap cant read `/srv` paths by default).
```bash
# install Hugo extended (example)
VER=0.146.0
cd /tmp
wget https://github.com/gohugoio/hugo/releases/download/v${VER}/hugo_extended_${VER}_Linux-amd64.tar.gz
tar -xzf hugo_extended_${VER}_Linux-amd64.tar.gz
sudo mv hugo /usr/local/bin/hugo
hugo version # should say "extended" and >= 0.146.0
```
Create the site and theme:
```bash
sudo mkdir -p /srv/hugo && sudo chown -R "$USER":"$USER" /srv/hugo
cd /srv/hugo
hugo new site mysite
cd mysite
git init
git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod
```
**Config updates:** in newer Hugo, `paginate` is deprecated → use:
```toml
# config/_default/hugo.toml
title = "My Blog"
theme = "PaperMod"
enableRobotsTXT = true
[pagination]
pagerSize = 10
[params]
defaultTheme = "auto"
showReadingTime = true
showPostNavLinks = true
showBreadCrumbs = true
showCodeCopyButtons = true
```
I added per-environment overrides so I can build two outputs with different `baseURL`s:
```toml
# config/clearnet/hugo.toml
baseURL = "https://blog.alipourimjourneys.ir/"
# config/onion/hugo.toml
baseURL = "http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/"
```
---
## 4) Dual builds (clearnet + onion) and one-command deploy
I publish the same content twice—once for each base URL and docroot:
```bash
cd /srv/hugo/mysite
# clearnet build (served over HTTPS)
hugo --minify --environment clearnet -d public-clearnet
# onion build (served via Tor)
hugo --minify --environment onion -d public-onion
sudo systemctl reload nginx
```
Helper script I use:
```bash
sudo tee /usr/local/bin/build-both >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /srv/hugo/mysite
hugo --minify --environment clearnet -d public-clearnet
hugo --minify --environment onion -d public-onion
sudo systemctl reload nginx
echo "Deployed both at $(date)"
EOF
sudo chmod +x /usr/local/bin/build-both
```
While editing, I sometimes auto-rebuild on file save:
```bash
sudo apt install -y entr
cd /srv/hugo/mysite
find content layouts assets static config -type f | entr -r build-both
```
---
## 5) Clearnet behind Cloudflare + Lets Encrypt (manual DNS-01)
Cloudflare is set to **Full (strict)**. I issued a public cert for `blog.alipourimjourneys.ir` using **manual DNS-01** (no API token):
```bash
sudo snap install --classic certbot
sudo certbot certonly --manual --preferred-challenges dns -d blog.alipourimjourneys.ir --agree-tos -m you@example.com --no-eff-email
```
Certbot tells you to add a TXT record `_acme-challenge.blog.alipourimjourneys.ir`. Add it in Cloudflare DNS, verify with `dig`, then continue. Cert ends up at:
```
/etc/letsencrypt/live/blog.alipourimjourneys.ir/fullchain.pem
/etc/letsencrypt/live/blog.alipourimjourneys.ir/privkey.pem
```
Clearnet Nginx vhosts:
```nginx
# HTTP → HTTPS redirect (optional; CF usually talks HTTPS to origin anyway)
server {
listen 80; listen [::]:80;
server_name blog.alipourimjourneys.ir;
return 301 https://blog.alipourimjourneys.ir$request_uri;
}
# HTTPS origin (behind Cloudflare)
server {
listen 443 ssl http2; listen [::]:443 ssl http2;
server_name blog.alipourimjourneys.ir;
ssl_certificate /etc/letsencrypt/live/blog.alipourimjourneys.ir/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.alipourimjourneys.ir/privkey.pem;
# HSTS on clearnet only
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Help Tor Browser discover the onion mirror (safe on clearnet)
add_header Onion-Location "http://<your-56-char>.onion$request_uri" always;
root /srv/hugo/mysite/public-clearnet;
index index.html;
location / { try_files $uri $uri/ /index.html; }
}
```
> Note: dont add HSTS or HTTPS redirects to the **onion** vhost. Keep onion pure HTTP on localhost.
---
## 6) Troubleshooting I ran into (and fixes)
- **Tor unit confusion:** `tor.service` is a tiny master; the real daemon is `tor@default`. Use that unit and verify config as `debian-tor`.
- **Permissions:** `HiddenServiceDir` must be owned by `debian-tor` and mode `700`.
- **Mapping mismatch:** If `HiddenServicePort 80 127.0.0.1:3301` is set, visit `http://<onion>/` (no `:3301`). If you set `HiddenServicePort 3301 127.0.0.1:3301`, you must use `http://<onion>:3301/`.
- **Curl & .onion:** modern curl refuses `.onion` unless you use remote DNS via SOCKS:
```bash
curl -I --socks5-hostname 127.0.0.1:9050 "http://<onion>/"
```
- **Two Nginx vhosts on the same port:** I had a duplicate server on `127.0.0.1:3301` pointing somewhere else, which caused onion 404s. Keep only one (or mark one `default_server`).
- **Regex in `server_name`:** if you use it, write `server_name ~* \.onion$` (space after `~*`). I fixed an `invalid variable name` error caused by a missing space.
- **PaperMod with old Hugo:** upgraded to **extended ≥ 0.146.0**. Also updated `paginate` → `[pagination].pagerSize`.
- **Snap confinement:** Snaps `hugo` couldnt read `/srv` (`.../void: permission denied`). Switched to the tarball build in `/usr/local/bin`.
---
## 7) “Not secure” in Tor Browser?
That message can appear because onion uses **HTTP**. Its OK: Tor provides e2e encryption + onion auth. If you enable HTTPS-Only Mode, add an exception for the site. Also ensure the onion build doesnt reference **clearnet** resources (scan the built HTML for `http(s)://` links that arent your onion).
---
## 8) Day-to-day workflow
- Create content:
```bash
hugo new posts/my-first-post.md # then set draft: false
```
- Publish both:
```bash
build-both
```
- (Optional) Auto on save:
```bash
find content layouts assets static config -type f | entr -r build-both
```
- (Optional) Git push-to-deploy with a bare repo + post-receive hook that runs `build-both`.
---
## Final notes
- Clearnet gets **HTTPS + HSTS** and an `Onion-Location` header.
- Onion gets **no HSTS/redirects**, and all assets are self-hosted to avoid mixed content.
- Serving both worlds from one Hugo repo is easy: **two builds, two vhosts, one workflow**.
---
{{< sigdl >}}