Skip to content

Azure Blob Storage

The Azure backend stores the kopia repository in an Azure Blob Storage container. Reach for it when your storage is Azure. For an S3-compatible store, use S3 instead.

  • A storage account and a blob container within it. Kopiur does not create them.
  • A credential for that container, exactly one of:
    • the storage-account access key, under AZURE_STORAGE_KEY; or
    • a SAS token, under AZURE_STORAGE_SAS_TOKEN, scoped to the container. This is the least-privilege option.

The mover loads this Secret with envFrom, so the keys reach kopia as environment variables.

Secret key Required What it is
AZURE_STORAGE_KEY one of¹ The storage-account access key.
AZURE_STORAGE_SAS_TOKEN one of¹ A SAS token scoped to the container (no leading ?).
KOPIA_PASSWORD yes The repository encryption password.

¹ Provide exactly one of the key or the SAS token. kopia uses whichever one is set.

stringData:
AZURE_STORAGE_KEY: "REPLACE_ME" # OR AZURE_STORAGE_SAS_TOKEN, not both
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: v1
kind: Secret
metadata:
name: azure-repo-creds
namespace: backups
type: Opaque
stringData:
# Provide EXACTLY ONE of these two (kopia picks up whichever is set):
AZURE_STORAGE_KEY: "REPLACE_ME" # the storage-account access key, OR ...
# AZURE_STORAGE_SAS_TOKEN: "REPLACE_ME" # ... a SAS token scoped to the container
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: azure-primary
namespace: backups
spec:
backend:
azure:
container: kopia-backups # the blob container holding the repository
prefix: prod/ # optional blob-name prefix within the container
storageAccount: mystorageacct # account name (when not inferred from creds)
auth:
secretRef:
name: azure-repo-creds
encryption:
passwordSecretRef:
name: azure-repo-creds
key: KOPIA_PASSWORD
create:
enabled: true
Field Required Default Example What it controls
container yes — kopia-backups The blob container holding the repository. The container name only: not a URL, not account/container.
prefix no container root prod/ Blob-name prefix so several repos can share one container. End it with /.
storageAccount no inferred mystorageacct Account name. Set it always in practice. SAS tokens don’t carry it, and being explicit costs nothing with a key.
auth.secretRef no¹ — { name: azure-repo-creds } Names the credential Secret above. Same namespace as the Repository; a ClusterRepository adds namespace:. Mutually exclusive with workloadIdentity.
auth.workloadIdentity.serviceAccountName no¹ — backup-mover Run the mover Jobs as this ServiceAccount instead of a key or SAS token. You create it and federate it with Entra. See Workload identity. Requires storageAccount.

¹ Set exactly one of auth.secretRef or auth.workloadIdentity. The webhook enforces this. You may omit auth entirely when the AZURE_* key lives in the encryption-password Secret.

Customization — the values you actually change

Section titled “Customization — the values you actually change”
  • container and prefix set where snapshots land.
  • storageAccount is usually required, because SAS tokens don’t encode the account.
  • Key vs. SAS. You switch between them by which Secret key you set. See the SAS variant below.
  • create.enabled initializes the repository if it’s missing. The creation-time encryption, splitter, and hash values are fixed forever. See creation.

A SAS token scoped to the container is time-limited, and it avoids handing the mover the full account key:

deploy/examples/backends/azure-sas.yaml
# Backend: Azure Blob Storage — SAS-token auth
#
# A variant of the Azure backend that authenticates with a SAS (shared access
# signature) token scoped to the container, instead of the storage-account key.
# Provide EXACTLY ONE of AZURE_STORAGE_KEY or AZURE_STORAGE_SAS_TOKEN — kopia uses
# whichever is set. A scoped, time-limited SAS token grants least privilege.
#
# Field shapes verified against crates/api: externally-tagged backend
# (`backend.azure`).
---
apiVersion: v1
kind: Secret
metadata:
name: azure-sas-repo-creds
namespace: backups
type: Opaque
stringData:
# The SAS token, WITHOUT a leading '?'. Generate one scoped to the container
# with read/write/list/delete (Blob), e.g. in the portal or:
# az storage container generate-sas --account-name mystorageacct \
# --name kopia-backups --permissions racwdl --expiry 2027-01-01 -o tsv
AZURE_STORAGE_SAS_TOKEN: "REPLACE_ME"
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: azure-sas-primary
namespace: backups
spec:
backend:
azure:
container: kopia-backups # the blob container holding the repository
prefix: prod/ # optional blob-name prefix within the container
storageAccount: mystorageacct # account name (SAS tokens do not encode it)
auth:
secretRef:
name: azure-sas-repo-creds
encryption:
passwordSecretRef:
name: azure-sas-repo-creds
key: KOPIA_PASSWORD
create:
enabled: true

