Skip to content

v0.17.0 - Non-Plaintext SSH Passwords: Keychain and GPG (2026-07-11)

What Changed?

This release adds two new SSH password sources — the macOS Keychain (#41) and GPG-encrypted files (#42) — as non-plaintext alternatives to storing passwords in .env. It also splits the growing env.py module into three focused files (#55) now that it orchestrates three password sources instead of one.


What's New

Main Feature: Keychain and GPG Password Sources

What it does: fnb.get_ssh_password() now checks passwords in this order: host-specific FNB_PASSWORD_* env var → macOS KeychainGPG-encrypted fileFNB_PASSWORD_DEFAULT → interactive SSH prompt. Both new sources are opt-in (nothing changes if you don't set them up) and fail gracefully to the next source on any error.

  • macOS Keychain: fnb looks up a generic password item named fnb-{host} via the security CLI. The lookup matches on service name only, so any account name works — add one manually via Keychain Access.app or security add-generic-password. No-op on non-macOS platforms.
  • GPG-encrypted files: fnb decrypts ~/.config/fnb/passwords/{normalized_host}.gpg, falling back to default.gpg, via gpg --decrypt --quiet --batch. Cross-platform, and works with both symmetric (passphrase) and public-key encrypted files since decryption is fully delegated to gpg/gpg-agent. fnb warns (without blocking) if a password file is readable by group or other.

How to use it:

# Keychain (macOS)
security add-generic-password -s "fnb-user@remote-host" -a "user@remote-host" -w "your-password"

# GPG (any platform)
mkdir -p ~/.config/fnb/passwords
echo -n "your-password" | gpg --symmetric -o ~/.config/fnb/passwords/user_remote_host.gpg

See the Security Guide for full setup and migration steps from .env.


Installation

Quick Start

# Get the release
git checkout v0.17.0

# Setup
uv sync --all-groups

# Run CLI
uv run fnb --help

What's Different from the Last Version?

✅ Added

  • macOS Keychain password source (src/fnb/keychain.py)
  • GPG-encrypted password file source (src/fnb/gpg.py)
  • Migration guides and troubleshooting for both in the Security Guide

🔧 Changed

  • env.py (472 lines) split into env.py (261 lines, orchestration only), keychain.py, and gpg.py — no behavior change, pure internal reorganization
  • env.get_ssh_password()'s documented lookup order now includes the Keychain and GPG steps

🐛 Fixed

  • None in this release

Is It Safe to Upgrade?

Backward Compatible: Yes.

  • No configuration changes required; both new password sources are entirely opt-in
  • If neither a Keychain item nor a GPG file exists for a host, behavior is identical to before this release
  • The env.py module split is a pure refactor — fetcher.py/uploader.py (the only external consumers) still import get_ssh_password/get_timeout from fnb.env unchanged

Tests Passed

  • ✅ 188 unit and integration tests passed
  • ✅ 90% code coverage (100% on both new keychain.py and gpg.py)
  • ✅ All pre-commit hooks pass (mypy, ruff, trailing whitespace, etc.)
  • uv run --group docs zensical build — no new warnings from the doc changes
  • ✅ Manual verification on real macOS: security add/find/delete-generic-password cycle, and gpg --symmetric encrypt → decrypt cycle including the graceful-fallback and permission-warning paths (throwaway test entries only, removed after verification)

Release Details

  • Date: 2026-07-11
  • Version: v0.17.0
  • Commits: 7 feature/refactor/doc commits since v0.16.0, closing out issues #41, #42, and #55
  • 9667e2b: feat(env): add macOS Keychain lookup for SSH passwords
  • a786550: docs: document the macOS Keychain integration
  • 33197ee: docs(env): use a meaningful account name in Keychain examples
  • 5520645: feat(env): add GPG-encrypted password files for SSH passwords
  • 58f7589: docs: document the GPG-encrypted password file integration
  • 6ba747b: refactor(env): extract macOS Keychain lookup into keychain.py
  • 1c660ad: refactor(env): extract GPG-encrypted file lookup into gpg.py
  • d0345b9: docs: reflect the env.py/keychain.py/gpg.py split

Next Steps

  • Consider a dedicated --ask-password flag for a masked interactive prompt, distinct from --ssh-password VALUE (which is inherently recorded in shell history once typed) — currently deferred
  • Phase 6 (Optional) items from #41/#42 remain out of scope: Windows Credential Manager, Linux Secret Service, multiple GPG keys, password rotation, gpg-agent passphrase caching UX