Storing connection secrets in the OS keychain

DevOpsPad never writes a password, SSH passphrase or AI API key into its configuration file. Connection metadata (host, port, user, TLS and SSH settings) goes to a JSON file. Secrets go to the operating system’s credential store, under the service name devopspad. If the keychain is not available, saving a secret fails. There is no plaintext fallback.

That rule sounds simple. Most of the work was in the failure cases: what happens when one store succeeds and the other does not.

The problem

Desktop database clients have a history of saving passwords in a config file, sometimes “encrypted” with a key that ships inside the app. Those files end up in backups, dotfile repositories, support bundles and screenshots. The OS keychain (macOS Keychain, Windows Credential Manager, the Secret Service API on Linux) exists to hold exactly this kind of secret, with access control tied to the user session.

Splitting data across two stores creates a consistency problem, though. A connection with SSH has up to three secrets plus a metadata record. If the metadata write fails after the keychain write succeeded, or the other way round, you end up with a profile that claims a password it cannot find, or orphaned secrets nobody can see or delete.

The design

Two stores, one key scheme

Each saved connection gets a UUID. Its secrets are stored under predictable keys:

Keychain key Holds
<connection-id> Database password
<connection-id>:ssh-password SSH password
<connection-id>:ssh-passphrase Private key passphrase
ai-api-key AI provider API key

The metadata only records whether each secret exists (hasPassword, hasPassphrase), never its value. When the workspace file is loaded, any profile that contains a password field is treated as damaged rather than accepted. That stops an old or hand-edited file from quietly reintroducing plaintext.

Changes commit together or not at all

Saving a connection is a nested transaction across both stores. For each secret that changes, DevOpsPad:

  1. reads and remembers the old value, if there was one,
  2. writes the new value (or deletes it),
  3. runs the next step: the next secret, and finally the metadata write.

If any later step fails, each earlier keychain change is undone: the old value is restored, or a newly added value is deleted, and the in-memory list goes back to its previous state. Deleting a connection runs the same chain in reverse, so a failed metadata write does not leave the profile pointing at secrets that are already gone.

Secrets do not round-trip to the UI

After saving, the frontend never receives a stored secret again. A blank password field in Edit connection means “keep the saved one”. Typing a value replaces it. When you switch SSH from password to key authentication, or turn SSH off, the secrets that are no longer used are removed in the same save. Changing the AI provider’s endpoint without entering a new key deletes the old key, so a key for one provider is never sent to another.

Secrets are read from the keychain just before an operation needs them, and the AI provider URL and its key are read under the same lock. A concurrent settings change therefore cannot pair one provider’s URL with another provider’s key.

The metadata file is written atomically

workspace.json lives in the OS user config directory (~/Library/Application Support/DevOpsPad/ on macOS). Every write goes to a temporary file in the same directory. The temporary file is created with owner-only permissions, synced to disk and then renamed over the original, so a crash leaves either the old file or the new one, never half of each. A second copy, workspace.json.backup, is written after each successful save. The file is capped at 4 MiB.

When the file cannot be read, fails validation or was written by a newer version, DevOpsPad does not replace it with an empty workspace. It blocks saving and reports the problem in its workspace health view, so the original can be inspected or restored from the backup.

Trade-offs

  • The keychain is per machine. Profiles shared through files or Git arrive without secrets; each person enters their own. That is intended. Credentials can differ per person while connection names match.
  • Restoring metadata from a backup does not restore secrets. The backup holds metadata only, and passwords may need to be re-entered.
  • Rollback can itself fail. If the keychain rejects the undo write, both errors are reported together. A two-store transaction has no perfect answer, but it should never fail silently.
  • Linux depends on a Secret Service provider (such as GNOME Keyring) being present and unlocked. Without one, saving secrets fails by design.

Not covered

Query drafts, if you turn on Restore query drafts on startup, are kept in local webview storage and are not encrypted. Query text can contain sensitive values, and the setting says so. It is off by default. Logs and results are not persisted at all.

Practical details are in keychain secrets and where connections are stored. The security model page puts this together with SSH host keys and the read-only query design.