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.
Provider prerequisites¶
- 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 storage-account access key, under
Creating the container and credential with the Azure CLI
$ az storage container create \
--account-name mystorageacct --name kopia-backups
# Option A — the account access key (full-account access):
$ az storage account keys list \
--account-name mystorageacct --query "[0].value" -o tsv
# Option B — a container-scoped SAS token (least privilege; note the expiry):
$ az storage container generate-sas \
--account-name mystorageacct --name kopia-backups \
--permissions racwdl --expiry 2027-06-12 -o tsv
The SAS --permissions must include read, add, create, write, delete, and list, which is racwdl. kopia lists and deletes blobs during retention and maintenance, not just at backup time.
Paste the CLI output exactly as it comes out. It has no leading ?.
The Secret shape¶
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"
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: 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
Fields reference (backend.azure)¶
| 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¶
containerandprefixset where snapshots land.storageAccountis 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.enabledinitializes the repository if it's missing. The creation-timeencryption,splitter, andhashvalues are fixed forever. See creation.
SAS-token auth (least privilege)¶
A SAS token scoped to the container is time-limited, and it avoids handing the mover the full account key:
# 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
Workload identity (AKS)¶
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:
- A managed identity, or an app registration, with
Storage Blob Data Contributoron the container, plus a federated credential forsystem:serviceaccount:<namespace>:<sa-name>. - A ServiceAccount annotated
azure.workload.identity/client-id: <id>, present in every namespace mover Jobs run in. auth.workloadIdentity.serviceAccountNameon the backend. Setting it makesstorageAccountrequired, and the webhook enforces that. The identity webhook injects the tenant, client id, and token, but not the account name.
# 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
Replication: don't mix static and workload-identity Azure pairs
A RepositoryReplication between two Azure backends must use the same auth style on both sides. If both use workloadIdentity, they must name the same ServiceAccount.
A mixed pair is rejected at admission. The replication pod's environment would carry the static side's AZURE_* credentials, and the federated side's environment-driven flags would pick them up.
As a ClusterRepository¶
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.
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 azure-primary repository above. It takes you from "applied" to "a snapshot in my container" in one go.
Fill in the credentials first
The smoke backup only goes green once the REPLACE_ME value in the Secret is a real storage key or SAS token. With placeholders the Repository stalls at Failed, because kopia can't reach the container, and the Snapshot stays Pending.
1. Apply the bundle. That is the backups namespace, the Secret, the Repository, and the smoke-test objects:
2. Wait for the repository to be Ready. Everything else waits on this:
$ 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:
$ kubectl create -f deploy/examples/backends/azure.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 container:
$ 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:
$ 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¶
Provide exactly one credential
Setting both AZURE_STORAGE_KEY and AZURE_STORAGE_SAS_TOKEN is ambiguous. Provide one.
A SAS token must be pasted without a leading ?, and it must grant read, write, list, and delete on the container.
SAS tokens expire, and take your backups offline with them
A SAS token carries an expiry, shown as se= in the token. When it lapses, every mover run starts failing with AuthenticationFailed, even though nothing in the cluster changed.
Pick an expiry you'll actually rotate before, put the rotation in your calendar, and update the Secret in place. The operator watches the Secret and re-verifies the repository without you touching the Repository object.
AuthenticationFailed. The key is wrong, the SAS token has expired, or the SAS is scoped to the wrong container. Check these=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, orstorageAccountis unset. A SAS token doesn't encode the account name.
See also¶
- Object lock (ransomware protection):
spec.parameters.blobRetentionworks on Azure Blob too. The S3 page documents it in full. - Repositories & backends: the concepts, meaning scope, encryption, and creation.
- Movers, RBAC & credentials: where the credential Secret must live.
- Sibling backends: S3 · GCS · B2.