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.

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.

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-time encryption/splitter/hash are 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:

  1. A managed identity (or 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 — and storageAccount becomes 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):

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

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 the se= 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) or storageAccount is unset; a SAS token doesn't encode the account name.

See also