Metadata-Version: 2.5
Name: ratelkey-burrow-sdk
Version: 1.3.0
Summary: Read secrets from a self-hosted RatelKey Burrow with a machine identity.
Project-URL: Homepage, https://ratelkey.com
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: burrow,ratelkey,sdk,secrets
Requires-Python: >=3.9
Requires-Dist: cryptography>=41.0.0
Description-Content-Type: text/markdown

# Burrow SDK for Python

Read secrets from a self-hosted [RatelKey](https://ratelkey.com) Burrow with a machine identity.

- Single and structured secrets, and individual fields of a structured secret.
- The machine's own access map (`list_projects`, `list_secrets`).
- Certificate pinning, or standard CA trust (`TlsMode`).
- Live secrets that follow every change on the Burrow (`live`, `live_environment`).
- Certificates from a dynamic certificate secret, such as SSH logins, for a key made in memory (`get_certificate`, `live_certificate`).
- An OIDC token instead of a key (`Burrow.oidc`).

## Requirements

- Python 3.9+.
- One dependency: [`cryptography`](https://pypi.org/project/cryptography/) (Ed25519 signing and the certificate pin).

## Install

```
pip install ratelkey-burrow-sdk
```

## Bootstrap a machine

From the Burrow dashboard, **Machines → Add machine**, and run the install it gives you on the target host.
The identity lands at `~/.burrow/burrows/<slug>/identity.json`.

## Quick start

```python
from ratelkey_burrow import Burrow

# The single machine identity on this host (under ~/.burrow, or $BURROW_HOME).
burrow = Burrow()

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

# A structured secret — one read, projected locally (no re-fetch):
db = burrow.get_secret("payments/prod/DATABASE")
everything = db.to_dict()                                  # every field as name -> value
host = db.get_field("host")                                # one field
user, password = db.get_fields("username", "password")     # in order

# What this machine can read:
for p in burrow.list_projects():
    print(p.project, p.categories)
```

Point it at a specific identity by selector or path:

```python
Burrow("burrow-a1b2c3d4e5f60718")                            # slug / name / machine id / URL
Burrow("/home/you/.burrow/burrows/burrow-a1b2c3d4e5f60718")  # path (a burrow dir or identity.json)
```

## Without `~/.burrow`

For an app that manages its own key material:

```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"],  # PKCS#8 PEM
)
```

## Live secrets

A live handle holds a secret's current value and follows every change on the Burrow as it happens.

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

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

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

- The call returns after the first read. A missing secret or grant is refused there.
- All live handles on a client share one connection on a background thread. It reconnects on its own and re-reads
  anything that changed while it was down.
- `status` is `live`, `reconnecting`, `revoked` (access was taken away) or `removed` (deleted on the Burrow). The
  last two are final, and the handle keeps its last value.
- Listeners run on the SDK's threads. One that raises is reported to `on_error`.
- `burrow.close()` closes every handle.
- Needs a Burrow with live updates; an older one is refused at `live()`.

## Certificates

A dynamic certificate secret, such as an SSH user secret, gives each machine that asks a short-lived certificate of
its own. The SDK makes a new Ed25519 key in memory for each one and sends only its public half.

```python
login = burrow.get_certificate("api/prod/DEPLOY_SSH")

login.private_key     # the OpenSSH private key
login.certificate     # its certificate (ssh-ed25519-cert-v01@openssh.com ...)
login.principals      # the logins it signs in as
login.host            # where the secret says to connect, or None
login.valid_before    # when it stops being accepted (UTC)
```

Hand the key and certificate to your SSH library, or write them to `id_ed25519` (mode 600) and
`id_ed25519-cert.pub` for `ssh -i`. `validity=timedelta(minutes=5)` (or a number of seconds) asks for one that lasts
less than the secret allows.

`live_certificate` keeps one current:

```python
login = burrow.live_certificate("api/prod/DEPLOY_SSH")

login.current                                               # the certificate and key to use now
login.on_change(lambda next, prev: connections.use_key(next))
login.renew()                                               # a new one at once, e.g. after a host refused it
```

- It renews once two thirds of the certificate's time have passed, and as soon as the secret's settings change.
  Each renewal makes a new key.
- Revoking one certificate from the dashboard sends no event; `renew()` gets a new one.
- `get_secret` on a certificate secret raises `BurrowServerError` (409) naming `get_certificate`.

## In-memory identity

Enroll with a machine token's code. The key is generated in memory and never written to disk; the code sets how the Burrow's certificate is trusted.

```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
```

The machine is enrolled on the first read. Shortly before it expires, the SDK enrolls a fresh one with the same code and retires the old one. `close()` (or leaving the `with` block) expires the machine at once.

`Burrow()` enrolls from `BURROW_ENROLL_CODE` when the host has no identity under `~/.burrow`. A one-time invitation code is refused; use a machine token.

## OIDC

```python
import os
from ratelkey_burrow import Burrow

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

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

`audience` defaults to `burrow_url`. GitHub Actions requires `permissions: id-token: write`. GitLab CI/CD (gitlab.com) requires the job to declare its token as `RATEL_ID_TOKEN`:

```yaml
id_tokens:
  RATEL_ID_TOKEN:
    aud: https://burrow.example.com
```

Bitbucket Pipelines requires the step to list the Burrow as an OIDC audience:

```yaml
- step:
    oidc:
      audiences:
        - https://burrow.example.com
```

## Certificate trust

```python
from ratelkey_burrow import Burrow, TlsMode

burrow = Burrow(tls=TlsMode.SystemTrust)
```

Without an explicit `tls`, the SDK uses the trust chosen when the machine was added: encrypted by default (works everywhere), or verified against the Burrow's own key or a CA if that was selected.

- `TlsMode.Pinned` — trust-on-first-use, stored at `~/.burrow/burrows/<slug>/known_cert`.
- `TlsMode.PinnedTo("aa:bb:...")` — a fixed fingerprint (from the Burrow's network settings).
- `TlsMode.SystemTrust` — CA + hostname verification, for a Burrow on a custom domain or Let's Encrypt.
- `TlsMode.Insecure` — no verification; accepts any certificate. Opt-in only, for a network you already trust. Shortcut: `Burrow().insecure_tls(True)`.

## Webhooks

Verify a delivery from a Burrow's Signed webhook. Needs only the webhook's signing secret, not a 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 body, before JSON parsing (`request.body` in Django). A retry of the same event carries the same `event.id`, so keep the ids you've handled to ignore repeats.

## Errors

Every failure is a `BurrowError`: `BurrowAuthError` (401), `BurrowForbiddenError` (403, no grant),
`BurrowNotFoundError` (404), `BurrowServerError` (5xx, or 409 when a secret refuses the request),
`BurrowCertificateError` (pin mismatch),
`BurrowTransportError` (unreachable / unparsable), `BurrowIdentityError` (no/ambiguous/invalid identity),
plus two projection errors on the result handle — `SecretShapeError` (e.g. `.value` on a structured secret)
and `FieldNotFoundError`. The message carries the Burrow's own reason. `verify_webhook` raises
`BurrowWebhookError` for a missing or wrong signature.