Repository: kopia/kopia
Pull request #3407 (2023-10-26 13:41:14)

feat(repository): Add Azure PIT support
Adds support for Azure PIT


Patch:
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
index 3fb959ed..95bc5b99 100644
--- a/.github/workflows/auto-merge.yml
+++ b/.github/workflows/auto-merge.yml
@@ -7,7 +7,7 @@ jobs:
   auto-merge:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       - uses: ahmadnassri/action-dependabot-auto-merge@v2
         with:
           # auto-merge rules are in /.github/auto-merge.yml
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index 48e9661f..ca2c8053 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index e7575098..d0c842c8 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -15,6 +15,6 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: 'Checkout Repository'
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       - name: 'Dependency Review'
         uses: actions/dependency-review-action@v3
diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml
index 80928eda..c55d634f 100644
--- a/.github/workflows/license-check.yml
+++ b/.github/workflows/license-check.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index cf514cca..97d4a4cb 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -26,7 +26,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml
index 85db303f..bec706cf 100644
--- a/.github/workflows/make.yml
+++ b/.github/workflows/make.yml
@@ -40,7 +40,7 @@ jobs:
     continue-on-error: ${{ contains(matrix.os, 'self-hosted') }}
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
@@ -138,7 +138,7 @@ jobs:
     needs: build
     if: github.event_name != 'pull_request' && github.repository == 'kopia/kopia'
     steps:
-    - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
     - name: Set up QEMU
       uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
     - name: Set up Docker Buildx
diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml
index ea6a6a9f..1ea5746d 100644
--- a/.github/workflows/ossf-scorecard.yml
+++ b/.github/workflows/ossf-scorecard.yml
@@ -26,7 +26,7 @@ jobs:
     steps:
       -
         name: "Checkout repo"
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           persist-credentials: false
       -
@@ -39,7 +39,7 @@ jobs:
       -
         # Upload the results to GitHub's code scanning dashboard.
         name: "Upload to results to dashboard"
-        uses: github/codeql-action/upload-sarif@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4
+        uses: github/codeql-action/upload-sarif@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
         with:
           sarif_file: results.sarif
       -
diff --git a/.github/workflows/providers-core.yml b/.github/workflows/providers-core.yml
index f7eb0bd2..335fa8ac 100644
--- a/.github/workflows/providers-core.yml
+++ b/.github/workflows/providers-core.yml
@@ -24,7 +24,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
         ref: ${{ github.event.inputs.ref_name || github.ref }}
@@ -43,6 +43,10 @@ jobs:
         KOPIA_AZURE_TEST_STORAGE_ACCOUNT: ${{ secrets.KOPIA_AZURE_TEST_STORAGE_ACCOUNT }}
         KOPIA_AZURE_TEST_STORAGE_KEY: ${{ secrets.KOPIA_AZURE_TEST_STORAGE_KEY }}
         KOPIA_AZURE_TEST_SAS_TOKEN:  ${{ secrets.KOPIA_AZURE_TEST_SAS_TOKEN }}
+        KOPIA_AZURE_TEST_IMMUTABLE_CONTAINER: ${{ secrets.KOPIA_AZURE_TEST_IMMUTABLE_CONTAINER }}
+        KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_ACCOUNT: ${{ secrets.KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_ACCOUNT }}
+        KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_KEY: ${{ secrets.KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_KEY }}
+        KOPIA_AZURE_TEST_IMMUTABLE_SAS_TOKEN:  ${{ secrets.KOPIA_AZURE_TEST_IMMUTABLE_SAS_TOKEN }}
     - name: GCS
       run: make provider-tests PROVIDER_TEST_TARGET=gcs
       env:
diff --git a/.github/workflows/providers-extra.yml b/.github/workflows/providers-extra.yml
index 61ccc899..0033cad3 100644
--- a/.github/workflows/providers-extra.yml
+++ b/.github/workflows/providers-extra.yml
@@ -24,7 +24,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
         ref: ${{ github.event.inputs.ref_name || github.ref }}
