Skip to content

Repository files navigation

Locker Secrets Python SDK

PyPI Python License: Apache-2.0

The official Python SDK for reading and managing secrets in Locker Secrets Manager.

The PyPI distribution is named lockersm; the Python import package is named locker. Version 2 communicates with the Locker CLI through the stable locker.sdk JSON-RPC protocol instead of parsing human-facing command output. It supports both Locker Cloud and self-hosted Locker deployments.

Install

python -m pip install --upgrade lockersm

You do not need to install the Locker CLI separately in the standard managed mode. The SDK downloads a supported binary on first use and verifies the release signature, SHA-256 digest, platform, architecture, and protocol compatibility before execution.

Support matrix

Component Supported versions
Python 3.10, 3.11, 3.12, 3.13, 3.14
Linux managed CLI x86-64, ARM64
macOS managed CLI Intel, Apple silicon
Windows managed CLI x86-64
SDK protocol locker.sdk v1

For an air-gapped or centrally managed deployment, provide an explicit Locker CLI path with LOCKER_CLI_PATH or binary_path.

Quick start

Create an access key in your Locker Secrets project, then expose the credentials to the application environment.

Linux and macOS:

export LOCKER_ACCESS_KEY_ID="your-access-key-id"
export LOCKER_SECRET_ACCESS_KEY="your-secret-access-key"

Windows PowerShell:

$env:LOCKER_ACCESS_KEY_ID = "your-access-key-id"
$env:LOCKER_SECRET_ACCESS_KEY = "your-secret-access-key"

Read a required secret:

from locker import Locker

client = Locker.from_env()
database_password = client.get_required(
    "DATABASE_PASSWORD",
    environment_name="production",
)

# Pass database_password directly to the component that needs it.
# Never print or log secret values.

get_required() raises ResourceNotFoundError when the key does not exist. Use get() only when a fallback is intentionally safe:

log_level = client.get(
    "LOG_LEVEL",
    environment_name="production",
    default_value="info",
)

The default value is returned only for a genuine not-found response. Authentication, permission, network, protocol, storage, and server failures are still raised.

Configure the client

Locker.from_env() is the recommended constructor. It recognizes:

Environment variable Purpose
LOCKER_ACCESS_KEY_ID Project access key ID
LOCKER_SECRET_ACCESS_KEY Project secret access key
LOCKER_API_BASE Cloud or self-hosted API base URL
LOCKER_CLI_PATH Absolute path to a deployment-managed CLI
LOCKER_LOG debug, info, warning, or error

The default cloud endpoint is https://api.locker.io/locker_secrets.

Self-hosted deployment:

from locker import Locker

client = Locker.from_env(
    api_base="https://secrets.example.com/locker_secrets",
)

Deployment-managed CLI:

from locker import Locker

client = Locker.from_env(
    binary_path="/opt/locker/bin/locker",
)

The CLI path must be absolute and point to a regular, non-link file. Explicit paths bypass managed updates and are never resolved through ambient PATH.

For migration from version 1, the SDK still recognizes ACCESS_KEY_ID, SECRET_ACCESS_KEY, LOCKER_ACCESS_KEY_SECRET, and ACCESS_KEY_SECRET. New deployments should use only the canonical LOCKER_* names.

Timeouts, retries, and cancellation

Automatic retries are disabled by default. Applications that need transient network resilience can opt in:

client = Locker.from_env(
    timeout=5,
    max_network_retries=2,
)

Retries use bounded exponential backoff with jitter and occur only when the CLI marks an error as retryable and the RPC is read-only. Secret and environment create or update operations are never retried because protocol v1 does not provide an idempotency key that can resolve an unknown commit outcome.

Signed CLI resolution/update, updater-lock waiting, capability negotiation, read attempts, and retry backoff share the configured timeout as one total protocol budget. The retry count can also be overridden for one read with max_network_retries=....

During capability negotiation, SDK 2.x opts into the CLI's typed-v1 error contract only when the installed CLI advertises it. This enables precise conflict, validation, and integrity codes without breaking older CLI releases or already deployed protocol-v1 clients.

A threading.Event can cooperatively cancel a request during CLI resolution, capability negotiation, optional client/updater-lock waiting, retry backoff, or an in-flight CLI process:

import threading

cancel = threading.Event()
value = client.get_required("DATABASE_PASSWORD", cancel_event=cancel)

Cancellation prevents a subsequent operation from starting after the event is set and terminates the complete in-flight CLI process tree. As with any distributed write, cancellation after a create or update has reached the server can leave its remote commit outcome unknown; the SDK never retries that mutation automatically. The process also remains bounded by the remaining request timeout. On Linux, the CLI receives a kernel parent-death guard before execution, so a terminated Python host cannot leave an orphaned secret operation running.

The SDK does not store plaintext secrets. resttime (default 120 seconds) and fetch are delegated to the CLI's encrypted, revision-aware cache:

client = Locker.from_env(resttime=30, fetch=False)

resttime=0 disables offline reuse. fetch=True requires a successful server refresh and never falls back to cached vault data. A transient outage may use only a still-fresh cache last validated successfully by the server; authentication, authorization, TLS, integrity, malformed-response, and local storage failures always fail closed.

Secrets

Read secret metadata

retrieve() returns the complete secret resource object:

secret = client.retrieve(
    "DATABASE_PASSWORD",
    environment_name="production",
)
print(secret.id, secret.key, secret.environment_name)

Secret objects contain plaintext values. Do not serialize, print, or include them in logs.

List secrets

for secret in client.list(environment_name="production"):
    print(secret.id, secret.key, secret.environment_name)

For large projects, use bounded cursor pagination:

cursor = None

while True:
    page = client.list_page(
        environment_name="production",
        page_size=100,
        cursor=cursor,
    )
    for secret in page.items:
        print(secret.id, secret.key)
    cursor = page.next_cursor
    if cursor is None:
        break

Create and update secrets

Read secret input without echoing it in a terminal:

from getpass import getpass

created = client.create(
    key="PAYMENT_API_KEY",
    value=getpass("New secret value: "),
    environment_name="staging",
)

updated = client.modify(
    key=created.key,
    value=getpass("Updated secret value: "),
    environment_name="staging",
)

Secret values are sent to locker sdk in the JSON request on standard input. They are not placed in process arguments or logs.

Export secrets

export() returns a plaintext str in dotenv or compact json format:

dotenv_payload = client.export(
    environment_name="production",
    output_format="dotenv",
)

Treat the returned string as sensitive. Avoid logs, shell arguments, command history, and unprotected files.

Secret deletion is not part of protocol v1. The canonical Locker client does not expose a delete method; legacy resource-object delete helpers fail closed with InvalidRequestError.

Environments

environments = client.list_environments()
for environment in environments:
    print(environment.name, environment.external_url)

Use list_environments_page() for cursor pagination.

The two lookup methods intentionally have different not-found contracts:

  • get_environment("production") returns None.
  • retrieve_environment("production") raises ResourceNotFoundError.

Create or update an environment:

created = client.create_environment(
    name="staging",
    external_url="https://staging.example.com",
)

updated = client.modify_environment(
    name=created.name,
    external_url="https://new-staging.example.com",
)

Environment deletion is not part of protocol v1.

Errors

Locker-defined transport, protocol, authentication, and API failures derive from locker.error.LockerError. Standard Python argument errors can still be raised for locally invalid values.

import logging
import os

from locker.error import AlreadyExistsError, LockerError, RateLimitError

try:
    created = client.create(
        key="PAYMENT_API_KEY",
        value=os.environ["BOOTSTRAP_PAYMENT_API_KEY"],
    )
except AlreadyExistsError as exc:
    logging.info(
        "The Locker secret already exists (request_id=%s, kind=%s)",
        exc.request_id,
        exc.kind,
    )
except RateLimitError as exc:
    logging.warning(
        "Locker request was rate limited (request_id=%s, retryable=%s)",
        exc.request_id,
        exc.retryable,
    )
    raise
except LockerError as exc:
    logging.error(
        "Locker request failed (type=%s, code=%s, kind=%s, request_id=%s, server_request_id=%s)",
        type(exc).__name__,
        exc.code,
        exc.kind,
        exc.request_id,
        exc.server_request_id,
    )
    raise
RPC code Exception Meaning
-32001 AuthenticationError Credentials were rejected
-32003 PermissionDeniedError Access is not permitted
-32004 ResourceNotFoundError Resource does not exist
-32009 ConflictError Operation conflicts with current resource state
-32009 + *_already_exists kind AlreadyExistsError Secret or environment already exists
-32022 ValidationError Operation data failed Locker validation
-32029 RateLimitError Request was rate limited
-32050 APIConnectionError Locker API could not be reached
-32051 APIServerError Locker API failed the request
-32060 LocalStorageError Local secure state failed
-32070 IntegrityError Cryptographic integrity validation failed
-32000 OperationError Legacy or unclassified operation failure
-32000 + cancelled kind OperationCancelledError Operation was cancelled; remote mutation outcome may be unknown
-32000 + request_rejected kind RequestRejectedError Legacy request rejection
-32000 + response_too_large kind ResponseTooLargeError Result exceeds the protocol response limit
-32700, -32600..-32603 ProtocolError Invalid JSON-RPC exchange

AlreadyExistsError derives from ConflictError; all service-operation exceptions derive from APIError, and every Locker-defined exception derives from LockerError. This allows applications to catch either a precise condition or a stable broader category. Older CLI releases that report -32000 with conflict or duplicate_hash are refined to conflict types for compatibility. The ambiguous legacy request_rejected kind maps to RequestRejectedError, not to a conflict or validation error.

