How Tabby Handles SSH Password and Private Key Storage Securely

Tabby encrypts SSH credentials using either an AES-256-CBC encrypted vault file or the OS native keychain, ensuring passwords and private-key passphrases are never stored in plain text.

The open-source terminal emulator Eugeny/tabby routes all sensitive credential handling through a centralized abstraction layer. This article examines how Tabby securely stores SSH passwords and private key passphrases by analyzing the PasswordStorageService implementation and its dual-backend architecture.

The PasswordStorageService Architecture

Tabby’s credential system is implemented in tabby-ssh/src/services/passwordStorage.service.ts. The service provides a unified API for saving and loading secrets while abstracting two distinct storage backends: an encrypted JSON vault and the operating system’s native credential store.

VaultService (Encrypted Vault)

When enabled, VaultService (located in tabby-core/src/services/vault.service.ts) stores secrets in a JSON vault file encrypted with AES-256-CBC. The user supplies a passphrase during vault initialization, which is derived into a cryptographic key via deriveVaultKey and never persisted to disk. The passphrase lives only in memory (cached in the module-private variable _rememberedPassphrase) while the vault is unlocked.

Secrets are stored as typed objects with the following structure:

  • ssh:password – Plain SSH passwords (VAULT_SECRET_TYPE_PASSWORD)
  • ssh:key-passphrase – Private-key passphrases (VAULT_SECRET_TYPE_PASSPHRASE)

Keytar (OS Keychain Integration)

If the vault is disabled, Tabby falls back to the keytar npm module, which proxies to the host OS secure storage:

  • macOS: Keychain Services
  • Windows: Credential Manager
  • Linux: Secret Service (via libsecret)

Keytar encrypts data using OS-level facilities, ensuring secrets remain outside Tabby’s JavaScript runtime when at rest.

How Credentials Are Saved and Retrieved

The PasswordStorageService exposes four primary methods that SSH sessions and UI components consume. The implementation checks this.vault.isEnabled() to determine which backend to use.

Storing SSH Passwords

Passwords are saved via savePassword() after a successful authentication or through the settings UI component (sshProfileSettings.component.ts):

// passwordStorage.service.ts
async savePassword (profile: SSHProfile, password: string, username?: string) {
    const account = username ?? profile.options.user;
    if (this.vault.isEnabled()) {
        const key = this.getVaultKeyForConnection(profile, account);
        this.vault.addSecret({ 
            type: VAULT_SECRET_TYPE_PASSWORD, 
            key, 
            value: password 
        });
    } else {
        const key = this.getKeytarKeyForConnection(profile);
        return keytar.setPassword(key, account, password);
    }
}

For vault storage, the key is a JSON object containing { user, host, port }. For keytar, the service constructs a deterministic service string: ssh@host or ssh@host:port.

Loading SSH Passwords on Session Start

When an SSH session initializes (in tabby-ssh/src/session/ssh.ts), it attempts to auto-populate authentication methods:

// ssh.ts
const storedPassword = await this.passwordStorage.loadPassword(
    this.profile, 
    this.authUsername
);

The loadPassword() method mirrors the save logic, checking the vault first, then falling back to keytar.getPassword().

Handling Private-Key Passphrases

Encrypted private keys require passphrase caching. Tabby handles these separately using savePrivateKeyPassword() and loadPrivateKeyPassword():

// passwordStorage.service.ts
async savePrivateKeyPassword (id: string, password: string) {
    if (this.vault.isEnabled()) {
        const key = this.getVaultKeyForPrivateKey(id);
        this.vault.addSecret({ 
            type: VAULT_SECRET_TYPE_PASSPHRASE, 
            key, 
            value: password 
        });
    } else {
        const key = this.getKeytarKeyForPrivateKey(id);
        return keytar.setPassword(key, 'user', password);
    }
}

The id parameter represents a hash of the private-key file content, ensuring unique storage per key file.

Vault Encryption Implementation

The vault file resides at $HOME/.config/tabby/vault.json by default. Encryption utilizes Node.js’s crypto module:

// vault.service.ts
const cipher = crypto.createCipheriv(CRYPT_ALG, key, iv);
const ciphertext = Buffer.concat([
    cipher.update(JSON.stringify(content)), 
    cipher.final()
]);
  • Algorithm: AES-256-CBC (crypt constant)
  • Key derivation: User passphrase processed through deriveVaultKey
  • IV: Randomly generated for each encryption operation
  • Storage format: Encrypted JSON containing secret objects with type, key, and value fields

OS Keychain Fallback Mechanism

When the vault is disabled, Tabby constructs deterministic service keys for keytar lookups:

// passwordStorage.service.ts
private getKeytarKeyForConnection (profile: SSHProfile): string {
    let key = `ssh@${profile.options.host}`;
    if (profile.options.port) {
        key = `ssh@${profile.options.host}:${profile.options.port}`;
    }
    return key;
}

The account name corresponds to the SSH username. This scheme allows Tabby to retrieve the correct password for a specific user@host combination without maintaining a local database.

Summary

  • Tabby never stores raw SSH passwords or private-key passphrases in plain text; all credentials route through PasswordStorageService.
  • Dual-backend architecture supports either an AES-256-CBC encrypted vault file or the OS native keychain (via keytar).
  • Vault passphrase protection keeps the encryption key only in memory (_rememberedPassphrase) and never writes it to disk.
  • Deterministic key naming ensures consistent retrieval of credentials based on connection profiles (user, host, port) or private-key hashes.
  • Source implementation spans tabby-ssh/src/services/passwordStorage.service.ts, tabby-core/src/services/vault.service.ts, and tabby-ssh/src/session/ssh.ts.

Frequently Asked Questions

Does Tabby store SSH passwords in plain text?

No. According to the source code in passwordStorage.service.ts, Tabby encrypts all credentials using either the AES-256-CBC vault or the OS keychain. Raw passwords are never written to configuration files or logs.

What encryption algorithm does Tabby's vault use?

The vault implements AES-256-CBC encryption via Node.js crypto.createCipheriv, using a key derived from the user’s vault passphrase. The encrypted data is stored in a JSON file at $HOME/.config/tabby/vault.json.

Where is the vault file located on disk?

By default, the vault file is located at $HOME/.config/tabby/vault.json on Linux, with equivalent paths for macOS and Windows. The file contains encrypted JSON and is unreadable without the vault passphrase.

How does Tabby identify which password belongs to which SSH connection?

For vault storage, Tabby constructs a key object containing { user, host, port }. For OS keychain storage, it creates a service string like ssh@hostname:port and uses the username as the account name. This deterministic naming scheme ensures credentials map uniquely to specific connection profiles.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →