The official Python SDK for SikkerKey. Read-only access to secrets using Ed25519 machine authentication.
pip install sikkerkeyRequires Python 3.10+. Single dependency: cryptography (for Ed25519 signing).
from sikkerkey import SikkerKey
sk = SikkerKey("vault_abc123")
api_key = sk.get_secret("sk_stripe_key")The SDK reads the machine identity from ~/.sikkerkey/vaults/<vault-id>/identity.json, signs every request with the machine's Ed25519 private key, and returns the decrypted value.
# Explicit vault ID
sk = SikkerKey("vault_abc123")
# Direct path to identity file
sk = SikkerKey("/etc/sikkerkey/vaults/vault_abc123/identity.json")
# Auto-detect from SIKKERKEY_IDENTITY env or single vault on disk
sk = SikkerKey()Raises ConfigurationError if the identity is missing, the key can't be loaded, or multiple vaults exist without a specified vault ID.
api_key = sk.get_secret("sk_stripe_prod")fields = sk.get_fields("sk_db_prod")
host = fields["host"] # "db.example.com"
password = fields["password"] # "hunter2"Raises SecretStructureError if the secret value is not a JSON object.
password = sk.get_field("sk_db_prod", "password")Raises FieldNotFoundError if the field doesn't exist. The error message includes available field names.
# All secrets this machine can access
secrets = sk.list_secrets()
for s in secrets:
print(f"{s.id}: {s.name}")
# Secrets in a specific project
secrets = sk.list_secrets_by_project("proj_production")Each SecretListItem has id, name, field_names (None for single-value), and project_id.
# All secrets as a flat dict
env = sk.export()
# {"API_KEY": "sk-live-...", "DB_CREDS_HOST": "db.example.com", "DB_CREDS_PASSWORD": "s3cret"}
# Scoped to a project
env = sk.export("proj_production")
# Inject into environment
import os
os.environ.update(sk.export())Structured secrets are flattened: SECRET_NAME_FIELD_NAME.
Watch secrets for real-time updates. When a secret is rotated, updated, or deleted, the callback fires with the new value. Polling happens on a background daemon thread - your application is never blocked.
from sikkerkey import WatchStatus
def on_change(event):
if event.status == WatchStatus.CHANGED:
print(f"New value: {event.value}")
# Structured secrets include parsed fields
print(f"Fields: {event.fields}")
elif event.status == WatchStatus.DELETED:
print("Secret was deleted")
elif event.status == WatchStatus.ACCESS_DENIED:
print("Access revoked")
elif event.status == WatchStatus.ERROR:
print(f"Error: {event.error}")
sk.watch("sk_db_password", on_change)# Auto-rotate database credentials
def rotate_db(event):
if event.status == WatchStatus.CHANGED:
db.configure_credentials(
username=event.fields["username"],
password=event.fields["password"],
)
sk.watch("sk_db_credentials", rotate_db)The default poll interval is 15 seconds. The server enforces a minimum of 10 seconds.
sk.set_poll_interval(30) # seconds# Stop watching a specific secret
sk.unwatch("sk_db_password")
# Stop all watches and shut down polling
sk.close()SikkerKey can be used as a context manager:
with SikkerKey("vault_abc123") as sk:
sk.watch("sk_api_key", on_change)
# ... application logic ...
# Automatically closed on exitprod = SikkerKey("vault_a1b2c3")
staging = SikkerKey("vault_x9y8z7")
prod_key = prod.get_secret("sk_api_key")
staging_key = staging.get_secret("sk_api_key")vaults = SikkerKey.list_vaults()
# ["vault_a1b2c3", "vault_x9y8z7"]sk.machine_id # "550e8400-e29b-41d4-a716-446655440000"
sk.machine_name # "api-server-1"
sk.vault_id # "vault_abc123"
sk.api_url # "https://api.sikkerkey.com"from sikkerkey import SikkerKey, NotFoundError, AccessDeniedError, AuthenticationError
try:
secret = sk.get_secret("sk_nonexistent")
except NotFoundError:
# Secret doesn't exist
except AccessDeniedError:
# Machine not approved or no grant
except AuthenticationError:
# Invalid signature or unknown machineSikkerKeyError
├── ConfigurationError — identity file missing, bad key, invalid config
├── SecretStructureError — secret is not a JSON object (get_fields)
├── FieldNotFoundError — field not in structured secret (get_field)
└── ApiError — HTTP error (has http_status attribute)
├── AuthenticationError — 401
├── AccessDeniedError — 403
├── NotFoundError — 404
├── ConflictError — 409
├── RateLimitedError — 429
└── ServerSealedError — 503
- Explicit path — starts with
/or containsidentity.json - Vault ID — looks up
~/.sikkerkey/vaults/{vault_id}/identity.json SIKKERKEY_IDENTITYenv — path to identity file- Auto-detect — single vault on disk
The vault_ prefix is added automatically if not present.
| Variable | Description |
|---|---|
SIKKERKEY_IDENTITY |
Path to identity.json — overrides vault lookup |
SIKKERKEY_HOME |
Base config directory (default: ~/.sikkerkey) |
429 and 503 responses are retried up to 3 times with exponential backoff (1s, 2s, 4s). Each retry uses a fresh timestamp and nonce. Network errors are also retried.
Every request includes Ed25519-signed headers: X-Machine-Id, X-Timestamp, X-Nonce, X-Signature. HTTPS enforced for non-localhost. 15-second timeout.
| Method | Returns | Description |
|---|---|---|
SikkerKey(vault_or_path?) |
SikkerKey |
Create client |
SikkerKey.list_vaults() |
list[str] |
List registered vault IDs (static) |
get_secret(secret_id) |
str |
Read a secret value |
get_fields(secret_id) |
dict[str, str] |
Read structured secret |
get_field(secret_id, field) |
str |
Read single field |
list_secrets() |
list[SecretListItem] |
List all accessible secrets |
list_secrets_by_project(project_id) |
list[SecretListItem] |
List secrets in a project |
export(project_id?) |
dict[str, str] |
Export as env map |
watch(secret_id, callback) |
None |
Watch a secret for changes |
unwatch(secret_id) |
None |
Stop watching a secret |
set_poll_interval(seconds) |
None |
Set poll interval (min 10s) |
close() |
None |
Stop all watches, shut down polling |
cryptography>=41.0— Ed25519 key loading and signing
All other functionality uses Python stdlib: urllib, json, hashlib, os, pathlib.
MIT — see LICENSE for details.