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 onlyDelete
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
| Exception | When |
|---|---|
AuthError | HTTP 401, or no API key at construction |
PermissionError | HTTP 403: the key lacks the scope |
NotFoundError | HTTP 404 |
ConflictError | HTTP 409, for example a stale if_match. .etag holds the current ETag |
SecretServerError | Base 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)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 withlist/get/create/update/delete. - Keys and certificates: certificates, SSH, GPG, OpenSSL keys, JKS keystores, TOTP, YubiKey and operation-only
sign().