Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,17 @@ 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:
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
|
|
||||||
@@ -54,14 +57,23 @@ 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: 'https://netbird.ahmza.com'
|
||||||
|
|
||||||
- name: Deploy to Cluster
|
- name: Deploy to RPIC-Cluster
|
||||||
uses: appleboy/ssh-action@v1.2.2
|
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"]
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
blog:
|
||||||
|
image: ghcr.io/ahmzaa/blog-ahmza-com
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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