tempkeys
Keep a temporary keyset on a Linux machine: send in a full set of secrets, use them for a while, and have them become unrecoverable on their own.
Each secret is stored as its own encrypted file. The only thing the kernel holds
is the random encryption key, in the Linux kernel keyring. When the
machine reboots or you clear the keyset, the files can no longer be decrypted
and are deleted. Keys don't expire by default; pass --ttl to have the kernel
expire the key on a timer too.
This is handy when you need to send keys to another machine. Stream a keyset
through SSH into tempkeys load, then query individual keys on the remote host
only when needed:
$ pass show myenv | ssh user@box 'tempkeys --user load'
$ ssh user@box 'tempkeys --user get API_TOKEN'
The remote host keeps encrypted files and a kernel-held key, without writing
plaintext secrets to its filesystem or keeping them in long-lived environment
variables. get prints a secret to stdout when requested; run can instead
pass selected keys to one command's environment when that command requires it.
$ pass show myenv | tempkeys load
loaded 2 keys into "default", no expiry
$ echo ghp_xxx | tempkeys set GH_TOKEN # add or replace one key
set GH_TOKEN in "default"
$ tempkeys list
default 3 keys no expiry
$ tempkeys get API_TOKEN
hunter2
$ tempkeys run -e GH_TOKEN=github_token -- gh pr list
tempkeys: populating GH_TOKEN=github_token
$ tempkeys clear
What Linux provides, and what tempkeys adds
The hard part already exists in Linux: the kernel key retention
service. It holds secrets in kernel memory rather than on disk,
checks per-key permissions and possession, expires keys on a timer, garbage
collects them, and lets a process family share a private session keyring across
fork and exec. keyctl exposes it. tempkeys doesn't reimplement any of that;
it calls the same syscalls (and the kernel's CSPRNG for keys and nonces).
What the raw keyring leaves to you, and what tempkeys does about it:
With plain keyctl |
With tempkeys |
|---|---|
A user key holds at most 32 KB, and non-root users get about 200 keys and 20 KB in total (/proc/sys/kernel/keys/) |
The kernel holds one 32-byte key per keyset. Secrets are encrypted files of any size and number |
| A key is a single opaque payload; there is no "set of secrets" | A keyset is loaded, replaced, merged into (load --merge) or edited (set) as a unit, and swapped in atomically, so readers never see half of it |
| Nothing ties stored data to the key's lifetime | When the key expires, is cleared, or the machine reboots, the files are unreadable and pruned, so nothing lingers on disk |
| Data is only as safe as the permissions on it | Files are XChaCha20-Poly1305 with the keyset and key names and the expiry authenticated: swapped, moved, edited or re-dated files fail to decrypt |
A private per-process-family keyring is a manual recipe: join a session, set permissions, add keys, exec |
tempkeys session does it in the safe order: permissions are set before any key goes in, members get no right to widen access, and --session can't fall into your login's shared keyring |
You choose between @u and @s for every call |
Inside a session, reads look in the session scope first and fall back to the user scope per keyset, and writes go to the session; --user and --session override |
Getting a secret to a program means keyctl search, keyctl pipe, and your own plumbing |
get KEY, or run -e GH_TOKEN=github_token -- cmd, which reports which variables it populated, names only |
Needs keyutils installed |
A single binary; no keyctl or libkeyutils |
tempkeys adds no new trust boundary. The kernel still decides who can fetch the key (see what this does and doesn't protect against), so the protection is exactly the keyring's: same-UID processes in the user scope, one process family in the session scope.
Install
Linux only, with kernel keyring support (CONFIG_KEYS, on in mainstream
distributions). No keyctl or libkeyutils needed: tempkeys calls the syscalls
directly.
$ cargo install --git https://github.com/vivainio/tempkeys
Platforms
Linux, including WSL2 (developed and tested on WSL2, whose kernel has keyring
support). There is no native Windows or macOS support: tempkeys depends on an
in-kernel keyring with timeouts and per-process-family sessions, which those
systems don't have. The keyring calls are isolated in src/sys.rs, so a port
would replace that layer rather than rewrite the rest.
Commands
| Command | What it does |
|---|---|
load [--set NAME] [--ttl DUR] [--merge] |
Replace a keyset with the full set read from stdin, or add to it with --merge |
set [--set NAME] [--ttl DUR] [--raw] KEY |
Add or replace one key, value from stdin |
get [--set NAME] KEY |
Decrypt and print one key's raw value |
list [--set NAME] |
List keysets, or key names in one (never values) |
run [--set NAME] [-e VAR=KEY]... -- CMD... |
Run a command with keys as environment variables |
git-credential [--set NAME] [--key KEY] [--host HOST] [--username NAME] get |
Serve a token to Git for HTTPS credentials |
clear [--set NAME | --all] |
Delete a keyset and its encryption key now |
session [--set NAME] [--ttl DUR] [-- CMD...] |
Start a command in a private session keyring (see below) |
load reads dotenv lines (KEY=VALUE, # comments, optional export, one
pair of surrounding quotes stripped) or a JSON object of strings. Key names must
be valid environment variable names. Loading is all-or-nothing, and replaces the
whole keyset: keys missing from the new input are gone. With --merge the input
is added to the existing keyset instead: keys in the input are added or
replaced, and every other key stays. A merge keeps the keyset's encryption key
and expiry, so --ttl is rejected for an existing keyset; if the keyset doesn't
exist yet, --merge behaves like a normal load. Secrets are only ever
read from stdin, never from arguments. Empty values are rejected.
run lists the variables it populates on stderr (names only, never values):
VAR=KEY when a variable takes its value from a differently named key, or just
VAR when the names match. With -e VAR=KEY (repeatable; -e VAR means
VAR=VAR) only the variables you name are populated, so the command sees
nothing else from the keyset. Without -e, every key is injected under its own
name. A missing key fails before the command starts. -q silences the listing.
set adds or replaces a single key without touching the rest. In an existing
keyset the new file is encrypted under the keyset's current key and keeps its
expiry. If the keyset doesn't exist it is created, and --ttl applies only then
(use load to change an existing keyset's expiry). The value comes from stdin,
or from a no-echo prompt on a terminal, never from arguments. One trailing
newline is dropped so echo token | tempkeys set KEY stores token; --raw
keeps the bytes exactly.
The default keyset name is default. There is no expiry unless you pass
--ttl (90s, 15m, 8h, 2d): the kernel then expires the key on that
timer, and the files are deleted once it does. Without one, the keyset lasts
until you clear it, the machine reboots, or (in session mode) the process
family exits.
Git HTTPS credential helper
tempkeys git-credential implements Git's credential helper protocol. It reads
GH_TOKEN from the default keyset when Git requests credentials for
https://github.com. The default username is x-access-token; GitHub uses the
token for authentication, so the username does not need to be stored. Configure
Git for this host with:
$ git config --global --replace-all credential.https://github.com.helper ''
$ git config --global --add credential.https://github.com.helper '!tempkeys --user git-credential'
The empty helper setting resets previously configured helpers for this host.
--user always reads the user keyring, even inside a tempkeys session. The
helper returns credentials only for the configured HTTPS host. store and
erase do nothing; no token is written to Git's credential files. Use
--set, --key, --host, or --username to change the defaults. SSH Git
remotes use SSH keys and do not call this helper.
How it works
<dir>/user/default/API_TOKEN.enc one XChaCha20-Poly1305 file per secret
<dir>/user/default/DB_PASS.enc
kernel @u: tempkeys:key:default:<generation> 32-byte key (optional timeout)
<dir> follows the XDG Base Directory spec: $XDG_RUNTIME_DIR/tempkeys
(tmpfs, cleared at logout) when that directory is usable (absolute, owned by you,
mode 0700), else $XDG_CACHE_HOME/tempkeys, else ~/.cache/tempkeys. Files are
mode 0600 in 0700 directories. User keysets live in <dir>/user/, and each
session's in <dir>/session-<keyring id>/.
File format. "TKY1" | generation (16) | expiry (8) | nonce (24) | ciphertext+tag.
The header is cleartext so readers can find the right key and prune expired
files, but it is authenticated: the associated data is the header plus the
keyset and key names. A file can't be swapped with another key's file, moved to
another keyset, or have its expiry extended without decryption failing.
Loading. Each load (without --merge) generates a fresh random key, stores it in the kernel
keyring, writes the new files to a temporary directory, and swaps that directory
in, so readers never see a half-written keyset. Older keys for that keyset are
then revoked. Bad input leaves the current keyset untouched.
Expiry. The kernel enforces the key's timeout. Files whose expiry has passed,
whose key is gone, or that belong to a dead session are deleted by the next
tempkeys command. Undecryptable files are useless in the meantime.
Permissions. The kernel key is owner-only (0x003f0000): possessor, group
and other get nothing.
Session mode
By default the key lives in your user keyring (@u), which every process
running as your UID can use. For tighter scoping, session puts the key in a
private session keyring that only one process family can use:
$ pass show myenv | tempkeys session --ttl 8h -- ./myapp
$ tempkeys session # a shell with an empty private session
$ tempkeys load # ...load into the session, not @u
$ tempkeys get API_TOKEN # ...and read it back
session joins a fresh anonymous session keyring, gives it possessor-only
permissions before any key goes in, and execs the command, which inherits it.
If stdin is piped, the keyset is read from it and loaded with its key in the
session keyring; nothing touches @u, and a command is then required because
stdin is used up. Session members can add and remove keys but can't change
permissions. The session scope exists only inside a session that tempkeys
created, so secrets can't land in your login's shared session keyring by mistake.
The keyset lasts as long as the process family, or the TTL, whichever ends first.
Which scope does a command use?
Outside a session everything uses the user scope. Inside one:
| Commands | Default | Override |
|---|---|---|
get, run, list -s NAME |
the session scope first, then the user scope | --session or --user restricts to one |
load, set, clear |
the session scope only | --user writes to the user scope |
list |
both scopes, labelled | --session or --user restricts to one |
The fallback is per keyset: if the session scope has a keyset called
default, that keyset is used whole, and a key missing from it is an error
rather than being taken from the user scope's default. run says where its
values came from, for example populating GH_TOKEN=github_token (from session keyset "default"), so a fallback can't go unnoticed. Files for the two scopes
live in separate directories and their keys in separate keyrings, so the same
keyset name in both never mixes.
What this does and doesn't protect against
- Protects: the files at rest (disk, backups, other users, after reboot or expiry). Plaintext is never written to disk.
- Session mode additionally hides the key from unrelated processes running
as you: a process outside the session that knows the key's numeric ID still
gets
Permission denied. - Default
@umode trusts every program running as your UID. They can fetch the key and read the files. Usesessionwhen that isn't acceptable. runputs secrets in environment variables, which same-UID processes can read from/proc/PID/environand which grandchildren inherit. Prefergetwhen the application can call it.- Root can read anything, and ptrace-based inspection of a process that has decrypted a secret is out of scope. A revoked key can't erase copies already read into memory. Best-effort zeroing is done on tempkeys's own buffers only.
Limits
- Values are raw bytes: files hold the exact payload, any size, and
getreturns it unchanged.set --raw KEY < cert.derstores binary data.loadis text-only (UTF-8 dotenv or JSON), andruncan't inject a value containing a NUL byte, since environment variables can't hold one. - Non-root users have kernel key quotas (
/proc/sys/kernel/keys/maxkeysandmaxbytes). tempkeys stores one 32-byte key per keyset, so this is rarely an issue.
Background
Design notes on kernel keyrings, ownership vs. possession, session and user keyrings: Linux kernel keyrings.
Development
$ cargo test
Metadata
Release files for tempkeys 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| tempkeys-0.1.3.tar.gz | 33.1 kB | With provenance | Details for tempkeys-0.1.3.tar.gz |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tempkeys-0.1.3-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details for tempkeys-0.1.3-py3-none-manylinux_2_28_x86_64.whl |
| tempkeys-0.1.3-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details for tempkeys-0.1.3-py3-none-manylinux_2_28_aarch64.whl |
Total release size: 1.2 MB
Release files / tempkeys-0.1.3.tar.gz
| Download URL | tempkeys-0.1.3.tar.gz |
|---|---|
| Size | 33.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
762edb5cac7cff117a98ffc820caf97f632d1c80b2fa666167c83c2436317ca0
|
|
BLAKE2b-256 checksum How to use checksums |
f24c6039fa30b92c9e708eadf4ec5068245fc60d1a9c9699a3411b73f44782fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / tempkeys-0.1.3-py3-none-manylinux_2_28_x86_64.whl
| Download URL | tempkeys-0.1.3-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 592.2 kB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
ca89f7638d4dcc2c6611df40f22062f23f7081321963cb392445d0e1d19b2701
|
|
BLAKE2b-256 checksum How to use checksums |
82b7469106c422c454f22b92933a2470b8adb6dbfdcb149c92fa29328c5b6373
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / tempkeys-0.1.3-py3-none-manylinux_2_28_aarch64.whl
| Download URL | tempkeys-0.1.3-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 562.2 kB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
336f5d83290b07826837068fadc94f2ded570132449dfb15cd9e74fa9bc8953a
|
|
BLAKE2b-256 checksum How to use checksums |
c19850b29198b3fada226d6b4c5548befd3aaad8775fda0c8ee343ee25c703d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logSecurity for tempkeys 0.1.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 3 files in this release have been uploaded with provenance metadata.
Attestation for tempkeys-0.1.3.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.