Skip to content

Store API tokens in the OS keyring instead of plaintext credentials.yml #31

@weppos

Description

@weppos

Summary

Today the CLI stores DNSimple API tokens in plaintext at ~/Library/Application Support/dnsimple/credentials.yml (and equivalents on Linux/Windows). Anyone with read access to that file — including malware running as the user, accidental backups, screen-shared terminals running cat, etc. — gets the token.

We should move tokens out of the file and into the OS keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager), with the on-disk file storing only non-sensitive metadata (host, account ID, context name, user email).

Motivation

  • Tokens are bearer credentials with full account access. Plaintext-on-disk is the lowest possible bar.
  • Users on shared workstations, devs running with set -x, and folks who commit dotfiles have all hit this category of leak in other CLIs.
  • This is the standard pattern for CLIs that handle long-lived API tokens (gh, aws, op, glab, doctl).

Reference: how gh does it

The gh CLI uses the same pattern we'd want:

  • ~/.config/gh/hosts.yml stores hostname → user → non-sensitive metadata.
  • The actual OAuth tokens live in the OS keyring, keyed by (hostname, user).
  • A plaintext fallback exists for environments without a keyring (containers, headless CI), gated behind a configuration option.
  • Resolution chain at runtime: env var → keyring(host, active user) → keyring(host) (legacy fallback).

The relevant code is in cli/cli internal/keyring and internal/config (AuthConfig.ActiveToken, TokenFromKeyringForUser). The underlying library is zalando/go-keyring (or 99designs/keyring, depending on the abstraction we want).

Proposed change

  1. Add a keyring package wrapping a vendor library (likely zalando/go-keyring for its smaller surface area; revisit if we hit platform gaps).
  2. Update the credentials file schema (post-Support multiple authenticated contexts with a persistent active selection #28) to store everything except the token. The keyring entry is keyed by (host, account_id) or (context_name) — to be decided in implementation.
  3. Migrate existing plaintext tokens into the keyring on first run, then strip them from the file. Migration should be silent on success and leave a .bak file for one cycle.
  4. Add a fallback mode for environments where the keyring is unavailable (CI, containers without dbus, SSH sessions without an unlocked keychain). The fallback should be opt-in, not automatic, and emit a warning the first time it's used. Suggested gate: DNSIMPLE_KEYRING=plaintext env var.
  5. Honour DNSIMPLE_TOKEN env var override exactly as today — env always wins over keyring.

Open questions

  • Library choice. zalando/go-keyring is simpler; 99designs/keyring (which gh uses) supports more backends including encrypted file fallback. Decision deferred to implementation.
  • Keyring entry key. (host, account_id) is the most explicit but couples the keyring schema to the credentials schema. context_name is cleaner but means renaming a context requires re-keying the keyring. Decision deferred.
  • Headless / CI behaviour. Hard-fail or fall back to plaintext with a warning? Recommend the env-var-gated fallback in step 4.
  • Migration of existing plaintext tokens. Run on first load after upgrade, or on first explicit auth login? Recommend first load, with the .bak safety net.

Dependencies

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request
    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions