Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e133b01b00 | ||
|
|
b7145e1323 | ||
|
|
87867b9f30 | ||
|
|
8a7833e8f5 | ||
|
|
4426668518 | ||
|
|
eacf71d676 | ||
|
|
b05814980f | ||
|
|
eb67d6361e | ||
|
|
8062b9d208 | ||
|
|
aa01def998 | ||
|
|
f23fe01f3c | ||
|
|
1ad79d40d8 | ||
|
|
7d4f1668a6 | ||
|
|
498231c160 | ||
|
|
898e3003a2 | ||
|
|
0d425c0ed1 | ||
|
|
cd6f85f903 | ||
|
|
0bef6a1907 | ||
|
|
83ca9b61b8 | ||
|
|
e9bcafb64b | ||
|
|
2480790384 | ||
|
|
22d3759756 | ||
|
|
9a6555f1c3 | ||
|
|
d71dc7a86b | ||
|
|
da5d05404f | ||
|
|
3322229b97 | ||
|
|
1ccf4d3d72 | ||
|
|
a433830d1d | ||
|
|
36dae5fdf7 | ||
|
|
ba646f28f1 | ||
|
|
d0f304ef95 | ||
|
|
311627d9c8 | ||
|
|
6d8f616bf0 | ||
|
|
4fb2156fa9 |
@@ -5,8 +5,8 @@ on:
|
|||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: tea.ahmza.com
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -22,12 +22,12 @@ jobs:
|
|||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to Gitea Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ gitea.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITEATOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -38,14 +38,20 @@ jobs:
|
|||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm64
|
context: .
|
||||||
|
platforms: linux/arm64, linux/amd64
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=true
|
BUILDKIT_CONTEXT_KEEP_GIT_DIR=true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository_owner }}/blog-ahmza-com:latest
|
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:latest
|
||||||
ghcr.io/${{ github.repository_owner }}/blog-ahmza-com:${{ github.event.head_commit.id }}
|
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:${{ gitea.event.head_commit.id }}
|
||||||
deployment:
|
# 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
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
|
|
||||||
@@ -54,14 +60,27 @@ jobs:
|
|||||||
uses: Alemiz112/netbird-connect@v1
|
uses: Alemiz112/netbird-connect@v1
|
||||||
with:
|
with:
|
||||||
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
|
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
|
||||||
hostname: 'blog-gh-actions'
|
hostname: 'gitea-actions-blog'
|
||||||
management-url: ${{ secrets.NETBIRD_URL }}
|
management-url: ${{ secrets.NETBIRD_URL }}
|
||||||
|
|
||||||
- name: Deploy to Cluster
|
- name: Deploy to RPIC-Cluster
|
||||||
uses: appleboy/ssh-action@v1.2.2
|
# 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:
|
with:
|
||||||
host: ${{ secrets.SSH_HOST }}
|
host: ${{ secrets.RPIC_HOST }}
|
||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.RPIC_KEY }}
|
||||||
port: ${{ secrets.SSH_PORT }}
|
port: ${{ secrets.SSH_PORT }}
|
||||||
script: 'kubectl rollout restart deployment blog -n blog-ahmza-com'
|
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
@@ -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
|
WORKDIR /tmp/hugo
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN hugo --gc --minify
|
RUN hugo --gc --minify
|
||||||
|
|
||||||
FROM nginx:alpine-slim
|
FROM ghcr.io/static-web-server/static-web-server:2
|
||||||
WORKDIR /usr/share/nginx/html
|
COPY sws.toml /etc/sws.toml
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
COPY --from=build /tmp/hugo/public /public
|
||||||
COPY --from=build /tmp/hugo/public .
|
ENV SERVER_CONFIG_FILE=/etc/sws.toml
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
|
CMD ["--config-file", "/etc/sws.toml"]
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
This repository contains the source code for my personal blog hosted at [blog.ahmza.com](https://blog.ahmza.com).
|
This repository contains the source code for my personal blog hosted at [blog.ahmza.com](https://blog.ahmza.com).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
Using GitLab provides several advantages:
|
Using Gitea provides several advantages:
|
||||||
- **Highly Customizable CI/CD Pipelines**: Automated processes for building and deploying the blog.
|
- **Highly Customizable CI/CD Pipelines**: Automated processes for building and deploying the blog via Gitea Actions.
|
||||||
- **GitLab Container Image Repository**: Efficient management and storage of container images.
|
- **Gitea Container Image Repository**: Efficient management and storage of container images.
|
||||||
- **Custom GitLab Runners**: Flexibility in managing the CI/CD environment.
|
- **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
|
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/)
|
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
|
## Implementation
|
||||||
- **Hugo**: Converts Markdown files into static HTML/CSS/JS files.
|
- **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.
|
- **Kubernetes Cluster**: Serves the static files to users.
|
||||||
|
|
||||||
## To-Do List
|
## To-Do List
|
||||||
@@ -47,7 +47,8 @@ sacrificing performance.
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
### Cloning the Repository
|
### 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
|
```bash
|
||||||
git clone git@gitlab.com:ahmza/blog.git
|
git clone ssh://gitea@tea.ahmza.com:2222/ahmza/blog-ahmza-com.git
|
||||||
```
|
```
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 782 KiB After Width: | Height: | Size: 441 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 73 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
blog:
|
||||||
|
image: tea.ahmza.com/ahmza/blog-ahmza-com:latest
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
restart: unless-stopped
|
||||||
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
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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.
|
||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -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.
|
||||||
-70
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,46 @@
|
|||||||
|
[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
|
||||||
|
|
||||||
|
# SPA-style fallback: serve index.html when a path is not found on disk,
|
||||||
|
# mirroring the previous nginx `try_files $uri $uri/ /index.html`.
|
||||||
|
page-fallback = "/public/index.html"
|
||||||
|
|
||||||
|
#### 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
|
||||||
Reference in New Issue
Block a user