Rotate credentials¶
Replace a credential with a new one: periodically, or immediately if you suspect it was leaked. In all cases the new credential is saved first in 1Password (vault HomeLab) and then brought to the cluster.
Before you start
The 1Password app unlocked, with Integrate with other apps, and the account in the environment variable:
Ansible commands are run from ansible/ in homelab.
Summary¶
| Credential | Item in 1Password | Applied with |
|---|---|---|
Private key of a GitHub App (maceosoft-terraform, maceosoft-arc-runners, maceosoft-argocd) |
the item named after the App | playbook-platform-secrets.yml |
| FTP credentials of a repo | ftp-<repo> |
playbook-platform-secrets.yml + infra pipeline |
| Dex client secret (GitHub login) | maceosoft-dex |
playbook-dex-oauth-secrets.yml |
| Discord webhooks, healthchecks.io ping URL, and Grafana password (monitoring) | discord-webhooks, healthchecks-ping, grafana-admin |
playbook-platform-secrets.yml (see below) |
| Open WebUI session key (AI assistant) | open-webui (field WEBUI_SECRET_KEY) |
playbook-platform-secrets.yml (how to generate it) |
| Ollama router tokens (one per client and the node registration token) | ollama-router (fields registry_token, open-webui, k8sgpt, reports, alert-triage, translate) |
playbook-platform-secrets.yml (see below) |
alert-triage webhook token and mcpo API key |
alert-triage-webhook (field token), mcp-api-key (field key) |
playbook-platform-secrets.yml (see below) |
| Open Terminal API key | open-terminal (field api_key) |
playbook-platform-secrets.yml (see below) |
#ia channel webhook (AI reports) |
discord-webhooks (field ai) |
playbook-platform-secrets.yml (create the channel) |
| Node root password | root@<nodo> (created by the playbook) |
playbook-rotate-root-passwords.yml |
| Sealed Secrets key | sealed-secrets |
playbook-platform-secrets.yml (note: see below) |
Argo CD token from the gitops pipeline (account ci) |
argocd-ci |
playbook-platform-secrets.yml (how to generate it) |
Private key of a GitHub App¶
- In GitHub: maceosoft → Settings → Developer settings → GitHub Apps → the App → Private keys → Generate a private key. This is done only from the web UI: neither
ghnor the API allow generating or deleting keys of a GitHub App. -
Save the new key in 1Password:
In 1Password, replace the
private_keyfield of that App's item with the full content of the new.pem.With 1Password unlocked (if you have more than one account, export
OP_ACCOUNTfirst). The.pemis read from the file, so the key is not written in the command:op item edit maceosoft-terraform "private_key[password]=$(cat ~/Downloads/maceosoft-terraform.<fecha>.private-key.pem)" # comprueba que tiene contenido, sin mostrarlo op read "op://HomeLab/maceosoft-terraform/private_key" | wc -cChange
maceosoft-terraformto the item of the App you are rotating (maceosoft-arc-runnersormaceosoft-argocd). -
Verify and apply:
-
If it is the ARC App (
maceosoft-arc-runners), restart its listener so it picks up the new key:kubectl -n arc-systems get podsand delete the listener pod (it recreates itself). New runners already use the new key. In a single command:kubectl -n arc-systems delete pod -l app.kubernetes.io/component=runner-scale-set-listener(restarts all listeners in the namespace, which causes no issues). - Once everything works, delete the old key in GitHub (same page, Delete next to the previous key) and the downloaded
.pem.
With the CLI
To delete the downloaded .pem and review the App ID and Installation ID:
# en macOS: rm -P
shred -u ~/Downloads/maceosoft-terraform.<fecha>.private-key.pem
# App ID e Installation ID
gh api orgs/maceosoft/installations --jq '.installations[] | [.app_slug, .app_id, .id] | @tsv'
gh requires the organization permission: gh auth refresh -s admin:read:org.
FTP credentials of a repo¶
-
Change the password in cPanel and update the
ftp-<repo>item: -
Apply and re-run the pipeline, which loads the new secrets into the repo environments:
../.venv/bin/python scripts/apply_platform_secrets.py --check ansible-playbook -i inventory.ini playbook-platform-secrets.ymlThen re-run the
maceosoft/infrapipeline onmain:
Dex client secret¶
- In the Maceosoft Dex OAuth App: Generate a new client secret. GitHub shows it only once. Generating the secret in the OAuth App is only done from the web UI.
-
Save the new secret:
-
Apply (updates
argocd-secretand restarts Dex): -
Test GitHub login in Argo CD and then delete the old secret in the OAuth App.
Monitoring: Discord webhooks, healthchecks.io ping, and Grafana¶
They are optional (4.13). All three live in 1Password and the playbook
places them in the monitoring namespace; they are never written to Git.
-
Generate the new value:
- Discord webhook: Discord does not allow regenerating a webhook token. In Server Settings → Integrations → Webhooks, use Delete Webhook and create a new one (how); copy its URL.
- healthchecks.io ping URL: create a new check with the same configuration (Period of 5 minutes, Grace Time of 10 and its Discord integration), copy its ping URL and delete the old one. The steps are in Discord and healthchecks.io.
- Grafana password: a new one in 1Password.
-
Discord webhook: deleting the old one can be done with its own URL (paste it when prompted, it is not shown); the new one is created only from the Discord web UI or app (how):
-
healthchecks.io ping URL: using the API and the project's API key (read-write). Create the new check with the same values (
timeoutis the Period andgracethe Grace Time, in seconds;"channels":"*"connects the integrations you already have, such as Discord), save its ping URL in 1Password and delete the old one:# pega la API key y Enter (no se ve) read -rs HC_KEY OLD=$(curl -fsS -H "X-Api-Key: $HC_KEY" https://healthchecks.io/api/v3/checks/ \ | jq -r '.checks[] | select(.name=="homelab-monitoreo") | .uuid') NEW_URL=$(curl -fsS -H "X-Api-Key: $HC_KEY" \ -d '{"name":"homelab-monitoreo","timeout":300,"grace":600,"channels":"*"}' \ https://healthchecks.io/api/v3/checks/ | jq -r .ping_url) op item edit healthchecks-ping "url[password]=$NEW_URL" curl -fsS -X DELETE -H "X-Api-Key: $HC_KEY" "https://healthchecks.io/api/v3/checks/$OLD" unset HC_KEY NEW_URL OLDWith this the
urlfield is already updated and you can skip step 2 for the ping URL. -
Grafana password: generated and saved in step 2, with a single command.
-
Replace the field in its item (
critical,warningsordeploymentsindiscord-webhooks,urlinhealthchecks-ping,passwordingrafana-admin):In the 1Password app, open the item and paste the new value into its field.
-
Verify and apply:
-
If after a few minutes Alertmanager is still using the previous value, restart its pod; for the Grafana password, restart its Deployment (the volume is ephemeral and reads it again on startup):
-
Test with the test alert from step 7 of 4.13.
Ollama and internal services tokens¶
Clients of the Ollama router authenticate with a Bearer token, one per client, so that one can be rotated without affecting the rest. All are generated the same way: 32 random bytes in hexadecimal (64 characters).
| 1Password Item | Fields | Used by |
|---|---|---|
ollama-router |
registry_token |
Ollama nodes, to register with the router |
ollama-router |
open-webui, k8sgpt, reports, alert-triage, translate |
Each router client. translate is also the OLLAMA_TOKEN secret in GitHub Actions, used by the translation of this documentation |
alert-triage-webhook |
token |
Alertmanager, when calling alert-triage |
mcp-api-key |
key |
Open WebUI, when calling MCP tools (mcpo) |
open-terminal |
api_key |
Open WebUI, when calling Open Terminal (guide). After applying, restart the Open Terminal and Open WebUI pods |
To rotate one:
-
Generate the new value and replace only that field in 1Password (minimum 32 characters, no spaces):
Generate the value with the command above (
openssl rand -hex 32) and paste it into the item's field, in the 1Password app.The value is generated inside the command, so you never see it. For example, for the
reportstoken and formcp-api-key:op item edit ollama-router "reports[password]=$(openssl rand -hex 32)" op item edit mcp-api-key "key[password]=$(openssl rand -hex 32)"Other commands: Secrets management.
-
Verify and apply:
-
Restart the service that reads it so it picks up the new value (
kubectl -n <namespace> rollout restart deploy/<nombre>): the router and the client whose token changed (the router also reloads tokens every 30 seconds). Forregistry_token, also update the token on each node (Linux: re-applyplaybook-ollama.yml --tags register; Mac: replace thetokenfile in the agent folder, or the one in the Keychain, andollama-node off/ollama-node on). - Per-node registration tokens (optional). If you use a
registry_token-<nombre-del-nodo>key in theai/ollama-router-secretsSecret, only that token registers that node. To rotate it: change that value in the Secret, update the token on that node and verify it still appears in/_registry/nodes. The shared token still works for nodes that don't have their own, so to fully retire the shared one you must give each node its own first. -
If you rotated
translate, also update theOLLAMA_TOKENsecret in GitHub Actions:
Root password for nodes¶
Each run generates and applies a new password per node, and saves it in the
root@<nodo> item. It does not affect SSH (Secure Shell): root login via SSH
is already disabled; this password is for the console.
# todos
ansible-playbook -i inventory.ini playbook-rotate-root-passwords.yml
ansible-playbook -i inventory.ini playbook-rotate-root-passwords.yml --limit deborah
Sealed Secrets Key¶
Do not rotate lightly. The SealedSecret already in Git were encrypted with the current key: if you change it, those files can no longer be decrypted. The key does not expire in practice (the certificate is valid for 10 years) and automatic renewal is disabled.
Only rotate it if the private key was compromised. In that case:
- Generate a new key pair (command).
- Before replacing anything, reseal each secret with the new key (
kubeseal --cert <tls.crt nuevo> …) and commit the newSealedSecret. -
Replace
certificateandprivate_keyin thesealed-secretsitem, re-runplaybook-platform-secrets.ymland restart the controller:kubectl -n kube-system rollout restart deploy/sealed-secrets.
Reinstalling K3s is not a rotation: with the item in 1Password, the playbook recreates the same Secret and the SealedSecret in Git keep working. Do this before enabling sealed-secrets in root-appset.yaml.
Verify¶
apply_platform_secrets.py --checkshows all ✓.- Argo CD keeps syncing
gitops(kubectl get applications -n argocd). - The runner appears Online in maceosoft → Settings → Actions → Runners (via CLI:
gh api orgs/maceosoft/actions/runners --jq '.runners[] | [.name, .status] | @tsv'). - GitHub login works in Argo CD.