ssh-agent-keyring
ssh-agent-keyring is a Python keyring backend that keeps secrets encrypted at rest and unlocks them transparently through ssh-agent.
It is designed for SSH-first environments such as headless Linux servers, remote development machines, and terminal-centric workflows where desktop keyring services are not available or not desirable.
Why this exists
On a plain SSH server, the usual choices are often unsatisfying:
- desktop-oriented keyring backends are unavailable
- file-based backends usually require a separate keyring password
- plaintext storage is convenient, but unacceptable for real secrets
ssh-agent-keyring takes a different approach:
- secrets are stored encrypted at rest
- unlock happens through a configured SSH key already available in
ssh-agent keyring set/get/delwork in ordinary SSH sessions without a second keyring password
Features
keyringbackend implementation:ssh_agent_keyring.backend.SSHAgentKeyring- encrypted local store
- no plaintext secret storage
- no plaintext
service/usernamemetadata in the store - no root or system service dependency
- works well in SSH sessions when
SSH_AUTH_SOCKis available - simple user-space configuration
How it works
The backend stores a random data-encryption key (DEK) in wrapped form. To unwrap it, the backend:
- signs a backend-specific challenge directly through the
ssh-agentwire protocol (viaparamiko), producing output byte-for-byte identical tossh-keygen -Y signwith a configured SSH public key - relies on
ssh-agentto produce a deterministic signature over that challenge - derives a key-encryption key from that signature with HKDF-SHA256
- unwraps the DEK
- uses the DEK to encrypt and decrypt secret records with
ChaCha20Poly1305
Record identifiers are derived with HMAC, so the on-disk store does not expose service and username pairs in plaintext.
Because the key-encryption key is derived from the raw signature bytes, only key types with deterministic signing are supported: ssh-ed25519 and ssh-rsa. ECDSA (ecdsa-*) and security-key (sk-*) types are rejected — OpenSSH's ECDSA signing uses a random per-signature nonce (no RFC 6979), so the same challenge does not yield the same signature and the store could not be unlocked reliably.
Requirements
- Python 3.10+
keyringcryptographyparamiko- a running
ssh-agent SSH_AUTH_SOCKset in the current environment- the configured SSH key loaded in the agent
Installation
From PyPI:
pip install ssh-agent-keyring
If you are installing in a user environment on a remote machine, a typical setup is:
python3 -m pip install --user ssh-agent-keyring
Configuration
The backend reads configuration from:
~/.config/ssh-agent-keyring/config.json
Example:
{
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleBase64KeyMaterial",
"store_path": "~/.local/share/ssh-agent-keyring/store.json"
}
Then select the backend in:
~/.config/python_keyring/keyringrc.cfg
[backend]
default-keyring=ssh_agent_keyring.backend.SSHAgentKeyring
Configuration fields
public_key: SSH public key to use for unlocking the storepublic_key_path: path to a public key file; useful instead of embedding the key in JSONstore_path: path to the encrypted JSON store
Environment variable overrides
These override file-based configuration:
SSH_AGENT_KEYRING_PUBLIC_KEYSSH_AGENT_KEYRING_PUBLIC_KEY_PATHSSH_AGENT_KEYRING_STORE_PATH
Usage
With the Python API:
import keyring
keyring.set_password("example-service", "alice", "s3cr3t")
print(keyring.get_password("example-service", "alice"))
keyring.delete_password("example-service", "alice")
With the CLI:
keyring set example-service alice
keyring get example-service alice
keyring del example-service alice
With this backend, keyring set prompts only for the secret being stored. It does not prompt for a separate keyring password.
Operational model
Transparent unlock works when all of the following are true:
SSH_AUTH_SOCKis available in the current sessionssh-agentis reachable- the configured key is loaded in that agent
- the current session is allowed to use that agent
If the agent is unavailable, the wrong key is configured, or the key is not loaded, the encrypted store cannot be unlocked.
Security notes
This package improves on plaintext file storage, but it intentionally shifts trust to the SSH agent session.
Important implications:
- anyone able to use your
SSH_AUTH_SOCKcan potentially unlock the keyring during that session - agent forwarding policy matters
- the security of the backend depends on the security of the selected SSH key and the agent socket
- this is not a hardware-backed secret manager or multi-user secret vault
- the derived signature (which acts as the store's master key) is only as safe as the agent
process itself; a compromised or malicious
ssh-agenton the other end ofSSH_AUTH_SOCKcan always observe what it is asked to sign, regardless of how the request reaches it
The store is designed to avoid:
- plaintext secrets on disk
- plaintext service/user metadata on disk
- a second unlock password unrelated to your SSH workflow
The backend speaks the ssh-agent wire protocol directly (via paramiko) instead of shelling out
to ssh-keygen/ssh-add. It never spawns a subprocess or resolves an executable from PATH, so a
malicious ssh-keygen binary placed ahead of the genuine one on PATH cannot observe or tamper
with the challenge or signature. The challenge message and the resulting signature only ever exist
in memory and on the SSH_AUTH_SOCK wire, never on disk or in a temporary file.
Limitations
- this backend depends on the standard
ssh-agentwire protocol - it is aimed at Unix-like environments
- it does not provide secret sharing, rotation workflows, audit logs, or remote synchronization
- it assumes the configured SSH key can be used for agent-backed signing
Testing
Run the test suite with:
python3 -m unittest discover -s tests -v
The project includes tests for roundtrips, tampering, key mismatch, signer failures, and store handling edge cases.
tests/test_real_agent.py additionally spins up a real ssh-agent and verifies, against real
ssh-keygen -Y sign output, that the backend's own agent-protocol signing is byte-for-byte
compatible. It's skipped automatically if ssh-agent/ssh-keygen/ssh-add aren't on PATH.
Development
Project layout:
ssh_agent_keyring/agent.py- agent and signing integrationssh_agent_keyring/crypto.py- key derivation and encryption helpersssh_agent_keyring/backend.py-keyringbackend implementationtests/test_backend.py- automated test suitetests/test_real_agent.py- realssh-agent/ssh-keygencompatibility tests
License
License information has not been added yet.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ssh_agent_keyring-0.5.0-py3-none-any.whl.
File metadata
- Download URL: ssh_agent_keyring-0.5.0-py3-none-any.whl
- Upload date:
- Size: 10.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5b91f10c9f83a779291fb50b824c1f47a29782d4e3f7eed73fb6bea9477bf68
|
|
| MD5 |
2558935825a921cc6fa55d8bf537b4ba
|
|
| BLAKE2b-256 |
b7821322b4bb11d95648fa113c0247404b7d35d41632dc6a37d2fd2fac87b360
|
Provenance
The following attestation bundles were made for ssh_agent_keyring-0.5.0-py3-none-any.whl:
Publisher:
pypi-publish.yml on monperrus/ssh_agent_keyring
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ssh_agent_keyring-0.5.0-py3-none-any.whl -
Subject digest:
e5b91f10c9f83a779291fb50b824c1f47a29782d4e3f7eed73fb6bea9477bf68 - Sigstore transparency entry: 2391861989
- Sigstore integration time:
-
Permalink:
monperrus/ssh_agent_keyring@a5fab7f97b9670f57a5847e6de50fdc4435a8282 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/monperrus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@a5fab7f97b9670f57a5847e6de50fdc4435a8282 -
Trigger Event:
push
-
Statement type: