Initial import of Hugo site to Forgejo
This commit is contained in:
commit
cb1ba0317f
1259 changed files with 236349 additions and 0 deletions
290
public-onion/sources/posts/tor_clearnet_blog.md
Normal file
290
public-onion/sources/posts/tor_clearnet_blog.md
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
---
|
||||
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, Let’s Encrypt cert, `Onion-Location` header pointing to the onion mirror.
|
||||
|
||||
---
|
||||
|
||||
## 1) Tor hidden service (onion) basics
|
||||
|
||||
I used Tor’s 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 Tor’s 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 Snap’s sandbox limitations (Snap can’t 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 + Let’s 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: don’t 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:** Snap’s `hugo` couldn’t 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**. It’s OK: Tor provides e2e encryption + onion auth. If you enable HTTPS-Only Mode, add an exception for the site. Also ensure the onion build doesn’t reference **clearnet** resources (scan the built HTML for `http(s)://` links that aren’t 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 >}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue