Skip to main content

Keylocker CLI (YAML Edition)

PyPI Downloads Downloads codecov

A command-line interface (CLI) tool and Python library for managing secrets directly within YAML configuration files. Allows encrypting individual values using the !SEC tag and loading values from environment variables using the !ENV tag.

Alt

Encryption Key

Keylocker uses a Fernet key for encryption and decryption. The key is obtained in the following order of priority:

  1. KEYLOCKER_SECRET_KEY Environment Variable: If this environment variable is set, its value is used directly as the Fernet key (it should be a valid base64 encoded key).
  2. Key file: If the environment variable is not set, the key is read from a file: the key_file argument / --key-file option if given, otherwise the path in the KEYLOCKER_KEY_FILE environment variable, otherwise storage.key in the current directory.

Use the keylocker init command to generate a key file if you prefer file-based key storage. Note that if KEYLOCKER_SECRET_KEY is set, it will always take precedence over the file for encryption/decryption operations.

keylocker init creates the key file readable by its owner only (mode 0600 on Linux/macOS; missing parent directories are created with mode 0700). Never commit it or bake it into container images: add storage.key to both .gitignore and .dockerignore, and prefer KEYLOCKER_SECRET_KEY injected at runtime in CI and containers.

The default ./storage.key depends on the working directory and usually sits right next to config.yaml, where it is easy to commit by accident. To keep the key outside the project, point KEYLOCKER_KEY_FILE to a per-user location:

export KEYLOCKER_KEY_FILE=~/.config/keylocker/storage.key
keylocker init   # creates ~/.config/keylocker/ (0700) and the key (0600)

Several keys and key rotation

Both the key file and KEYLOCKER_SECRET_KEY may hold several keys. The first key is primary: it encrypts new values. All keys are tried for decryption, so values encrypted with an older key keep working. In the file, put one key per line (lines starting with # are comments); in the environment variable, separate keys with commas: KEYLOCKER_SECRET_KEY="new_key,old_key".

Rotating a file-based key:

keylocker init --rotate                      # prepend a new primary key, keep the old one
keylocker rotate config.yaml other.yaml      # re-encrypt every !SEC value with the new key
# then delete the old key (every line after the first) from the key file

keylocker rotate rewrites only the !SEC tokens in place: comments, formatting and values already encrypted with the primary key stay untouched, so running it twice changes nothing. If any value cannot be decrypted, the file is left unchanged and the command exits with code 1.

init --rotate only touches the key file. When the key lives in KEYLOCKER_SECRET_KEY (typical for CI), build the new value yourself; the order matters, the new key goes first:

NEW_KEY=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
# 1. Store "new,old" in the CI secret (commas or newlines both work):
export KEYLOCKER_SECRET_KEY="$NEW_KEY,$OLD_KEY"
# 2. Re-encrypt and commit the configs:
keylocker rotate config.yaml other.yaml
# 3. Once every branch and deployment uses the rotated files, set the secret to "$NEW_KEY" alone.

Until step 3, both keys decrypt, so configs that are not rotated yet keep working.

Installation

You can install the package using pip:

pip install keylocker

The package is available on PyPI: https://pypi.org/project/keylocker/

Runtime dependencies are declared in pyproject.toml and installed automatically by pip. Pipfile and both requirements files use the same supported ranges; Pipfile.lock records exact versions for reproducible development and CI. Tests check these declarations for drift, and CI smoke-tests the built wheel in a fresh virtualenv without Pipenv or an editable source install.

Example YAML file (config.yaml):

# config.yaml
database:
  host: db.example.com
  username: user
  # Encrypted password using 'keylocker encrypt'
  password: !SEC gAAAAABh...[rest of encrypted data]...
api:
  endpoint: [https://api.example.com](https://api.example.com)
  # Load key from the API_KEY environment variable
  key: !ENV API_KEY
  # Fall back to a default when LOG_LEVEL is unset or empty
  log_level: !ENV LOG_LEVEL:-info
deployment:
  region: us-east-1
  # Another secret
  ssh_key_pass: !SEC gAAAAABh...[another encrypted data]...

CLI Usage:

Global options (accepted before or after the command): --key-file PATH (alias --key_file) and -q/--quiet (only warnings and errors on stderr). Every command writes data to stdout and diagnostics to stderr; failures exit with code 1, invalid usage with code 2.

  1. Initialization (create key file):

    • Generates the key file (if it doesn't exist). This file is used only if the KEYLOCKER_SECRET_KEY environment variable is not set. --rotate adds a new primary key to an existing file (see key rotation).
    keylocker init [--force | --rotate]
    
  2. Encrypt value for YAML:

    • Encrypts a string using the available key (Environment Variable or Key File) and prints the !SEC tag to stdout for you to copy into your YAML file. Hints and diagnostics go to stderr.
    • Without an argument the secret is read from stdin, or from a hidden prompt (entered twice) when run in a terminal. Avoid passing the secret as an argument: it ends up in shell history and is visible in the process list. An argument is encrypted verbatim ('{"a": 1}' stays exactly that string).
    keylocker encrypt
    # Secret to encrypt:
    # Repeat secret:
    # Add the following line to your YAML file:
    # !SEC gAAAAABh...[encrypted data]...
    
    # Non-interactive (one trailing newline is stripped):
    printf '%s' "$DB_PASSWORD" | keylocker encrypt
    
  3. View decrypted YAML:

    • Loads the specified YAML file, resolves !ENV variables, decrypts !SEC values using the available key (Environment Variable or Key File), and prints the resulting YAML to standard output. Only YAML is written to stdout, so it can be piped into yq. If any !SEC value cannot be decrypted, nothing is printed and the command exits with code 1.
    • --strict also fails when a !ENV variable without a default is unset (otherwise it is shown as null). Use it everywhere: it will become the default in the next major release.
    # Set environment variables if your YAML uses !ENV
    export API_KEY="your_actual_api_key"
    
    keylocker view --strict config.yaml
    # Example output (values resolved/decrypted):
    # database:
    #   host: db.example.com
    #   username: user
    #   password: my_secret_value
    # api:
    #   endpoint: [https://api.example.com](https://api.example.com)
    #   key: your_actual_api_key
    #   log_level: info
    # deployment:
    #   region: us-east-1
    #   ssh_key_pass: decrypted_pass
    

    CI/CD caution: view prints every secret in plaintext. Never run it bare in a CI job: its output ends up in build logs. Capture it into a variable or pipe it straight into the consumer (keylocker view --strict config.yaml | yq ...), and register extracted values with your CI's secret masking (for example echo "::add-mask::$DB_PASS" on GitHub Actions).

  4. Edit secrets in your editor (like ansible-vault edit):

    • Decrypts every !SEC value into a temporary copy, opens it in $VISUAL / $EDITOR (default: notepad on Windows, vi elsewhere) and encrypts the file back when the editor exits. Comments and layout are kept.
    • In the editor secrets look like password: !SEC "plaintext". Change them, or add new ones as !SEC new value: every !SEC value is encrypted on save. Unchanged secrets keep their exact ciphertext, so the git diff shows only what you edited. A value changed to equal another secret gets its own fresh ciphertext, so identical tokens in the file do not reveal that two values are equal.
    • A value without the !SEC tag is saved in plaintext. If the edited file has fewer !SEC values than before (for example, the tag was deleted by accident), edit warns and asks before saving; without a terminal it refuses.
    • The temporary copy is created with mode 0600 (in RAM-backed /dev/shm when available) and deleted afterwards. If the editor exits with an error, nothing is saved; if the result is not valid YAML, you are offered to re-open the editor.
    • GUI editors must wait until the file is closed, e.g. EDITOR="code --wait".
    keylocker edit config.yaml
    

    The editor can leave plaintext copies behind. keylocker deletes its temporary file, but not what the editor writes on its own, and that usually lands on disk outside /dev/shm. On macOS and Windows there is no /dev/shm, so the temporary file itself goes to $TMPDIR / %TEMP% on disk. Configure the editor before editing secrets:

    • Vim / Neovim: disable the swap file, viminfo/shada, undo files and backups: EDITOR="vim -n -i NONE --cmd 'set noundofile nobackup nowritebackup'" (the same flags work for nvim).
    • VS Code: Local History keeps copies of saved files; exclude keylocker's temporary files with "workbench.localHistory.exclude": {"**/keylocker-*": true} or set "workbench.localHistory.enabled": false. Hot exit ("files.hotExit") backs up unsaved changes, so save and close the tab before quitting, or turn it "off".
    • Other editors: turn off autosave/backup/recovery files for the temporary file, or point TMPDIR to a RAM disk.
  5. Re-encrypt with the primary key (rotation):

    keylocker rotate config.yaml [more.yaml ...]
    

!ENV references

  • !ENV NAME resolves to the value of NAME. If it is unset, the value is None and a warning is logged; in strict mode (strict=True / view --strict) a MissingEnvVarError is raised instead.
  • !ENV NAME:-default resolves to default when NAME is unset or empty, like ${NAME:-default} in the shell. !ENV NAME:- gives an empty string. A default is never an error, even in strict mode.

Usage in Python Code:

import os
from keylocker import load_yaml_secrets, KeylockerError

# Set environment variables if your YAML uses !ENV
os.environ['API_KEY'] = 'your_actual_api_key_for_python'
# Set KEYLOCKER_SECRET_KEY if using env var for the encryption key
# os.environ['KEYLOCKER_SECRET_KEY'] = 'your_base64_encoded_fernet_key'

config_file = 'config.yaml'
# Optional: without it, $KEYLOCKER_KEY_FILE or ./storage.key is used.
# Ignored when KEYLOCKER_SECRET_KEY is set.
key_file_path = 'storage.key'

try:
    # Load, resolve !ENV, and decrypt !SEC automatically. strict=True fails closed.
    secrets = load_yaml_secrets(config_file, key_file_path, strict=True)
except KeylockerError as e:
    # Never fall back to a placeholder value: fail closed.
    raise SystemExit(f"Cannot load secrets: {e}")

db_password = secrets['database']['password']
api_key = secrets['api']['key']

All errors inherit from KeylockerError:

Exception Raised when
DecryptionError a !SEC value cannot be decrypted (no key, wrong key, corrupted value)
ConfigError the YAML file is missing, unreadable or invalid (strict=True)
MissingEnvVarError a !ENV variable without a default is unset (strict=True)
EncryptionError a SecureString cannot be encrypted, so it is never written out in plaintext

For backward compatibility strict defaults to False: a missing or invalid file then returns None (with a FutureWarning) and an unset !ENV variable resolves to None. strict=True will become the default in the next major release; pass it explicitly now (and use keylocker view --strict), so the switch changes nothing for you.

Editing YAML from code without noisy diffs

Fernet ciphertext is randomized, so encrypting the same value twice gives different tokens. To change a config programmatically without touching unrelated !SEC lines in the git diff, load it in round-trip mode: !SEC values come back as SecureString objects that remember their token, and !ENV values as unresolved EnvVariable references. keylocker_dumper() reuses the remembered token as long as the value is unchanged.

import yaml
from keylocker import keylocker_loader, keylocker_dumper, SecureString

with open('config.yaml') as f:
    data = yaml.load(f, Loader=keylocker_loader(round_trip=True))

data['database']['password'] = SecureString('new password')   # only this line changes

with open('config.yaml', 'w') as f:
    yaml.dump(data, f, Dumper=keylocker_dumper(), sort_keys=False)

(PyYAML drops comments when dumping. To change secrets while keeping comments and layout, use keylocker edit.)

Logging

The library logs through the standard logging module under the keylocker logger and never prints. Routine messages (such as which key source is used) are DEBUG; misconfiguration (an invalid key, an unset !ENV variable) is WARNING/ERROR. Without any logging configuration Python prints only warnings and errors to stderr. To silence or redirect them:

import logging
logging.getLogger('keylocker').setLevel(logging.ERROR)

Security notes

  • Ciphertext is not bound to its location. Fernet has no associated data, so a !SEC token copied from db.password to admin.password (in the same or another file encrypted with the same key) decrypts without error. Anyone who can edit the YAML can move secrets around; protect the integrity of config files (code review, signed commits) if that matters in your threat model, and use separate keys for separate trust boundaries.
  • Fernet authenticates every token: a modified token fails to decrypt (DecryptionError) instead of yielding garbage.
  • Loading YAML uses yaml.SafeLoader; error messages report the line and column of a failing value but never the YAML source, which could contain other secrets.

Usage in Bash:

To extract specific values from the resolved/decrypted YAML for use in scripts.

#!/bin/bash

# Set environment variables if your YAML uses !ENV
export API_KEY="your_bash_api_key"
# Set KEYLOCKER_SECRET_KEY if using env var for the encryption key
# export KEYLOCKER_SECRET_KEY='your_base64_encoded_fernet_key'

# Get the decrypted YAML content
CONFIG_YAML=$(keylocker view --strict config.yaml)

# Check if the command executed successfully
if [ $? -ne 0 ]; then
  echo "Error executing keylocker view"
  exit 1
fi

# Example extraction using yq (requires yq installation)
# DB_PASS=$(echo "$CONFIG_YAML" | yq e '.database.password' -)
# API_KEY_FROM_YAML=$(echo "$CONFIG_YAML" | yq e '.api.key' -)

# Example extraction using grep/awk (less robust)
DB_PASS=$(echo "$CONFIG_YAML" | grep -A 1 'database:' | grep 'password:' | awk '{print $2}')
API_KEY_FROM_YAML=$(echo "$CONFIG_YAML" | grep -A 1 'api:' | grep 'key:' | awk '{print $2}')


echo "Extracted DB Password (from Bash): $DB_PASS"
echo "Extracted API Key (from Bash): $API_KEY_FROM_YAML"

# Further use of variables in your script
# e.g., ./deploy_script.sh --db-password "$DB_PASS" --api-key "$API_KEY_FROM_YAML"

Source Code:


Metadata

Release files for keylocker 0.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for keylocker 0.6.1
File Size Uploaded Provenance Actions
keylocker-0.6.1.tar.gz 23.0 kB With provenance Details for keylocker-0.6.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for keylocker 0.6.1
File Interpreter ABI Platform Actions
keylocker-0.6.1-py3-none-any.whl Python 3 none any Details for keylocker-0.6.1-py3-none-any.whl

Total release size: 41.6 kB

Release files / keylocker-0.6.1.tar.gz

Download URL keylocker-0.6.1.tar.gz
Size 23.0 kB
Tags Source
SHA-256 checksum
How to use checksums
9aedcb4f07917c2322af3d96eac137c0008534dd9aa2931f9164d908a0051235
BLAKE2b-256 checksum
How to use checksums
c7340081c6d2f981f7348e64359f7dc49c8a1c56d3d110ebabd8505a60df5958
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 31, 2026.

Transparency log

Release files / keylocker-0.6.1-py3-none-any.whl

Download URL keylocker-0.6.1-py3-none-any.whl
Size 18.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2d7defd0127b464e97d1bfe19028ecbcefe74577c7930edd744d1232d0035717
BLAKE2b-256 checksum
How to use checksums
7fabdaa608f471bdd850ad0da54264a7d2a6a792908abe9486cc9c7abcf90177
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 31, 2026.

Transparency log

Security for keylocker 0.6.1

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 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for keylocker-0.6.1.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 31, 2026.

Transparency log

A 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.

Release history Release notifications | RSS feed

This release

0.6.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page