Python SDK

Read secrets from any Python application using Ed25519 machine authentication, on persistent hosts or serverless and ephemeral environments.

Installation

pip install sikkerkey

Requires Python 3.10+. Single runtime dependency: cryptography (for Ed25519 signing).

Quick Start

from sikkerkey import SikkerKey

sk = SikkerKey("vault_abc123")
secret = sk.get_secret("sk_a1b2c3d4e5")

The SDK loads the machine identity from ~/.sikkerkey/vaults/{vaultId}/identity.json, signs every request with the machine's Ed25519 private key, and returns the decrypted value.

Client Creation

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

Serverless (Memory-Only Bootstrap)

On a long-lived host the SDK loads a persistent identity from disk. Serverless and other ephemeral or read-only-filesystem environments (Vercel, AWS Lambda, Google Cloud Run, Fly.io, and similar) have no identity to persist. SikkerKey.bootstrap_in_memory() handles that case: it generates an Ed25519 keypair in memory, enrolls a short-lived ephemeral machine with an enrollment token, and returns a ready client, without writing anything to disk.

import os
from sikkerkey import SikkerKey

sk = SikkerKey.bootstrap_in_memory(
    os.environ["SIKKERKEY_VAULT_ID"],
    os.environ["SIKKERKEY_ENROLLMENT_TOKEN"],
)

db_url = sk.get_secret("sk_db_prod")

Create an enrollment token in the dashboard and supply its plaintext plus your vault ID. The token only registers an ephemeral machine scoped to the policy you set (projects, secrets, lifetime); it cannot read secrets on its own.

How It Works

  • bootstrap_in_memory() enrolls once, when you call it: it generates a keypair in memory, registers an ephemeral machine, and returns a ready client.
  • The returned client signs every read with the in-memory private key, exactly like a disk-based client.
  • Nothing is written to disk. The private key lives only in process memory and is gone when the process exits.
  • The ephemeral machine lives for the lifetime set on the enrollment token. Reading after it expires raises AuthenticationError, so set the token's machine lifetime to suit your workload.

Enrollment errors (bad token, sealed vault, IP not allowed) raise from the bootstrap_in_memory() call, so a misconfigured deployment fails at startup rather than on a later read. The common path is to read secrets at startup and hold the values, so enrollment runs once per process rather than per request.

Options

bootstrap_in_memory() accepts two keyword-only options:

OptionDefaultDescription
hostname$HOSTNAME, then serverlessLabel recorded on the machine. Must match the token's hostname pattern if one is set
namenoneMachine name to request. Overridden when the enrollment token defines a name pattern (the server generates the name from it)
sk = SikkerKey.bootstrap_in_memory(
    vault_id,
    token,
    hostname="worker-1",
    name="batch-runner",
)

Provisioning the Token for Serverless

When you create the enrollment token for a serverless deployment:

  • Set a short machine lifetime (minutes). Each cold start mints a fresh ephemeral machine, and short-lived ones free their slot quickly as they expire.
  • Set max-uses high enough for your cold-start and concurrency volume.
  • Leave the source-CIDR restriction unset, since serverless egress IPs are dynamic.
  • If the vault has an IP allowlist, make sure it permits the platform's egress or leave it off. Enrollment enforces the allowlist.
  • Set a name pattern on the token (for example serverless-{uuid8}) so each cold-start machine gets a clean, unique name in the dashboard. A name pattern takes precedence over any name the SDK sends. {uuidN} inserts N random characters (4 to 32, default 8); {uuid} inserts 8.

Each live ephemeral machine counts against your plan's machine limit until it expires and is cleaned up, so size your plan for your expected concurrency. Requires a Python runtime with outbound HTTPS.

Reading Secrets

Single Value

api_key = sk.get_secret("sk_stripe_prod")

Structured (Multiple Fields)

db = sk.get_fields("sk_db_prod")
host = db["host"]       # "db.example.com"
user = db["username"]   # "admin"
password = db["password"]  # "hunter2"

Raises SecretStructureError if the secret value is not a JSON object.

Single Field

password = sk.get_field("sk_db_prod", "password")

Raises FieldNotFoundError if the field doesn't exist. The error message includes available field names.

Listing Secrets

# 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
project_secrets = sk.list_secrets_by_project("proj_abc123")

Returns list[SecretListItem] with attributes id, name, field_names (optional), and project_id (optional).

Export

# 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(project_id="proj_abc123")

# Inject into environment
import os
os.environ.update(sk.export())

Structured secrets are flattened: SECRET_NAME_FIELD_NAME.

