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 (
AZURE_STORAGE_KEY), or - a SAS token (
AZURE_STORAGE_SAS_TOKEN) scoped to the container — least privilege.
- the storage-account access key (
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 (racwdl) — kopia lists and deletes blobs during
retention and maintenance, not just at backup time. Paste
the CLI output as-is: it has no leading ?.
The Secret shape¶
Loaded with envFrom; 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 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 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: 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. In practice set it always: 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 (user-created, Entra-federated) ServiceAccount instead of a key/SAS — see Workload identity. Requires storageAccount. |
¹ Set exactly one of auth.secretRef or auth.workloadIdentity
(webhook-enforced). auth itself may be omitted when the AZURE_* key rides
the encryption-password Secret.
Customization — the values you actually change¶
container/prefix— where snapshots land.storageAccount— usually required; SAS tokens don't encode the account.- Key vs. SAS — switch by which Secret key you set (see the SAS variant below).
create.enabled— initialize the repository if missing. Creation-timeencryption/splitter/hashare fixed forever — see creation.
SAS-token auth (least privilege)¶
A SAS token scoped to the container, time-limited, 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 injects
AZURE_TENANT_ID / AZURE_CLIENT_ID / 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 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 — andstorageAccountbecomes required (webhook-enforced): 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 (or both workloadIdentity with the same ServiceAccount) —
a mixed pair is rejected at admission, because the replication pod's env would
carry the static side's AZURE_* credentials where the federated side's
env-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 + a Snapshot) that targets the
azure-primary repository above, so you can go from "applied" to "a snapshot in
my container" in one arc.
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 (kopia can't reach the container) 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/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
(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
(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 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 illustrative — sizes and the ID vary.) Non-zero bytesNew is the
proof 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 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¶
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 must grant
read/write/list/delete on the container.
SAS tokens expire — and take your backups offline with them
A SAS token carries an expiry (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— wrong key, expired SAS token (check these=timestamp inside the token), or a SAS scoped to the wrong container. Regenerate scoped to this container.ContainerNotFound— create the container first; Kopiur won't.- Works with the key, fails with SAS — the SAS is missing a permission
(needs
racwdl) orstorageAccountis unset; a SAS token doesn't encode the account name.
See also¶
- Repositories & backends — concepts: scope, encryption, creation.
- Movers, RBAC & credentials — where the credential Secret must live.
- Sibling backends: S3 · GCS · B2.