Author SHA1 Message Date
ahmza f402556380 Fix audit findings: bugs, security, perf, content
Bugs:
- setup-wsl: fix broken /img/WSL/* image paths (now bundle-relative;
  images were broken on the published post) + better alt text
- pi-docker-swarm: remove broken missing cluster.webp reference
- archetypes/posts.md: fix malformed {{ }} template so 'hugo new' works
- rename arrstack/arrstack.md -> index.md so it's a proper leaf bundle
- static/admin/config.yml: fix 'htpps' -> 'https' site_url typo
- fix silently-dropped <placeholder> text (unsafe=false dropped them):
  wrap <distro>/<username>/<user>/<version>/<ip address> etc in backticks
  across setup-wsl, securing-ssh, pi-docker-swarm, mc-server-on-docker

Security:
- render-link.html: add rel=noopener noreferrer to external target=_blank
- extend-footer.html: fix rel='me' trapped in class attr (broke Mastodon
  verification); add proper rel/target
- static/admin/index.html: pin Sveltia CMS to 0.172.4 via jsDelivr + SRI

Performance:
- hugo.toml: ignoreFiles for *.aseprite/*.bak (stop shipping source art;
  non-page files 23 -> 13)
- video shortcode: preload=metadata; image shortcode: loading=lazy decoding=async
- sws.toml: drop SPA page-fallback so missing paths return real 404 (404.html)

Content/SEO:
- normalize tag casing (HomeLab/RemoteAccess/SSH/HUGO -> lowercase),
  fix 'prodcutivity' typo, arrstack tags:[""] -> [], add welcome description
- delete stray index.md.bak
2026-07-25 03:36:50 +01:00
ahmza b7145e1323 Remove temporary Netbird connectivity debug step
Build Hugo / build (push) Successful in 1m32s
Build Hugo / deploy (push) Successful in 39s
The deploy pipeline works end-to-end now (Gimli deploys successfully),
so the diagnostic step used to trace the WireGuard/TUN issue is no
longer needed. RPIC continue-on-error is kept until the cluster returns.
2026-07-25 03:07:00 +01:00
ahmza 87867b9f30 Point compose.yml at Gitea registry image with :latest tag
Build Hugo / build (push) Successful in 1m27s
Build Hugo / deploy (push) Successful in 40s
CI pushes to tea.ahmza.com/ahmza/blog-ahmza-com, not ghcr.io, and a
bare reference could resolve to the non-runnable :buildcache tag. Pin
the image to tea.ahmza.com/ahmza/blog-ahmza-com:latest so docker compose
pulls the actual runnable image.
2026-07-25 03:02:39 +01:00
ahmza 8a7833e8f5 Make RPIC deploy non-blocking while cluster is out of service
Build Hugo / build (push) Successful in 1m26s
Build Hugo / deploy (push) Failing after 38s
The Raspberry Pi cluster is temporarily out of service. Mark its deploy
step continue-on-error so its failure no longer skips the Gimli deploy
or fails the whole job. Revert once the cluster is back online.
2026-07-25 02:42:15 +01:00
ahmza 4426668518 Use registry build cache instead of gha cache
Build Hugo / build (push) Successful in 3m23s
Build Hugo / deploy (push) Failing after 39s
The GitHub-hosted 'gha' cache backend is unreachable from the
self-hosted Gitea runner (dial tcp 172.18.0.2:44181 i/o timeout), which
failed the build step even though the image pushed successfully. Switch
cache-from/cache-to to a registry-backed cache stored as a :buildcache
tag in the same Gitea registry, which works on self-hosted runners.
2026-07-25 02:21:38 +01:00
ahmza eacf71d676 Debug: check runner WireGuard capability instead of UDP ports
Build Hugo / build (push) Failing after 13m45s
Build Hugo / deploy (push) Skipped
Other Netbird peers connect fine, so the server and 51820/udp are OK --
the hang is specific to the Gitea runner. Replace the UDP port probes
with runner-side checks: /dev/net/tun presence, effective capabilities
(NET_ADMIN), WireGuard tooling/module, and a definitive 'ip tuntap add'
test. A runner container lacking TUN/NET_ADMIN cannot create the
WireGuard interface, which explains 'netbird up' timing out after the
control plane connects.
2026-07-25 01:53:09 +01:00
ahmza b05814980f Update Netbird debug to probe correct data-plane ports
Build Hugo / build (push) Successful in 1m47s
Build Hugo / deploy (push) Failing after 1m15s
The Netbird server multiplexes management + signal + dashboard behind
Traefik on :443 (no separate :10000 signal port). The tunnel data plane
is WireGuard on 51820/udp and STUN on 3478/udp. Update the debug step to
probe those UDP ports plus a general outbound-UDP check (public STUN) to
determine whether the runner network blocks UDP egress -- the likely
cause of 'netbird up' timing out after the control plane connects fine.
2026-07-25 01:46:28 +01:00
ahmza eb67d6361e Add Netbird connectivity debug step to deploy job
Build Hugo / build (push) Successful in 1m44s
Build Hugo / deploy (push) Failing after 1m8s
Temporary diagnostic step before 'Netbird Connect' that checks secret
presence (masked), DNS resolution, HTTPS reachability to the management
API, and the signal server TCP port from the runner. Helps distinguish
DNS/connectivity issues from auth/registration failures behind the
recurring 'netbird up' DeadlineExceeded error. Marked continue-on-error
so it never blocks the build.
2026-07-25 01:39:09 +01:00
ahmza 8062b9d208 Read Netbird management URL from secret
Build Hugo / build (push) Successful in 1m46s
Build Hugo / deploy (push) Failing after 58s
Switch the deploy job's Netbird Connect step to read management-url from
the NETBIRD_URL secret instead of a hardcoded value, so the URL can be
managed in the Gitea UI alongside the setup key.
2026-07-25 01:33:47 +01:00
ahmza aa01def998 Merge pull request 'Optimizations: switch to static-web-server + Caddy reverse proxy docs' (#1) from optimizations into main
Build Hugo / build (push) Successful in 1m51s
Build Hugo / deploy (push) Failing after 57s
Reviewed-on: #1
2026-07-25 00:19:53 +00:00
ahmza f23fe01f3c Fix GitLab/Gitea inconsistency in README
Build Hugo / deploy (push) Canceled after 0s
Build Hugo / build (push) Canceled after 48s
Update the README to reflect the actual infrastructure: CI/CD runs on
Gitea Actions (not GitLab), images live in the Gitea container registry,
and the clone URL points to tea.ahmza.com. Also clarify the Blowfish
theme is a Hugo module, so no submodules are needed.
2026-07-25 01:19:29 +01:00
ahmza 1ad79d40d8 Use caddy-cf image with Cloudflare DNS-01 challenge
Switch the Caddy doc to the custom caddy-cf image
(ghcr.io/ahmzaa/caddy-cf) which bundles the Cloudflare DNS module, so
TLS certs are issued via the ACME DNS-01 challenge instead of HTTP-01.
This removes the need to expose ports 80/443 to the public internet,
which suits the Netbird-only infrastructure.

- Add (cloudflare) snippet + import cloudflare to both Caddyfiles
- Read CF_API_TOKEN from env (.env for Docker, Secret for K8s)
- Document token scope (Zone:DNS:Edit) and Cloudflare-hosted DNS
  requirement
- Validated Caddyfile against the caddy-cf image (caddy adapt)
2026-07-25 01:17:35 +01:00
ahmza 7d4f1668a6 Add Caddy reverse proxy documentation
Document running Caddy in front of the SWS origin for both deployment
targets (Docker Compose on Gimli and the Kubernetes Pi cluster), with
Caddy handling automatic HTTPS, compression, and security headers.

Explains the layer separation: SWS serves static files + Cache-Control,
Caddy owns TLS/compression/security headers (matching sws.toml where
those are disabled). Caddyfile validated with 'caddy validate'.
2026-07-25 01:13:10 +01:00
ahmza 498231c160 Tune SWS config for running behind a reverse proxy
The site sits behind a reverse proxy that already handles compression
and security headers. Disable those in SWS to avoid redundant work and
duplicate/conflicting headers, letting each layer own one concern.

- compression / compression-static: off (proxy compresses)
- security-headers: off (proxy sets them)
- cache-control-headers, page-fallback, health: kept (origin concerns)
2026-07-25 01:09:42 +01:00
ahmza 898e3003a2 Switch static server from nginx to static-web-server
Replace nginx:alpine-slim with the scratch-based static-web-server
image, reducing the final image size ~32MB -> ~23MB (28% smaller).

- Dockerfile: serve from static-web-server:2, copy site to /public
- sws.toml: mirror previous nginx behavior (compression, cache-control,
  SPA index fallback) plus security headers and /health endpoint
- Remove now-unused nginx.conf
2026-07-25 01:04:17 +01:00
ahmza 0d425c0ed1 Update .github/workflows/build-docker-images.yml
Build Hugo / build (push) Failing after 18m24s
Build Hugo / deploy (push) Has been cancelled
2026-03-06 01:31:33 +00:00
ahmza cd6f85f903 Update .github/workflows/build-docker-images.yml
Build Hugo / build (push) Successful in 52s
Build Hugo / deploy (push) Failing after 1m14s
2026-03-06 00:58:29 +00:00
ahmza 0bef6a1907 Update .github/workflows/build-docker-images.yml
Build Hugo / build (push) Successful in 52s
Build Hugo / deploy (push) Failing after 56s
2026-03-06 00:52:33 +00:00
ahmza 83ca9b61b8 Update .github/workflows/build-docker-images.yml
Build Hugo / build (push) Successful in 54s
Build Hugo / deploy-to-gimli-blog (push) Has been cancelled
Build Hugo / deploy-to-rpic (push) Has been cancelled
2026-03-06 00:39:57 +00:00
ahmza e9bcafb64b Update Dockerfile
Build Hugo / build (push) Failing after 47s
Build Hugo / deploy-to-rpic (push) Has been skipped
Build Hugo / deploy-to-gimli-blog (push) Has been skipped
2026-03-06 00:36:04 +00:00
ahmza 2480790384 Update .github/workflows/build-docker-images.yml
Build Hugo / build (push) Failing after 37s
Build Hugo / deploy-to-rpic (push) Has been skipped
Build Hugo / deploy-to-gimli-blog (push) Has been skipped
2026-03-06 00:30:19 +00:00
ahmza 22d3759756 Update .github/workflows/build-docker-images.yml
Build Hugo / build (push) Failing after 8s
Build Hugo / deploy-to-rpic (push) Has been skipped
Build Hugo / deploy-to-gimli-blog (push) Has been skipped
2026-03-06 00:26:33 +00:00
ahmza 9a6555f1c3 Update .github/workflows/build-docker-images.yml
Build Hugo / build (push) Failing after 9s
Build Hugo / deploy-to-rpic (push) Has been skipped
Build Hugo / deploy-to-gimli-blog (push) Has been skipped
2026-03-06 00:24:26 +00:00
ahmza d71dc7a86b modified registry auth 2026-03-06 00:23:39 +00:00
ahmza da5d05404f whoknows
Build Hugo / build (push) Failing after 1m20s
Build Hugo / deploy-to-rpic (push) Has been skipped
Build Hugo / deploy-to-gimli-blog (push) Has been skipped
2026-02-15 23:42:18 +00:00
ahmza 3322229b97 Updated hardening-linux post 2026-02-15 23:41:22 +00:00
ahmza 1ccf4d3d72 added deployment to gimli 2026-02-12 23:14:20 +00:00
ahmza a433830d1d Remove DOCKER_BUILDKIT setting in workflow
Remove unnecessary environment variable setting for Docker Buildx.
2026-02-12 23:00:44 +00:00
ahmza 36dae5fdf7 added github actions cache 2026-02-12 22:58:17 +00:00
ahmza ba646f28f1 Update ssh-action version in workflow file 2026-02-12 22:51:22 +00:00
ahmza d0f304ef95 Update ssh-action version in deployment workflow 2026-02-12 22:43:50 +00:00
ahmza 311627d9c8 added deployment to gimli 2026-02-12 22:40:32 +00:00
ahmza 6d8f616bf0 added deployment to gimli 2026-02-12 22:39:40 +00:00
ahmza 4fb2156fa9 added image creation for linux/amd64 2026-02-12 22:01:27 +00:00
33 changed files with 1023 additions and 481 deletions
+33 -14
View File
@@ -5,8 +5,8 @@ on:
branches: [ "main" ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
REGISTRY: tea.ahmza.com
IMAGE_NAME: ${{ gitea.repository }}
jobs:
build:
@@ -22,12 +22,12 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
username: ${{ gitea.repository_owner }}
password: ${{ secrets.GITEATOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -38,14 +38,20 @@ jobs:
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/arm64
context: .
platforms: linux/arm64, linux/amd64
push: true
build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=true
tags: |
ghcr.io/${{ github.repository_owner }}/blog-ahmza-com:latest
ghcr.io/${{ github.repository_owner }}/blog-ahmza-com:${{ github.event.head_commit.id }}
deployment:
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:latest
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:${{ gitea.event.head_commit.id }}
# Registry-backed build cache. The GitHub-hosted 'gha' cache is not
# reachable from self-hosted Gitea runners, so store the cache as a
# dedicated tag in the same registry instead.
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:buildcache,mode=max
deploy:
runs-on: ubuntu-latest
needs: build
@@ -54,14 +60,27 @@ jobs:
uses: Alemiz112/netbird-connect@v1
with:
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
hostname: 'blog-gh-actions'
hostname: 'gitea-actions-blog'
management-url: ${{ secrets.NETBIRD_URL }}
- name: Deploy to Cluster
uses: appleboy/ssh-action@v1.2.2
- name: Deploy to RPIC-Cluster
# RPIC cluster is temporarily out of service. Don't let its failure
# block the Gimli deploy or fail the whole job. Remove this line once
# the cluster is back.
continue-on-error: true
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
host: ${{ secrets.RPIC_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
key: ${{ secrets.RPIC_KEY }}
port: ${{ secrets.SSH_PORT }}
script: 'kubectl rollout restart deployment blog -n blog-ahmza-com'
- name: Deploy to Gimli
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.GIMLI_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.GIMLI_KEY }}
port: ${{ secrets.SSH_PORT }}
script: 'docker compose down && docker compose pull && docker compose up -d'
+6 -5
View File
@@ -1,10 +1,11 @@
FROM ghcr.io/hugomods/hugo:0.128.2 AS build
FROM ghcr.io/hugomods/hugo:0.157.0 AS build
WORKDIR /tmp/hugo
COPY . .
RUN hugo --gc --minify
FROM nginx:alpine-slim
WORKDIR /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=build /tmp/hugo/public .
FROM ghcr.io/static-web-server/static-web-server:2
COPY sws.toml /etc/sws.toml
COPY --from=build /tmp/hugo/public /public
ENV SERVER_CONFIG_FILE=/etc/sws.toml
EXPOSE 80/tcp
CMD ["--config-file", "/etc/sws.toml"]
+8 -7
View File
@@ -4,10 +4,10 @@
This repository contains the source code for my personal blog hosted at [blog.ahmza.com](https://blog.ahmza.com).
## Features
Using GitLab provides several advantages:
- **Highly Customizable CI/CD Pipelines**: Automated processes for building and deploying the blog.
- **GitLab Container Image Repository**: Efficient management and storage of container images.
- **Custom GitLab Runners**: Flexibility in managing the CI/CD environment.
Using Gitea provides several advantages:
- **Highly Customizable CI/CD Pipelines**: Automated processes for building and deploying the blog via Gitea Actions.
- **Gitea Container Image Repository**: Efficient management and storage of container images.
- **Custom Gitea Runners**: Flexibility in managing the CI/CD environment.
The CI/CD pipeline is configured to build the blog for both amd64 and arm64
architectures, allowing me to host the site on my [Pi cluster](https://blog.ahmza.com/posts/pi-docker-swarm/)
@@ -33,7 +33,7 @@ sacrificing performance.
## Implementation
- **Hugo**: Converts Markdown files into static HTML/CSS/JS files.
- **GitLab CI/CD**: Automates the build process and creates Docker images.
- **Gitea Actions**: Automates the build process and creates Docker images.
- **Kubernetes Cluster**: Serves the static files to users.
## To-Do List
@@ -47,7 +47,8 @@ sacrificing performance.
## Notes
### Cloning the Repository
To clone this repository along with its submodules, use the following command:
The Blowfish theme is managed as a Hugo module, so no submodules are required.
Clone the repository with:
```bash
git clone git@gitlab.com:ahmza/blog.git
git clone ssh://gitea@tea.ahmza.com:2222/ahmza/blog-ahmza-com.git
```
+1 -1
View File
@@ -1,7 +1,7 @@
---
title: {{ replace .TranslationBaseName "-" " " | title }}
description: ""
tags: [""]
tags: []
author: "ahmza"
date: {{ .Date }}
draft: true
+6
View File
@@ -0,0 +1,6 @@
services:
blog:
image: tea.ahmza.com/ahmza/blog-ahmza-com:latest
ports:
- 80:80
restart: unless-stopped
+5
View File
@@ -14,6 +14,11 @@ pagination.pagerSize = 10
buildDrafts = false
buildFuture = false
# Keep source-art and editor backup files out of the built site. Without this,
# files inside page bundles (e.g. feature.aseprite) are copied into public/ as
# page resources and served publicly.
ignoreFiles = ['\.aseprite$', '\.bak$']
enableEmoji = true
# googleAnalytics = "G-XXXXXXXXX"
@@ -1,7 +1,7 @@
---
title: Arrstack
description:
tags: [""]
tags: []
author: "ahmza"
date: 2025-05-09T23:44:27+01:00
draft: true
@@ -4,7 +4,7 @@ description: In the modern age we are overwhelmed with stimuli. Learn to cut out
tags:
- lifestyle
- minimalism
- prodcutivity
- productivity
author: ahmza
date: 2024-01-02
draft: false
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

@@ -1,113 +0,0 @@
---
title: Hardening Linux - An Update to Securing SSH
description: "A Guide to Making Linux a Little more secure"
tags: ["Linux", "Ubuntu"]
author: "ahmza"
date: 2025-06-05T15:34:10+01:00
draft: true
weight:
featureimage:
lastmod:
---
---
{{< alert icon="fire" >}}
> - This Guide assumes a Linux -> Linux SSH Session
> - Please take care with production systems
> - I take no responsibility for misconfigured systems and lockouts
> - Try in a VM first and thank me later :)
> - My Commands will be for Ubuntu Server 24.04
{{< /alert >}}
---
## Introduction
This guide will go over the basic steps of hardening a Linux machine with a
focus on remote access. The reason this is important is because when we begin to
expose services to the internet and make them publicly accessible then that means
anyone can probe around.
There are better ways around this especially if you are the only user, or If
your users dont mind taking an extra step before being able to access your
resources. If you have not heard of _Overlay Networks_ before they're similar to
VPNs however do not need any open ports as they use NAT Punching AKA Hole
Punching.
Check out my post on [Tailscale](/posts/no-more-port-forwarding/)
regardless, it's always a good idea to implement good security practice so
follow along and get your remote systems secure !!
## Initial Steps
Before we get into the meat and potatos of this guide, it would be a good idea
to go over the following steps, especially if you are configuring a new install.
### Software Updates
Software updates are key for security as they will often include security
patches. Depending on your flavour of Linux this command will vary.
```bash
sudo apt update && sudo apt upgrade
```
{{< alert "bell" >}}
If you do not expect to be logging into this remote machine very often it may be
a good idea to [setup automatic updates](/posts/setting-up-automatic-updates)
however if you want more control then using [Ansible to update Remote Machines](/posts/ansible-to-manage-remote-servers)
would be a better option.
{{< /alert >}}
### User Accounts
It's not a good idea to continue to use a root account after install, it's much
more secure to setup another user account with sudo access. The reason is the
root account has complete control over the system, where a user account is more
restricted by default. As well as this logging is more granular with different
user accounts.
The following command will allow you to create a new user, it will also ask you
some questions about user information however the only required fields are the
username and password if you want one.
```bash
adduser username
```
now we can add our newly created user to the sudo group to give admin permissions
``` bash
usermod -aG sudo username
```
{{< alert "bell" >}}
Remember to replace username with your desired username
{{< /alert >}}
you can now logout of root and log back in as your user. Further action on the
server should now be executed from this user account.
## Creating SSH Keys
Now that you have your own user account, we can create an SSH key for it. An SSH
Key is a Public and Private key pair which allow client and server to
authenticate using Public Key cryptography. Public Key cryptography is much
harder to crack compared to a password, as well as this we no longer need to
send passwords across networks which can be at risk of interception.
SSH can also be more convenient once you set up ssh-agent which will be covered
later in the guide, with password authentication you need to enter a password
everytime, however with ssh-agent you can enter your ssh-key password once and
it remains unlocked for the remainder of the session.
{{< alert "fire" >}}
**IMPORTANT:** **NEVER** share your private ssh key ... it's private for a reason.
{{< /alert >}}
By running the following command you will create an ssh-key !!
```bash
ssh-keygen -t ed25519 -C NameYourKey
```
- `-t` - specifies the type of cryptographic algorithm we will use
- `-C` - notice it's capital, A name for your ssh keys. I usually give them the
hostname of the local machine
## Conclusion
+551
View File
@@ -0,0 +1,551 @@
---
title: Hardening Linux - An Update to Securing SSH
description: "A Guide to Making Linux a Little more secure"
tags: ["Linux", "Ubuntu"]
author: "ahmza"
date: 2025-06-05T15:34:10+01:00
draft: true
weight:
featureimage:
lastmod:
---
Update to [Securing SSH](/posts/securing-ssh/)
{{< alert icon="fire" >}}
> - This Guide assumes a Linux -> Linux SSH Session
> - Please take care with production systems
> - I take no responsibility for misconfigured systems and lockouts
> - Try in a VM first and thank me later :)
> - My Commands will be for Debian 13 Trixie
{{< /alert >}}
## Introduction
This guide will go over the basic steps of hardening a Linux machine with a
focus on remote access. The reason this is important is because when we begin to
expose services to the internet and make them publicly accessible then that means
anyone can probe around.
If you have not heard of _Overlay Networks_ before they're similar to
VPNs however do not need any open ports as they use NAT Punching.
Check out my post on [Netbird](/posts/no-more-port-forwarding/#netbird) an alternative to [Tailscale](/posts/no-more-port-forwarding/#tailscale)
Follow along and get your remote systems secure !!
## Initial Steps
Before we get into the meat and potatoes of this guide, it would be a good idea
to go over the following steps, especially if you are configuring a new install.
### Software Updates
Software updates are key for security as they will often include security
patches. Depending on your flavour of Linux this command will vary.
```bash
sudo apt update && sudo apt upgrade
```
{{< alert "bell" >}}
If you do not expect to be logging into this remote machine very often
then using [Ansible to update Remote Machines](/posts/ansible-to-manage-remote-servers)
is a great alternative to the manual approach
{{< /alert >}}
### User Accounts
It's not a good idea to continue using the root account after install, reason being
the root account has complete control over the system, whereas a user account is more
restricted by default. Logging is more granular with different user accounts,
especially with more than one user.
The following command will allow you to create a new user, it will also ask you
some questions about user information however the only required fields are the
username and password.
```bash
adduser username
```
now we can add our newly created user to the sudo group to give admin permissions
```bash
usermod -aG sudo username
```
{{< alert "bell" >}}
Remember to replace username with your desired username
{{< /alert >}}
you can now logout of root and log back in as your user. Further action on the
server should now be executed from this user account.
## Creating SSH Keys
Now that you have your own user account, we can create an SSH key for it. An SSH
Key is a Public and Private key pair which allow client and server to
authenticate using Public Key cryptography. Public Key cryptography is much
harder to crack compared to a password, also we no longer send passwords across
networks which can be at risk of interception.
SSH can also be more convenient once you set up ssh-agent which will be covered
later in the guide, with password authentication you need to enter a password
everytime, however with ssh-agent you can enter your ssh-key password once and
it remains unlocked for the remainder of the session.
{{< alert "fire" >}}
**IMPORTANT:** **NEVER** share your private ssh key ... it's private for a reason.
{{< /alert >}}
By running the following command you will create an ssh-key !!
```bash
ssh-keygen -t ed25519 -C NameYourKey
```
- `-t` - specifies the type of cryptographic algorithm we will use
- `-C` - notice it's capital, A name for your ssh keys. I usually give them the
hostname of the local machine
{{< alert "bell" >}}
If you would like a deep dive into why I pick this algorithm, click the drop down, else skip ahead :D
{{< /alert >}}
{{< details >}}
The `-t` flag specifies the type of encryption to use. The reasons I have gone with `ed25519`:
- `ed25519` is faster and generation, signing and verification operations
- Believed to be more resistant to certain attacks
- shorter key length at equivalent security levels
The following output is a speed test of different encryption algorithms
```bash
~ ❯ openssl speed rsa1024 rsa2048 rsa4096 ecdsap256 ed25519
Doing 1024 bits private rsa's for 10s: 73191 1024 bits private RSA's in 9.99s
Doing 1024 bits public rsa's for 10s: 1161066 1024 bits public RSA's in 9.99s
Doing 2048 bits private rsa's for 10s: 11346 2048 bits private RSA's in 9.99s
Doing 2048 bits public rsa's for 10s: 389696 2048 bits public RSA's in 9.99s
Doing 4096 bits private rsa's for 10s: 1643 4096 bits private RSA's in 10.00s
Doing 4096 bits public rsa's for 10s: 112285 4096 bits public RSA's in 9.99s
Doing 256 bits sign ecdsa's for 10s: 294504 256 bits ECDSA signs in 9.85s
Doing 256 bits verify ecdsa's for 10s: 101912 256 bits ECDSA verify in 9.99s
Doing 253 bits sign Ed25519's for 10s: 147102 253 bits Ed25519 signs in 9.99s
Doing 253 bits verify Ed25519's for 10s: 53317 253 bits Ed25519 verify in 10.00s
version: 3.0.13
...
...
sign verify sign/s verify/s
rsa 1024 bits 0.000136s 0.000009s 7326.4 116222.8
rsa 2048 bits 0.000880s 0.000026s 1135.7 39008.6
rsa 4096 bits 0.006086s 0.000089s 164.3 11239.7
sign verify sign/s verify/s
256 bits ecdsa (nistp256) 0.0000s 0.0001s 29898.9 10201.4
sign verify sign/s verify/s
253 bits EdDSA (Ed25519) 0.0001s 0.0002s 14724.9 5331.7
```
in my testing it seems as though ecdsa is actually a touch faster than ed25519 but my
testing is in no way scientific or thorough enough Ill leave that to the Pros.
`RSA 4096` is considered to be quite secure however the speed difference compared to
the more modern algorithms is considerably longer. Each algorithm has its strengths
and weaknesses, so research which works best for you.
In a homelab environment how noticeable this would actually be im not sure. We are
talking tiny fracions of a second.
for more insight: [https://serverfault.com/questions/1081266/ssh-keys-ed25519-vs-rsa-performance-demystified](https://serverfault.com/questions/1081266/ssh-keys-ed25519-vs-rsa-performance-demystified)
{{< /details >}}
When creating the SSH key you will be presented with a two questions.
- Location of the key
- Password
The default location of the key `$HOME/.ssh/id_ed25519` is fine for me.
I would strongly advise setting a good password for you SSH keys.
Now we can start copying these keys to remote hosts. The following command
will copy the public key to the remote host.
```
ssh-copy-id user@remote-host
```
{{< alert "bell" >}}
Change `user` and `remote-host` to your own values
{{< /alert >}}
If that succeeded then we should be anble to connect to the remote host using our ssh key
Hoping that you set a password for your ssh key, when attempting to connect the password prompt
will look slightly different. Instead of asking for the remote users password its asking for your
SSH keys password.
And just like that your system is a lot more secure.
## SSH key distribution
One pain point with SSH keys is when you have multiple ssh keys and multiple remote servers.
Having to distribute these keys can be time consuming, and honestly a waste of time.
One method thats easy to get up and running with minimal effort is using Github. We can add our
public SSH keys to github and run a command on our servers to pull those public keys.
{{< alert "bell" >}}
There are more advanced ways to get around this such as [SSH Certificate Authorities](posts/ssh-certificate-authority)
but thats out of the scope for this post.
{{< /alert >}}
To add an SSH key to github go to [github.com](https://github.com) and navigate to the SSH and GPG keys page
`Profile Icon > Settings > SSH and GPG keys > New SSH key`
[quicklink](https://github.com/settings/ssh/new)
- Title: Give a descriptive name for the key
- Key Type: Authentication key
- Key: Paste in the public key
to get your public key
```
cat $HOME/.ssh/id_ed25519.pub
```
Finally click `Add SSH Key`
### Importing the keys
When we are on the remote host we can run the following command to import our public ssh keys from github
Make sure the username is YOUR username.
```
ssh-import-id-gh username
```
## SSH Agent
Now we can setup the ssh agent to enable us to keep ssh-keys "logged-in" per session reducing the amount
we need to enter our password each time we want to spawn a new ssh session.
I make use of a bash script which is called in my `.zshrc` or `.bashrc`. So each time I spawn a new session
(Usually computer restart) it will spawn the ssh-agent and I can unlock my key for later use.
```bash
##### START Fix for ssh-agent #####
# Orig Ref: http://mah.everybody.org/docs/ssh
# Ref: https://web.archive.org/web/20220513101246/http://mah.everybody.org/docs/ssh
-
SSH_ENV="$HOME/.ssh/environment"
-
function start_agent {
echo "Initialising new SSH agent..."
/usr/bin/ssh-agent | sed 's/^echo/#echo/' > "${SSH_ENV}"
echo succeeded
chmod 600 "${SSH_ENV}"
. "${SSH_ENV}" > /dev/null
/usr/bin/ssh-add;
}
-
# Source SSH settings, if applicable
if [ -f "${SSH_ENV}" ]; then
. "${SSH_ENV}" > /dev/null
#ps ${SSH_AGENT_PID} doesn't work under cywgin
ps -ef | grep ${SSH_AGENT_PID} | grep ssh-agent$ > /dev/null || {
start_agent;
}
else
start_agent;
fi
##### END Fix for ssh-agent #####
```
The next time your .rc file is sourced you will be asked for your ssh key password.
## Modify SSH Config
{{< alert "bell" >}}
Im using `vim`, if you are unfamilliar with vim you may find it easier to use `nano`
instead. If you want to try vim just remember you start in command mode, to insert
text you can press `i` to enter insert mode. When ready to save and quit press `esc`
to go back to command mode and type `:wq` the colon allows you to write commands,
`w` represents write and `q` represents quit.
{{< /alert >}}
```
sudo vim /etc/ssh/sshd_config
```
Some options are already in the file, some just need uncommenting and some you will
need to add yourself.
```
Protocol 2
Port 2222
PermitRootLogin no
MaxAuthTries 5
PasswordAuthentication no
PermitEmptyPasswords no
AllowUsers username username2
ClientAliveInterval 180
ClientAliveCountMax 1
```
- Protocol 2 - is the latest and most secure ssh protocol version
- Port 2222 - change the default port from 22 to 2222
- Stops port scanners on the internet hunting for default open ports
- PermitRootLogin no - Prevents the root user being able to login via ssh
- MaxAuthTries 5 - The number of allowed authentication attempts per connection, i.e if an attacker is trying to brute force a server after 3 failed attempts - or half of the set value - these failed attempts will be logged (useful to know for fail2ban) and then after 4 get locked out.
- PasswordAuthentication no - Disable password authentication since we use SSH keys now !!
- PermitEmptyPasswords no - may be redundant since we set no password authentication …
- AllowUsers - only allow certain users to use SSH
- ClientAliveInterval 180 - SSH daemon will send a packet to the client every 180s
- ClientAliveCountMax 1 - when the ClientAliveInterval packet is sent if the client does not respond the session is closed. If this value is 2 for example the SSH daemon will have a fault tolerance of 2 meaning that the client has to miss 2 packets each 180s apart before the session closes.
With the current ClientAliveInterval and ClientAliveCountMax you have 3 minutes of "downtime" before the
session is dropped. Downtime could be the internet connection dropped or you closed your laptop lid and
the machine goes to sleep. Essentially any state where the client machine cannot respond to the remote
servers "ping" every 3 minutes.
If 3 minutes is too little time for you, you may increase the interval or increase the ClientAliveCountMax
value.
## Fail2Ban
Fail2Ban is an IPS (Intrusion Protection System) which monitors ssh logs looking for failed login attempts
If multiple failed login attempts are detected then the source IP address is blocked in the firewall.
### Install Fail2Ban
```
sudo apt install fail2ban
```
### Fail2Ban Configuration
```
sudo vim /etc/fail2ban/jail.local
```
I use the following configuration
```fail2ban
[DEFAULT]
bantime = 8h
ignoreip = 127.0.0.1/8 xxx.xxx.xxx.xxx/xx
ignoreself = true
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 2
```
- bantime - how long to block network connections for
- ignoreip - IP addresses / ranges that are ignored by fail2ban useful if you have static IPs set on your local machines or want to ignore your whole local network.
- ignoreself - dont ban itself xD
- sshd
- enabled - enables log monitoring for ssh
- port - the port we are using for ssh
- logpath - where your ssh logs are
- maxretry - how many retries before a connection is blocked
{{< alert "bell" >}}
maxretry in fail2ban is set to 2 because in our sshd_config file we set MaxAuthTries
to 5. So SSH will start to log after 3 failed attempts. When we hit 5 failed
attempt fail2Ban sees 2 failed attempts so it will block network connections.
So at 5 failed attempts **TOTAL** fail2ban blocks connections
{{< /alert >}}
these settings can be a bit aggressive in some situations so feel free to make them
a bit more relaxed however with our SSH keys setup properly we should not be
getting failed login attempts so this is _in my opinion_ already quite relaxed.
## Firewall
One thing I would like to mention here before we continue is that our network
security should not only be hardened on the edge. Each layer of our network
should be secured. The reason for this practice is that if an attacker does
manage to break through your edge then you can still isolate them and protect
resources from within the network. So even though we may have an edge firewall
we should consider setting up a firewall on each of our servers.
On Ubuntu server `ufw` is installed by default, thats what Ill be going over here.
You can install `ufw` on other distributions however they usually have default
firewall implementations like `iptables`
By Default the firewall on ubuntu servers is disabled. To check this:
```
sudo ufw status
```
{{< alert "bell" >}}
While its inactive we dont need to worry about locking ourselves out however
be aware that once your firewall is active any incoming traffic will be dropped
if we do not create accept rules for them.
{{< /alert >}}
We are going to create 2 catch all rules.
```
sudo ufw default allow outgoing
sudo ufw default deny incoming
```
- Allow all traffic going out of the server
- Deny all traffic coming into the server
Create a rule to allow our SSH traffic, make sure you set the port number you use to ssh
```
sudo ufw allow 2222
```
If you need any other incoming ports open, do that here.
We can now enable the firewall.
```
sudo ufw enable
```
check the status
```
sudo ufw status
```
## SSH Config
When we start using unconventional settings such as port 2222 it can be useful
to setup an ssh config file on our client machines.
My config files looks something like
This makes any host on my local network use the username and 2222.
I also have an alias for my VPS server.
```
Host 10.10.100.*
User username
Port 2222
Host vps-deb13-2c8g
HostName ipaddress-specific-server
User username
Port 2222
```
## 2 Factor Authentication - Optional
We can use 2 factor authentication on our servers much like you would with
something like github. By enabling the TOTP pam module when we login to the
server it will ask for a Time Based TOTP code.
These keys are time based and so it's vital that the time on the server and
client are correct.
The module I found is developed by Google and is open source
[google/google-authenticator-libpam](https://github.com/google/google-authenticator-libpam)
Install the module
```
sudo apt install libpam-google-authenticator
```
Edit the config
```
sudo vim /etc/pam.d/sshd
```
Comment out the following line
```
# @include common-auth
```
If we do not uncomment this then even though we are using SSH keys we will be
asked to use our user password to authenticate too.
Add the following line to the bottom of the file
```
auth required pam_google_authenticator.so
```
{{< alert "bell" >}}
If you have multiple users then it may be worth adding the nullok option as
this will prevent users who have not setup TOTP from being locked out.
```
auth required pam_google_authenticator.so nullok
```
{{< /alert >}}
Edit the ssh config
```
sudo vim /etc/ssh/sshd_config
```
Edit / Add the following
```
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
```
We are now ready to generte the TOTP codes for the user, run the following
```
google-authenticator
```
- `Do you want authentication tokens to be time-based (y/n)` - y
- Scan the Code into your Authenticator app or if you have a password manager use that :D
- You will be asked to enter a TOTP from the authenticator app
- You will be given recovery codes, save them somewhere
- `Do you want me to update your "/home/user/.google-authenticator" file ? (y/n)` - y
- The following questions are your choice however I answered
- `Do you want Disallow multiple uses of the same authentication token … (y/n)` - y
- `… In order to compensate for possible time-skew … (y/n)` - n (if you do experience problems you may change this option)
- `… 3 Login attempts every 30s … (y/n)` - y
![google-authenticator-cli](google-authenticator-cli.webp)
## Restart
Now is the moment of truth, restarting the sshd service and fail2ban
```
sudo service ssh restart
sudo systemctl restart sshd.service
sudo systemctl restart fail2ban
```
Once you are comfortable you have everything setup I would suggest a full system
restart as updates were performed earlier too.
## Conclusion
This is a great start to improving security on your remote servers. But it's
only the start. There are still many things you could change / implement to
further increase security.
There are also network considerations like VLANs and ensuring solid firewall
rules.
As mentioned earlier in the post you could implement overlay networks such as
Netbird or Tailscale when working over the internet instead of exposing all
the ports.
View File
View File
+2 -2
View File
@@ -185,8 +185,8 @@ cd .server-data
2. **Download a Minecraft server jar file:**
- [Vanilla Minecraft Server](https://www.minecraft.net/en-us/download/server)
- Right-click the green link that says minecraft_server.<version>.jar and copy the link address.
- Download with: curl -o server.jar <link you copied here>
- Right-click the green link that says `minecraft_server.<version>.jar` and copy the link address.
- Download with: `curl -o server.jar <link you copied here>`
- [Fabric Mod loader server](https://fabricmc.net/use/server/)
- Select the Minecraft version, Fabric Loader Version, and the Installer Version of your choice. I tend to go with the latest.
- Download with: Copy the curl command under CLI download
@@ -1,243 +0,0 @@
---
title: Minecraft Server on Docker ⛏ 🐳
date: 2022-05-24
lastmod: 2024-07-20
author: "ahmza"
draft: false
weight:
tags: ["games", "docker"]
description: "Ever wanted to build a Minecraft server in docker !! Start here"
---
## **Introduction**
Minecraft is a great game that becomes even more enjoyable when played with
friends. One of the best ways to do this is by setting up a dedicated server.
This guide will help you do just that!
Setting up and running a Minecraft server does not require a lot of resources,
but based on my experience, allocating more resources significantly enhances
performance. Especially when you start to add mods and increase player count.
## **Prerequisites**
- Docker
- Docker Compose
See [Pi Cluster](/posts/pi-docker-swarm/)
for more information about my docker setup.
## **Requirements**
_Requirements Gathered from [here](https://minecraft.fandom.com/wiki/Server/Requirements/Dedicated#Unix_.28Linux.2C_BSD.2C_macOS.29)_
### **Minimum (1-5 Players)**
- Intel Pentium 4 or AMD Athlon based CPU
- 512 MB RAM
- 2 GB Storage space
### **Recommended (10-15 Players)**
- Intel Core or AMD K8 based CPU
- 3 GB RAM
- 16 GB Storage Space
### **Optimal (20+ Players)**
- Intel i5/i7 or AMD Ryzen 5/7 Based CPU
- 6 GB RAM
- 35 GB Storage Space
**My opinion**:
I personally went with the **optimal** settings as I wanted to run mods as well
as have the capacity for ~10 players. This gives the server breathing room.
However if you will be runing no mods or a few light weight mods and only a few
players then there really is no need to go higher than 3-4 GB.
## **Building the Container**
Lets get started on building the Minecraft server
1. **Create a directory for the Minecraft server files**:
```bash
# Make the folder
mkdir minecraft-server
# Enter the folder
cd minecraft-server
```
2. **Create an environment file for the Docker Container**:
- The _.env_ file allows us to specify variables used in the build process.
```bash
# Create a .env file
touch .env
# Open in a text editor
vim .env
# Enter the following variables
CONTAINER_NAME=minecraft-server
SERVER_MEM=4096 #change this to your desired value (in MB)
SERVER_JAR=<downloaded-server-file>.jar # Minecraft server jar file. Change this later
```
3. **Create the _docker-compose.yml_ file**:
```bash
# Create the file
touch docker-compose.yml`
# Open in a text editor
vim docker-compose.yml
```
- _docker-compose.yml_ is the spec of our Minecraft server.
- Build a docker image using context . and pass some ARGS.
- Attach volume .server-data, where Minecraft's files will be stored.
- Open network ports to access to the Docker container
- Give the docker container a name
- The container will always restart unless we tell it to stop.
```yaml
services:
minecraft-server:
build:
context: .
args:
SERVER_MEM: ${SERVER_MEM}
SERVER_JAR: ${SERVER_JAR}
volumes:
- .server-data:/server
ports:
- 25565:25565 # Game
- 25575:25575 # RCON
container_name: ${CONTAINER_NAME}
restart: unless-stopped
volumes:
server-data:
```
4. **Create the _Dockerfile_ file**:
- requested in docker-compose.yml build stage as context .
- The _Dockerfile_ is the spec for software running inside the container
```bash
# Create the file
touch Dockerfile
# Open the file in a text editor
vim Dockerfile
```
- We use the [eclipse-temurin:21](https://hub.docker.com/_/eclipse-temurin) image to provide the java JRE
- Use the required java version for the version of Minecraft you are running
- Collect the ARGs from the .env file
- Set the environment variables
- Set the working directory to /server which on our local system is .server-data
- Run the minecraft server jar with the ENV variables
```Dockerfile
FROM eclipse-temurin:21
ARG SERVER_MEM
ARG SERVER_JAR
ENV SERVER_MEM=${SERVER_MEM}
ENV SERVER_JAR=${SERVER_JAR}
WORKDIR /server
CMD echo "Minecraft Server is starting\n" && java -Xms128M -Xmx${SERVER_MEM}M -jar ${SERVER_JAR} nogui
```
## **Setup the Server files**
We’re almost there, just a few steps to go.
When you try to start the container, you might encounter the following error:
```bash
Minecraft Server is starting
Error: Unable to access jarfile server.jar
```
This happens because we haven't downloaded the server jar file yet. Let's do
that now
1. **Create a folder in the minecraft-server folder called .server-data**:
```bash
mkdir .server-data
cd .server-data
```
2. **Download a Minecraft server jar file**:
- [Vanilla Minecraft Server](https://www.minecraft.net/en-us/download/server)
- Right click the green link that says minecraft_server.<version>.jar and copy the link address
- Download with: `curl -o server.jar <link you copied here>`
- [Fabric Mod loader server](https://fabricmc.net/use/server/)
- Select the Minecraft version, Fabric Loader Version and the Installer Version of your choice. I tend to go with the latest.
- Download with: `Copy the curl command under CLI download`
3. **Go back to the root minecraft-server folder**
```bash
cd ..
```
4. **Edit SERVER_JAR in .env with the name of the .jar you just downloaded**:
```bash
# Open in a text editor
vim .env
# Enter the following variables
CONTAINER_NAME=minecraft-server
SERVER_MEM=4096
SERVER_JAR=<downloaded-server-file>.jar # Change this to the name of the .jar you downloaded
```
5. **Run the container to generate necessary files like 'eula.txt'**:
```bash
docker compose up
```
you will see something like this:
```bash
minecraft-server | Starting net.minecraft.server.Main
minecraft-server | [18:05:50] [ServerMain/INFO]: You need to agree to the EULA in order to run the server. Go to eula.txt for more info.
minecraft-server exited with code 0
```
6. **Accept the EULA**:
```bash
# edit eula.txt in a text editor
vim .server-data/eula.txt
# change `eula=false` to `eula=true`
eula=true
```
7. **Make any additional changes you want**:
- Notable files:
- **server.properties** - Configure server name, game mode, world type etc…
- **ops.json** - server operators (can be set in-game)
- **whitelist.json** - players allowed to join the server (can be set in-game)
## Run the Minecraft Server
1. **Run the container in detached mode**:
```bash
# The -d flag allows the container to run in detached mode, i.e., in the background
docker compose up -d
```
```bash
# monitor the logs as the server starts. Use CTRL+C to exit
docker logs minecraft-server --follow
# If you see the following lines, your server is up and running:
[18:17:01] [Server thread/INFO]: Time elapsed: 25529 ms
[18:17:01] [Server thread/INFO]: Done (34.571s)! For help, type "help"
```
## **Conclusion**
Congratulations! You've successfully set up a Minecraft server. Now it's time
to play!
There are many exciting possibilities from here, such as installing mods or
creating scripts to manage the server. Enjoy your Minecraft adventures!
View File
View File
+1 -3
View File
@@ -39,8 +39,6 @@ a **bare metal server that sucks a lot of power to a low power solution.**
- 4 x PoE hats
- 1 x 5 port PoE Switch
![Cluster(fix this image)](/img/pi-docker-swarm/cluster.webp)
### **Point Of This post**
After following this post you will have setup _Docker swarm_ on a cluster of Pis
@@ -329,7 +327,7 @@ vim hosts
```
- **_[piCluster]_** - specifies a group of hosts
- **_<ip address>_** - change these to the Pis IP addresses
- **_`<ip address>`_** - change these to the Pis IP addresses
**create the Pi Intercommunication keys**
View File
+4 -4
View File
@@ -2,9 +2,9 @@
title: Securing SSH 🤖
description: Ever wanted to make your SSH connections more secure. Read this
tags:
- HomeLab
- RemoteAccess
- SSH
- homelab
- remoteaccess
- ssh
author: ahmza
date: 2021-12-07
draft: false
@@ -146,7 +146,7 @@ fine 😊
- Max Auth Tries 5
- PasswordAuthentication no
- PermitEmptyPasswords no
- AllowUsers <user>
- AllowUsers `<user>`
- ClientAliveInterval 180
``` bash
+1 -1
View File
@@ -3,7 +3,7 @@ title: Setup a HUGO Site
description: Always wanted to setup a static website. Look no further !! In this post we go over the basics of setting up a HUGO site
tags:
- blog
- HUGO
- hugo
- web
- development
author: ahmza
View File
+5 -5
View File
@@ -36,7 +36,7 @@ wsl --install
Go to the Microsoft store and search for a Linux distribution. I prefer
to use the "Ubuntu" distribution.
![Microsoft Store](/img/WSL/ubuntu.webp)
![Ubuntu in the Microsoft Store](ubuntu.webp)
Once installed, go ahead and run the distribution, after it finishes setting
itself up it will ask you for a username and password. These credentials are
@@ -51,7 +51,7 @@ I use the windows terminal app to run the Ubuntu distribution, it makes
customising the environment a lot easier. To download it head over to the
Microsoft store and search for Windows Terminal and download it.
![Microsoft Store](/img/WSL/winterm.webp)
![Windows Terminal in the Microsoft Store](winterm.webp)
When you login to WSL it will not default to your home directory. To fix that go
to the Windows Terminal Settings look for Ubuntu or your distributions name and
@@ -61,11 +61,11 @@ change the Starting directory to
\\wsl$\<distro>\home\<username>
```
> Be sure to change "<distro>" to your distributions name
> Be sure to change `<distro>` to your distributions name
> Be sure to change "<username>" to your username
> Be sure to change `<username>` to your username
![winterm-settings](/img/WSL/winterm-settings.webp)
![Windows Terminal settings](winterm-settings.webp)
## **Conclusion**
+1
View File
@@ -1,5 +1,6 @@
---
title: Welcome
description: The mandatory hello world post — a quick intro to who I am and what this blog is about.
tags:
- blog
- misc
+333
View File
@@ -0,0 +1,333 @@
# Caddy as a Reverse Proxy
This document describes how to run [Caddy](https://caddyserver.com/) as a
reverse proxy in front of the blog. It covers both deployment targets used by
this project: the Kubernetes (Raspberry Pi) cluster and the Docker Compose host
(Gimli).
It uses the custom [`caddy-cf`](https://github.com/ahmzaa/caddy-cf) image
(`ghcr.io/ahmzaa/caddy-cf:latest`) — a Caddy build with the **Cloudflare DNS
module** baked in. The stock Caddy image cannot solve the ACME **DNS-01**
challenge for Cloudflare; this custom build can, which is what lets us obtain
certificates without exposing ports `80`/`443` to the public internet.
## Why a reverse proxy?
The blog is served by [Static Web Server (SWS)](https://static-web-server.net/),
a tiny Rust binary that ships static files over plain HTTP on port `80`. SWS is
intentionally kept lean — it does **not** terminate TLS, and its compression and
security headers are disabled (see `sws.toml`). Those concerns are delegated to
the reverse proxy so that each layer owns exactly one job:
| Concern | Owned by | Notes |
| -------------------- | -------- | ------------------------------------------------- |
| TLS / HTTPS certs | Caddy | Automatic certs via Cloudflare **DNS-01** challenge |
| Compression (gzip/zstd) | Caddy | `encode` directive; SWS compression is off |
| Security headers | Caddy | Single source of truth, avoids duplicate headers |
| Serving static files | SWS | Reads from `/public`, sets `Cache-Control`/`ETag` |
| Cache-Control / ETag | SWS | Declared at the origin, passed through by Caddy |
### Why the Cloudflare DNS-01 challenge?
Caddy does **automatic HTTPS** out of the box, but the default HTTP-01 /
TLS-ALPN-01 challenges require ports `80`/`443` to be publicly reachable so
Let's Encrypt can verify domain ownership. This project's infrastructure is
reached over [Netbird](https://netbird.io/) (a private overlay network), so
those inbound challenges are not an option.
The **DNS-01** challenge instead proves domain ownership by creating a TXT
record via the Cloudflare API — no inbound connectivity required. The `caddy-cf`
image bundles the Cloudflare DNS provider plugin so Caddy can do this. All it
needs is a Cloudflare API token with **Zone:DNS:Edit** permission on the
`ahmza.com` zone.
## Origin contract
Whatever the deployment, the SWS origin exposes:
- **Protocol:** plain HTTP
- **Port:** `80`
- **Health endpoint:** `GET /health` → `200 OK`
- **Cache headers:** already set by SWS (Caddy should pass them through, not override)
Caddy only needs to terminate TLS (via Cloudflare DNS-01), add compression +
security headers, and forward requests to that origin.
---
## Docker Compose (Gimli)
Run Caddy and the blog container on the same Docker network. Caddy reaches the
blog by its service name (`blog`) over the internal network, so the blog's port
does **not** need to be published to the host.
The Cloudflare API token is passed to Caddy as the `CF_API_TOKEN` environment
variable and referenced from the Caddyfile — keep it out of the committed
config. Store it in a `.env` file (git-ignored) next to `compose.yml`:
```dotenv
# .env
CF_API_TOKEN=your-cloudflare-api-token-with-zone-dns-edit
```
### `compose.yml`
```yaml
services:
blog:
image: ghcr.io/ahmzaa/blog-ahmza-com
restart: unless-stopped
# No host port mapping needed — only Caddy talks to it, over the
# internal Docker network. Remove the previous "80:80" publish.
expose:
- "80"
caddy:
image: ghcr.io/ahmzaa/caddy-cf:latest # Caddy + Cloudflare DNS module
restart: unless-stopped
container_name: caddy-cf
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 (QUIC)
environment:
# Consumed by the {env.CF_API_TOKEN} placeholder in the Caddyfile.
- CF_API_TOKEN=${CF_API_TOKEN}
volumes:
- ./conf:/etc/caddy # contains the Caddyfile
- ./data:/data # persists certificates — DO NOT lose this
- ./config:/config
```
Place the Caddyfile at `./conf/Caddyfile`.
### `./conf/Caddyfile`
```caddy
# Reusable Cloudflare DNS-01 TLS snippet. The token is read from the
# CF_API_TOKEN environment variable so it never lives in the config file.
(cloudflare) {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
}
blog.ahmza.com {
# Obtain/renew the cert via the Cloudflare DNS-01 challenge.
import cloudflare
# Reverse proxy to the blog container by its compose service name.
reverse_proxy blog:80 {
# Use the SWS health endpoint for active upstream checks.
health_uri /health
health_interval 10s
health_timeout 2s
}
# Compression is done here, not in SWS.
encode zstd gzip
# Security headers live here so there is a single source of truth.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
# Remove Caddy's default Server header
-Server
}
log {
output stdout
format console
}
}
```
> **Certificate persistence:** the `./data` volume stores the issued
> certificates and ACME account key. Keep it — deleting it forces Caddy to
> re-request certificates and can hit Let's Encrypt rate limits.
Bring it up with:
```bash
docker compose up -d
```
---
## Kubernetes (Raspberry Pi cluster)
The blog runs as a `Deployment` behind a `Service`. Caddy runs as its own
`Deployment` and proxies to that Service via in-cluster DNS
(`blog.blog-ahmza-com.svc.cluster.local`, or just `blog` from within the same
namespace).
### Cloudflare API token Secret
Store the Cloudflare API token in a `Secret` and inject it into the Caddy
container as the `CF_API_TOKEN` environment variable (never bake it into the
ConfigMap):
```bash
kubectl create secret generic caddy-cloudflare \
--namespace blog-ahmza-com \
--from-literal=CF_API_TOKEN='your-cloudflare-api-token-with-zone-dns-edit'
```
### Caddyfile via ConfigMap
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: caddy-config
namespace: blog-ahmza-com
data:
Caddyfile: |
(cloudflare) {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
}
blog.ahmza.com {
import cloudflare
reverse_proxy blog:80 {
health_uri /health
health_interval 10s
health_timeout 2s
}
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
log {
output stdout
format console
}
}
```
### Caddy Deployment + Service
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: caddy
namespace: blog-ahmza-com
spec:
replicas: 1
selector:
matchLabels:
app: caddy
template:
metadata:
labels:
app: caddy
spec:
containers:
- name: caddy
image: ghcr.io/ahmzaa/caddy-cf:latest # Caddy + Cloudflare DNS module
ports:
- containerPort: 80
- containerPort: 443
- containerPort: 443
protocol: UDP
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: caddy-cloudflare
key: CF_API_TOKEN
volumeMounts:
- name: config
mountPath: /etc/caddy
- name: data
mountPath: /data # certificate storage — must persist
volumes:
- name: config
configMap:
name: caddy-config
- name: data
persistentVolumeClaim:
claimName: caddy-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: caddy-data
namespace: blog-ahmza-com
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: caddy
namespace: blog-ahmza-com
spec:
type: LoadBalancer # or NodePort, depending on the cluster
selector:
app: caddy
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443
```
> **Blog Service:** the existing `blog` Service should expose port `80` and
> target the SWS container port `80`. Caddy reaches it as `blog:80` from inside
> the same namespace.
> **Certificate persistence:** the `caddy-data` PVC is the K8s equivalent of the
> `./data` Docker volume. Without a persistent volume, a pod restart wipes
> the certificates and re-triggers ACME issuance.
> **DNS-01 and Netbird:** because certificates are issued via the Cloudflare
> DNS-01 challenge, the cluster does **not** need inbound `80`/`443` from the
> public internet for ACME. The `LoadBalancer`/`NodePort` above only needs to be
> reachable by your clients (e.g. over Netbird or your LAN).
---
## Notes and gotchas
- **DNS-01 needs no inbound reachability.** Because the `caddy-cf` image solves
the ACME challenge via the Cloudflare API (DNS-01), Caddy does **not** need
ports `80`/`443` exposed to the public internet to issue certificates. This is
what makes it work behind Netbird. It does need outbound HTTPS to the Let's
Encrypt and Cloudflare APIs.
- **Cloudflare API token scope.** The token needs **Zone → DNS → Edit** on the
`ahmza.com` zone. Keep it in a `.env` file (Docker) or a `Secret` (K8s), never
in the committed Caddyfile — the config references it as `{env.CF_API_TOKEN}`.
- **DNS must be managed by Cloudflare.** The `ahmza.com` zone has to be hosted on
Cloudflare for the DNS-01 challenge to create the required `_acme-challenge`
TXT record.
- **Do not double-compress.** SWS compression is off by design; Caddy's `encode`
directive is the only place compression happens. Enabling both wastes CPU.
- **Do not duplicate headers.** Security headers are set in the `Caddyfile`. SWS
has `security-headers = false` so there is no conflict.
- **Cache-Control comes from the origin.** SWS already sets long-lived caching on
static assets and short/no-cache on HTML. Caddy passes these through; avoid
adding conflicting `Cache-Control` header directives in the `Caddyfile`.
- **Health checks.** Both examples use SWS's `/health` endpoint for active
upstream health checking, and it also works as a K8s liveness/readiness probe
on the blog pod itself.
- **HTTP/3.** The Docker example publishes `443/udp` to enable QUIC. Remove it if
your network path does not support UDP.
+1 -1
View File
@@ -1 +1 @@
<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if strings.HasPrefix .Destination "http" }} target="_blank"{{ end }}>{{ .Text | safeHTML }}</a>
<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if strings.HasPrefix .Destination "http" }} target="_blank" rel="noopener noreferrer"{{ end }}>{{ .Text | safeHTML }}</a>
+3 -3
View File
@@ -1,6 +1,6 @@
<br>
<div class="flex items-center gap-x-5">
<a class="text-sm text-neutral-500 dark:text-neutral-400 rel='me'" href="https://mastodon.social/@ahmza">Mastodon</a>
<a class="text-sm text-neutral-500 dark:text-neutral-400 rel='me'" href="https://github.com/ahmzaa">Github</a>
<a class="text-sm text-neutral-500 dark:text-neutral-400 rel='me'" href="https://www.linkedin.com/in/ahmza/">Linkedin</a>
<a class="text-sm text-neutral-500 dark:text-neutral-400" rel="me noopener noreferrer" target="_blank" href="https://mastodon.social/@ahmza">Mastodon</a>
<a class="text-sm text-neutral-500 dark:text-neutral-400" rel="me noopener noreferrer" target="_blank" href="https://github.com/ahmzaa">Github</a>
<a class="text-sm text-neutral-500 dark:text-neutral-400" rel="me noopener noreferrer" target="_blank" href="https://www.linkedin.com/in/ahmza/">Linkedin</a>
</div>
+1 -1
View File
@@ -1 +1 @@
<img src="{{ .Get "url"}}" alt="{{ .Get "alt"}}" style="display:block;margin-left:auto;margin-right:auto;width:50%;height:auto;padding:10px;">
<img src="{{ .Get "url"}}" alt="{{ .Get "alt"}}" loading="lazy" decoding="async" style="display:block;margin-left:auto;margin-right:auto;width:50%;height:auto;padding:10px;">
+1 -1
View File
@@ -1,4 +1,4 @@
<video autoplay loop muted playsinline aria-label='{{ .Get "label"}}' style="display:block;margin-left:auto;margin-right:auto;width:50%;height:auto;padding:10px;">
<video autoplay loop muted playsinline preload="metadata" aria-label='{{ .Get "label"}}' style="display:block;margin-left:auto;margin-right:auto;width:50%;height:auto;padding:10px;">
{{ with .Get "mp4" }}<source src="{{ . }}" type="video/mp4">{{ end }}
{{ with .Get "webm" }}<source src="{{ . }}" type="video/webm">{{ end }}
</video>
-70
View File
@@ -1,70 +0,0 @@
# auto detects a good number of processes to run
worker_processes auto;
#Provides the configuration file context in which the directives that affect connection processing are specified.
events {
# Sets the maximum number of simultaneous connections that can be opened by a worker process.
worker_connections 1024;
# Tells the worker to accept multiple connections at a time
multi_accept on;
}
http {
# what times to include
include /etc/nginx/mime.types;
# what is the default one
default_type application/octet-stream;
# Sets the path, format, and configuration for a buffered log write
log_format compression '$remote_addr - $remote_user [$time_local] '
'"$request" $status $upstream_addr '
'"$http_referer" "$http_user_agent"';
gzip on;
gzip_disable "MSIE [1-6]\.(?!.*SV1)";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 9;
gzip_buffers 10 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css text/xml text/javascript image/webp image/svg+xml image/x-icon application/javascript application/x-javascript application/json application/xml application/xml+rss;
server {
# listen on port 80
listen 80;
# save logs here
access_log /var/log/nginx/access.log compression;
# where's the root dir
root /usr/share/nginx/html;
# what file to serve as index
index index.html index.htm;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to redirecting to index.html
try_files $uri $uri/ /index.html;
}
# Media: images, icons, video, audio, HTC
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|webp)$ {
expires 1y;
access_log off;
add_header Cache-Control "public";
}
# Javascript and CSS files
location ~* \.(?:css|js)$ {
try_files $uri =404;
expires 1y;
access_log off;
add_header Cache-Control "public";
}
# Any route containing a file extension (e.g. /devicesfile.js)
location ~ ^.+\..+$ {
try_files $uri =404;
}
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ media_folder: "assets/uploaded/img"
public_folder: "/uploaded/img"
## SiteTitle
site_url: htpps://blog.ahmza.com
site_url: https://blog.ahmza.com
display_url: blog.ahmza.com
logo_url: https://blog.ahmza.com/logo.webp
+8 -2
View File
@@ -8,7 +8,13 @@
<!-- Include the script that enables Netlify Identity on this page. -->
</head>
<body>
<!-- Include the script that builds the page and powers Decap CMS -->
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
<!-- Sveltia CMS, pinned to a specific version with Subresource Integrity.
Bump the version + regenerate the integrity hash when upgrading:
curl -sS https://cdn.jsdelivr.net/npm/@sveltia/cms@<ver>/dist/sveltia-cms.js \
| openssl dgst -sha384 -binary | openssl base64 -A -->
<script
src="https://cdn.jsdelivr.net/npm/@sveltia/cms@0.172.4/dist/sveltia-cms.js"
integrity="sha384-Sj4Mfbg9OjjwG2ZE/YeUYu7xbZRTXGFg/wa/nszj3KzItKuRLEmGqSpV5C9YI2Ge"
crossorigin="anonymous"></script>
</body>
</html>
+47
View File
@@ -0,0 +1,47 @@
[general]
#### Server
# Listen on all interfaces, port 80 (matches previous nginx setup)
host = "0.0.0.0"
port = 80
# Root directory of the static site (Hugo `public/` output)
root = "/public"
# Number of worker threads (0 = auto-detect based on CPU cores)
threads-multiplier = 1
# Log level
log-level = "info"
#### Performance / caching
# Compression is handled by the upstream reverse proxy, so it is disabled
# here to avoid doing the work twice (wasted CPU per request). Re-enable if
# SWS is ever exposed directly without a compressing proxy in front.
compression = false
compression-static = false
# Emit Cache-Control + ETag headers with sensible per-content-type defaults.
# The origin is the correct place to declare cacheability; the proxy/CDN
# passes these through. SWS applies long-lived caching to static assets
# (images, css, js, fonts) and short/no-cache to HTML.
cache-control-headers = true
#### Routing
# No SPA-style page fallback: this is a static Hugo site, not a single-page app.
# Missing paths should return Hugo's generated 404.html with a real 404 status
# (SWS serves /public/404.html automatically) rather than masking broken links
# with a 200 homepage response, which harms SEO (soft 404s).
#### Security
# Security headers are set by the upstream reverse proxy, so they are
# disabled here to avoid duplicate/conflicting headers. Let one layer own
# the security posture. Re-enable if SWS is exposed without such a proxy.
security-headers = false
# Health endpoint at /health for container/K8s liveness probes
health = true