Skip to content

Commit 85e4ede

Browse files
authored
wrote about ssh client in cronjob for updating cert files in different office servers. (#28)
Signed-off-by: David Söderlund <[email protected]>
1 parent 20abda8 commit 85e4ede

File tree

4 files changed

+262
-0
lines changed

4 files changed

+262
-0
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
---
2+
title: Certificates for your homelab or office servers
3+
published: true
4+
---
5+
6+
# Certificates for your homelab or office servers
7+
8+
In this post I will show you how to use cronjobs in kubernetes to automate the updating of certificates from cert-manager to different servers in your local network.
9+
10+
## Background
11+
12+
### Tell me, where do certificates come from?
13+
14+
[In a previous post](../the-joy-of-kubernetes-2-let-us-encrypt) we looked at how to use cert-manager to automate the creation of certificates from Let's Encrypt. This is increadibly useful of course to slap on your ingress so that any traffic coming in is encrypted and that the user can trust that they have come to the right place.
15+
16+
But what if you want to use certificates outside of kubernetes, like for servers in your homelab or other equipment in your office?
17+
18+
### My list of servers
19+
20+
In my homelab I deal with a few different websites regularly and accepting the http warning every time I visit them is a bit of a nuisance.
21+
22+
I wanted to fix:
23+
24+
- The router, running openwrt (uHTTPd)
25+
- DNS server, running pi-hole (lighttpd)
26+
- Synology NAS (nginx)
27+
- Proxmox itself that my cluster runs on (has a great web api)
28+
29+
### Cron jobs
30+
31+
Cron jobs let's us schedule jobs to run on a regular basis. Jobs create pods which are expected to complete instead of keep running as is the case for deployments, statefulsets and daemonsets. These pods can have volumes mounted to them which is terrific since our certificates are already stored as secrets in kubernetes.
32+
33+
The plan then is to create certificate request for each server and then create a cron job that will run every now and then and update the certificate on the server.
34+
35+
## Bringing the plan to life
36+
37+
### Some preparation
38+
39+
For pi-hole, I had to install an ssl plugin and write some configuration.
40+
41+
``` bash
42+
# prereq software
43+
sudo apt install lighthpd-mod-openssl
44+
45+
# configuration file
46+
sudo cat > /etc/lighttpd/conf-enabled/20-pihole-external.conf <<EOL
47+
#Loading openssl
48+
server.modules += ( "mod_openssl" )
49+
50+
setenv.add-environment = ("fqdn" => "true")
51+
\$SERVER["socket"] == ":443" {
52+
ssl.engine = "enable"
53+
ssl.pemfile = "/etc/lighttpd/combined.pem"
54+
ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.3", "Options" => "-ServerPreference")
55+
}
56+
57+
# Redirect HTTP to HTTPS
58+
\$HTTP["scheme"] == "http" {
59+
\$HTTP["host"] =~ ".*" {
60+
url.redirect = (".*" => "https://%0\$0")
61+
}
62+
}
63+
EOL
64+
65+
# server restart
66+
sudo service lighttpd restart
67+
```
68+
69+
As you can see the configuration now calls for combined.pem, which will be updated by the cron job.
70+
71+
> Other servers had similar prep work needed, if you are interested reach out and I will give you the details.
72+
Pi-hole struck a balance between how simple it was and how poor the existing documentation was, so hopefully this post will help some people with pi-hole specifically.
73+
74+
### The certificate request
75+
76+
Similar to in [the previous post](../the-joy-of-kubernetes-2-let-us-encrypt), in this cluster I have set up a ClusterIssuer called letsencrypt-prod. Each certificate has this form:
77+
78+
``` yaml
79+
apiVersion: cert-manager.io/v1
80+
kind: Certificate
81+
metadata:
82+
name: pihole-tls
83+
spec:
84+
secretName: pihole-tls
85+
dnsNames:
86+
- "pihole.office.dsoderlund.consulting"
87+
duration: 2160h0m0s # 90d
88+
renewBefore: 168h0m0s # 7d
89+
privateKey:
90+
algorithm: RSA
91+
encoding: PKCS1
92+
size: 2048
93+
usages:
94+
- "digital signature"
95+
- "key encipherment"
96+
issuerRef:
97+
name: letsencrypt-prod
98+
kind: ClusterIssuer
99+
```
100+
I started by syncing my argocd app for these certificates to make sure it would all still work as before.
101+
102+
![argocd showing the creation of the certificates](../assets/2025-01-11-20-24-00.png)
103+
104+
In this cluster I have configured cert-manager to use cloudflare to respond to the challenges from letsencrypt.
105+
106+
### The cron jobs
107+
108+
#### Certificate files through ssh
109+
110+
For three of the servers I could use a simple ssh client to just manipulate the files on each server.
111+
112+
First I needed to make sure that a new ssh key pair got generated and added for the appropriate local user of each server.
113+
114+
115+
``` bash
116+
# new keys
117+
ssh-keygen -t ed25519 -b 4096 -f id_ed25519 -C "kubernetes@talos" -N ""
118+
119+
# copies the content of the public key file and appends it to authorized_keys on the pi-hole
120+
scp id_ed25519.pub [email protected]:~/.ssh/tempfile
121+
ssh [email protected] "cat ~/.ssh/tempfile >> ~/.ssh/authorized_keys"
122+
ssh [email protected] "rm tempfile"
123+
124+
```
125+
126+
So now pi-hole for example will allow the holder of the private key named "kubernetes@talos" to log in as me (root) on the pi-hole server.
127+
128+
Continuing on I can now store the private key in a kubernetes secret that every job can share and the fingerprint of each individual server in a configmap.
129+
130+
``` bash
131+
# figure out the fingerprint of pi-hole
132+
cat ~/.ssh/known_hosts | grep pihole.office.dsoderlund.consulting > hostinfo.txt
133+
134+
# creates a kubernetes secret from the private key and host info
135+
kubectl create secret generic -n office talos-ssh-key --from-file=id_ed25519 --dry-run=client -o yaml | kubeseal > gitops/apps/child-app-definitions/office-certificates/talos-ssh-key.yaml -o yaml
136+
kubectl create configmap -n office pihole-host --from-file=hostinfo.txt --dry-run=client -o yaml >> gitops/apps/child-app-definitions/office-certificates/pihole-tls.yaml
137+
138+
```
139+
140+
After syncing the argocd app the sealed secret will turn into a secret which can be used by each cron job that will run an ssh-client.
141+
142+
Now we have all of the information in kubernetes that our script will need to be able to:
143+
- trust the pi-hole server identity
144+
- authenticate with an ssh key
145+
- manipulate the certificates from letsencrypt into the format that pi-hole expects
146+
- ssh into pi-hole and set the files and restart the services as needed
147+
148+
Rince and repeat for each server.
149+
150+
#### Running the script
151+
152+
I found [a good and minimalistic container image](https://hub.docker.com/r/kroniak/ssh-client/) with the ssh client software in it.
153+
154+
After pulling it down I tagged it with my registry and pushed it into my cluster so that I am not dependent on the internet, or this image going away / being updated.
155+
156+
``` bash
157+
podman pull docker.io/kroniak/ssh-client@sha256:49328ac11407c80e74d5712a668fab6c2a1521eecb272f6712e99fd58cea29a9
158+
podman tag docker.io/kroniak/ssh-client@sha256:49328ac11407c80e74d5712a668fab6c2a1521eecb272f6712e99fd58cea29a9 images.mgmt.dsoderlund.consulting/ssh-client:latest
159+
podman push images.mgmt.dsoderlund.consulting/ssh-client:latest
160+
```
161+
162+
This is the cronjob including the script I ended up writing for pi-hole, the others are similar (openwrt being super simple and synology being a bit more painful).
163+
164+
``` yaml
165+
# gitops/apps/child-app-definitions/office-certificates/pihole-tls.yaml
166+
# certificate request removed for brevity
167+
---
168+
apiVersion: batch/v1
169+
kind: CronJob
170+
metadata:
171+
name: certupdater-pihole
172+
namespace: office
173+
spec:
174+
schedule: "@weekly"
175+
successfulJobsHistoryLimit: 2
176+
jobTemplate:
177+
spec:
178+
template:
179+
metadata:
180+
labels:
181+
app: certupdater-pihole
182+
spec:
183+
restartPolicy: Never
184+
containers:
185+
- name: certupdater-pihole
186+
image: images.mgmt.dsoderlund.consulting/ssh-client:latest
187+
imagePullPolicy: Always
188+
volumeMounts:
189+
- name: pihole-creds
190+
mountPath: "/pihole-creds"
191+
readOnly: true
192+
- name: pihole-certs
193+
mountPath: "/certs"
194+
readOnly: true
195+
- name: pihole-host
196+
mountPath: "/pihole-host"
197+
readOnly: true
198+
command: ["/bin/bash"]
199+
args:
200+
- -c
201+
- |
202+
mkdir ~/.ssh && touch ~/.ssh/known_hosts
203+
cat /pihole-host/hostinfo.txt >> ~/.ssh/known_hosts
204+
cp /pihole-creds/id_ed25519 ~/.ssh/id_ed25519
205+
chmod 400 ~/.ssh/id_ed25519
206+
cat /certs/tls.key /certs/tls.crt > combined.pem
207+
scp combined.pem [email protected]:~/combined.pem
208+
ssh [email protected] "sudo mv combined.pem /etc/lighttpd/combined.pem"
209+
ssh [email protected] "sudo chown www-data /etc/lighttpd/combined.pem"
210+
ssh [email protected] "sudo service lighttpd restart"
211+
212+
volumes:
213+
- name: pihole-creds
214+
secret:
215+
secretName: talos-ssh-key
216+
- name: pihole-certs
217+
secret:
218+
secretName: pihole-tls
219+
- name: pihole-host
220+
configMap:
221+
name: pihole-host
222+
---
223+
apiVersion: v1
224+
data:
225+
hostinfo.txt: |
226+
pihole.office.dsoderlund.consulting ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFlUvjN/xJ4hFpb7E2Bbq0ZFp3K+uZo3wDMIKBDtf2rx
227+
kind: ConfigMap
228+
metadata:
229+
name: pihole-host
230+
namespace: office
231+
```
232+
233+
### Proxmox web api
234+
235+
The odd one out, [I found an existing solution](https://github.com/jforman/proxmox_certupdater) through my amazing google-fu.
236+
237+
I cloned the repo, made the container image a bit smaller and pushed it into my registry.
238+
239+
``` Dockerfile
240+
FROM docker.io/python@sha256:43e2664b1c5cc23c9f1db305f2689191f1235de390610a317af7241fe70e19cc
241+
WORKDIR /usr/src/app
242+
COPY certupdater.py ./
243+
COPY requirements.txt ./
244+
RUN pip install --no-cache-dir -r requirements.txt
245+
```
246+
247+
``` bash
248+
podman build -t images.mgmt.dsoderlund.consulting/proxmox-certupdater:latest .
249+
podman push images.mgmt.dsoderlund.consulting/proxmox-certupdater:latest
250+
```
251+
252+
From there it was pretty much the same routine, generate api token for proxmox, store in a secret, set up a cronjob to run the python script. I ended up running with pretty much which was in the repo that I linked, very nice.
253+
254+
### The final result
255+
256+
After synching the app one final time, the cronjobs, configmaps, and everything else shows up nicely. I added a kustomize patch to set the schedule a bit early to watch the fireworks.
257+
258+
![Argocd showing the creation of the cronjobs](../assets/2025-01-12-21-22-49.png)
259+
260+
Then I stacked my windows on top of each other so I could marvel in seeing those shields and padlocks that firefox give you in the address bar when all is well.
261+
262+
![Firefox showing four windows all with working https and certificates](../assets/2025-01-12-21-30-35.png)
107 KB
Loading
105 KB
Loading
678 KB
Loading

0 commit comments

Comments
 (0)