Files
ansiblings/packages/keyman
Benjamin Diedrichsen 653d348ecc [keyman] phase 4: decrypt stops destroying keys and stops the 0644 window
Verified before the fix: `age -d -o <existing>` overwrites without a word
("PRECIOUS EXISTING KEY" became "secret"), and the old `cp` for the public
key did the same. Decrypting a vault entry on top of a newer working key
in ~/.ssh destroyed it with no prompt, no backup and no mention. It is the
only finding in the audit that loses data the user never asked to touch.

Every collision — private and public, both output modes — is now settled
before anything is written, so the questions are asked about files that
still exist. Default is to keep what is there.

cp and chmod are gone. Three spawns per key become one, it works where
those binaries do not, and the chmod happens in-process immediately after
age returns: age creates its output 0644 regardless of umask, so a
plaintext private key was world-readable for the length of two spawns and
stayed 0644 whenever the chmod itself failed. ~/.ssh is created 0700 when
absent rather than assumed.

decrypt.test.ts stops asserting on which binaries were spawned. The age
stand-in now writes its -o file at 0644 the way age does, and the tests
assert the bytes and the mode on disk — the outcome rather than the
mechanism.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 14:39:30 +02:00
..
2026-07-27 13:09:00 +02:00

Keyman - SSH Key Management with Age Encryption

Keyman is a simple command line tool built around the age encryption tool. It allows you to manage SSH keys in public GitHub repositories securely by encrypting the private keys.

Features

  • 🔐 Encrypt SSH private keys with age encryption
  • 📁 Organized vault structure: vault/keys/ for encrypted keys, vault/tmp/ for decrypted keys
  • ⚙️ Configurable via .keymanrc.json with sensible defaults
  • 🔍 Interactive CLI for encrypting, decrypting, and listing keys
  • 🔄 Support for key rotation

Quick Start

1. Generate Age Encryption Key

# Create vault structure
mkdir -p vault/keys vault/tmp

# Generate age encryption key (keep this secret!)
age-keygen -o vault/age.key

# Add to .gitignore
echo "vault/age.key" >> .gitignore
echo "vault/tmp/" >> .gitignore

2. Generate SSH Keys

# Generate SSH key pair
ssh-keygen -t ed25519 -f vault/tmp/id_deploy -N "" -C "deploy@myapp.dev"

3. Run Keyman

# Run keyman interactively
VAULT_ROOT=./vault keyman

# Or if you have .keymanrc.json configured, just run:
keyman

Configuration

Keyman uses sensible defaults but can be customized via .keymanrc.json:

{
  "vaultRoot": "./vault",
  "keysDir": "keys",
  "tmpDir": "tmp",
  "ageKeyFile": "age.key"
}

Configuration Priority

  1. VAULT_ROOT environment variable (highest priority)
  2. .keymanrc.json file (searched from current directory upward)
  3. Default values (lowest priority)

Default Values

  • vaultRoot: "vault"
  • keysDir: "keys"
  • tmpDir: "tmp"
  • ageKeyFile: "age.key"

Vault Structure

project/
├── vault/
│   ├── age.key         # Master encryption key (NEVER commit!)
│   ├── keys/           # Encrypted keys (safe to commit)
│   │   └── deploy/     # Each key has its own folder
│   │       ├── id_deploy.pub        # Public key
│   │       └── id_deploy.age        # Encrypted private key
│   └── tmp/            # Decrypted keys (NEVER commit!)
│       ├── id_deploy       # Decrypted private key
│       └── id_deploy.pub   # Public key
└── .keymanrc.json      # Configuration (optional)

Operations

Keyman provides an interactive menu-driven interface with the following operations:

  • 📋 List keys - Compact view showing all keys with checkbox indicators for their locations
  • 🔒 Encrypt keys - Encrypt SSH keys from vault/tmp/ and store in vault/keys/
  • 🔓 Decrypt keys - Decrypt keys from vault/keys/ to vault/tmp/ or ~/.ssh/
  • Quit - Exit the program

After completing any operation, keyman automatically returns to the main menu, allowing you to perform multiple operations in a single session without restarting the tool.

List Keys Output

The list command shows a compact, unified view of all SSH keys with their locations:

🔑 SSH Keys:

  Key Name                      [Vault] [Tmp] [.ssh]
  ──────────────────────────────────────────────────────────
  ✅ id_deploy (.pub)              [✓]   [ ]  [✓]
  🔓 id_github (.pub)              [✓]   [✓]  [ ]
  🔒 id_backup (.pub)              [✓]   [ ]  [ ]
  ⚠️  id_local (.pub)               [ ]   [ ]  [✓]

  Legend:
  ✅ = Managed (encrypted in vault + active in .ssh)
  🔓 = Decrypted (in vault + decrypted to tmp)
  🔒 = Encrypted only (in vault, not decrypted)
  ⚠️  = Unmanaged (in .ssh or tmp, not encrypted in vault)

Features:

  • Public keys are indicated with (.pub) suffix instead of separate entries
  • Status emoji shows management state at a glance
  • Checkboxes [✓] show presence in three locations:
    • [Vault] - Encrypted in vault/keys/
    • [Tmp] - Decrypted in vault/tmp/
    • [.ssh] - Active in ~/.ssh/
  • Alphabetically sorted for easy scanning
  • New 🔓 status for keys decrypted to tmp but not yet in .ssh

Example Usage

# Using environment variable
VAULT_ROOT=../../vault keyman

# Using default configuration
keyman

# Keyman will show:
# 📁 Vault Root: /path/to/vault
# 🔑 Keys Directory: /path/to/vault/keys
# 📂 Temp Directory: /path/to/vault/tmp
# 🔐 Age Key: /path/to/vault/age.key

Best Practices

  1. Never commit vault/age.key or vault/tmp/ to version control
  2. Always backup your age.key securely (password manager, encrypted USB)
  3. Commit vault/keys/ - encrypted keys are safe to share
  4. Use environment variables for CI/CD: VAULT_ROOT=/path/to/vault keyman
  5. Keep .keymanrc.json in your project root for team consistency