|
| 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 | + |
| 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 | + |
| 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 | + |
0 commit comments