Python SDK

Read your Burrow's secrets from a Python application as an enrolled machine, and verify its signed webhooks.

Updated Oct 2, 2026

The Python SDK reads secrets from your Burrow in a Python application, signed in as a machine you enrolled. It reads single-value and structured secrets, keeps them current as they change, lists what the machine has access to, and verifies your Burrow's signed webhooks. It needs Python 3.9 or later.

Install#

bash
pip install ratelkey-burrow-sdk

Enroll the machine#

The SDK reads as a machine. Enroll the host your application runs on as described in Command / Ed25519. The command stores the machine's identity under ~/.burrow on that host, where the SDK finds it. Then give the machine the projects and environments it reads from Machine grants.

Read a secret#

A secret is addressed as project/environment/NAME. A structured secret comes back in one request, and its fields are read from the result without asking the Burrow again.

python
from ratelkey_burrow import Burrow

burrow = Burrow()

# A single-value secret
api_key = burrow.get_secret("payments/prod/API_KEY").value

# A structured secret
db = burrow.get_secret("payments/prod/DATABASE")
everything = db.to_dict()                                  # every field, name to value
host = db.get_field("host")                                # one field
user, password = db.get_fields("username", "password")     # several, in order

To see what the machine can read:

python
for p in burrow.list_projects():
    print(p.project, p.categories)

list_secrets lists the secrets in an environment the machine can read.

Live secrets#

A live secret holds the secret's current value and follows every change on the Burrow as it is saved. Read it whenever you need the value, and listen for changes to act on them.

python
db = burrow.live("payments/prod/DATABASE")

db.get_field("password")                                  # always the current value
db.on_change(lambda next, prev: pool.reconnect(next))

live_environment("payments/prod") does the same for a whole environment: env.get("API_KEY") reads one secret, and on_change receives what changed, was added or was removed.

The call returns after the first read, so a missing secret or grant fails there. All live secrets on a client share one connection on a background thread, which reconnects on its own and picks up anything that changed while it was down. Listeners run on that thread. close() on the client closes them all.

Status

Meaning

live

Connected; the value is the current one.

reconnecting

The connection dropped; the value is the last one read.

revoked

The machine can no longer read it. It keeps its last value and stops updating.

removed

It was deleted on the Burrow. It keeps its last value and stops updating.

Use on_status to follow it, and on_error for a listener that raised or a read that failed.

Choose an identity#

With one machine identity on the host, Burrow() uses it. With several, name the one to use by its Burrow's slug or name, the machine id, or the Burrow's URL, or give the path to its folder or its identity.json. To keep identities somewhere other than ~/.burrow, set BURROW_HOME.

python
Burrow("burrow-a1b2c3d4e5f60718")
Burrow("/home/you/.burrow/burrows/burrow-a1b2c3d4e5f60718")

Supply the key yourself#

An application that keeps its own key material can pass the Burrow's address, the machine id and the machine's private key, as PKCS#8 PEM, directly:

python
import os
from ratelkey_burrow import Burrow

burrow = Burrow.connect(
    burrow_url="https://burrow.internal:12010",
    machine_id="...",
    private_key_pem=os.environ["BURROW_MACHINE_KEY"],
)

Enroll in memory#

To keep nothing on disk, as in a container, enroll with the code of a machine token. The SDK generates the machine's key in memory and enrolls on the first read. The code carries the Burrow's address and how to trust its certificate.

python
import os
from ratelkey_burrow import Burrow

with Burrow.enroll(os.environ["BURROW_ENROLL_CODE"]) as burrow:
    api_key = burrow.get_secret("payments/prod/API_KEY").value

Before the machine expires, the SDK enrolls a fresh one with the same code. close(), or leaving the with block, ends the machine at once, so it shows as Expired on the Burrow. With no identity under ~/.burrow, Burrow() enrolls from BURROW_ENROLL_CODE.

OIDC#

In a CI job such as GitHub Actions, the SDK can authenticate with the token the CI provider mints for each run, with no key on the runner. Register the machine for the workflow first, then:

python
import os
from ratelkey_burrow import Burrow

burrow = Burrow.oidc(
    burrow_url="https://burrow.example.com",
    fingerprint=os.environ.get("BURROW_FINGERPRINT"),  # leave out for a publicly-trusted certificate
)

api_key = burrow.get_secret("payments/prod/API_KEY").value

The audience defaults to the Burrow's address. If you registered the machine with a different one, pass it as audience.

Certificate trust#

By default the SDK trusts the Burrow the way you chose under Connection when the machine was added. To override it, pass tls:

python
from ratelkey_burrow import Burrow, TlsMode

burrow = Burrow(tls=TlsMode.SystemTrust)

Mode

Use it when

TlsMode.Pinned

The Burrow is self-signed. The certificate seen on the first connection is remembered and required from then on.

TlsMode.PinnedTo("aa:bb:...")

The Burrow is self-signed and you have its fingerprint from Settings, Network.

TlsMode.SystemTrust

The Burrow has a publicly-trusted certificate, such as on a custom domain or from Let's Encrypt.

TlsMode.Insecure

Only on a network you already trust. The connection is encrypted, but the Burrow's certificate isn't checked. Burrow().insecure_tls(True) does the same.

Verify webhooks#

For a Signed webhook, verify_webhook checks that a delivery came from your Burrow and returns the event. It needs only the webhook's signing secret, with no machine identity.

python
import os
from flask import Flask, request
from ratelkey_burrow import verify_webhook

app = Flask(__name__)

@app.post("/hooks/burrow")
def burrow_hook():
    event = verify_webhook(request.get_data(), request.headers, os.environ["BURROW_WEBHOOK_SECRET"])
    print(event.event, event.data.summary)
    return "", 204

Pass the raw request body, before any JSON parsing (request.body in Django). A retried delivery carries the same event.id, so keep the ids you've handled and skip repeats.

Errors#

Every failure raises a BurrowError, and its message carries the Burrow's own reason.

Error

When

BurrowAuthError

The Burrow didn't accept the machine's signature or token.

BurrowForbiddenError

The machine has no grant for that project or environment.

BurrowNotFoundError

No such project, environment or secret, or the Burrow has no live updates.

BurrowServerError

The Burrow failed to answer the request.

BurrowCertificateError

The Burrow's certificate doesn't match the pinned one.

BurrowTransportError

The Burrow couldn't be reached, or its answer couldn't be read.

BurrowIdentityError

No machine identity was found, more than one matched, it couldn't be read, the enrollment code isn't valid, or the client was closed.

SecretShapeError

The result was read the wrong way, such as .value on a structured secret.

FieldNotFoundError

The structured secret has no field by that name.

BurrowWebhookError

A webhook delivery's signature is missing or wrong.