The Go SDK reads secrets from your Burrow in a Go application, signed in as a machine you enrolled. It reads single-value and structured secrets, keeps them current as they change, gets certificates from SSH user secrets, lists what the machine has access to, and verifies your Burrow's signed webhooks. It needs Go 1.22 or later and nothing outside the standard library.
Install#
go get github.com/RatelKeyOrg/burrow-sdk-goEnroll 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. Each call takes a context, which can cancel it or give it a deadline.
import "github.com/RatelKeyOrg/burrow-sdk-go"
client, err := burrow.Open()
if err != nil {
log.Fatal(err)
}
defer client.Close()
// A single-value secret
key, err := client.GetSecret(ctx, "payments/prod/API_KEY")
apiKey, err := key.Value()
// A structured secret
db, err := client.GetSecret(ctx, "payments/prod/DATABASE")
all, err := db.ToMap() // every field, name to value
host, err := db.GetField("host") // one field
creds, err := db.GetFields("username", "password") // several, in orderTo see what the machine can read:
projects, err := client.ListProjects(ctx)
for _, p := range projects {
fmt.Println(p.Project, p.Categories)
}ListSecrets lists every secret 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.
db, err := client.Live(ctx, "payments/prod/DATABASE")
password, err := db.GetField("password") // always the current value
db.OnChange(func(next, prev *burrow.Secret) { pool.Reconnect(next) })LiveEnvironment(ctx, "payments/prod") does the same for a whole environment: env.Get("API_KEY") reads one secret, and OnChange lists 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, which reconnects on its own and picks up anything that changed while it was down. Close() on the client closes them all.
Status | Meaning |
|---|---|
| Connected; the value is the current one. |
| The connection dropped; the value is the last one read. |
| The machine can no longer read it. It keeps its last value and stops updating. |
| It was deleted on the Burrow. It keeps its last value and stops updating. |
OnStatus follows it, and OnError hears of a listener that panicked or a read that failed. Listeners run one at a time, in order, on a goroutine of the handle's own.
Fallback cache#
The fallback cache keeps a copy of each secret the client reads, and answers with it when your Burrow can't be reached. It's off until you turn it on:
client, err := burrow.Open(burrow.WithCache(true))Without WithCache, a client from Open uses the cache when ratel cache enable turned it on for the machine, and WithCache(false) keeps it off. With Connect, it needs PinStoreDir to keep the copies in. Enroll and OIDC can't use it.
A read uses the cache only when it fails because your Burrow can't be reached or can't answer. A secret answered from the cache says when its copy was saved:
key, err := client.GetSecret(ctx, "payments/prod/API_KEY")
if !key.CachedAt().IsZero() {
log.Printf("using the copy saved at %s", key.CachedAt())
}A live secret or environment doesn't answer from the cache. While your Burrow can't be reached it keeps the value it last read, and each read it makes keeps the copies up to date. When your Burrow refuses a read, because the machine was removed or disabled, lost access, or the secret is gone, the read fails as usual and the refused copies are deleted. A dynamic secret's login comes back as it was saved, so it may have expired.
Certificates#
A dynamic certificate secret, such as an SSH user secret, issues a short-lived certificate to each machine that asks. GetCertificate makes a new Ed25519 key in memory, sends only its public half to your Burrow, and returns the certificate with the key.
login, err := client.GetCertificate(ctx, "myapp/prod/DEPLOY_SSH")
login.PrivateKey() // the OpenSSH private key
login.Certificate // its certificate
login.Principals // the logins it signs in as
login.Host // where the secret says to connect, or ""
login.ValidBefore // when it stops being acceptedHand the key and the certificate to your SSH library, or write them to id_ed25519 (mode 600) and id_ed25519-cert.pub for ssh -i. burrow.WithValidity(5*time.Minute) asks for a certificate that lasts less than the secret allows. Each call issues a new certificate for a new key, and printing one leaves the private key out.
LiveCertificate keeps one current. It renews the certificate once two thirds of its time have passed, and as soon as the secret's settings change, each time with a new key. It shares the connection and the statuses of live secrets.
login, err := client.LiveCertificate(ctx, "myapp/prod/DEPLOY_SSH")
login.Current() // the certificate and key to use now
login.OnChange(func(next, prev *burrow.Certificate) { connections.UseKey(next) })
renewed, err := login.Renew(ctx) // a new one at onceA member revoking one certificate from the dashboard sends no event, so call Renew when a host refuses the certificate. GetSecret on a certificate secret returns a *burrow.ServerError, naming GetCertificate.
Choose an identity#
With one machine identity on the host, burrow.Open() 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.
burrow.Open(burrow.WithIdentity("burrow-a1b2c3d4e5f60718"))
burrow.Open(burrow.WithIdentity("/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:
client, err := burrow.Connect(burrow.ConnectConfig{
BurrowURL: "https://burrow.internal:12010",
MachineID: "...",
PrivateKeyPEM: os.Getenv("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.
client, err := burrow.Enroll(os.Getenv("BURROW_ENROLL_CODE"))
key, err := client.GetSecret(ctx, "payments/prod/API_KEY")
// On shutdown
client.Close()Before the machine expires, the SDK enrolls a fresh one with the same code. Close() ends the machine at once, so it shows as Expired on the Burrow. With no identity under ~/.burrow, burrow.Open() enrolls from BURROW_ENROLL_CODE.
OIDC#
In a GitHub Actions workflow, a GitLab CI/CD job or a Bitbucket Pipelines step, the SDK authenticates with the token the provider issues for each run, with no key on the runner. Register the machine first, then:
client, err := burrow.OIDC(burrow.OIDCConfig{
BurrowURL: "https://burrow.example.com",
Fingerprint: os.Getenv("BURROW_FINGERPRINT"), // leave empty for a publicly-trusted certificate
})
key, err := client.GetSecret(ctx, "payments/prod/API_KEY")The audience defaults to the Burrow's address. If you registered the machine with a different one, set it as Audience.
On GitLab CI/CD the job needs an ID token named RATEL_ID_TOKEN whose aud is the audience, which the SDK reads from the job. A token for another audience is refused before it's sent, naming the one to set.
id_tokens:
RATEL_ID_TOKEN:
aud: https://burrow.example.comOn Bitbucket Pipelines the step needs oidc with the audience among its audiences, and the SDK reads the step's token from BITBUCKET_STEP_OIDC_TOKEN.
- step:
oidc:
audiences:
- https://burrow.example.comCertificate trust#
By default the SDK trusts the Burrow the way you chose under Connection when the machine was added. To override it, pass burrow.WithTLS:
client, err := burrow.Open(burrow.WithTLS(burrow.TLSSystemTrust()))Mode | Use it when |
|---|---|
| The Burrow is self-signed. The key it presents on the first connection is remembered and required from then on. |
| The Burrow is self-signed and you have its fingerprint from Settings, Network. |
| The Burrow has a publicly-trusted certificate, such as on a custom domain or from Let's Encrypt. A certificate issued through the compromised eMudhra certificate authorities (CVE-2026-91403) is refused, whatever the system trusts. |
| Only on a network you already trust. The connection is encrypted, but the Burrow's certificate isn't checked. |
Verify webhooks#
For a Signed webhook, VerifyWebhook checks that a delivery came from your Burrow and returns the event. It needs only the webhook's signing secret, with no machine identity.
http.HandleFunc("/hooks/burrow", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
event, err := burrow.VerifyWebhook(body, r.Header, os.Getenv("BURROW_WEBHOOK_SECRET"))
if err != nil {
http.Error(w, "unverified", http.StatusUnauthorized)
return
}
log.Println(event.Event, event.Data.Summary)
w.WriteHeader(http.StatusNoContent)
})Pass the raw request body, before any JSON parsing. A retried delivery carries the same event.ID, so keep the ids you've handled and skip repeats.
Errors#
Every failure returns one of the SDK's error types, matched with errors.As, and its message carries the Burrow's own reason.
var notFound *burrow.NotFoundError
if errors.As(err, ¬Found) {
// no such project, environment or secret
}Error | When |
|---|---|
| The Burrow didn't accept the machine's signature or token. |
| The machine has no grant for that project or environment. |
| No such project, environment or secret, or the Burrow has no live updates. |
| The Burrow failed to answer the request, or refused it because of the secret's state, such as a dynamic secret that isn't issuing. |
| The Burrow's key doesn't match the pinned one, or under system trust its certificate was issued through a compromised eMudhra certificate authority. |
| The Burrow couldn't be reached, or its answer couldn't be read. |
| 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. |
| The result was read the wrong way, such as |
| The structured secret has no field by that name. |
| A webhook delivery's signature is missing or wrong. |