# ADR 024: Doppler for homelab secrets

- HTML version: https://robbiepalmer.me/projects/homelab/adrs/024-doppler-secrets
- Project: Home Lab (https://robbiepalmer.me/projects/homelab.md)
- Status: Accepted
- Date: 2025-12-31
- Inherited from project: personal-site (https://robbiepalmer.me/projects/personal-site/adrs/035-doppler.md)

The homelab adopts the existing Doppler decision. The rules below cover the
parts that differ from the recipe stack.

# Project and config

Keep homelab secrets in the separate Doppler project `homelab`. Production
workloads use its `prd` config. This prevents Ansible and K3s credentials from
granting access to the recipe stack.

Human-run mise commands authenticate through the local Doppler CLI. A command
that needs secrets uses `doppler run --project homelab --config prd` and
allowlists the names it needs.

Run this non-secret check from an operator machine:

```bash
mise run //homelab:doppler-check
```

# Ansible

Ansible reads secrets from the controller environment. Every task that consumes
a secret must set `no_log: true`. Do not copy a complete Doppler config to a
managed host.

# Unattended workloads

Give each unattended workload a read-only Doppler service token scoped to one
config. Never use a personal or CLI token. Doppler documents this boundary in
its [service-token guidance](https://docs.doppler.com/docs/service-tokens).

When the K3s pilot needs its first secret, install the
[Doppler Kubernetes Operator](https://docs.doppler.com/docs/doppler-k8s-operator-syncing-secrets).
Bootstrap its token through a local mise command that does not print it, then
use namespace-scoped `DopplerSecret` resources. Workloads receive synchronized
Kubernetes Secrets, not the Doppler token.

# Homelab-specific alternatives

## Ansible Vault

[Ansible Vault](https://docs.ansible.com/projects/ansible/latest/vault_guide/vault.html)
can encrypt variables and files kept with the playbooks. It fits Ansible, but
it only protects data at rest. A new operator machine still needs the vault
password through a separate recovery path, and K3s workloads need another
secret-distribution system. Reject it as the homelab source of truth. Ansible
tasks still need `no_log: true` whichever store supplies their secrets.

## SOPS with age

[SOPS](https://getsops.io/docs/) with age would keep encrypted values beside
the Ansible and K3s configuration. It would also allow recovery without
Doppler once an operator restores the age identity. It needs its own key
distribution and recovery procedure, plus decryption plumbing for both
Ansible and K3s. Reconsider it if dependence on Doppler becomes unacceptable.

## Plain Kubernetes Secrets

Kubernetes Secret manifests encode values with base64; that does not make them
safe to commit. Creating them manually would also leave rotation and fresh
cluster recovery outside the declarative configuration. Use Kubernetes Secrets
as the runtime format produced by the Doppler operator, not as the source of
truth.

## K3s secrets encryption

[K3s secrets encryption](https://docs.k3s.io/security/secrets-encryption)
protects Secret data in the cluster datastore and its backups. It does not
decide where values originate, rotate application credentials, or supply
Ansible. Enable it as a separate defence before sensitive workloads move to
K3s. Keep Doppler as the source of truth.

## Sealed Secrets

[Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) would make
encrypted Secret manifests safe to store in Git. Its controller keeps the
private sealing keys in the cluster, so disaster recovery must also back up
and restore those keys. It does not supply Ansible. Reject it because the
Doppler project already provides one source for both deployment paths.

# Ente exception

The Ente CLI database at `~/.ente/ente-cli.db` remains outside Doppler. The
database stores an authenticated application session rather than a key-value
secret. Keep it local with mode `0600`, and recreate it through Ente's
interactive login on a replacement Mac.

# Recovery and migration

A new operator machine installs the Doppler CLI, signs in, checks out this
repository, and runs the access check above. Store Doppler account recovery
codes outside the homelab and repository.

Existing Kubernetes Secrets keep workloads running during a Doppler outage.
Rotation and fresh cluster bootstrap wait for Doppler to recover. Keep current
gitignored workload files until you have tested each Doppler consumer and its
rollback path.

## Notes for Home Lab

The homelab adopts the existing Doppler decision. The rules below cover the
parts that differ from the recipe stack.

# Project and config

Keep homelab secrets in the separate Doppler project `homelab`. Production
workloads use its `prd` config. This prevents Ansible and K3s credentials from
granting access to the recipe stack.

Human-run mise commands authenticate through the local Doppler CLI. A command
that needs secrets uses `doppler run --project homelab --config prd` and
allowlists the names it needs.

Run this non-secret check from an operator machine:

```bash
mise run //homelab:doppler-check
```

# Ansible

Ansible reads secrets from the controller environment. Every task that consumes
a secret must set `no_log: true`. Do not copy a complete Doppler config to a
managed host.

# Unattended workloads

Give each unattended workload a read-only Doppler service token scoped to one
config. Never use a personal or CLI token. Doppler documents this boundary in
its [service-token guidance](https://docs.doppler.com/docs/service-tokens).

When the K3s pilot needs its first secret, install the
[Doppler Kubernetes Operator](https://docs.doppler.com/docs/doppler-k8s-operator-syncing-secrets).
Bootstrap its token through a local mise command that does not print it, then
use namespace-scoped `DopplerSecret` resources. Workloads receive synchronized
Kubernetes Secrets, not the Doppler token.

# Homelab-specific alternatives

## Ansible Vault

[Ansible Vault](https://docs.ansible.com/projects/ansible/latest/vault_guide/vault.html)
can encrypt variables and files kept with the playbooks. It fits Ansible, but
it only protects data at rest. A new operator machine still needs the vault
password through a separate recovery path, and K3s workloads need another
secret-distribution system. Reject it as the homelab source of truth. Ansible
tasks still need `no_log: true` whichever store supplies their secrets.

## SOPS with age

[SOPS](https://getsops.io/docs/) with age would keep encrypted values beside
the Ansible and K3s configuration. It would also allow recovery without
Doppler once an operator restores the age identity. It needs its own key
distribution and recovery procedure, plus decryption plumbing for both
Ansible and K3s. Reconsider it if dependence on Doppler becomes unacceptable.

## Plain Kubernetes Secrets

Kubernetes Secret manifests encode values with base64; that does not make them
safe to commit. Creating them manually would also leave rotation and fresh
cluster recovery outside the declarative configuration. Use Kubernetes Secrets
as the runtime format produced by the Doppler operator, not as the source of
truth.

## K3s secrets encryption

[K3s secrets encryption](https://docs.k3s.io/security/secrets-encryption)
protects Secret data in the cluster datastore and its backups. It does not
decide where values originate, rotate application credentials, or supply
Ansible. Enable it as a separate defence before sensitive workloads move to
K3s. Keep Doppler as the source of truth.

## Sealed Secrets

[Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) would make
encrypted Secret manifests safe to store in Git. Its controller keeps the
private sealing keys in the cluster, so disaster recovery must also back up
and restore those keys. It does not supply Ansible. Reject it because the
Doppler project already provides one source for both deployment paths.

# Ente exception

The Ente CLI database at `~/.ente/ente-cli.db` remains outside Doppler. The
database stores an authenticated application session rather than a key-value
secret. Keep it local with mode `0600`, and recreate it through Ente's
interactive login on a replacement Mac.

# Recovery and migration

A new operator machine installs the Doppler CLI, signs in, checks out this
repository, and runs the access check above. Store Doppler account recovery
codes outside the homelab and repository.

Existing Kubernetes Secrets keep workloads running during a Doppler outage.
Rotation and fresh cluster bootstrap wait for Doppler to recover. Keep current
gitignored workload files until you have tested each Doppler consumer and its
rollback path.

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
