7.4 KiB
7.4 KiB
OpenBao Secrets Migration
Overview
This document covers migrating from ragenix (age-encrypted secrets) to OpenBao for centralized secret management, enabling zero-config machine onboarding.
Goals
- Zero-config machine onboarding: New machine = install NixOS + add Zitadel machine key + done
- Eliminate re-keying workflow: No more updating secrets.nix and re-encrypting .age files for each new machine
- Runtime secret dependencies: Services wait for secrets via systemd, not build-time conditionals
- Consolidated SSH keys: Use single
nix2nixkey for all NixOS machine SSH (keepnix2tfor work) - Declarative policy management: OpenBao policies auto-applied after unseal with reconciliation
- Directional Tailscale ACLs: Restrict work machine from reaching NixOS hosts (one-way access)
- Per-host variable registry:
_variables.nixpattern for ports/UIDs/GIDs to prevent conflicts
Current State
Ragenix Secrets in Use (21 active)
SSH Keys (for client auth):
- nix2github, nix2bitbucket, nix2gitforgejo
- nix2nix (shared), nix2t (work - keep separate)
- nix2lio (remote builds), nix2oren, nix2gpdPocket3
- nix2h001, nix2h003, nix2linode, nix2oracle
API Tokens:
- github_read_token (Nix private repo access)
- linode_rw_domains (ACME DNS challenge)
- litellm_public_api_key (nginx auth)
VPN:
- headscale_auth (Tailscale auth)
- us_chi_wg (NixArr WireGuard)
Application Secrets:
- oauth2_proxy_key_file
- openwebui_env
- zitadel_master_key
- vaultwarden_env
Skipping (unused):
- nix2h002, nix2joe, nix2l002, nix2gitjosh, obsidian_sync_env
Already Migrated to OpenBao (juni)
- headscale_auth, atuin-key-josh, 12 SSH keys
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ New Machine Onboarding │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Install NixOS with full config │
│ - All services defined but waiting on secrets │
│ │
│ 2. Create Zitadel machine user + copy key │
│ - /machine-key.json → JWT auth to OpenBao │
│ │
│ 3. vault-agent fetches secrets │
│ - kv/data/machines/home_roaming/* → /var/lib/openbao-secrets│
│ │
│ 4. systemd dependencies resolve │
│ - secret-watcher completes → hardDepend services start │
│ │
│ 5. Machine fully operational │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Design Decisions
Secret Path Convention
kv/data/machines/
├── home_roaming/ # Shared across all NixOS machines
│ ├── nix2nix # SSH key
│ ├── nix2github # SSH key
│ ├── headscale_auth # Tailscale auth
│ └── ...
├── home/ # h001-specific (not roaming)
│ ├── linode_rw_domains
│ ├── zitadel_master_key
│ └── ...
└── oracle/ # o001-specific
├── vaultwarden_env
└── ...
Runtime Dependencies vs Build-Time Conditionals
Before (ragenix pattern - bad for onboarding):
let hasSecret = name: (config.age.secrets or {}) ? ${name};
in {
config = lib.mkIf (hasSecret "openwebui_env") {
services.open-webui.enable = true;
};
}
After (OpenBao pattern - zero-config onboarding):
ringofstorms.secretsBao.secrets.openwebui_env = {
kvPath = "kv/data/machines/home_roaming/openwebui_env";
hardDepend = [ "open-webui" ]; # Service waits for secret at runtime
configChanges.services.open-webui = {
enable = true;
environmentFile = "$SECRET_PATH";
};
};
Per-Host File Structure
hosts/h001/
├── _variables.nix # Ports, UIDs, GIDs - single source of truth
├── secrets.nix # All secrets + their configChanges
├── flake.nix # Imports, basic host config
├── nginx.nix # Pure config (no conditionals)
└── mods/
├── openbao-policies.nix # Auto-apply after unseal
└── ...
OpenBao Policy Management
Policies auto-apply after unseal with full reconciliation:
# openbao-policies.nix
let
policies = {
machines = ''
path "kv/data/machines/home_roaming/*" {
capabilities = ["read", "list"]
}
'';
};
reservedPolicies = [ "default" "root" ];
in {
systemd.services.openbao-apply-policies = {
after = [ "openbao-auto-unseal.service" ];
requires = [ "openbao-auto-unseal.service" ];
wantedBy = [ "multi-user.target" ];
# Script: apply all policies, delete orphans not in config
};
}
Headscale ACL Policy
Directional access control:
# nix machines: full mesh access
{ action = "accept"; src = ["group:nix-machines"]; dst = ["group:nix-machines:*"]; }
# nix machines → work: full access
{ action = "accept"; src = ["group:nix-machines"]; dst = ["tag:work:*"]; }
# work → nix machines: LIMITED (only specific ports)
{ action = "accept"; src = ["tag:work"]; dst = ["h001:22,443"]; }
Implementation Phases
Phase 1: SSH Key Preparation
- Add nix2nix SSH key to all hosts authorized_keys (alongside existing)
- Deploy with
nh os switchto all hosts
Phase 2: Infrastructure
- Create
_variables.nixpattern for h001 (pilot) - Create
openbao-policies.nixwith auto-apply + reconciliation - Create
headscale-policy.nixwith directional ACLs - Create per-host
secrets.nixpattern
Phase 3: Secret Migration
- Migrate h001 secrets (linode_rw_domains, us_chi_wg, oauth2_proxy_key_file, openwebui_env, zitadel_master_key)
- Migrate o001 secrets (vaultwarden_env, litellm_public_api_key)
- Migrate common modules (tailnet, ssh, nix_options)
- Migrate SSH client keys
Phase 4: Consumer Updates
- Update ssh.nix to use OpenBao paths
- Remove hasSecret conditionals from all modules
- Remove ragenix imports and secrets flake
Phase 5: Testing & Finalization
- Populate all secrets in OpenBao KV store
- Test onboarding workflow on fresh VM
- Document new machine onboarding process
Related Ideas
impermanence_everywhere.md- Impermanence persists/var/lib/openbao-secretsand/machine-key.jsonresilience.md- OpenBao server (h001) is a SPOF; consider backup/failoverservice_backups.md-/var/lib/openbaoand/bao-keysneed backup
Notes
- OpenBao hosted on h001 at sec.joshuabell.xyz
- JWT auth via Zitadel machine users
- vault-agent on each host fetches secrets
secCLI tool available for manual lookups