Watching for Changes

Watch secrets for real-time updates. When a secret is rotated, updated, or deleted, the callback fires with the new value.

from sikkerkey import WatchStatus

def on_db_change(event):
    if event.status == WatchStatus.CHANGED:
        # event.value has the new value
        # event.fields has parsed key-value pairs for structured secrets
        db.configure_credentials(
            username=event.fields["username"],
            password=event.fields["password"],
        )
    elif event.status == WatchStatus.DELETED:
        print("Secret deleted")
    elif event.status == WatchStatus.ACCESS_DENIED:
        print("Access revoked")
    elif event.status == WatchStatus.ERROR:
        print(f"Error: {event.error}")

sk.watch("sk_db_credentials", on_db_change)

Polling starts automatically on the first watch() call and runs on a background daemon thread. Default interval is 15 seconds (server enforces a 10-second minimum).

sk.set_poll_interval(30)  # seconds
sk.unwatch("sk_db_credentials")  # stop watching one secret
sk.close()  # stop all watches

SikkerKey can be used as a context manager for automatic cleanup.

Multi-Vault

prod = SikkerKey("vault_a1b2c3d4e5f6g7h8")
staging = SikkerKey("vault_x9y8z7w6v5u4t3s2")

prod_db = prod.get_secret("sk_db_prod")
staging_db = staging.get_secret("sk_db_staging")

List Registered Vaults

vaults = SikkerKey.list_vaults()
# ["vault_a1b2c3d4e5f6g7h8", "vault_x9y8z7w6v5u4t3s2"]

Static method on the SikkerKey class.

Identity Resolution

  1. Explicit path - starts with / or contains identity.json
  2. Vault ID - looks up ~/.sikkerkey/vaults/{vaultId}/identity.json
  3. SIKKERKEY_IDENTITY env - path to identity file
  4. Auto-detect - single vault on disk

The vault_ prefix is added automatically if not present. Override base directory with SIKKERKEY_HOME.

Error Handling

from sikkerkey import SikkerKey, NotFoundError, AccessDeniedError, AuthenticationError

try:
    secret = sk.get_secret("sk_nonexistent")
except NotFoundError:
    # 404 - secret doesn't exist
except AccessDeniedError:
    # 403 - machine not approved or no grant
except AuthenticationError:
    # 401 - invalid signature or unknown machine
except RateLimitedError:
    # 429 - too many requests (retried automatically)
except ServerSealedError:
    # 503 - server needs unseal (retried automatically)
except ConfigurationError:
    # identity file missing, key not found
except ApiError as e:
    # any other HTTP error
    print(e.http_status)

Exception Hierarchy

SikkerKeyError
├── ConfigurationError      - identity/key issues
├── SecretStructureError    - secret is not a JSON object (get_fields)
├── FieldNotFoundError      - field not in structured secret (get_field)
└── ApiError                - HTTP error (has http_status: int)
    ├── AuthenticationError - 401
    ├── AccessDeniedError   - 403
    ├── NotFoundError       - 404
    ├── ConflictError       - 409
    ├── RateLimitedError    - 429
    └── ServerSealedError   - 503

Properties

PropertyTypeDescription
machine_idstrMachine UUID
machine_namestrHostname from bootstrap
vault_idstrVault this identity belongs to
api_urlstrSikkerKey API URL

Read-only @property decorators.

Method Reference

MethodReturnsDescription
SikkerKey(vault_or_path?)SikkerKeyCreate client from disk identity
SikkerKey.bootstrap_in_memory(vault_id, token, *, hostname?, name?)SikkerKeyMemory-only serverless bootstrap (classmethod)
SikkerKey.list_vaults()list[str]List registered vault IDs (static)
get_secret(secret_id)strRead a secret value
get_fields(secret_id)dict[str, str]Read structured secret
get_field(secret_id, field)strRead 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)NoneWatch a secret for changes
unwatch(secret_id)NoneStop watching a secret
set_poll_interval(seconds)NoneSet poll interval (min 10s)
close()NoneStop all watches, shut down polling

Retry Behavior

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.

Environment Variables

VariableDescription
SIKKERKEY_IDENTITYPath to identity.json - overrides vault lookup
SIKKERKEY_HOMEBase config directory (default: ~/.sikkerkey)

Dependencies

DependencyVersionPurpose
cryptography>=41.0Ed25519 key loading and signing

All other functionality uses Python stdlib: urllib.request, json, hashlib, secrets. Requires Python 3.10+. All HTTP requests have a 15-second timeout.