Managing homelab secrets
In the last post we manually created a Cloudflare secret directly using kubectl, which goes against what we are trying to achieve with GitOps…
I want to be able to destroy and recreate the cluster all from one repository, so the secrets need to be stored in git, and thus we will need to store the secrets in a secure way.
I am using sops (with age encryption) https://github.com/getsops/sops
First setup sops and age, for me thats…
sudo pacman -S sops age
Then generate a key pair and save to your password manager. (we will be deleting the age.agekey file after we are done)
age-keygen -o age.agekey
cat age.agekey
The contents of the file will look something like this…
Public key: age1j6pyg3y73pt5leldl2csjk8a369fa2xxmuanywx6mn0mfhwc4dzs0lvthv
# created: 2025-06-24T10:04:52+02:00
# public key: age1j6pyg3y73pt5leldl2csjk8a369fa2xxmuanywx6mn0mfhwc4dzs0lvthv
AGE-SECRET-KEY-13HMN4HPFLZ60ECZUHHYT5JW75WNG3AL3JCYJ36H90209PKRRYWHQ0320ZS
Make sure you set an environment variable with your public key:
export AGE_PUBLIC=age1j6pyg3y73pt5leldl2csjk8a369fa2xxmuanywx6mn0mfhwc4dzs0lvthv
Next we will generate a secret manifest that uses our points to our Cloudflare credentials file.
kubectl create secret generic tunnel-credentials \
--from-file=credentials.json=58c97568-9eda-4dbf-9857-b6cf9cf91259.json \
--dry-run=client -o yaml > cloudflare-secret.yaml
Kubernetes secrets are NOT secret. We have to enrcypt the content still.
The below command references your env variable with the public key, targets the specifice section of your yaml with regex, and makes an in-place edit of the secret file.
sops --age=$AGE_PUBLIC \
--encrypt --encrypted-regex '^(data|stringData)$' --in-place cloudflare-secret.yaml
If you cat the file you will now see the contents of the credintials file have been encrypted in place
apiVersion: v1
data:
credentials.json: ENC[AES256_GCM,data:aHXoMNZSR1mzMrnWMrpwNVJ65MF3axL43OanZnmdjYKViSphDBl491s+tMhI0G3xeQiFBKcBFsHmKT7c8PaV+WgfJG7yg6J36QtJxLz7xG87d9Z5PkWfSo390tPxSr8GLpbi3Qgr+AVxvwjY3F3E8H19uFEyJQQxWDS/sTSNm/wDIdhpIsuTzqUZvVuZLlJmV4Unj5TaZUk1/divBvivShLLdPh1smFjqdH+r1f5uTb2lr6sM82U5do0A89FOOwM+yd6J2c7P9kGQJ+yMktEW0hF/gJkSYRjsB/8YEAZ+JvhEkE69Njoxtmu4jDSbZSN5Ndr13yhYWNviVZwnMUNa1x/fGv6JoA4A2VVGT65OYY4jZvl/xQBsD8uuq1YvtGz8tmGoZHIjFT8U0MGY8Mu6l5uN8sB44k8LhEHTq2+HC+34o6GqYZX7y5zQWJlrJ2NOW4fFVZCr+U6YvWr1BxCdhX6rk6GOMnZ18zWvY6LNHr+RaUH+AR50fmW+IO19NC2v3spxrSjBHH+oSmdksXVU4FVxLJskwyCbNXfcTVG9OIH3FnB3lilxvvy6b1CA5WA6Z5LXSNUxLEi99IqDjQz5SyY7LclRXOgJxz3kh4Zq0m+RaAMJTaSO4JKPoTpDtfbECYnMxYWeJhrKdOVQLNuOZWIg6S9O6CuxmOOezkq/9M=,iv:2h/BVKyWDyQIRYarBlYYraXjD2hENDyA9HfjMnu5eg0=,tag:bMC3lUKS/9gnX4BJT9Q/qQ==,type:str]
kind: Secret
metadata:
creationTimestamp: null
name: tunnel-credentials
sops:
age:
- recipient: age1j6pyg3y73pt5leldl2csjk8a369fa2xxmuanywx6mn0mfhwc4dzs0lvthv
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBESUtsT2dvWkJFdTJCaGgr
azNvOXl1NjFCWU1wTi8rb0RLRjhYeFM2QTFBCkdZVitlVUlCNkxvQlBWVnJZUUlG
SDQ2R1NZMkZVY0FuVUhpT05nbkc4bUEKLS0tIEwwK3FZN1lrK05pcHhpOGlldVN2
Sk1zdnRTdWd1QmhFZXZRWlZKMHJEQnMKyIMSF5qOq6z7AF/4vTVczlYS0P6mV3q2
WBq/t/UVAmts0TCn8t5xv4M+EXghs1QBkr35A2ClYBYgjd7qQLSZgw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-06-28T08:23:19Z"
mac: ENC[AES256_GCM,data:af++553y8OX+VAy7Ygg0zIYdMdEPUfwI6mSzoCqgTa2/Uqp//o94NwUicjp7ZGUmoX6BM6gK5ULd5k8Du1ncj5K5e2boFeTBk099accVeYtVwiknAWViVw4DD2Mnm9vCmDJJMNacB4Ax6i/Z0+D/MNy/076xUMyq23tqmPK98KA=,iv:LPa9LpP+iJmzrdKqpzysQdXtYHPwoHHuZqLhSIF7LiI=,tag:jU1NX0fo4mlxezAh7CourQ==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.10.2
In order for sops do decrypt our secret it needs the private key (this is a manual step when setting up your cluster)
cat age.agekey |
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=/dev/stdin
The last thing is to amend our apps.yaml file to include sops for decryption.
# update spec of apps.yaml so decryption works
decryption:
provider: sops
secretRef:
name: sops-age
Push to git and flux will reconcile and decrypt your secret! 💥