1Password x Kubernetes
After years of manual tweaks and failed attempts at custom scripts, I've been converting my home lab to use Kubernetes. And it's been a great way to get familiar with the various K8s resources and conventions. Claude Code has been a solid pairing partner when setting this up, guiding me through the fiddly bits, setting up basic templates that I can configure, and teaching me the conventions.
Secrets management initially gave me pause. I don't need production-grade rigor, but I obviously don't want to commit them to the repo. I use 1Password for storing all manner of login details and credentials, so it seemed like the right fit ... but I didn't want to manually manage them every time I need to deploy.
But did you know that 1Password has a CLI? It's an easy way to securely access your secrets programmatically. Let's walk through what I did.
1Password Setup
You'll need to install the CLI package (I did this via homebrew), and then enable CLI integration in the desktop app. Should take only a minute. The official docs make every step clear.
For this script, we'll also use jq to parse the JSON that I get back from the 1Password CLI.
You can install jq via homebrew as well.
Secret Setup
I chose to create a new vault (named homelab) for all my K8s secrets. Optional, but then it's simple to find all the secrets you care about:
op item list --vault homelab
Then, I added the secrets as credentials within that vault. 1Password allows you to create multiple password-type fields within a record, and give them custom names. Some of the applications I'm using have a single secret, some have multiple.
I also chose to add the relevant k8s namespace as a tag on the records. Then my script can filter by tags when deploying a given namespace:
op item list --vault homelab --tags "$NAMESPACE"
This also makes it simple to share secrets across apps if necessary (so far, only useful for sharing my tailscale key with all apps).
Scripting
When you list items in a particular vault, you'll get a table like this:
❯ op item list --vault homelab
ID TITLE VAULT EDITED
bq5... maybe-secrets homelab 3 days ago
uet... plex-secrets homelab 2 days ago
zgj... copyparty-secrets homelab 2 days ago
zfk... nextcloud-secrets homelab 3 days ago
3u6... tailscale-auth homelab 2 days ago
726... grafana-secrets homelab 2 days ago
jzz... linkwarden-secrets homelab 2 days ago
m3e... immich-secrets homelab 2 days ago
In my script, I parse out and iterate over the titles:
op item list --vault homelab --tags "$NAMESPACE" | sed '1d' | awk '{print $2}' | while read title; do
content=$(op item get "$title" --vault homelab --format json)
# ...
done
For each item, the JSON will look something like this:
{
"id": "...",
"title": "immich-secrets",
"tags": ["immich"],
"version": 1,
"vault": {
"id": "...",
"name": "homelab"
},
"category": "SERVER",
"last_edited_by": "...",
"created_at": "2025-09-14T13:36:02Z",
"updated_at": "2025-09-14T13:36:02Z",
"fields": [
{
"id": "notesPlain",
"type": "STRING",
"purpose": "NOTES",
"label": "notesPlain",
"reference": "op://homelab/immich-secrets/notesPlain"
},
{
"id": "password",
"type": "CONCEALED",
"label": "DB_PASSWORD",
"value": "my_password",
"reference": "op://homelab/immich-secrets/DB_PASSWORD"
},
{
"id": "...",
"type": "CONCEALED",
"label": "JWT_SECRET",
"value": "my_jqt_secret",
"reference": "op://homelab/immich-secrets/JWT_SECRET"
}
]
}
Each 1Password item will map to a single K8s secret.
Should we update the secret?
Early versions of this script would overwrite all k8s secrets even if there was no change; and for a small home lab, that's probably fine. But I wanted to add a comparison: if the 1Password item has not been updated since the K8s secret was created, we don't have to update that one.
We can use jq to find the age of 1Password item:
op_updated=$(echo "$content" | jq -r '.updated_at // .updatedAt // empty')
We can use kubectl to find the age of the secret:
k8s_created=$(kubectl get secret "$title" -n "$NAMESPACE" -o jsonpath='{.metadata.creationTimestamp}' 2>/dev/null)
We can use the date builtin to convert these timestamps to epochs; if the 1Password item is newer, we should recreate the secret. Otherwise, we can skip this one.
Creating the secret
If you look back at the JSON we're working with, you'll see a few important details:
- the secret values we care about are in the
fieldskey. - every 1Password item has a
notesPlainfield that you can't remove, but we want to ignore that field. - all the other fields should become key/value pairs within a K8s secret.
To create the secrets in K8s via kubectl, we need to create a command that follows this format:
kubectl create secret generic "$title" --namespace="$NAMESPACE" --from-literal=KEY1=value --from-literal=KEY2=value --dry-run=client -o yaml | kubectl apply -f -
We already know the $title and $NAMESPACE; we need to create the literals. To do this, we reach for jq again:
literals=$(echo "$content" | jq -r '.fields[] | select(.id!="notesPlain") | "--from-literal=\(.label)=\(.value)"' | tr '\n' ' ')
I'll be honest: jq syntax is always inscrutable to me, but here we're getting the list of fields, selecting only the ones that are NOT notesPlain, and then interpolating the keys and values into the string format we need.
Then tr puts them all on one line, space-separated.
And so our script ends with:
if eval "kubectl create secret generic \"$title\" --namespace=\"$NAMESPACE\" $literals --dry-run=client -o yaml | kubectl apply -f -"; then
echo "✅ successfully applied secret $title in namespace $NAMESPACE"
else
echo "‼️ failed to apply secret $title in namespace $NAMESPACE"
fi
This post highlights the most interesting parts of the script, but you can find the whole script in my pastebin.
Usage
Calling the script is simple, the only argument is the K8s namespace whose secrets you want to update:
./scripts/secrets.sh plex
Right now, I'm only ever deploying a single app at a time, so this works fine.