diff --git a/.github/workflows/race-detector.yml b/.github/workflows/race-detector.yml
index e2098497..d73e2432 100644
--- a/.github/workflows/race-detector.yml
+++ b/.github/workflows/race-detector.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/app/package-lock.json b/app/package-lock.json
index 1aa55756..661cc320 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -11,7 +11,7 @@
       "dependencies": {
         "auto-launch": "^5.0.6",
         "electron-is-dev": "^2.0.0",
-        "electron-log": "^4.4.8",
+        "electron-log": "^5.0.0",
         "electron-store": "^8.1.0",
         "electron-updater": "^6.1.4",
         "minimist": "^1.2.8",
@@ -20,11 +20,11 @@
       },
       "devDependencies": {
         "@electron/notarize": "^2.1.0",
-        "@playwright/test": "^1.38.1",
+        "@playwright/test": "^1.39.0",
         "asar": "^3.2.0",
-        "concurrently": "^8.2.1",
+        "concurrently": "^8.2.2",
         "dotenv": "^16.3.1",
-        "electron": "^26.2.4",
+        "electron": "^27.0.3",
         "electron-builder": "^24.6.4",
         "electron-store": "^8.1.0",
         "playwright": "^1.37.1",
@@ -353,12 +353,12 @@
       }
     },
     "node_modules/@playwright/test": {
-      "version": "1.38.1",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
-      "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
+      "version": "1.39.0",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz",
+      "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==",
       "dev": true,
       "dependencies": {
-        "playwright": "1.38.1"
+        "playwright": "1.39.0"
       },
       "bin": {
         "playwright": "cli.js"
@@ -1193,9 +1193,9 @@
       "dev": true
     },
     "node_modules/concurrently": {
-      "version": "8.2.1",
-      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.1.tgz",
-      "integrity": "sha512-nVraf3aXOpIcNud5pB9M82p1tynmZkrSGQ1p6X/VY8cJ+2LMVqAgXsJxYYefACSHbTYlm92O1xuhdGTjwoEvbQ==",
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
+      "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
       "dev": true,
       "dependencies": {
         "chalk": "^4.1.2",
@@ -1583,9 +1583,9 @@
       }
     },
     "node_modules/electron": {
-      "version": "26.2.4",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-26.2.4.tgz",
-      "integrity": "sha512-weMUSMyDho5E0DPQ3breba3D96IxwNvtYHjMd/4/wNN3BdI5s3+0orNnPVGJFcLhSvKoxuKUqdVonUocBPwlQA==",
+      "version": "27.0.3",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-27.0.3.tgz",
+      "integrity": "sha512-VaB9cI1se+mUtz366NP+zxFVnkHLbCBNO4wwouw3FuGyX/m7/Bv1I89JhWOBv78tC+n11ZYMrVD23Jf6EZgVcg==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
@@ -1670,9 +1670,13 @@
       }
     },
     "node_modules/electron-log": {
-      "version": "4.4.8",
-      "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.8.tgz",
-      "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA=="
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.0.0.tgz",
+      "integrity": "sha512-vB3akupmQvA8jAyNL9rULZtf6WoP8vsabjXsRtiqXS6/D37SwN/4LEyj4JD+9Bv6xoTcx/LrVnsIKEEWdq5ClQ==",
+      "engines": {
+        "electron": ">= 13",
+        "node": ">= 14"
+      }
     },
     "node_modules/electron-publish": {
       "version": "24.5.0",
@@ -2785,12 +2789,12 @@
       }
     },
     "node_modules/playwright": {
-      "version": "1.38.1",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
-      "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
+      "version": "1.39.0",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz",
+      "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==",
       "dev": true,
       "dependencies": {
-        "playwright-core": "1.38.1"
+        "playwright-core": "1.39.0"
       },
       "bin": {
         "playwright": "cli.js"
@@ -2803,9 +2807,9 @@
       }
     },
     "node_modules/playwright-core": {
-      "version": "1.38.1",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
-      "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
+      "version": "1.39.0",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz",
+      "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==",
       "dev": true,
       "bin": {
         "playwright-core": "cli.js"
@@ -3780,12 +3784,12 @@
       }
     },
     "@playwright/test": {
-      "version": "1.38.1",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
-      "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
+      "version": "1.39.0",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz",
+      "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==",
       "dev": true,
       "requires": {
-        "playwright": "1.38.1"
+        "playwright": "1.39.0"
       }
     },
     "@sindresorhus/is": {
@@ -4439,9 +4443,9 @@
       "dev": true
     },
     "concurrently": {
-      "version": "8.2.1",
-      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.1.tgz",
-      "integrity": "sha512-nVraf3aXOpIcNud5pB9M82p1tynmZkrSGQ1p6X/VY8cJ+2LMVqAgXsJxYYefACSHbTYlm92O1xuhdGTjwoEvbQ==",
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
+      "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
       "dev": true,
       "requires": {
         "chalk": "^4.1.2",
@@ -4723,9 +4727,9 @@
       }
     },
     "electron": {
-      "version": "26.2.4",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-26.2.4.tgz",
-      "integrity": "sha512-weMUSMyDho5E0DPQ3breba3D96IxwNvtYHjMd/4/wNN3BdI5s3+0orNnPVGJFcLhSvKoxuKUqdVonUocBPwlQA==",
+      "version": "27.0.3",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-27.0.3.tgz",
+      "integrity": "sha512-VaB9cI1se+mUtz366NP+zxFVnkHLbCBNO4wwouw3FuGyX/m7/Bv1I89JhWOBv78tC+n11ZYMrVD23Jf6EZgVcg==",
       "dev": true,
       "requires": {
         "@electron/get": "^2.0.0",
@@ -4787,9 +4791,9 @@
       "integrity": "sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA=="
     },
     "electron-log": {
-      "version": "4.4.8",
-      "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.8.tgz",
-      "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA=="
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.0.0.tgz",
+      "integrity": "sha512-vB3akupmQvA8jAyNL9rULZtf6WoP8vsabjXsRtiqXS6/D37SwN/4LEyj4JD+9Bv6xoTcx/LrVnsIKEEWdq5ClQ=="
     },
     "electron-publish": {
       "version": "24.5.0",
@@ -5640,19 +5644,19 @@
       }
     },
     "playwright": {
-      "version": "1.38.1",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
-      "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
+      "version": "1.39.0",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz",
+      "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==",
       "dev": true,
       "requires": {
         "fsevents": "2.3.2",
-        "playwright-core": "1.38.1"
+        "playwright-core": "1.39.0"
       }
     },
     "playwright-core": {
-      "version": "1.38.1",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
-      "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
+      "version": "1.39.0",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz",
+      "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==",
       "dev": true
     },
     "plist": {
diff --git a/app/package.json b/app/package.json
index 335d108f..eeeff7a2 100644
--- a/app/package.json
+++ b/app/package.json
@@ -5,7 +5,7 @@
   "dependencies": {
     "auto-launch": "^5.0.6",
     "electron-is-dev": "^2.0.0",
-    "electron-log": "^4.4.8",
+    "electron-log": "^5.0.0",
     "electron-updater": "^6.1.4",
     "electron-store": "^8.1.0",
     "minimist": "^1.2.8",
@@ -112,11 +112,11 @@
   },
   "devDependencies": {
     "@electron/notarize": "^2.1.0",
-    "@playwright/test": "^1.38.1",
+    "@playwright/test": "^1.39.0",
     "asar": "^3.2.0",
-    "concurrently": "^8.2.1",
+    "concurrently": "^8.2.2",
     "dotenv": "^16.3.1",
-    "electron": "^26.2.4",
+    "electron": "^27.0.3",
     "electron-store": "^8.1.0",
     "electron-builder": "^24.6.4",
     "playwright": "^1.37.1",
diff --git a/cli/command_repository_set_parameters.go b/cli/command_repository_set_parameters.go
index d9b76ea2..e7204259 100644
--- a/cli/command_repository_set_parameters.go
+++ b/cli/command_repository_set_parameters.go
@@ -228,7 +228,8 @@ func (c *commandRepositorySetParameters) run(ctx context.Context, rep repo.Direc
 	requiredFeatures = c.addRemoveUpdateRequiredFeatures(requiredFeatures, &anyChange)
 
 	if !anyChange {
-		return errors.Errorf("no changes")
+		log(ctx).Info("no changes")
+		return nil
 	}
 
 	if blobcfg.IsRetentionEnabled() {
diff --git a/cli/storage_azure.go b/cli/storage_azure.go
index 7fd1a921..1c9ec017 100644
--- a/cli/storage_azure.go
+++ b/cli/storage_azure.go
@@ -2,8 +2,10 @@
 
 import (
 	"context"
+	"time"
 
 	"github.com/alecthomas/kingpin/v2"
+	"github.com/pkg/errors"
 
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/blob/azure"
@@ -25,11 +27,32 @@ func (c *storageAzureFlags) Setup(svc StorageProviderServices, cmd *kingpin.CmdC
 	cmd.Flag("client-secret", "Azure service principle client secret (overrides AZURE_CLIENT_SECRET environment variable)").Envar(svc.EnvName("AZURE_CLIENT_SECRET")).StringVar(&c.azOptions.ClientSecret)
 
 	commonThrottlingFlags(cmd, &c.azOptions.Limits)
+
+	var pointInTimeStr string
+
+	pitPreAction := func(pc *kingpin.ParseContext) error {
+		if pointInTimeStr != "" {
+			t, err := time.Parse(time.RFC3339, pointInTimeStr)
+			if err != nil {
+				return errors.Wrap(err, "invalid point-in-time argument")
+			}
+
+			c.azOptions.PointInTime = &t
+		}
+
+		return nil
+	}
+
+	cmd.Flag("point-in-time", "Use a point-in-time view of the storage repository when supported").PlaceHolder(time.RFC3339).PreAction(pitPreAction).StringVar(&pointInTimeStr)
 }
 
 func (c *storageAzureFlags) Connect(ctx context.Context, isCreate bool, formatVersion int) (blob.Storage, error) {
 	_ = formatVersion
 
+	if isCreate && c.azOptions.PointInTime != nil && !c.azOptions.PointInTime.IsZero() {
+		return nil, errors.New("Cannot specify a 'point-in-time' option when creating a repository")
+	}
+
 	//nolint:wrapcheck
 	return azure.New(ctx, &c.azOptions, isCreate)
 }
diff --git a/cli/storage_s3.go b/cli/storage_s3.go
index 7522851d..d3a74f78 100644
--- a/cli/storage_s3.go
+++ b/cli/storage_s3.go
@@ -49,8 +49,8 @@ func (c *storageS3Flags) Setup(svc StorageProviderServices, cmd *kingpin.CmdClau
 
 	cmd.Flag("point-in-time", "Use a point-in-time view of the storage repository when supported").PlaceHolder(time.RFC3339).PreAction(pitPreAction).StringVar(&pointInTimeStr)
 
-	cmd.Flag("root-ca-pem-base64", "Certficate authority in-line (base64 enc.)").Envar(svc.EnvName("ROOT_CA_PEM_BASE64")).PreAction(c.preActionLoadPEMBase64).StringVar(&c.rootCaPemBase64)
-	cmd.Flag("root-ca-pem-path", "Certficate authority file path").PreAction(c.preActionLoadPEMPath).StringVar(&c.rootCaPemPath)
+	cmd.Flag("root-ca-pem-base64", "Certificate authority in-line (base64 enc.)").Envar(svc.EnvName("ROOT_CA_PEM_BASE64")).PreAction(c.preActionLoadPEMBase64).StringVar(&c.rootCaPemBase64)
+	cmd.Flag("root-ca-pem-path", "Certificate authority file path").PreAction(c.preActionLoadPEMPath).StringVar(&c.rootCaPemPath)
 }
 
 func (c *storageS3Flags) preActionLoadPEMPath(_ *kingpin.ParseContext) error {
diff --git a/debug/debug.go b/debug/debug.go
new file mode 100644
index 00000000..6c19c348
--- /dev/null
+++ b/debug/debug.go
@@ -0,0 +1,386 @@
+// Package debug for debug helper functions.
+package debug
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"runtime"
+	"runtime/pprof"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/kopia/kopia/repo/logging"
+)
+
+var log = logging.Module("kopia/debug")
+
+// ProfileName the name of the profile (see: runtime/pprof/Lookup).
+type ProfileName string
+
+const (
+	pair = 2
+	// PPROFDumpTimeout when dumping PPROF data, set an upper bound on the time it can take to log.
+	PPROFDumpTimeout = 15 * time.Second
+)
+
+const (
+	// DefaultDebugProfileRate default sample/data fraction for profile sample collection rates (1/x, where x is the
+	// data fraction sample rate).
+	DefaultDebugProfileRate = 100
+	// DefaultDebugProfileDumpBufferSizeB default size of the pprof output buffer.
+	DefaultDebugProfileDumpBufferSizeB = 1 << 17
+)
+
+const (
+	// EnvVarKopiaDebugPprof environment variable that contains the pprof dump configuration.
+	EnvVarKopiaDebugPprof = "KOPIA_DEBUG_PPROF"
+)
+
+// flags used to configure profiling in EnvVarKopiaDebugPprof.
+const (
+	// KopiaDebugFlagForceGc force garbage collection before dumping heap data.
+	KopiaDebugFlagForceGc = "forcegc"
+	// KopiaDebugFlagDebug value of the profiles `debug` parameter.
+	KopiaDebugFlagDebug = "debug"
+	// KopiaDebugFlagRate rate setting for the named profile (if available). always an integer.
+	KopiaDebugFlagRate = "rate"
+)
+
+const (
+	// ProfileNameBlock block profile key.
+	ProfileNameBlock ProfileName = "block"
+	// ProfileNameMutex mutex profile key.
+	ProfileNameMutex = "mutex"
+	// ProfileNameCPU cpu profile key.
+	ProfileNameCPU = "cpu"
+)
+
+// ProfileConfig configuration flags for a profile.
+type ProfileConfig struct {
+	flags []string
+	buf   *bytes.Buffer
+}
+
+// ProfileConfigs configuration flags for all requested profiles.
+type ProfileConfigs struct {
+	mu sync.Mutex
+
+	// +checklocks:mu
+	pcm map[ProfileName]*ProfileConfig
+}
+
+//nolint:gochecknoglobals
+var pprofConfigs = &ProfileConfigs{}
+
+type pprofSetRate struct {
+	setter       func(int)
+	defaultValue int
+}
+
+//nolint:gochecknoglobals
+var pprofProfileRates = map[ProfileName]pprofSetRate{
+	ProfileNameBlock: {
+		setter:       func(x int) { runtime.SetBlockProfileRate(x) },
+		defaultValue: DefaultDebugProfileRate,
+	},
+	ProfileNameMutex: {
+		setter:       func(x int) { runtime.SetMutexProfileFraction(x) },
+		defaultValue: DefaultDebugProfileRate,
+	},
+}
+
+// GetValue get the value of the named flag, `s`.  False will be returned
+// if the flag does not exist. True will be returned if flag exists without
+// a value.
+func (p ProfileConfig) GetValue(s string) (string, bool) {
+	for _, f := range p.flags {
+		kvs := strings.SplitN(f, "=", pair)
+		if kvs[0] != s {
+			continue
+		}
+
+		if len(kvs) == 1 {
+			return "", true
+		}
+
+		return kvs[1], true
+	}
+
+	return "", false
+}
+
+func parseProfileConfigs(bufSizeB int, ppconfigs string) map[ProfileName]*ProfileConfig {
+	pbs := map[ProfileName]*ProfileConfig{}
+	allProfileOptions := strings.Split(ppconfigs, ":")
+
+	for _, profileOptionWithFlags := range allProfileOptions {
+		// of those, see if any have profile specific settings
+		profileFlagNameValuePairs := strings.SplitN(profileOptionWithFlags, "=", pair)
+		flagValue := ""
+
+		if len(profileFlagNameValuePairs) > 1 {
+			flagValue = profileFlagNameValuePairs[1]
+		}
+
+		flagKey := ProfileName(strings.ToLower(profileFlagNameValuePairs[0]))
+		pbs[flagKey] = newProfileConfig(bufSizeB, flagValue)
+	}
+
+	return pbs
+}
+
+// newProfileConfig create a new profiling configuration.
+func newProfileConfig(bufSizeB int, ppconfig string) *ProfileConfig {
+	q := &ProfileConfig{
+		buf: bytes.NewBuffer(make([]byte, 0, bufSizeB)),
+	}
+
+	flgs := strings.Split(ppconfig, ",")
+	if len(flgs) > 0 && flgs[0] != "" { // len(flgs) > 1 && flgs[0] == "" should never happen
+		q.flags = flgs
+	}
+
+	return q
+}
+
+func setupProfileFractions(ctx context.Context, profileBuffers map[ProfileName]*ProfileConfig) {
+	for k, pprofset := range pprofProfileRates {
+		v, ok := profileBuffers[k]
+		if !ok {
+			// profile not configured - leave it alone
+			continue
+		}
+
+		if v == nil {
+			// profile configured, but no rate - set to default
+			pprofset.setter(pprofset.defaultValue)
+			continue
+		}
+
+		s, _ := v.GetValue(KopiaDebugFlagRate)
+		if s == "" {
+			// flag without an argument - set to default
+			pprofset.setter(pprofset.defaultValue)
+			continue
+		}
+
+		n1, err := strconv.Atoi(s)
+		if err != nil {
+			log(ctx).With("cause", err).Warnf("invalid PPROF rate, %q, for %s: %v", s, k)
+			continue
+		}
+
+		log(ctx).Debugf("setting PPROF rate, %d, for %s", n1, k)
+		pprofset.setter(n1)
+	}
+}
+
+// clearProfileFractions set the profile fractions to their zero values.
+func clearProfileFractions(profileBuffers map[ProfileName]*ProfileConfig) {
+	for k, pprofset := range pprofProfileRates {
+		v := profileBuffers[k]
+		if v == nil { // fold missing values and empty values
+			continue
+		}
+
+		_, ok := v.GetValue(KopiaDebugFlagRate)
+		if !ok { // only care if a value might have been set before
+			continue
+		}
+
+		pprofset.setter(0)
+	}
+}
+
+// StartProfileBuffers start profile buffers for enabled profiles/trace.  Buffers
+// are returned in an slice of buffers: CPU, Heap and trace respectively.  class is used to distinguish profiles
+// external to kopia.
+func StartProfileBuffers(ctx context.Context) {
+	ppconfigs := os.Getenv(EnvVarKopiaDebugPprof)
+	// if empty, then don't bother configuring but emit a log message - use might be expecting them to be configured
+	if ppconfigs == "" {
+		log(ctx).Debug("no profile buffers enabled")
+		return
+	}
+
+	bufSizeB := DefaultDebugProfileDumpBufferSizeB
+
+	// look for matching services.  "*" signals all services for profiling
+	log(ctx).Debug("configuring profile buffers")
+
+	// acquire global lock when performing operations with global side-effects
+	pprofConfigs.mu.Lock()
+	defer pprofConfigs.mu.Unlock()
+
+	pprofConfigs.pcm = parseProfileConfigs(bufSizeB, ppconfigs)
+
+	// profiling rates need to be set before starting profiling
+	setupProfileFractions(ctx, pprofConfigs.pcm)
+
+	// cpu has special initialization
+	v, ok := pprofConfigs.pcm[ProfileNameCPU]
+	if ok {
+		err := pprof.StartCPUProfile(v.buf)
+		if err != nil {
+			log(ctx).With("cause", err).Warn("cannot start cpu PPROF")
+			delete(pprofConfigs.pcm, ProfileNameCPU)
+		}
+	}
+}
+
+// DumpPem dump a PEM version of the byte slice, bs, into writer, wrt.
+func DumpPem(bs []byte, types string, wrt *os.File) error {
+	// err0 for background process
+	var err0 error
+
+	blk := &pem.Block{
+		Type:  types,
+		Bytes: bs,
+	}
+	// wrt is likely a line oriented writer, so writing individual lines
+	// will make best use of output buffer and help prevent overflows or
+	// stalls in the output path.
+	pr, pw := io.Pipe()
+	// encode PEM in the background and output in a line oriented
+	// fashion - this prevents the need for a large buffer to hold
+	// the encoded PEM.
+	go func() {
+		// writer close on exit of background process
+		//nolint:errcheck
+		defer pw.Close()
+		// do the encoding
+		err0 = pem.Encode(pw, blk)
+		if err0 != nil {
+			return
+		}
+	}()
+
+	// connect rdr to pipe reader
+	rdr := bufio.NewReader(pr)
+
+	// err1 for reading
+	// err2 for writing
+	var err1, err2 error
+	for err1 == nil && err2 == nil {
+		var ln []byte
+		ln, err1 = rdr.ReadBytes('\n')
+		// err1 can return ln and non-nil err1, so always call write
+		_, err2 = wrt.Write(ln)
+	}
+
+	// got a write error.  this has precedent
+	if err2 != nil {
+		return fmt.Errorf("could not write PEM: %w", err2)
+	}
+
+	// did not get a read error.  file ends in newline
+	if err1 == nil {
+		return nil
+	}
+
+	// if file does not end in newline, then output one
+	if errors.Is(err1, io.EOF) {
+		_, err2 = wrt.WriteString("\n")
+		if err2 != nil {
+			return fmt.Errorf("could not write PEM: %w", err2)
+		}
+
+		return io.EOF
+	}
+
+	return fmt.Errorf("error reading bytes: %w", err1)
+}
+
+func parseDebugNumber(v *ProfileConfig) (int, error) {
+	debugs, ok := v.GetValue(KopiaDebugFlagDebug)
+	if !ok {
+		return 0, nil
+	}
+
+	debug, err := strconv.Atoi(debugs)
+	if err != nil {
+		return 0, fmt.Errorf("could not parse number %q: %w", debugs, err)
+	}
+
+	return debug, nil
+}
+
+// StopProfileBuffers stop and dump the contents of the buffers to the log as PEMs.  Buffers
+// supplied here are from StartProfileBuffers.
+func StopProfileBuffers(ctx context.Context) {
+	pprofConfigs.mu.Lock()
+	defer pprofConfigs.mu.Unlock()
+
+	if pprofConfigs == nil {
+		log(ctx).Debug("profile buffers not configured")
+		return
+	}
+
+	log(ctx).Debug("saving PEM buffers for output")
+	// cpu and heap profiles requires special handling
+	for k, v := range pprofConfigs.pcm {
+		log(ctx).Debugf("stopping PPROF profile %q", k)
+
+		if v == nil {
+			continue
+		}
+
+		if k == ProfileNameCPU {
+			pprof.StopCPUProfile()
+			continue
+		}
+
+		_, ok := v.GetValue(KopiaDebugFlagForceGc)
+		if ok {
+			log(ctx).Debug("performing GC before PPROF dump ...")
+			runtime.GC()
+		}
+
+		debug, err := parseDebugNumber(v)
+		if err != nil {
+			log(ctx).With("cause", err).Warn("invalid PPROF configuration debug number")
+			continue
+		}
+
+		pent := pprof.Lookup(string(k))
+		if pent == nil {
+			log(ctx).Warnf("no system PPROF entry for %q", k)
+			delete(pprofConfigs.pcm, k)
+
+			continue
+		}
+
+		err = pent.WriteTo(v.buf, debug)
+		if err != nil {
+			log(ctx).With("cause", err).Warn("error writing PPROF buffer")
+
+			continue
+		}
+	}
+	// dump the profiles out into their respective PEMs
+	for k, v := range pprofConfigs.pcm {
+		if v == nil {
+			continue
+		}
+
+		unm := strings.ToUpper(string(k))
+		log(ctx).Infof("dumping PEM for %q", unm)
+
+		err := DumpPem(v.buf.Bytes(), unm, os.Stderr)
+		if err != nil {
+			log(ctx).With("cause", err).Error("cannot write PEM")
+		}
+	}
+
+	// clear the profile rates and fractions to effectively stop profiling
+	clearProfileFractions(pprofConfigs.pcm)
+	pprofConfigs.pcm = map[ProfileName]*ProfileConfig{}
+}
diff --git a/go.mod b/go.mod
index 6ef87626..84bb52c2 100644
--- a/go.mod
+++ b/go.mod
@@ -3,12 +3,12 @@ module github.com/kopia/kopia
 go 1.21
 
 require (
-	cloud.google.com/go/storage v1.33.0
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0
+	cloud.google.com/go/storage v1.35.1
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0
 	github.com/Azure/azure-storage-blob-go v0.15.0
-	github.com/alecthomas/kingpin/v2 v2.3.2
+	github.com/alecthomas/kingpin/v2 v2.4.0
 	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137
 	github.com/chmduquesne/rollinghash v4.0.0+incompatible
 	github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998
@@ -16,24 +16,24 @@ require (
 	github.com/coreos/go-systemd/v22 v22.5.0
 	github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
 	github.com/edsrzf/mmap-go v1.1.0
-	github.com/fatih/color v1.15.0
+	github.com/fatih/color v1.16.0
 	github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c
 	github.com/gofrs/flock v0.8.1
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/golang/protobuf v1.5.3
 	github.com/google/fswalker v0.3.2
 	github.com/google/go-cmp v0.6.0
-	github.com/google/uuid v1.3.1
-	github.com/gorilla/mux v1.8.0
+	github.com/google/uuid v1.4.0
+	github.com/gorilla/mux v1.8.1
 	github.com/hanwen/go-fuse/v2 v2.4.0
 	github.com/hashicorp/cronexpr v1.1.2
-	github.com/klauspost/compress v1.17.2
+	github.com/klauspost/compress v1.17.3
 	github.com/klauspost/pgzip v1.2.6
 	github.com/klauspost/reedsolomon v1.11.8
 	github.com/kopia/htmluibuild v0.0.1-0.20231019063300-75c2a788c7d0
 	github.com/kylelemons/godebug v1.1.0
 	github.com/mattn/go-colorable v0.1.13
-	github.com/minio/minio-go/v7 v7.0.63
+	github.com/minio/minio-go/v7 v7.0.64
 	github.com/natefinch/atomic v1.0.1
 	github.com/pierrec/lz4 v2.6.1+incompatible
 	github.com/pkg/errors v0.9.1
@@ -49,34 +49,34 @@ require (
 	github.com/tg123/go-htpasswd v1.2.1
 	github.com/zalando/go-keyring v0.2.3
 	github.com/zeebo/blake3 v0.2.3
-	go.opentelemetry.io/otel v1.19.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0
-	go.opentelemetry.io/otel/sdk v1.19.0
-	go.opentelemetry.io/otel/trace v1.19.0
+	go.opentelemetry.io/otel v1.21.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
+	go.opentelemetry.io/otel/sdk v1.21.0
+	go.opentelemetry.io/otel/trace v1.21.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.26.0
-	golang.org/x/crypto v0.14.0
+	golang.org/x/crypto v0.16.0
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
-	golang.org/x/mod v0.13.0
-	golang.org/x/net v0.17.0
-	golang.org/x/oauth2 v0.13.0
-	golang.org/x/sync v0.4.0
-	golang.org/x/sys v0.13.0
-	golang.org/x/term v0.13.0
-	golang.org/x/text v0.13.0
-	google.golang.org/api v0.148.0
+	golang.org/x/mod v0.14.0
+	golang.org/x/net v0.19.0
+	golang.org/x/oauth2 v0.15.0
+	golang.org/x/sync v0.5.0
+	golang.org/x/sys v0.15.0
+	golang.org/x/term v0.15.0
+	golang.org/x/text v0.14.0
+	google.golang.org/api v0.152.0
 	google.golang.org/grpc v1.59.0
 	google.golang.org/protobuf v1.31.0
 	gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216
 )
 
 require (
-	cloud.google.com/go v0.110.8 // indirect
-	cloud.google.com/go/compute v1.23.0 // indirect
+	cloud.google.com/go v0.110.10 // indirect
+	cloud.google.com/go/compute v1.23.3 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
-	cloud.google.com/go/iam v1.1.2 // indirect
+	cloud.google.com/go/iam v1.1.5 // indirect
 	github.com/Azure/azure-pipeline-go v0.2.3 // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect
 	github.com/Azure/go-autorest/autorest/adal v0.9.16 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
 	github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
@@ -90,7 +90,7 @@ require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/felixge/fgprof v0.9.3 // indirect
 	github.com/frankban/quicktest v1.13.1 // indirect
-	github.com/go-logr/logr v1.2.4 // indirect
+	github.com/go-logr/logr v1.3.0 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gobwas/httphead v0.1.0 // indirect
 	github.com/gobwas/pool v0.2.1 // indirect
@@ -102,7 +102,7 @@ require (
 	github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
 	github.com/google/readahead v0.0.0-20161222183148-eaceba169032 // indirect
 	github.com/google/s2a-go v0.1.7 // indirect
-	github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
@@ -111,7 +111,7 @@ require (
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-ieproxy v0.0.1 // indirect
-	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
 	github.com/minio/md5-simd v1.1.2 // indirect
 	github.com/minio/sha256-simd v1.0.1 // indirect
@@ -125,14 +125,15 @@ require (
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
 	go.opencensus.io v0.24.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
-	go.opentelemetry.io/otel/metric v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
+	go.opentelemetry.io/otel/metric v1.21.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
+	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect
+	google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 44608519..b821cedf 100644
--- a/go.sum
+++ b/go.sum
@@ -1,22 +1,22 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME=
-cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk=
-cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
-cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
+cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
+cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
+cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
+cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
-cloud.google.com/go/iam v1.1.2 h1:gacbrBdWcoVmGLozRuStX45YKvJtzIjJdAolzUs1sm4=
-cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU=
-cloud.google.com/go/storage v1.33.0 h1:PVrDOkIC8qQVa1P3SXGpQvfuJhN2LHOoyZvWs8D2X5M=
-cloud.google.com/go/storage v1.33.0/go.mod h1:Hhh/dogNRGca7IWv1RC2YqEn0c0G77ctA/OxflYkiD8=
+cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
+cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
+cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w=
+cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
 github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 h1:9kDVnTz3vbfweTqAUmk/a/pH5pWFCHtvRpHYC0G/dcA=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 h1:d81/ng9rET2YqdVkVwkb6EXeRrLJIwyGnJcAlAWKwhs=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0/go.mod h1:c+Lifp3EDEamAkPVzMooRNOK6CZjNSdEnf1A7jsI9u4=
 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 h1:gggzg0SUMs6SQbEw+3LoSsYf9YMjkupeAnHMX8O9mmY=
@@ -41,8 +41,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
 github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
 github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
-github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU=
-github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
+github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
+github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
 github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
 github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
 github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
@@ -88,8 +88,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
-github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
 github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
 github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c h1:DBGU7zCwrrPPDsD6+gqKG8UfMxenWg9BOJE/Nmfph+4=
@@ -98,8 +98,8 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
 github.com/frankban/quicktest v1.13.1 h1:xVm/f9seEhZFL9+n5kv5XLrGwy6elc4V9v/XFY2vmd8=
 github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
-github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -163,14 +163,14 @@ github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
-github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ=
-github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
 github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
 github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
 github.com/hanwen/go-fuse/v2 v2.4.0 h1:12OhD7CkXXQdvxG2osIdBQLdXh+nmLXY9unkUIe/xaU=
@@ -182,8 +182,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
-github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
+github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
 github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
@@ -217,14 +217,14 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
 github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
 github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
-github.com/minio/minio-go/v7 v7.0.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ=
-github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
+github.com/minio/minio-go/v7 v7.0.64 h1:Zdza8HwOzkld0ZG/og50w56fKi6AAyfqfifmasD9n2Q=
+github.com/minio/minio-go/v7 v7.0.64/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc=
 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
 github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
@@ -303,22 +303,22 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
 github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
-go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I=
-go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
-go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
-go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
-go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
-go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
-go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
+go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
+go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
+go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
+go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
+go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
 go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
-go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
-go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
@@ -330,8 +330,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
-golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
@@ -339,8 +339,8 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
-golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -354,18 +354,18 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
-golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
-golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -383,21 +383,23 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
-golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -409,8 +411,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-google.golang.org/api v0.148.0 h1:HBq4TZlN4/1pNcu0geJZ/Q50vIwIXT532UIMYoo0vOs=
-google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU=
+google.golang.org/api v0.152.0 h1:t0r1vPnfMc260S2Ci+en7kfCZaLOPs5KI0sVV/6jZrY=
+google.golang.org/api v0.152.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
@@ -418,12 +420,12 @@ google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA+oRzP9k7cSwJlvDFiROO72uwD6i0=
-google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk=
-google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU=
-google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a h1:a2MQQVoTo96JC9PMGtGBymLp7+/RzpFc2yX/9WfFg1c=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0=
+google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
+google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
+google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
+google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
diff --git a/repo/blob/azure/azure_options.go b/repo/blob/azure/azure_options.go
index cd55c1da..80d11a50 100644
--- a/repo/blob/azure/azure_options.go
+++ b/repo/blob/azure/azure_options.go
@@ -1,6 +1,8 @@
 package azure
 
 import (
+	"time"
+
 	"github.com/kopia/kopia/repo/blob/throttling"
 )
 
@@ -29,4 +31,7 @@ type Options struct {
 	StorageDomain string `json:"storageDomain,omitempty"`
 
 	throttling.Limits
+
+	// PointInTime specifies a view of the (versioned) store at that time
+	PointInTime *time.Time `json:"pointInTime,omitempty"`
 }
diff --git a/repo/blob/azure/azure_pit.go b/repo/blob/azure/azure_pit.go
new file mode 100644
index 00000000..9d3b8901
--- /dev/null
+++ b/repo/blob/azure/azure_pit.go
@@ -0,0 +1,201 @@
+package azure
+
+import (
+	"context"
+	"time"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	azblobmodels "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"github.com/pkg/errors"
+
+	"github.com/kopia/kopia/repo/blob"
+	"github.com/kopia/kopia/repo/blob/readonly"
+	"github.com/kopia/kopia/repo/format"
+)
+
+type azPointInTimeStorage struct {
+	azStorage
+
+	pointInTime time.Time
+}
+
+func (az *azPointInTimeStorage) ListBlobs(ctx context.Context, blobIDPrefix blob.ID, cb func(bm blob.Metadata) error) error {
+	var (
+		previousID blob.ID
+		vs         []versionMetadata
+	)
+
+	err := az.listBlobVersions(ctx, blobIDPrefix, func(vm versionMetadata) error {
+		if vm.BlobID != previousID {
+			// different blob, process previous one
+			if v, found := newestAtUnlessDeleted(vs, az.pointInTime); found {
+				if err := cb(v.Metadata); err != nil {
+					return err
+				}
+			}
+
+			previousID = vm.BlobID
+			vs = vs[:0] // reset for next blob
+		}
+
+		vs = append(vs, vm)
+
+		return nil
+	})
+	if err != nil {
+		return errors.Wrapf(err, "could not list blob versions at time %s", az.pointInTime)
+	}
+
+	// process last blob
+	if v, found := newestAtUnlessDeleted(vs, az.pointInTime); found {
+		if err := cb(v.Metadata); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (az *azPointInTimeStorage) GetBlob(ctx context.Context, blobID blob.ID, offset, length int64, output blob.OutputBuffer) error {
+	// getMetadata returns the specific blob version at time t
+	m, err := az.getVersionedMetadata(ctx, blobID)
+	if err != nil {
+		return errors.Wrap(err, "getting metadata")
+	}
+
+	return az.getBlobWithVersion(ctx, blobID, m.Version, offset, length, output)
+}
+
+// newestAtUnlessDeleted returns the last version in the list older than the PIT.
+// Azure sorts in ascending order so return the last element in the list.
+func newestAtUnlessDeleted(vs []versionMetadata, t time.Time) (v versionMetadata, found bool) {
+	vs = getOlderThan(vs, t)
+
+	if len(vs) == 0 {
+		return versionMetadata{}, false
+	}
+
+	v = vs[len(vs)-1]
+
+	return v, !v.IsDeleteMarker
+}
+
+// Removes versions that are newer than t. The filtering is done in place
+// and uses the same slice storage as vs. Assumes entries in vs are in ascending
+// timestamp order (and version order), unlike S3 which assumes descending.
+// Versions in Azure follow the time.RFC3339Nano syntax.
+func getOlderThan(vs []versionMetadata, t time.Time) []versionMetadata {
+	for i := range vs {
+		if vs[i].Timestamp.After(t) {
+			return vs[:i]
+		}
+
+		// The DeleteMarker blob takes the Timestamp of the previous version but has its own Version.
+		// If there was a Kopia Delete Marker (the blob was protected) it will be caught above but if
+		// the container has versioning enabled but no blob retention protection (or the blob was deleted outside
+		// of the protection window) then we need to check the time of the VersionID because there could be a situation
+		// where Azure's DeleteMarker version has Timestamp 2023-10-20 but Version 2023-10-27...then if PIT was 2023-10-22 the DeleteMarker
+		// would be returned without this extra test
+		if vs[i].IsDeleteMarker {
+			versionTime, err := time.Parse(time.RFC3339Nano, vs[i].Version)
+			if err != nil {
+				return nil
+			}
+
+			if versionTime.After(t) {
+				return vs[:i]
+			}
+		}
+	}
+
+	return vs
+}
+
+// listBlobVersions returns a list of blob versions but the blob is deleted, it returns Azure's delete marker version but excludes
+// the Kopia delete marker version that is used to get around immutability protections.
+func (az *azPointInTimeStorage) listBlobVersions(ctx context.Context, prefix blob.ID, callback func(vm versionMetadata) error) error {
+	prefixStr := az.getObjectNameString(prefix)
+
+	pager := az.service.NewListBlobsFlatPager(az.container, &azblob.ListBlobsFlatOptions{
+		Prefix: &prefixStr,
+		Include: azblob.ListBlobsInclude{
+			Metadata:            true,
+			DeletedWithVersions: true, // this shows DeleteMarkers aka blobs with HasVersionsOnly set to true
+			Versions:            true,
+		},
+	})
+
+	for pager.More() {
+		page, err := pager.NextPage(ctx)
+		if err != nil {
+			return translateError(err)
+		}
+
+		for _, it := range page.Segment.BlobItems {
+			vm := az.getVersionedBlobMeta(it)
+
+			if err := callback(vm); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func (az *azPointInTimeStorage) getVersionedMetadata(ctx context.Context, blobID blob.ID) (versionMetadata, error) {
+	var vml []versionMetadata
+
+	if err := az.getBlobVersions(ctx, blobID, func(vm versionMetadata) error {
+		if !vm.Timestamp.After(az.pointInTime) {
+			vml = append(vml, vm)
+		}
+
+		return nil
+	}); err != nil {
+		return versionMetadata{}, errors.Wrapf(err, "could not get version metadata for blob %s", blobID)
+	}
+
+	if v, found := newestAtUnlessDeleted(vml, az.pointInTime); found {
+		return v, nil
+	}
+
+	return versionMetadata{}, blob.ErrBlobNotFound
+}
+
+// isAzureDeleteMarker checks for Azure created delete markers.
+func (az *azPointInTimeStorage) isAzureDeleteMarker(it *azblobmodels.BlobItem) bool {
+	var isDeleteMarker bool
+	// HasVersionsOnly - Indicates that this root blob has been deleted
+	if it.HasVersionsOnly != nil {
+		isDeleteMarker = *it.HasVersionsOnly
+	}
+
+	return isDeleteMarker
+}
+
+// maybePointInTimeStore wraps s with a point-in-time store when s is versioned
+// and a point-in-time value is specified. Otherwise, s is returned.
+func maybePointInTimeStore(ctx context.Context, s *azStorage, pointInTime *time.Time) (blob.Storage, error) {
+	if pit := s.Options.PointInTime; pit == nil || pit.IsZero() {
+		return s, nil
+	}
+
+	// Versioning is needed for PIT. This check will fail if someone deleted the Kopia Repository file.
+	props, err := s.service.ServiceClient().
+		NewContainerClient(s.container).
+		NewBlobClient(s.getObjectNameString(format.KopiaRepositoryBlobID)).
+		GetProperties(ctx, nil)
+	if err != nil {
+		return nil, errors.Wrapf(err, "could not get determine if container '%s' supports versioning", s.container)
+	}
+
+	if props.VersionID == nil {
+		return nil, errors.Errorf("cannot create point-in-time view for non-versioned container '%s'", s.container)
+	}
+
+	return readonly.NewWrapper(&azPointInTimeStorage{
+		azStorage:   *s,
+		pointInTime: *pointInTime,
+	}), nil
+}
diff --git a/repo/blob/azure/azure_storage.go b/repo/blob/azure/azure_storage.go
index 6e81e8c2..374fd2b9 100644
--- a/repo/blob/azure/azure_storage.go
+++ b/repo/blob/azure/azure_storage.go
@@ -4,6 +4,7 @@
 import (
 	"context"
 	"fmt"
+	"strings"
 	"time"
 
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
@@ -13,6 +14,7 @@
 	azblobblob "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
 	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
 	azblockblob "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
+	azblobmodels "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
 	"github.com/pkg/errors"
 
 	"github.com/kopia/kopia/internal/clock"
@@ -25,7 +27,8 @@
 )
 
 const (
-	azStorageType = "azureBlob"
+	azStorageType   = "azureBlob"
+	latestVersionID = ""
 
 	timeMapKey = "Kopiamtime" // this must be capital letter followed by lowercase, to comply with AZ tags naming convention.
 )
@@ -39,6 +42,10 @@ type azStorage struct {
 }
 
 func (az *azStorage) GetBlob(ctx context.Context, b blob.ID, offset, length int64, output blob.OutputBuffer) error {
+	return az.getBlobWithVersion(ctx, b, latestVersionID, offset, length, output)
+}
+
+func (az *azStorage) getBlobWithVersion(ctx context.Context, b blob.ID, versionID string, offset, length int64, output blob.OutputBuffer) error {
 	if offset < 0 {
 		return errors.Wrap(blob.ErrInvalidRange, "invalid offset")
 	}
@@ -56,7 +63,15 @@ func (az *azStorage) GetBlob(ctx context.Context, b blob.ID, offset, length int6
 		opt.Range.Count = l1
 	}
 
-	resp, err := az.service.DownloadStream(ctx, az.container, az.getObjectNameString(b), opt)
+	bc, err := az.service.ServiceClient().
+		NewContainerClient(az.container).
+		NewBlobClient(az.getObjectNameString(b)).
+		WithVersionID(versionID)
+	if err != nil {
+		return errors.Wrap(err, "failed to get versioned blob client")
+	}
+
+	resp, err := bc.DownloadStream(ctx, opt)
 	if err != nil {
 		return translateError(err)
 	}
@@ -71,7 +86,6 @@ func (az *azStorage) GetBlob(ctx context.Context, b blob.ID, offset, length int6
 	if err := iocopy.JustCopy(output, body); err != nil {
 		return translateError(err)
 	}
-
 	//nolint:wrapcheck
 	return blob.EnsureLengthExactly(output.Length(), length)
 }
@@ -215,6 +229,27 @@ func (az *azStorage) DisplayName() string {
 	return fmt.Sprintf("Azure: %v", az.Options.Container)
 }
 
+func (az *azStorage) getBlobName(it *azblobmodels.BlobItem) blob.ID {
+	n := *it.Name
+	return blob.ID(strings.TrimPrefix(n, az.Prefix))
+}
+
+func (az *azStorage) getBlobMeta(it *azblobmodels.BlobItem) blob.Metadata {
+	bm := blob.Metadata{
+		BlobID: az.getBlobName(it),
+		Length: *it.Properties.ContentLength,
+	}
+
+	// see if we have 'Kopiamtime' metadata, if so - trust it.
+	if t, ok := timestampmeta.FromValue(stringDefault(it.Metadata["kopiamtime"], "")); ok {
+		bm.Timestamp = t
+	} else {
+		bm.Timestamp = *it.Properties.LastModified
+	}
+
+	return bm
+}
+
 func (az *azStorage) putBlob(ctx context.Context, b blob.ID, data blob.Bytes, opts blob.PutOptions) (azblockblob.UploadResponse, error) {
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
@@ -368,7 +403,12 @@ func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error)
 		service:   service,
 	}
 
-	az := retrying.NewWrapper(raw)
+	st, err := maybePointInTimeStore(ctx, raw, opt.PointInTime)
+	if err != nil {
+		return nil, err
+	}
+
+	az := retrying.NewWrapper(st)
 
 	// verify Azure connection is functional by listing blobs in a bucket, which will fail if the container
 	// does not exist. We list with a prefix that will not exist, to avoid iterating through any objects.
diff --git a/repo/blob/azure/azure_versioned.go b/repo/blob/azure/azure_versioned.go
new file mode 100644
index 00000000..e3c1981b
--- /dev/null
+++ b/repo/blob/azure/azure_versioned.go
@@ -0,0 +1,49 @@
+package azure
+
+import (
+	"context"
+
+	azblobmodels "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+
+	"github.com/kopia/kopia/repo/blob"
+)
+
+// versionMetadata has metadata for a single BLOB version.
+type versionMetadata struct {
+	blob.Metadata
+
+	// Version has the format of time.RFC3339Nano
+	Version        string
+	IsDeleteMarker bool
+}
+
+type versionMetadataCallback func(versionMetadata) error
+
+func (az *azPointInTimeStorage) getVersionedBlobMeta(it *azblobmodels.BlobItem) versionMetadata {
+	bm := az.getBlobMeta(it)
+
+	return versionMetadata{
+		Metadata:       bm,
+		Version:        *it.VersionID,
+		IsDeleteMarker: az.isAzureDeleteMarker(it),
+	}
+}
+
+// getBlobVersions lists all the versions for the blob with the given prefix.
+func (az *azPointInTimeStorage) getBlobVersions(ctx context.Context, prefix blob.ID, callback versionMetadataCallback) error {
+	var foundBlobs bool
+
+	if err := az.listBlobVersions(ctx, prefix, func(vm versionMetadata) error {
+		foundBlobs = true
+
+		return callback(vm)
+	}); err != nil {
+		return err
+	}
+
+	if !foundBlobs {
+		return blob.ErrBlobNotFound
+	}
+
+	return nil
+}
diff --git a/repo/blob/storage.go b/repo/blob/storage.go
index d420a636..68483ca5 100644
--- a/repo/blob/storage.go
+++ b/repo/blob/storage.go
@@ -71,7 +71,7 @@ type Capacity struct {
 
 // Volume defines disk/volume access API to blob storage.
 type Volume interface {
-	// Capacity returns the capacity of a given volume.
+	// GetCapacity returns the capacity of a given volume.
 	GetCapacity(ctx context.Context) (Capacity, error)
 }
 
@@ -94,7 +94,7 @@ type Reader interface {
 	// connect to storage.
 	ConnectionInfo() ConnectionInfo
 
-	// Name of the storage used for quick identification by humans.
+	// DisplayName Name of the storage used for quick identification by humans.
 	DisplayName() string
 }
 
diff --git a/site/content/docs/Advanced/Actions/_index.md b/site/content/docs/Advanced/Actions/_index.md
index 8f8730ab..e1d580f9 100644
--- a/site/content/docs/Advanced/Actions/_index.md
+++ b/site/content/docs/Advanced/Actions/_index.md
@@ -245,8 +245,8 @@ if (([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::
 To install the actions:
 
 ```shell
-kopia policy set <target_dir> --before-folder-action "powershell -WindowStyle Hidden <path_to_script>\before.ps1"
-kopia policy set <target_dir> --after-folder-action  "powershell -WindowStyle Hidden <path_to_script>\after.ps1"
+kopia policy set <target_dir> --before-folder-action "powershell -WindowStyle Hidden -File <path_to_script>\before.ps1"
+kopia policy set <target_dir> --after-folder-action  "powershell -WindowStyle Hidden -File <path_to_script>\after.ps1"
 ```
 
 ### Contributions Welcome
diff --git a/site/content/docs/Advanced/Ransomware Protection/_index.md b/site/content/docs/Advanced/Ransomware Protection/_index.md
index aa5d1e16..6b2241c7 100644
--- a/site/content/docs/Advanced/Ransomware Protection/_index.md
+++ b/site/content/docs/Advanced/Ransomware Protection/_index.md
@@ -46,7 +46,6 @@ Some cloud storage solutions provide the ability to generate restricted access k
                         "s3:DeleteBucket",
                         "s3:DeleteBucketPolicy",
                         "s3:DeleteBucketWebsite",
-                        "s3:DeleteObject",
                         "s3:DeleteObjectVersion"
                     ],
                     "Resource": [


Test patch:
diff --git a/.github/workflows/compat-test.yml b/.github/workflows/compat-test.yml
index a3b84c21..559b619c 100644
--- a/.github/workflows/compat-test.yml
+++ b/.github/workflows/compat-test.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/endurance-test.yml b/.github/workflows/endurance-test.yml
index d056c9dd..af7c5414 100644
--- a/.github/workflows/endurance-test.yml
+++ b/.github/workflows/endurance-test.yml
@@ -19,7 +19,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/htmlui-tests.yml b/.github/workflows/htmlui-tests.yml
index 934c64a3..102242a4 100644
--- a/.github/workflows/htmlui-tests.yml
+++ b/.github/workflows/htmlui-tests.yml
@@ -27,7 +27,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/stress-test.yml b/.github/workflows/stress-test.yml
index b77984bd..91b6764f 100644
--- a/.github/workflows/stress-test.yml
+++ b/.github/workflows/stress-test.yml
@@ -18,7 +18,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 845ec5b3..34867868 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -38,7 +38,7 @@ jobs:
     continue-on-error: ${{ contains(matrix.os, 'self-hosted') }}
     steps:
     - name: Check out repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/cli/command_repository_set_parameters_test.go b/cli/command_repository_set_parameters_test.go
index fe8cc312..d5b2c3f8 100644
--- a/cli/command_repository_set_parameters_test.go
+++ b/cli/command_repository_set_parameters_test.go
@@ -35,8 +35,10 @@ func (s *formatSpecificTestSuite) TestRepositorySetParameters(t *testing.T) {
 	require.Contains(t, out, "Max pack length:     21 MB")
 	require.Contains(t, out, fmt.Sprintf("Format version:      %d", s.formatVersion))
 
+	_, out = env.RunAndExpectSuccessWithErrOut(t, "repository", "set-parameters")
+	require.Contains(t, out, "no changes")
+
 	// failure cases
-	env.RunAndExpectFailure(t, "repository", "set-parameters")
 	env.RunAndExpectFailure(t, "repository", "set-parameters", "--index-version=33")
 	env.RunAndExpectFailure(t, "repository", "set-parameters", "--max-pack-size-mb=9")
 	env.RunAndExpectFailure(t, "repository", "set-parameters", "--max-pack-size-mb=121")
@@ -73,6 +75,10 @@ func (s *formatSpecificTestSuite) TestRepositorySetParametersRetention(t *testin
 	_, out = env.RunAndExpectSuccessWithErrOut(t, "repository", "set-parameters", "--retention-mode", "none")
 	require.Contains(t, out, "disabling blob retention")
 
+	// 2nd time also succeeds but disabling is skipped due to already being disabled. !anyChanges returns no error.
+	_, out = env.RunAndExpectSuccessWithErrOut(t, "repository", "set-parameters", "--retention-mode", "none")
+	require.Contains(t, out, "no changes")
+
 	out = env.RunAndExpectSuccess(t, "repository", "status")
 	require.NotContains(t, out, "Blob retention mode")
 	require.NotContains(t, out, "Blob retention period")
@@ -201,21 +207,26 @@ func (s *formatSpecificTestSuite) TestRepositorySetParametersDowngrade(t *testin
 			require.Contains(t, out, "Format version:      1")
 			require.Contains(t, out, "Epoch Manager:       disabled")
 			env.RunAndExpectFailure(t, "index", "epoch", "list")
+			// setting the current version again is ok
+			_, out = env.RunAndExpectSuccessWithErrOut(t, "repository", "set-parameters", "--index-version=1")
+			require.Contains(t, out, "no changes")
 		case format.FormatVersion2:
 			require.Contains(t, out, "Format version:      2")
 			require.Contains(t, out, "Epoch Manager:       enabled")
 			env.RunAndExpectSuccess(t, "index", "epoch", "list")
+			_, out = env.RunAndExpectFailure(t, "repository", "set-parameters", "--index-version=1")
+			require.Contains(t, out, "index format version can only be upgraded")
 		default:
 			require.Contains(t, out, "Format version:      3")
 			require.Contains(t, out, "Epoch Manager:       enabled")
 			env.RunAndExpectSuccess(t, "index", "epoch", "list")
+			_, out = env.RunAndExpectFailure(t, "repository", "set-parameters", "--index-version=1")
+			require.Contains(t, out, "index format version can only be upgraded")
 		}
 	}
 
 	checkStatusForVersion()
 
-	env.RunAndExpectFailure(t, "repository", "set-parameters", "--index-version=1")
-
 	checkStatusForVersion()
 
 	// run basic check to ensure that an upgrade can still be performed as expected
diff --git a/debug/debug_test.go b/debug/debug_test.go
new file mode 100644
index 00000000..e5ee7dd8
--- /dev/null
+++ b/debug/debug_test.go
@@ -0,0 +1,126 @@
+package debug
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestDebug_parseProfileConfigs(t *testing.T) {
+	tcs := []struct {
+		in     string
+		key    ProfileName
+		expect []string
+	}{
+		{
+			in:  "foo=bar",
+			key: "foo",
+			expect: []string{
+				"bar",
+			},
+		},
+		{
+			in:  "first=one=1",
+			key: "first",
+			expect: []string{
+				"one=1",
+			},
+		},
+		{
+			in:  "foo=bar:first=one=1",
+			key: "first",
+			expect: []string{
+				"one=1",
+			},
+		},
+		{
+			in:  "foo=bar:first=one=1,two=2",
+			key: "first",
+			expect: []string{
+				"one=1",
+				"two=2",
+			},
+		},
+		{
+			in:  "foo=bar:first=one=1,two=2:second:third",
+			key: "first",
+			expect: []string{
+				"one=1",
+				"two=2",
+			},
+		},
+		{
+			in:  "foo=bar:first=one=1,two=2:second:third",
+			key: "foo",
+			expect: []string{
+				"bar",
+			},
+		},
+		{
+			in:     "foo=bar:first=one=1,two=2:second:third",
+			key:    "second",
+			expect: nil,
+		},
+		{
+			in:     "foo=bar:first=one=1,two=2:second:third",
+			key:    "third",
+			expect: nil,
+		},
+	}
+	for i, tc := range tcs {
+		t.Run(fmt.Sprintf("%d %s", i, tc.in), func(t *testing.T) {
+			pbs := parseProfileConfigs(1<<10, tc.in)
+			pb, ok := pbs[tc.key] // no negative testing for missing keys (see newProfileConfigs)
+			require.True(t, ok)
+			require.NotNil(t, pb)                 // always not nil
+			require.Equal(t, 1<<10, pb.buf.Cap()) // bufsize is always 1024
+			require.Equal(t, 0, pb.buf.Len())
+			require.Equal(t, tc.expect, pb.flags)
+		})
+	}
+}
+
+func TestDebug_newProfileConfigs(t *testing.T) {
+	tcs := []struct {
+		in     string
+		key    string
+		expect string
+		ok     bool
+	}{
+		{
+			in:     "foo=bar",
+			key:    "foo",
+			ok:     true,
+			expect: "bar",
+		},
+		{
+			in:     "foo=",
+			key:    "foo",
+			ok:     true,
+			expect: "",
+		},
+		{
+			in:     "",
+			key:    "foo",
+			ok:     false,
+			expect: "",
+		},
+		{
+			in:     "foo=bar",
+			key:    "bar",
+			ok:     false,
+			expect: "",
+		},
+	}
+	for i, tc := range tcs {
+		t.Run(fmt.Sprintf("%d %s", i, tc.in), func(t *testing.T) {
+			pb := newProfileConfig(1<<10, tc.in)
+			require.NotNil(t, pb)                 // always not nil
+			require.Equal(t, pb.buf.Cap(), 1<<10) // bufsize is always 1024
+			v, ok := pb.GetValue(tc.key)
+			require.Equal(t, tc.ok, ok)
+			require.Equal(t, tc.expect, v)
+		})
+	}
+}
diff --git a/repo/blob/azure/azure_storage_test.go b/repo/blob/azure/azure_storage_test.go
index 78a162bf..2c7ba913 100644
--- a/repo/blob/azure/azure_storage_test.go
+++ b/repo/blob/azure/azure_storage_test.go
@@ -23,13 +23,17 @@
 )
 
 const (
-	testContainerEnv           = "KOPIA_AZURE_TEST_CONTAINER"
-	testStorageAccountEnv      = "KOPIA_AZURE_TEST_STORAGE_ACCOUNT"
-	testStorageKeyEnv          = "KOPIA_AZURE_TEST_STORAGE_KEY"
-	testStorageSASTokenEnv     = "KOPIA_AZURE_TEST_SAS_TOKEN"
-	testStorageTenantIDEnv     = "KOPIA_AZURE_TEST_TENANT_ID"
-	testStorageClientIDEnv     = "KOPIA_AZURE_TEST_CLIENT_ID"
-	testStorageClientSecretEnv = "KOPIA_AZURE_TEST_CLIENT_SECRET"
+	testContainerEnv                = "KOPIA_AZURE_TEST_CONTAINER"
+	testStorageAccountEnv           = "KOPIA_AZURE_TEST_STORAGE_ACCOUNT"
+	testStorageKeyEnv               = "KOPIA_AZURE_TEST_STORAGE_KEY"
+	testStorageSASTokenEnv          = "KOPIA_AZURE_TEST_SAS_TOKEN"
+	testImmutableContainerEnv       = "KOPIA_AZURE_TEST_IMMUTABLE_CONTAINER"
+	testImmutableStorageAccountEnv  = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_ACCOUNT"
+	testImmutableStorageKeyEnv      = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_KEY"
+	testImmutableStorageSASTokenEnv = "KOPIA_AZURE_TEST_IMMUTABLE_SAS_TOKEN"
+	testStorageTenantIDEnv          = "KOPIA_AZURE_TEST_TENANT_ID"
+	testStorageClientIDEnv          = "KOPIA_AZURE_TEST_CLIENT_ID"
+	testStorageClientSecretEnv      = "KOPIA_AZURE_TEST_CLIENT_SECRET"
 )
 
 func getEnvOrSkip(t *testing.T, name string) string {
@@ -119,7 +123,7 @@ func TestAzureStorage(t *testing.T) {
 		Container:      container,
 		StorageAccount: storageAccount,
 		StorageKey:     storageKey,
-		Prefix:         fmt.Sprintf("test-%v-%x-", clock.Now().Unix(), data),
+		Prefix:         fmt.Sprintf("test-%v-%x/", clock.Now().Unix(), data),
 	}, false)
 
 	cancel()
@@ -152,7 +156,7 @@ func TestAzureStorageSASToken(t *testing.T) {
 		Container:      container,
 		StorageAccount: storageAccount,
 		SASToken:       sasToken,
-		Prefix:         fmt.Sprintf("sastest-%v-%x-", clock.Now().Unix(), data),
+		Prefix:         fmt.Sprintf("sastest-%v-%x/", clock.Now().Unix(), data),
 	}, false)
 
 	require.NoError(t, err)
@@ -190,7 +194,7 @@ func TestAzureStorageClientSecret(t *testing.T) {
 		TenantID:       tenantID,
 		ClientID:       clientID,
 		ClientSecret:   clientSecret,
-		Prefix:         fmt.Sprintf("sastest-%v-%x-", clock.Now().Unix(), data),
+		Prefix:         fmt.Sprintf("sastest-%v-%x/", clock.Now().Unix(), data),
 	}, false)
 
 	require.NoError(t, err)
@@ -270,3 +274,17 @@ func TestAzureStorageInvalidCreds(t *testing.T) {
 		t.Errorf("unexpected success connecting to Azure blob storage, wanted error")
 	}
 }
+
+func getBlobCount(ctx context.Context, t *testing.T, st blob.Storage, prefix blob.ID) int {
+	t.Helper()
+
+	var count int
+
+	err := st.ListBlobs(ctx, prefix, func(bm blob.Metadata) error {
+		count++
+		return nil
+	})
+	require.NoError(t, err)
+
+	return count
+}
diff --git a/repo/blob/azure/azure_versioned_test.go b/repo/blob/azure/azure_versioned_test.go
new file mode 100644
index 00000000..f1d36dbe
--- /dev/null
+++ b/repo/blob/azure/azure_versioned_test.go
@@ -0,0 +1,227 @@
+package azure_test
+
+import (
+	"context"
+	"crypto/rand"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/clock"
+	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/testlogging"
+	"github.com/kopia/kopia/internal/testutil"
+	"github.com/kopia/kopia/repo/blob"
+	"github.com/kopia/kopia/repo/blob/azure"
+	"github.com/kopia/kopia/repo/format"
+)
+
+func TestGetBlobVersions(t *testing.T) {
+	t.Parallel()
+	testutil.ProviderTest(t)
+
+	// must be with Immutable Storage with Versioning enabled
+	container := getEnvOrSkip(t, testImmutableContainerEnv)
+	storageAccount := getEnvOrSkip(t, testImmutableStorageAccountEnv)
+	storageKey := getEnvOrSkip(t, testImmutableStorageKeyEnv)
+
+	createContainer(t, container, storageAccount, storageKey)
+
+	ctx := testlogging.Context(t)
+	data := make([]byte, 8)
+	rand.Read(data)
+	// use context that gets canceled after opening storage to ensure it's not used beyond New().
+	newctx, cancel := context.WithCancel(ctx)
+	t.Cleanup(cancel)
+
+	prefix := fmt.Sprintf("test-%v-%x/", clock.Now().Unix(), data)
+	opts := &azure.Options{
+		Container:      container,
+		StorageAccount: storageAccount,
+		StorageKey:     storageKey,
+		Prefix:         prefix,
+	}
+	st, err := azure.New(newctx, opts, false)
+	require.NoError(t, err)
+
+	t.Cleanup(func() {
+		st.Close(ctx)
+	})
+
+	// required for PIT versioning check
+	err = st.PutBlob(ctx, format.KopiaRepositoryBlobID, gather.FromSlice([]byte(nil)), blob.PutOptions{})
+	require.NoError(t, err)
+
+	const (
+		originalData = "original"
+		updatedData  = "some update"
+		latestData   = "latest version"
+	)
+
+	dataBlobs := []string{originalData, updatedData, latestData}
+
+	const blobName = "TestGetBlobVersions"
+	blobID := blob.ID(blobName)
+	dataTimestamps, err := putBlobs(ctx, st, blobID, dataBlobs)
+	require.NoError(t, err)
+
+	pastPIT := dataTimestamps[0].Add(-1 * time.Second)
+	futurePIT := dataTimestamps[2].Add(1 * time.Second)
+
+	for _, tt := range []struct {
+		testName         string
+		pointInTime      *time.Time
+		expectedBlobData string
+		expectedError    error
+	}{
+		{
+			testName:         "unset PIT",
+			pointInTime:      nil,
+			expectedBlobData: latestData,
+			expectedError:    nil,
+		},
+		{
+			testName:         "set in the future",
+			pointInTime:      &futurePIT,
+			expectedBlobData: latestData,
+			expectedError:    nil,
+		},
+		{
+			testName:         "set in the past",
+			pointInTime:      &pastPIT,
+			expectedBlobData: "",
+			expectedError:    blob.ErrBlobNotFound,
+		},
+		{
+			testName:         "original data",
+			pointInTime:      &dataTimestamps[0],
+			expectedBlobData: originalData,
+			expectedError:    nil,
+		},
+		{
+			testName:         "updated data",
+			pointInTime:      &dataTimestamps[1],
+			expectedBlobData: updatedData,
+			expectedError:    nil,
+		},
+		{
+			testName:         "latest data",
+			pointInTime:      &dataTimestamps[2],
+			expectedBlobData: latestData,
+			expectedError:    nil,
+		},
+	} {
+		fmt.Printf("Running test: %s\n", tt.testName)
+		opts.PointInTime = tt.pointInTime
+		st, err = azure.New(ctx, opts, false)
+		require.NoError(t, err)
+
+		var tmp gather.WriteBuffer
+		err = st.GetBlob(ctx, blobID, 0, -1, &tmp)
+		require.ErrorIs(t, err, tt.expectedError)
+		require.Equal(t, tt.expectedBlobData, string(tmp.ToByteSlice()))
+	}
+}
+
+func TestGetBlobVersionsWithDeletion(t *testing.T) {
+	t.Parallel()
+	testutil.ProviderTest(t)
+
+	// must be with Immutable Storage with Versioning enabled
+	container := getEnvOrSkip(t, testImmutableContainerEnv)
+	storageAccount := getEnvOrSkip(t, testImmutableStorageAccountEnv)
+	storageKey := getEnvOrSkip(t, testImmutableStorageKeyEnv)
+
+	createContainer(t, container, storageAccount, storageKey)
+
+	ctx := testlogging.Context(t)
+	data := make([]byte, 8)
+	rand.Read(data)
+	// use context that gets canceled after opening storage to ensure it's not used beyond New().
+	newctx, cancel := context.WithCancel(ctx)
+	t.Cleanup(cancel)
+
+	prefix := fmt.Sprintf("test-%v-%x/", clock.Now().Unix(), data)
+	opts := &azure.Options{
+		Container:      container,
+		StorageAccount: storageAccount,
+		StorageKey:     storageKey,
+		Prefix:         prefix,
+	}
+	st, err := azure.New(newctx, opts, false)
+	require.NoError(t, err)
+
+	t.Cleanup(func() {
+		st.Close(ctx)
+	})
+
+	// required for PIT versioning check
+	err = st.PutBlob(ctx, format.KopiaRepositoryBlobID, gather.FromSlice([]byte(nil)), blob.PutOptions{})
+	require.NoError(t, err)
+
+	const (
+		originalData = "original"
+		updatedData  = "some update"
+	)
+
+	dataBlobs := []string{originalData, updatedData}
+
+	const blobName = "TestGetBlobVersionsWithDeletion"
+	blobID := blob.ID(blobName)
+	dataTimestamps, err := putBlobs(ctx, st, blobID, dataBlobs)
+	require.NoError(t, err)
+
+	count := getBlobCount(ctx, t, st, blobID)
+	require.Equal(t, 1, count)
+
+	err = st.DeleteBlob(ctx, blobID)
+	require.NoError(t, err)
+
+	// blob no longer found
+	count = getBlobCount(ctx, t, st, blobID)
+	require.Equal(t, 0, count)
+
+	opts.PointInTime = &dataTimestamps[1]
+	st, err = azure.New(ctx, opts, false)
+	require.NoError(t, err)
+
+	// blob visible again with PIT set.
+	count = getBlobCount(ctx, t, st, blobID)
+	require.Equal(t, 1, count)
+
+	var tmp gather.WriteBuffer
+	err = st.GetBlob(ctx, blobID, 0, -1, &tmp)
+	require.NoError(t, err)
+	require.Equal(t, updatedData, string(tmp.ToByteSlice()))
+
+	opts.PointInTime = &dataTimestamps[0]
+	st, err = azure.New(ctx, opts, false)
+	require.NoError(t, err)
+	err = st.GetBlob(ctx, blobID, 0, -1, &tmp)
+	require.NoError(t, err)
+	require.Equal(t, originalData, string(tmp.ToByteSlice()))
+}
+
+func putBlobs(ctx context.Context, cli blob.Storage, blobID blob.ID, blobs []string) ([]time.Time, error) {
+	var putTimes []time.Time
+
+	for _, b := range blobs {
+		if err := cli.PutBlob(ctx, blobID, gather.FromSlice([]byte(b)), blob.PutOptions{}); err != nil {
+			return nil, errors.Wrap(err, "putting blob")
+		}
+
+		m, err := cli.GetMetadata(ctx, blobID)
+		if err != nil {
+			return nil, errors.Wrap(err, "getting metadata")
+		}
+
+		putTimes = append(putTimes, m.Timestamp)
+		// sleep because granularity is 1 second and we should separate to show PIT views.
+		time.Sleep(1 * time.Second)
+	}
+
+	return putTimes, nil
+}
diff --git a/tests/tools/kopiarunner/kopia_snapshotter.go b/tests/tools/kopiarunner/kopia_snapshotter.go
index 01b87a14..033a9a6c 100644
--- a/tests/tools/kopiarunner/kopia_snapshotter.go
+++ b/tests/tools/kopiarunner/kopia_snapshotter.go
@@ -1,12 +1,14 @@
 package kopiarunner
 
 import (
+	"bytes"
 	"context"
 	"crypto/sha256"
 	"crypto/x509"
 	"encoding/hex"
 	"encoding/pem"
 	"fmt"
+	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -397,6 +399,17 @@ func (ks *KopiaSnapshotter) ConnectOrCreateRepoWithServer(serverAddr string, arg
 	}
 
 	if err := certKeyExist(context.TODO(), tlsCertFile, tlsKeyFile); err != nil {
+		if cmd.Stderr == nil {
+			return nil, "", err
+		}
+
+		buf, ok := cmd.Stderr.(*bytes.Buffer)
+		if !ok {
+			return nil, "", err
+		}
+
+		log.Printf("failuire in certificate generation: %s", buf.String())
+
 		return nil, "", err
 	}