Backblaze B2¶
The B2 backend stores the kopia repository in a Backblaze B2 bucket using B2's native API and an application key. (B2 also exposes an S3-compatible API — if you prefer that, use the S3 backend with B2's S3 endpoint instead.)
Provider prerequisites¶
- A B2 bucket (Kopiur does not create it).
- An application key — a
keyID+applicationKeypair. Prefer a key scoped to the one bucket over the master key. Create it in the B2 console under App Keys.
Creating the bucket and a scoped key with the b2 CLI
$ b2 bucket create my-kopia-backups allPrivate
$ b2 key create --bucket my-kopia-backups kopia-mover \
listBuckets,listFiles,readFiles,writeFiles,deleteFiles
appKeyId001122... K001abcdef... # ← B2_KEY_ID and B2_KEY, in that order
The applicationKey (the second value) is shown once, at creation — copy it
straight into the Secret. deleteFiles is required: retention and
maintenance delete expired blobs, so a key without it makes
backups work and maintenance fail later.
Native B2 vs. B2's S3-compatible API
Both reach the same bucket. The native backend (this page) authenticates with an
application key and needs no endpoint. The S3 backend against
s3.<region>.backblazeb2.com is the right choice when you want one backend shape
across providers, or a tool in your pipeline only speaks S3. Pick one before
creating the repository and stay with it — the two write the same kopia format,
but switching means updating the Repository spec and credentials together.
The Secret shape¶
Loaded with envFrom; the keys reach kopia as environment variables.
| Secret key | Required | What it is |
|---|---|---|
B2_KEY_ID |
yes | The application key ID. |
B2_KEY |
yes | The application key paired with the ID. |
KOPIA_PASSWORD |
yes | The repository encryption password. |
stringData:
B2_KEY_ID: "REPLACE_ME"
B2_KEY: "REPLACE_ME"
KOPIA_PASSWORD: "choose-something-long-and-random"
Lose the password, lose the backups
KOPIA_PASSWORD encrypts the repository and cannot be recovered if lost. Store it
outside the cluster and back up the Secret. See Encryption.
The Repository¶
---
apiVersion: v1
kind: Secret
metadata:
name: b2-repo-creds
namespace: backups
type: Opaque
stringData:
# A Backblaze application key (keyID + key). Prefer a key scoped to the one
# bucket, not the master key.
B2_KEY_ID: "REPLACE_ME"
B2_KEY: "REPLACE_ME"
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: b2-primary
namespace: backups
spec:
backend:
b2:
bucket: my-kopia-backups # the B2 bucket holding the repository
prefix: prod/ # optional object-name prefix within the bucket
auth:
secretRef:
name: b2-repo-creds
encryption:
passwordSecretRef:
name: b2-repo-creds
key: KOPIA_PASSWORD
create:
enabled: true
Fields reference (backend.b2)¶
| Field | Required | Default | Example | What it controls |
|---|---|---|---|---|
bucket |
yes | — | my-kopia-backups |
The B2 bucket holding the repository. B2 bucket names are globally unique, like S3. |
prefix |
no | bucket root | clusters/prod/ |
Object-name prefix so several repos can share one bucket. End it with /. |
auth.secretRef |
no | — | { name: b2-repo-creds } |
Names the credential Secret above. Same namespace as the Repository; a ClusterRepository adds namespace:. |
B2 has no cloud-IAM federation, so its auth is Secret-only — there is no
workloadIdentity here (unlike S3/Azure/GCS).
A stray auth.workloadIdentity on a B2 backend is silently pruned by the
API server (it's not in the schema), not rejected.
Customization — the values you actually change¶
bucket/prefix— where snapshots land.create.enabled— initialize the repository if missing. Creation-time algorithms are fixed forever — see creation.moverDefaults.cache— mover cache sizing (movers).
As a ClusterRepository¶
The same backend.b2 stanza works on a cluster-scoped
ClusterRepository; every
Secret reference must carry an explicit namespace: and the credential Secret
must exist where the movers run — see Movers.
Try it end-to-end¶
Prove this backend really takes a backup. The same example file carries a tiny
smoke-test (a throwaway PVC + a SnapshotPolicy + a Snapshot) that targets the
b2-primary repository above, so you can go from "applied" to "a snapshot in my
bucket" in one arc.
Fill in the credentials first
The smoke backup only goes green once B2_KEY_ID/B2_KEY in the Secret are a
real application key. With the REPLACE_ME placeholders the Repository
stalls at Failed (kopia can't reach the bucket) and the Snapshot stays
Pending.
1. Apply the bundle (namespace backups, Secret, Repository, and the
smoke-test objects):
2. Wait for the repository to be Ready — the gate everything else waits on:
$ kubectl -n backups wait --for=condition=Ready repository/b2-primary --timeout=2m
repository.kopiur.home-operations.com/b2-primary condition met
3. Take the smoke backup. The Snapshot uses generateName, so create it
(the namespace, Secret, Repository, PVC, and policy already exist and report
unchanged — the Snapshot is the one new object):
$ kubectl create -f deploy/examples/backends/b2.yaml
snapshot.kopiur.home-operations.com/smoke-now-abc12 created
4. Watch it succeed:
$ kubectl -n backups get snapshots -w
NAME PHASE ORIGIN SNAPSHOT AGE
smoke-now-abc12 Pending manual 2s
smoke-now-abc12 Running manual 7s
smoke-now-abc12 Succeeded manual k1f1ec0a8 38s
(Output illustrative.) The Snapshot has no fixed Succeeded condition; to
wait on it in a script, key on the phase:
$ kubectl -n backups wait --for=jsonpath='{.status.phase}'=Succeeded \
snapshot/smoke-now-abc12 --timeout=5m
5. Deep proof — the data really moved. status.stats shows non-zero
bytesNew/filesNew, and status.snapshot.kopiaSnapshotID is the kopia
snapshot ID in your bucket:
$ kubectl -n backups get snapshot smoke-now-abc12 -o jsonpath='{.status.stats}'
{"sizeBytes":4096,"bytesNew":1280,"filesNew":2,"filesUnchanged":0}
$ kubectl -n backups get snapshot smoke-now-abc12 -o jsonpath='{.status.snapshot.kopiaSnapshotID}'
k1f1ec0a8
(Both outputs illustrative — sizes and the ID vary.) Non-zero bytesNew is the
proof the backup uploaded real content to B2.
6. Clean up the smoke-test when you're done:
$ kubectl -n backups delete snapshot --all # finalizer also deletes the kopia snapshot
$ kubectl -n backups delete snapshotpolicy smoke
$ kubectl -n backups delete pvc smoke-data
Deleting a Snapshot deletes its snapshot
A produced Snapshot defaults to deletionPolicy: Delete, so removing the CR
runs kopia snapshot delete via a finalizer. Use Retain (or Orphan) to keep
the data — see Backups → deletionPolicy.
From here the full lifecycle is backend-independent — only the Repository
differs. Put it on a cron with a SnapshotSchedule
(Backups & schedules,
Example 01) and restore by
picking a Snapshot (Restores,
Example 03).
Troubleshooting¶
Scope the application key to the bucket
A bucket-scoped key limits blast radius and is what kopia needs. The master key works but grants account-wide access — avoid it for a backup mover.
unauthorized/401— wrongB2_KEY_ID/B2_KEY, or the key isn't scoped to this bucket. Regenerate a key for this bucket. Note the ID/key pair must match — a fresh key under the old key ID fails the same way.- Maintenance fails, backups work — the application key is missing
deleteFiles(orlistBuckets). Recreate it with the full capability list above.
See also¶
- Repositories & backends — concepts: scope, encryption, creation.
- Movers, RBAC & credentials — where the credential Secret must live.
- Sibling backends: S3 (incl. B2's S3 API) · Azure · GCS.