Documentation menu

Clients & Libraries

Python

A client for the SecretServer REST API with no dependencies outside the Python standard library. Requires Python 3.8 or newer.

Install

The package name is secretserver (version 1.3.0 in pyproject.toml). Install it from a checkout of the clients repository:

pip install "git+https://github.com/afterdarksys/secretserver-clients.git#subdirectory=python"

# or from a checkout
git clone https://github.com/afterdarksys/secretserver-clients.git
pip install ./secretserver-clients/python

# or, for development (editable install)
pip install -e ./secretserver-clients/python

# check the import
python -c "from secretserver import SecretServerClient; print('OK')"

bash scripts/install-python.sh runs the same pip install; pass --dev for the editable install.

Authenticate

from secretserver import SecretServerClient

# Reads SS_API_KEY and SS_API_URL from the environment when not passed.
ss = SecretServerClient()

# Or pass everything explicitly.
ss = SecretServerClient(
    api_key="sk_...",
    api_url="https://api.secretserver.io",
    timeout=10,                    # seconds
    ca_file="/etc/ssl/private-ca.pem",  # optional private CA, added to system roots
)

The constructor raises AuthError when no key is available and ValueError for a non-https URL (plain http is allowed only for localhost, 127.0.0.1 and ::1) or for verify_ssl=False.

Read a secret

# Value only: name, container/key, or container/key/version (1..12)
password = ss.secret("db-password")
password = ss.secret("production/db-password")
previous = ss.secret("production/db-password/2")

# Full record. A read by name also carries the ETag header.
record = ss.get_secret("db-password")
print(record["data"]["value"], record.etag)

secret() returns the first string field it finds under data in this order: value, password, token, key, passphrase, bind_password, certificate.

Write a secret

Create

ss.create_secret(
    "db-password",
    "s3cr3t",
    description="Primary database",
    container_id="3f0c...uuid",   # a container UUID, not its slug
)

Update

update_secret sends only the arguments you pass. An omitted argument keeps the stored value and None clears it. It refuses to send unless the client opted in to partial updates or the call carries an ETag from a previous read.

# Safe on any server: prove support with the ETag from a read
record = ss.get_secret("db-password")
ss.update_secret("db-password", "new-value", if_match=record.etag)

# Or opt in when the server is build 3075630 or newer
ss = SecretServerClient(partial_updates=True)   # or SS_PARTIAL_UPDATES=1
ss.update_secret("db-password", description=None)  # clear description only

Delete

ss.delete_secret("db-password")

List secrets

for s in ss.list_secrets():
    print(s["name"], s.get("version"))

Listing returns metadata. Read a value with secret() or get_secret().

Error handling

ExceptionWhen
AuthErrorHTTP 401, or no API key at construction
PermissionErrorHTTP 403: the key lacks the scope
NotFoundErrorHTTP 404
ConflictErrorHTTP 409, for example a stale if_match. .etag holds the current ETag
SecretServerErrorBase class; other HTTP errors, connection failure, oversized or invalid responses

Every exception has status_code. Messages never include the server response body. Note that this PermissionError is the library's own class; import it from secretserver.

from secretserver import (
    SecretServerClient, SecretServerError,
    AuthError, PermissionError, NotFoundError, ConflictError,
)

try:
    value = ss.secret("production/db-password")
except NotFoundError:
    value = None
except (AuthError, PermissionError) as exc:
    raise SystemExit(f"access denied (HTTP {exc.status_code})")
except SecretServerError as exc:
    raise SystemExit(f"secretserver request failed: {exc}")

Complete example

rotate_example.py

import os
import sys

from secretserver import ConflictError, NotFoundError, SecretServerClient, SecretServerError


def main() -> int:
    ss = SecretServerClient()  # SS_API_KEY / SS_API_URL from the environment
    name = "example-api-token"

    try:
        record = ss.get_secret(name)
    except NotFoundError:
        ss.create_secret(name, "first-value", description="created by example")
        record = ss.get_secret(name)

    try:
        ss.update_secret(name, "second-value", if_match=record.etag)
    except ConflictError as exc:
        print(f"changed by someone else, current ETag {exc.etag}", file=sys.stderr)
        return 1

    names = [s["name"] for s in ss.list_secrets()]
    print(f"{len(names)} secrets visible to this key")
    print("previous value length:", len(ss.secret(name)))
    return 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except SecretServerError as exc:
        print(f"error: {exc}", file=sys.stderr)
        sys.exit(2)
Do not print or log secret values. The example prints only a length.

More operations

  • Any REST route: ss.request("GET", "/containers") (path relative to /api/v1).
  • Named variables: assign_variable, get_variable, list_variables, delete_variable, render(text), resolve_document(obj).
  • History and sharing: get_history, get_version, share, create_temp_access.
  • Extended credentials: ss.credentials("wifi-credentials") returns an object with list/get/create/update/delete.
  • Keys and certificates: certificates, SSH, GPG, OpenSSL keys, JKS keystores, TOTP, YubiKey and operation-only sign().