Missing or malformed credential syntax is rejected locally before the CLI is resolved. A missing half of the pair uses kind missing_credentials; the access key ID must be a UUIDv4 (invalid_access_key_id), and the secret access key must be non-empty canonical Base64 (malformed_secret_access_key). These conditions raise AuthenticationError with code -32001. A well-formed pair rejected by Locker raises the same broad exception with kind invalid_secret_access_key and explains that the secret access key does not match the access key ID. A backend HTTP 401 uses kind unauthorized and the canonical authentication failed message.

request_id identifies the local SDK-to-CLI JSON-RPC exchange. server_request_id is a separate optional, validated backend correlation ID for support and activity-log lookup; it never replaces request_id.

Each exception exposes a canonical safe message through user_message and retains code, kind, retryable, and request_id. The SDK derives that message from the numeric code and a recognized kind; it never prints arbitrary wire text returned by a custom or incompatible CLI. Numeric codes select the primary exception category, while a documented kind only refines it, such as distinguishing an already-existing resource from another conflict. Integrity failures, conflicts, already-existing resources, and validation failures are always non-retryable and fail closed, even if an older CLI reports inconsistent retry metadata. Avoid logging exception bodies or application data around a secret operation.

RateLimitError.retry_after_seconds exposes the server's bounded Retry-After delay when available (0..86400); it is otherwise None. Cancellation raises OperationCancelledError and remains non-retryable because it does not prove that a remote mutation failed to commit.

CLI management and verification

Importing locker and constructing Locker are side-effect free. The SDK does not create a cache directory, access the network, or start a process until the first operation or an explicit install_cli() call.

Managed mode:

  1. Checks the signed release channel on first use and at most once every six hours after a successful check.
  2. Verifies the embedded Locker Ed25519 trust root, signed latest document, signed manifest, artifact size, SHA-256, detached signature, executable header, and protocol range.
  3. Cryptographically re-verifies the signed manifest plus the selected artifact's size, executable header, and streamed SHA-256 immediately before every CLI process execution; file identity metadata is never treated as a substitute for the signed hash.
  4. Installs releases into immutable ~/.locker/sdk-cli/python/bin/releases/<version>/ directories.
  5. Switches the current pointer only after complete verification.
  6. Rejects rollback attempts and same-version mutation.

Force an immediate signed update check:

installed_path = client.install_cli()

If the release service is temporarily unreachable, a previously accepted binary may be reused only after its cached metadata and artifact are verified again. Invalid signatures, rollback state, incompatible platforms, and protocol errors always fail closed.

Logging

Set LOCKER_LOG or the constructor's log argument:

export LOCKER_LOG="info"
client = Locker.from_env(log="info")

SDK logs contain operational metadata such as method, request ID, duration, and process status. The SDK never emits credentials, request or response bodies, custom headers, or secret values.

Migrating to 2.x

Version 2:

  • Requires Python 3.10 or newer.
  • Uses the stable locker.sdk JSON-RPC protocol v1.
  • Downloads only signed Locker CLI releases in managed mode.
  • Uses canonical LOCKER_ACCESS_KEY_ID and LOCKER_SECRET_ACCESS_KEY variables.
  • Adds Locker.from_env(), get_required(), typed pagination, and explicit CLI lifecycle methods.
  • Returns a default value only for ResourceNotFoundError.

Applications must use this SDK or the locker sdk protocol. Do not parse human-facing CLI output.

Versioning

The package follows Semantic Versioning and publishes canonical PEP 440 versions:

  • PyPI package: MAJOR.MINOR.PATCH
  • Source tag: vMAJOR.MINOR.PATCH

Troubleshooting

  • AuthenticationError: verify that the access key ID is UUIDv4, the secret access key is canonical Base64, and both values belong to the same active Locker project.
  • PermissionDeniedError: verify the authenticated key's project/environment scope.
  • APIConnectionError: check the API base, private CA/proxy, and server reachability. Opt into bounded read retries only when appropriate.
  • Managed CLI installation errors: check system time, access to the Locker Secrets download service, and private ownership below ~/.locker/sdk-cli/python.
  • ProtocolError: upgrade the SDK and CLI together or remove an incompatible explicit LOCKER_CLI_PATH.
  • Unexpected stale reads: use fetch=True; do not loosen managed-cache permissions as a workaround.

Security

Report security vulnerabilities through the Locker Bug Bounty program. Do not disclose vulnerabilities in a public issue.

General product documentation and support are available at support.locker.io.

License

Copyright CyStack Corporation.

Licensed under the Apache License 2.0. Locker is developed and maintained by CyStack.

About

No description, website, or topics provided.

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages