ADR-012: Developer secret management with 1Password CLI
Status: Accepted Date: 2026-03-01 Decision Makers: Human + AI Prompted By: ADRs 008–010 established infrastructure that requires secrets (Cloudflare API token, GitHub PAT for Flux, Azure credentials). As the project grows to include PostgreSQL (ADR-007) and potentially a second contributor, a consistent workflow for how secrets are stored, accessed, and injected during development must be decided. Three distinct contexts require secret management: local development, CI/CD pipelines, and running infrastructure.
Context
Section titled “Context”The project currently has a small but growing set of secrets:
| Secret | Used by | Context |
|---|---|---|
| Cloudflare API token | Terraform (local + CI) | Infrastructure provisioning |
| GitHub PAT (Flux) | cloud-init, Terraform | K3s Flux bootstrap |
| Azure credentials | Terraform, Azure CLI | Infrastructure provisioning |
| SSH private key | Developer | VM access for debugging |
| PostgreSQL connection string | Kubernetes pods (future) | Application runtime |
These secrets span three contexts with different trust models:
Local development: A developer runs Terraform, Azure CLI, and kubectl from their machine or Codespace. Secrets must be available in the terminal session without being written to files on disk (no terraform.tfvars with real tokens committed, no plaintext exports in .bashrc).
CI/CD pipelines: GitHub Actions needs Azure and Cloudflare credentials to run Terraform plan/apply and push images to ACR. ADR-009 established OIDC federation for Azure (no stored secret) and GitHub Actions secrets for the Cloudflare token.
Running infrastructure: The K3s cluster needs to pull images from ACR (Managed Identity, no secret) and will eventually need database credentials (Key Vault, future). ADR-010 eliminated stored credentials for image pulls.
The gap is in local development. Without a defined workflow, secrets end up in shell history, plaintext dotfiles, terraform.tfvars files that risk being committed, or environment variables set manually each session. Kevin already uses 1Password — the question is whether to formalise it as the project’s local secret management tool.
Decision Drivers
Section titled “Decision Drivers”- No secrets on disk.
terraform.tfvarsmust never contain real secret values. No plaintext tokens in dotfiles, shell history, or environment variable exports. - Session injection. Secrets must be available in the terminal session for the duration of a command or session, then gone. Not persisted between sessions.
- Team-ready. A second contributor must be able to onboard by following documented steps, not by asking Kevin to share tokens via Slack.
- Consistent with CI/CD model. Local development should mirror CI/CD where possible — same variables, same providers, same authentication flow.
- Works in Codespaces. The development environment is GitHub Codespaces. The solution must work inside a Codespace, not just on a local machine.
- Existing tooling. Kevin already has a 1Password subscription. No new cost.
Options Considered
Section titled “Options Considered”Option A: 1Password CLI (op)
Section titled “Option A: 1Password CLI (op)”1Password’s CLI tool injects secrets from a 1Password vault into commands and environment variables at runtime. Secrets never touch disk.
Usage patterns:
# Inject secrets into a single commandop run --env-file=.env.tpl -- terraform plan
# Reference secrets in a template file (.env.tpl)CLOUDFLARE_API_TOKEN=op://DevOps/Cloudflare/api-tokenTF_VAR_cloudflare_api_token=op://DevOps/Cloudflare/api-tokenTF_VAR_github_token=op://DevOps/GitHub-Flux-PAT/token
# Read a single secretop read "op://DevOps/Cloudflare/api-token"The .env.tpl file is committed to the repo — it contains secret references (URIs), not secret values. Any developer with 1Password access and the shared vault can run the same commands.
Option B: Bitwarden CLI (bw)
Section titled “Option B: Bitwarden CLI (bw)”Bitwarden’s CLI can retrieve secrets via bw get password <name>. Open source, self-hostable.
The CLI is less ergonomic than 1Password for developer workflows. There is no op run equivalent — secrets must be captured into shell variables manually:
export TF_VAR_cloudflare_api_token=$(bw get password cloudflare-api-token)terraform planThis works but leaves the secret in the shell environment for the rest of the session. It also puts the secret in shell history if the export command is typed directly. Bitwarden does not support template files with secret references.
Kevin does not currently have a Bitwarden account — this would require migration.
Option C: Environment variables (manual)
Section titled “Option C: Environment variables (manual)”No password manager integration. Secrets are set manually at the start of each session:
export TF_VAR_cloudflare_api_token="cf-token-here"export TF_VAR_github_token="ghp-token-here"Simple and universal. No tooling dependency. But secrets appear in shell history, persist in the environment for the full session, and must be re-entered on every session start. No sharing mechanism beyond “send it to me on Slack.” Scales to one person, barely.
Option D: GitHub Codespaces secrets
Section titled “Option D: GitHub Codespaces secrets”Codespaces can inject secrets as environment variables via Settings → Codespaces → Secrets. These are available in every Codespace session automatically.
Solves the session injection problem for Codespaces specifically. Does not help when working outside Codespaces (local machine, VM, other CI). Secrets are tied to the GitHub account, not a shared vault — a second contributor would need their own copies. No template file or documentation of which secrets are required.
Option E: Azure Key Vault for local development
Section titled “Option E: Azure Key Vault for local development”Use Azure Key Vault as the secret store for local development. Retrieve secrets via az keyvault secret show.
Adds a cloud dependency to local development — every terraform plan requires an Azure connection to retrieve secrets before it can connect to Azure to plan. Circular complexity. Key Vault is the right choice for runtime secrets in the K3s cluster (future), not for developer workstation secrets.
Decision
Section titled “Decision”1Password CLI (op) for local developer secret management. GitHub Actions secrets for CI/CD. Azure Key Vault reserved for runtime infrastructure secrets (future). Option A for local, with the existing CI/CD and infrastructure patterns from ADRs 008–010 unchanged.
Three-tier secret management model
Section titled “Three-tier secret management model”| Context | Mechanism | Secret storage |
|---|---|---|
| Local development | 1Password CLI (op run with .env.tpl) | 1Password vault |
| CI/CD | OIDC federation (Azure), GitHub Actions secrets (Cloudflare) | GitHub / Azure AD |
| Running infrastructure | Managed Identity (ACR), Key Vault (future, PostgreSQL) | Azure |
Implementation
Section titled “Implementation”.env.tpl (committed to repo — contains references, not values):
TF_VAR_cloudflare_api_token=op://DevOps/Cloudflare/api-tokenTF_VAR_cloudflare_zone_id=op://DevOps/Cloudflare/zone-idTF_VAR_github_token=op://DevOps/GitHub-Flux-PAT/tokenTF_VAR_admin_ssh_public_key=op://DevOps/Azure-VM-SSH/public-keyARM_SUBSCRIPTION_ID=op://DevOps/Azure/subscription-idDeveloper workflow:
# One-time: install 1Password CLI and sign inop signin
# Run Terraform with secrets injectedcd infraop run --env-file=.env.tpl -- terraform planop run --env-file=.env.tpl -- terraform applyOnboarding a new contributor:
- Install 1Password CLI
- Request access to the DevOps vault in 1Password
op run --env-file=.env.tpl -- terraform planworks immediately
.env.tpl is NOT .env. The .tpl extension makes it clear this is a template with references. .env files are gitignored. .env.tpl is committed.
What this does NOT cover
Section titled “What this does NOT cover”- CI/CD secrets — unchanged from ADR-009. OIDC for Azure, GitHub Actions secrets for Cloudflare.
- Runtime secrets — Azure Managed Identity for ACR pulls (ADR-010). Key Vault for PostgreSQL credentials (future, when ADR-007 is implemented).
- Kubernetes secrets — External Secrets Operator or similar to sync Key Vault into Kubernetes (future).
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- No secrets on disk.
op runinjects secrets as environment variables for the duration of the command only. They do not persist in the shell, dotfiles, or files on disk - Committed secret references.
.env.tpldocuments exactly which secrets are required, where they live in 1Password, and what Terraform variable they map to. A new contributor reads the file and knows what to set up - Session-scoped injection. Secrets exist only for the lifetime of the
op runcommand. No residual tokens in the environment after the command completes - Team-ready. Sharing access means granting vault access in 1Password. No copying tokens over Slack, no per-person
.envfiles - Works in Codespaces. 1Password CLI runs in Codespaces.
op signinauthenticates via the browser, thenop runworks in the terminal
Negative
Section titled “Negative”- 1Password dependency. Every developer needs a 1Password account and the CLI installed. This is a paid tool (~$3/month per user). For a solo consultancy this is trivial; for open-source contributors it would be a barrier. Mitigation: the
.env.tplformat is self-documenting — a contributor without 1Password can manually export the same variables op signinper session. 1Password CLI sessions expire. Developers must re-authenticate periodically. Mitigation: 1Password integrates with system biometrics for faster re-auth- 1Password vault structure must be maintained. Secret references in
.env.tplare URIs to specific vault items. If items are renamed or moved, the references break. Mitigation: use a dedicated DevOps vault with stable naming conventions
.envfile accidentally committed. If a developer creates a.envfile with real values (bypassingop run), it could be committed. Mitigation:.envis in.gitignore. The pre-commit hook could be extended to reject files matching.envpatterns- 1Password outage blocks development. If 1Password is unavailable,
op runfails and Terraform cannot be run. Mitigation: 1Password CLI caches vault items locally for offline access. Extended outages are rare - Codespace-1Password authentication flow.
op signinin a Codespace requires browser-based authentication. If the Codespace cannot open a browser (e.g., SSH-only access), authentication requires a manual token flow. Mitigation: document theop signin --rawflow for headless environments
Agent Decisions
Section titled “Agent Decisions”To be completed after implementation.
| Decision | Rationale | Acceptable |
|---|---|---|
| Pending | Pending | Pending |
References
Section titled “References”- ADR-008: Infrastructure-as-Code with Terraform — Terraform variable injection
- ADR-009: CI/CD with GitHub Actions and Flux CD — CI/CD secret model
- ADR-010: ACR as primary registry, retain GHCR — Managed Identity for image pulls
- 1Password CLI documentation
- 1Password secret references
- 1Password
.envfile support