Skip to content

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, meaning a keyID and applicationKey pair. 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, which is the second value, is shown once, at creation. Copy it straight into the Secret.

deleteFiles is required, because retention and maintenance delete expired blobs. 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, which this page covers, 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 when 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 its credentials together.

The Secret shape

The mover loads this Secret with envFrom, so 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 it cannot be recovered if you lose it. 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, as in 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 to federate with, so its auth is Secret-only. There is no workloadIdentity here, unlike S3, Azure, and GCS.

A stray auth.workloadIdentity on a B2 backend is not rejected. The API server silently prunes it, because it isn't in the schema.

Customization — the values you actually change

  • bucket and prefix set where snapshots land.
  • create.enabled initializes the repository if it's missing. The creation-time algorithms are fixed forever. See creation.
  • moverDefaults.cache sizes the mover cache. See movers.

As a ClusterRepository

The same backend.b2 stanza works on a cluster-scoped ClusterRepository, with two requirements. Every Secret reference must carry an explicit namespace:, and the Secret must exist in the namespaces the movers run in. 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, and a Snapshot, all pointed at the b2-primary repository above. It takes you from "applied" to "a snapshot in my bucket" in one go.

Fill in the credentials first

The smoke backup only goes green once B2_KEY_ID and B2_KEY in the Secret are a real application key. With the REPLACE_ME placeholders the Repository stalls at Failed, because kopia can't reach the bucket, and the Snapshot stays Pending.

1. Apply the bundle. That is the backups namespace, the Secret, the Repository, and the smoke-test objects:

$ kubectl apply -f deploy/examples/backends/b2.yaml

2. Wait for the repository to be Ready. Everything else waits on this:

$ 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 rather than apply 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

The output above is illustrative. The Snapshot has no fixed Succeeded condition, so 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. Prove the data really moved. status.stats shows non-zero bytesNew and 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 are illustrative; sizes and the ID vary. Non-zero bytesNew proves 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 through a finalizer. Use Retain or Orphan to keep the data. See Backups → deletionPolicy.

From here the rest of the lifecycle is the same on every backend. Only the Repository differs. Put it on a cron with a SnapshotSchedule, described in Backups & schedules and Example 01. Restore by picking a Snapshot, described in Restores and Example 03.

Troubleshooting

Scope the application key to the bucket

A bucket-scoped key limits the blast radius, and it is all kopia needs. The master key works, but it grants account-wide access. Avoid it for a backup mover.

  • unauthorized or 401. Either B2_KEY_ID and B2_KEY are wrong, or the key isn't scoped to this bucket. Regenerate a key for this bucket. The ID and key must be a matching pair; a fresh key under the old key ID fails the same way.
  • Maintenance fails, backups work. The application key is missing deleteFiles, or listBuckets. Recreate it with the full capability list above.

See also