On AKS with the workload-identity add-on, or any cluster running the azure-workload-identity webhook, you can drop the storage key and SAS token entirely.

Set auth.workloadIdentity.serviceAccountName and every mover Job runs as that ServiceAccount. Kopiur stamps the mover pods with the azure.workload.identity/use: "true" label. The Azure webhook then injects AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_FEDERATED_TOKEN_FILE, and kopia authenticates with the federated token. The only secret left in the cluster is KOPIA_PASSWORD.

What you provide:

  1. A managed identity, or an app registration, with Storage Blob Data Contributor on the container, plus a federated credential for system:serviceaccount:<namespace>:<sa-name>.
  2. A ServiceAccount annotated azure.workload.identity/client-id: <id>, present in every namespace mover Jobs run in.
  3. auth.workloadIdentity.serviceAccountName on the backend. Setting it makes storageAccount required, and the webhook enforces that. The identity webhook injects the tenant, client id, and token, but not the account name.
deploy/examples/backends/azure-workload-identity.yaml
# Backend: Azure Blob with workload identity — AKS Workload Identity
#
# No storage key, no SAS token: the mover Jobs run as a ServiceAccount federated
# to a Microsoft Entra application/managed identity. Kopiur labels the mover
# pods `azure.workload.identity/use: "true"`, so the azure-workload-identity
# webhook injects AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_FEDERATED_TOKEN_FILE
# — exactly the env kopia's azure backend reads.
#
# Prerequisites (Azure side):
# - AKS with the workload-identity add-on (or the azure-workload-identity
# webhook installed),
# - a managed identity / app registration with `Storage Blob Data Contributor`
# on the container, federated to this ServiceAccount
# (`az identity federated-credential create ...`).
#
# NOTE: `storageAccount` is REQUIRED with workloadIdentity (webhook-enforced):
# the identity webhook injects the tenant/client/token, not the account name.
#
# Field shapes verified against crates/api: externally-tagged backend
# (`backend.azure`).
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: backup-mover
namespace: backups
annotations:
# The client id of the federated managed identity / app registration.
azure.workload.identity/client-id: 00000000-0000-0000-0000-REPLACE_ME
---
apiVersion: v1
kind: Secret
metadata:
name: azure-wi-repo-creds
namespace: backups
type: Opaque
stringData:
# Workload identity replaces AZURE_STORAGE_KEY/SAS — the repository
# encryption password is still required.
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: azure-wi
namespace: backups
spec:
backend:
azure:
container: kopia-backups
storageAccount: mystorageacct # REQUIRED with workloadIdentity
auth:
workloadIdentity:
serviceAccountName: backup-mover
encryption:
passwordSecretRef:
name: azure-wi-repo-creds
key: KOPIA_PASSWORD
create:
enabled: true

The same backend.azure stanza works on a cluster-scoped ClusterRepository; every Secret reference must carry an explicit namespace: and the Secret must be present where the movers run. See Movers.

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 azure-primary repository above. It takes you from “applied” to “a snapshot in my container” in one go.

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

Terminal window
$ kubectl apply -f deploy/examples/backends/azure.yaml

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

Terminal window
$ kubectl -n backups wait --for=condition=Ready repository/azure-primary --timeout=2m
repository.kopiur.home-operations.com/azure-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:

Terminal window
$ kubectl create -f deploy/examples/backends/azure.yaml
snapshot.kopiur.home-operations.com/smoke-now-abc12 created

4. Watch it succeed:

Terminal window
$ 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:

Terminal window
$ 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 container:

Terminal window
$ 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 Azure Blob.

6. Clean up the smoke-test when you’re done:

Terminal window
$ 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

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.

  • AuthenticationFailed. The key is wrong, the SAS token has expired, or the SAS is scoped to the wrong container. Check the se= timestamp inside the token. Regenerate the SAS scoped to this container.
  • ContainerNotFound. Create the container first. Kopiur won’t.
  • Works with the key, fails with SAS. Either the SAS is missing a permission, and it needs racwdl, or storageAccount is unset. A SAS token doesn’t encode the account name.