Repository: rook/rook
Pull request #14928 (2024-10-29 12:04:45)

docs: fix external doc broken links
some of the links have the full path
which is not working in the doc server,
but works locally

<!-- Thank you for contributing to Rook! -->

<!-- STEPS TO FOLLOW:
  1. Add a description of the changes (frequently the same as the commit description)
  2. Enter the issue number next to "Resolves #" below (if there is no tracking issue resolved, **remove that section**)
  3. Review our Contributing documentation at https://rook.io/docs/rook/latest/Contributing/development-flow/
  4. Follow the steps in the checklist below, starting with the **Commit Message Formatting**.
-->

<!-- Uncomment this section with the issue number if an issue is being resolved
**Issue resolved by this Pull Request:**
Resolves #
--->

**Checklist:**

- [ ] **Commit Message Formatting**: Commit titles and messages follow guidelines in the [developer guide](https://rook.io/docs/rook/latest/Contributing/development-flow/#commit-structure).
- [ ] Reviewed the developer guide on [Submitting a Pull Request](https://rook.io/docs/rook/latest/Contributing/development-flow/#submitting-a-pull-request)
- [ ] [Pending release notes](https://github.com/rook/rook/blob/master/PendingReleaseNotes.md) updated with breaking and/or notable changes for the next minor release.
- [ ] Documentation has been updated, if necessary.
- [ ] Unit tests have been added, if necessary.
- [ ] Integration tests have been added, if necessary.



Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 974e9a53f..2e9922c5a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,11 +21,11 @@ jobs:
     if: "!contains(github.event.pull_request.labels.*.name, 'skip-ci')"
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
@@ -74,12 +74,12 @@ jobs:
         go-version: ["1.22", "1.23"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
       - name: setup golang ${{ matrix.go-version }}
-        uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+        uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: ${{ matrix.go-version }}
 
diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml
index b4f24a465..74fb05769 100644
--- a/.github/workflows/codegen.yml
+++ b/.github/workflows/codegen.yml
@@ -30,11 +30,11 @@ jobs:
     if: "!contains(github.event.pull_request.labels.*.name, 'skip-ci')"
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
diff --git a/.github/workflows/codespell.yaml b/.github/workflows/codespell.yaml
index e5aaebd88..57ebcf8fd 100644
--- a/.github/workflows/codespell.yaml
+++ b/.github/workflows/codespell.yaml
@@ -24,7 +24,7 @@ jobs:
     name: codespell
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
       - name: codespell
@@ -53,7 +53,7 @@ jobs:
     name: misspell
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
       - name: misspell
diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml
index 1f7cb2f09..e388f2485 100644
--- a/.github/workflows/commitlint.yml
+++ b/.github/workflows/commitlint.yml
@@ -28,7 +28,7 @@ jobs:
     env:
       GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
       - uses: wagoid/commitlint-github-action@3d28780bbf0365e29b144e272b2121204d5be5f3 # v6.1.2
diff --git a/.github/workflows/crds-gen.yml b/.github/workflows/crds-gen.yml
index 55c43dcb0..8916627bf 100644
--- a/.github/workflows/crds-gen.yml
+++ b/.github/workflows/crds-gen.yml
@@ -29,11 +29,11 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
diff --git a/.github/workflows/daily-nightly-jobs.yml b/.github/workflows/daily-nightly-jobs.yml
index d211e03a1..a5bf2f89f 100644
--- a/.github/workflows/daily-nightly-jobs.yml
+++ b/.github/workflows/daily-nightly-jobs.yml
@@ -19,7 +19,7 @@ jobs:
 
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -29,7 +29,7 @@ jobs:
           use-tmate: ${{ secrets.USE_TMATE }}
 
       - name: setup golang
-        uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+        uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
@@ -116,7 +116,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -156,7 +156,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -196,7 +196,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -236,7 +236,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -276,7 +276,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -317,7 +317,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml
index 41b25dfd4..265ffff83 100644
--- a/.github/workflows/docs-check.yml
+++ b/.github/workflows/docs-check.yml
@@ -24,15 +24,15 @@ jobs:
     name: docs-check
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
-      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+      - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
         with:
           python-version: 3.9
 
diff --git a/.github/workflows/encryption-pvc-kms-ibm-kp/action.yml b/.github/workflows/encryption-pvc-kms-ibm-kp/action.yml
index 8dd63ee86..4e9975eab 100644
--- a/.github/workflows/encryption-pvc-kms-ibm-kp/action.yml
+++ b/.github/workflows/encryption-pvc-kms-ibm-kp/action.yml
@@ -10,9 +10,6 @@ inputs:
   artifact-name:
     description: the name of the artifact where logs will be stored
     required: true
-  ceph-image:
-    description: the name of ceph image
-    required: true
 
 runs:
   using: "composite"
@@ -30,7 +27,7 @@ runs:
 
     - name: set Ceph version in CephCluster manifest
       shell: bash --noprofile --norc -eo pipefail -x {0}
-      run: tests/scripts/github-action-helper.sh replace_ceph_image  "deploy/examples/cluster-test.yaml" "${{ inputs.ceph-image }}"
+      run: tests/scripts/github-action-helper.sh replace_ceph_image  "deploy/examples/cluster-test.yaml" "${{ github.event.inputs.ceph-image }}"
 
     - name: use local disk and create partitions for osds
       shell: bash --noprofile --norc -eo pipefail -x {0}
diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml
index 0d7198753..89df3980a 100644
--- a/.github/workflows/golangci-lint.yaml
+++ b/.github/workflows/golangci-lint.yaml
@@ -24,10 +24,10 @@ jobs:
     name: golangci-lint
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
       - name: golangci-lint
@@ -52,7 +52,7 @@ jobs:
     name: govulncheck
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
          go-version: "1.22.5"
          check-latest: true
diff --git a/.github/workflows/helm-lint.yaml b/.github/workflows/helm-lint.yaml
index b274a0103..8a57bf634 100644
--- a/.github/workflows/helm-lint.yaml
+++ b/.github/workflows/helm-lint.yaml
@@ -26,7 +26,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -35,7 +35,7 @@ jobs:
         with:
           version: v3.6.2
 
-      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+      - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
         with:
           python-version: 3.9
 
diff --git a/.github/workflows/linters.yaml b/.github/workflows/linters.yaml
index 59d8dba4e..810eb2417 100644
--- a/.github/workflows/linters.yaml
+++ b/.github/workflows/linters.yaml
@@ -23,12 +23,12 @@ jobs:
   yaml-linter:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
       - name: Set up Python
-        uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+        uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
         with:
           python-version: 3.9
 
@@ -41,12 +41,12 @@ jobs:
   pylint:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
       - name: Set up Python
-        uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+        uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
         with:
           python-version: 3.9
 
diff --git a/.github/workflows/mod-check.yml b/.github/workflows/mod-check.yml
index 5ff39fa02..108eac712 100644
--- a/.github/workflows/mod-check.yml
+++ b/.github/workflows/mod-check.yml
@@ -29,11 +29,11 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
diff --git a/.github/workflows/multus.yaml b/.github/workflows/multus.yaml
index 230f36303..21c172ebf 100644
--- a/.github/workflows/multus.yaml
+++ b/.github/workflows/multus.yaml
@@ -36,12 +36,12 @@ jobs:
       NUMBER_OF_COMPUTE_NODES: 5
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
       - name: Set up Go version
-        uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+        uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
diff --git a/.github/workflows/push-build.yaml b/.github/workflows/push-build.yaml
index adae7dd72..8f5c08302 100644
--- a/.github/workflows/push-build.yaml
+++ b/.github/workflows/push-build.yaml
@@ -21,11 +21,11 @@ jobs:
     if: github.repository == 'rook/rook'
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
diff --git a/.github/workflows/rbac-gen.yaml b/.github/workflows/rbac-gen.yaml
index b4887cb5f..be342bf72 100644
--- a/.github/workflows/rbac-gen.yaml
+++ b/.github/workflows/rbac-gen.yaml
@@ -29,11 +29,11 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index e7c57051f..3cebd2816 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -28,7 +28,7 @@ jobs:
 
     steps:
       - name: "Checkout code"
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           persist-credentials: false
 
diff --git a/.github/workflows/shellcheck.yaml b/.github/workflows/shellcheck.yaml
index bd8d999e8..4fdca1376 100644
--- a/.github/workflows/shellcheck.yaml
+++ b/.github/workflows/shellcheck.yaml
@@ -24,7 +24,7 @@ jobs:
     name: Shellcheck
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+    - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
     - name: Run ShellCheck
       uses: ludeeus/action-shellcheck@00b27aa7cb85167568cb48a3838b75f4265f2bca # master
       with:
diff --git a/.github/workflows/snyk.yaml b/.github/workflows/snyk.yaml
index aee260d93..193e78e53 100644
--- a/.github/workflows/snyk.yaml
+++ b/.github/workflows/snyk.yaml
@@ -16,7 +16,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.mergify.yml b/.mergify.yml
index 28efa4c2c..6dae0ac98 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -272,7 +272,7 @@ pull_request_rules:
       - "check-success=canary-tests / multi-cluster-mirroring (quay.io/ceph/ceph:v18)"
       - "check-success=canary-tests / rgw-multisite-testing (quay.io/ceph/ceph:v18)"
       - "check-success=canary-tests / encryption-pvc-kms-ibm-kp (quay.io/ceph/ceph:v18)"
-      - "check-success=canary-tests / multus-cluster-network (quay.io/ceph/ceph:v18)" # note: changed name for 1.16 (multus-public-and-cluster)
+      - "check-success=canary-tests / multus-cluster-network (quay.io/ceph/ceph:v18)"
       - "check-success=canary-tests / csi-hostnetwork-disabled (quay.io/ceph/ceph:v18)"
       - "check-success=TestCephSmokeSuite (v1.26.15)"
       - "check-success=TestCephSmokeSuite (v1.31.0)"
diff --git a/Documentation/CRDs/Cluster/external-cluster/advance-external.md b/Documentation/CRDs/Cluster/external-cluster/advance-external.md
index 5034b0da9..4a273fc49 100644
--- a/Documentation/CRDs/Cluster/external-cluster/advance-external.md
+++ b/Documentation/CRDs/Cluster/external-cluster/advance-external.md
@@ -16,7 +16,7 @@ you can export the settings from this cluster with the following steps.
     kubectl -n rook-ceph cp deploy/examples/external/create-external-cluster-resources.py $toolbox:/etc/ceph
     ```
 
-2. Exec to the toolbox pod and execute create-external-cluster-resources.py with needed options to create required [users and keys](/Documentation/CRDs/Cluster/external-cluster/provider-export.md#1-create-all-users-and-keys).
+2. Exec to the toolbox pod and execute create-external-cluster-resources.py with needed options to create required [users and keys](provider-export.md#1-create-all-users-and-keys).
 
 !!! important
     For other clusters to connect to storage in this cluster, Rook must be configured with a networking configuration that is accessible from other clusters. Most commonly this is done by enabling host networking in the CephCluster CR so the Ceph daemons will be addressable by their host IPs.
diff --git a/Documentation/CRDs/Cluster/external-cluster/external-cluster.md b/Documentation/CRDs/Cluster/external-cluster/external-cluster.md
index 68b965fce..c788c508c 100644
--- a/Documentation/CRDs/Cluster/external-cluster/external-cluster.md
+++ b/Documentation/CRDs/Cluster/external-cluster/external-cluster.md
@@ -21,22 +21,22 @@ Create the desired types of storage in the provider Ceph cluster:
 
 ## Connect the external Ceph Provider cluster to the Rook consumer cluster
 
-1) [Export config from the Provider Ceph cluster](/Documentation/CRDs/Cluster/external-cluster/provider-export.md). Configuration must be exported by the Ceph admin, such as a Ceph keyring and mon endpoints, that allows connection to the Ceph cluster.
+1) [Export config from the Provider Ceph cluster](provider-export.md). Configuration must be exported by the Ceph admin, such as a Ceph keyring and mon endpoints, that allows connection to the Ceph cluster.
 
-2) [Import config to the Rook consumer cluster](/Documentation/CRDs/Cluster/external-cluster/consumer-import.md). The configuration exported from the Ceph cluster is imported to Rook to provide the needed connection details.
+2) [Import config to the Rook consumer cluster](consumer-import.md). The configuration exported from the Ceph cluster is imported to Rook to provide the needed connection details.
 
 ## Advance Options
 
-* [NFS storage](/Documentation/CRDs/Cluster/external-cluster/advance-external.md#nfs-storage)
+* [NFS storage](advance-external.md#nfs-storage)
 
-* [Exporting Rook to another cluster](/Documentation/CRDs/Cluster/external-cluster/advance-external.md#exporting-rook-to-another-cluster)
+* [Exporting Rook to another cluster](advance-external.md#exporting-rook-to-another-cluster)
 
-* [Run consumer Rook cluster with Admin privileges](/Documentation/CRDs/Cluster/external-cluster/advance-external.md#admin-privileges)
+* [Run consumer Rook cluster with Admin privileges](advance-external.md#admin-privileges)
 
-* [Connect to an External Object Store](/Documentation/CRDs/Cluster/external-cluster/advance-external.md#connect-to-an-external-object-store)
+* [Connect to an External Object Store](advance-external.md#connect-to-an-external-object-store)
 
 ## Upgrades
 
-* [Upgrade external cluster](/Documentation/CRDs/Cluster/external-cluster/upgrade-external.md#upgrade-external-cluster)
+* [Upgrade external cluster](upgrade-external.md#upgrade-external-cluster)
 
-* [Utilize new features in upgrade](/Documentation/CRDs/Cluster/external-cluster/upgrade-external.md#upgrade-cluster-to-utilize-new-feature)
+* [Utilize new features in upgrade](upgrade-external.md#upgrade-cluster-to-utilize-new-feature)
diff --git a/Documentation/CRDs/Cluster/external-cluster/provider-export.md b/Documentation/CRDs/Cluster/external-cluster/provider-export.md
index c8e2195a8..db64ab958 100644
--- a/Documentation/CRDs/Cluster/external-cluster/provider-export.md
+++ b/Documentation/CRDs/Cluster/external-cluster/provider-export.md
@@ -16,7 +16,7 @@ python3 create-external-cluster-resources.py --rbd-data-pool-name <pool_name> --
 * `--alias-rbd-data-pool-name`: Provides an alias for the  RBD data pool name, necessary if a special character is present in the pool name such as a period or underscore
 * `--rgw-endpoint`: (optional) The RADOS Gateway endpoint in the format `<IP>:<PORT>` or `<FQDN>:<PORT>`.
 * `--rgw-pool-prefix`: (optional) The prefix of the RGW pools. If not specified, the default prefix is `default`
-* `--rgw-tls-cert-path`: (optional) RADOS Gateway endpoint TLS certificate (or intermediate signing certificate) file path
+* `--rgw-tls-cert-path`: (optional) RADOS Gateway endpoint TLS certificate file path
 * `--rgw-skip-tls`: (optional) Ignore TLS certification validation when a self-signed certificate is provided (NOT RECOMMENDED)
 * `--rbd-metadata-ec-pool-name`: (optional) Provides the name of erasure coded RBD metadata pool, used for creating ECRBDStorageClass.
 * `--monitoring-endpoint`: (optional) Ceph Manager prometheus exporter endpoints (comma separated list of IP entries of active and standby mgrs)
diff --git a/Documentation/CRDs/Cluster/external-cluster/upgrade-external.md b/Documentation/CRDs/Cluster/external-cluster/upgrade-external.md
index 7ee3110ca..eddcb8283 100644
--- a/Documentation/CRDs/Cluster/external-cluster/upgrade-external.md
+++ b/Documentation/CRDs/Cluster/external-cluster/upgrade-external.md
@@ -47,11 +47,11 @@ Argument that are decided using the Priority: command-line-args has more priorit
 
 3. Make any desired modifications and additions to `config.ini``
 
-4. Run the python script again using the [config file](/Documentation/CRDs/Cluster/external-cluster/provider-export.md#config-file)
+4. Run the python script again using the [config file](provider-export.md#config-file)
 
-5. [Copy the bash output](/Documentation/CRDs/Cluster/external-cluster/provider-export.md#2-copy-the-bash-output)
+5. [Copy the bash output](provider-export.md#2-copy-the-bash-output)
 
-6. [Import-the-provider-data](/Documentation/CRDs/Cluster/external-cluster/consumer-import.md#import-the-provider-data)
+6. [Import-the-provider-data](consumer-import.md#import-the-provider-data)
 
 !!! warning
     If the last-applied config is unavailable, run the current version of the script again using previously-applied config and CLI flags.
diff --git a/Documentation/CRDs/Cluster/network-providers.md b/Documentation/CRDs/Cluster/network-providers.md
index c38607e4e..142d8cda4 100644
--- a/Documentation/CRDs/Cluster/network-providers.md
+++ b/Documentation/CRDs/Cluster/network-providers.md
@@ -414,3 +414,182 @@ spec:
       }
     }'
 ```
+
+## Holder Pod Deprecation
+
+Rook plans to remove CSI "holder" pods in Rook v1.16. CephCluster with `csi-*plugin-holder-*` pods
+present in the Rook operator namespace must plan to set `CSI_DISABLE_HOLDER_PODS` to `"true"` after
+Rook v1.14 is installed and before v1.16 is installed by following the migration sections below.
+CephClusters with no holder pods do not need to follow migration steps.
+
+Helm users will set `csi.disableHolderPods: true` in values.yaml instead of `CSI_DISABLE_HOLDER_PODS`.
+
+CephClusters that do not use `network.provider: multus` can follow the
+[Disabling Holder Pods](#disabling-holder-pods) section.
+
+CephClusters that use `network.provider: multus` will need to plan the migration more carefully.
+Read the [Disabling Holder Pods with Multus](#disabling-holder-pods-with-multus) section in full
+before beginning.
+
+!!! hint
+    To determine if holder pods are deployed, use
+    `kubectl --namespace $ROOK_OPERATOR get pods | grep plugin-holder`
+
+### Disabling Holder Pods with Multus
+
+This migration section applies when any CephCluster `network.provider` is `"multus"`. If the
+scenario does not apply, skip ahead to the [Disabling Holder Pods](#disabling-holder-pods) section.
+
+**Step 1**
+
+Before setting `CSI_ENABLE_HOST_NETWORK: "true"` and `CSI_DISABLE_HOLDER_PODS: "true"`, thoroughly
+read through the [Multus Prerequisites section](#multus-prerequisites). Use the prerequisites
+section to develop a plan for modifying host configurations as well as the public
+NetworkAttachmentDefinition.
+
+Once the plan is developed, execute the plan by following the steps below.
+
+**Step 2**
+
+First, modify the public NetworkAttachmentDefinition as needed. For example, it may be necessary to
+add the `routes` directive to the Whereabouts IPAM configuration as in
+[this example](#macvlan-whereabouts-node-static-ips).
+
+**Step 3**
+
+Next, modify the host configurations in the host configuration system. The host configuration system
+may be something like PXE, ignition config, cloud-init, Ansible, or any other such system. A node
+reboot is likely necessary to apply configuration updates, but wait until the next step to reboot
+nodes.
+
+If desired, check that the NetworkAttachmentDefinition modification and host configurations are
+compatible using the [Multus validation tool](#validating-multus-configuration). For the upgrade
+case, use the `hostCheckOnly: true` config option or `--host-check-only` CLI flag.
+
+**Step 4**
+
+After the NetworkAttachmentDefinition is modified, OSD pods must be restarted. It is easiest to
+complete this requirement at the same time nodes are being rebooted to apply configuration updates.
+
+For each node in the Kubernetes cluster:
+
+1. `cordon` and `drain` the node
+2. Wait for all pods to drain
+3. Reboot the node, ensuring the new host configuration will be applied
+4. `uncordon` and `undrain` the node
+5. Wait for the node to be rehydrated and stable
+6. Proceed to the next node
+
+By following this process, host configurations will be updated, and OSDs are also automatically
+restarted as part of the `drain` and `undrain` process on each node.
+
+OSDs can be restarted manually if node configuration updates do not require reboot.
+
+**Step 5**
+
+Once all nodes are running the new configuration and all OSDs have been restarted, check that the
+new node and NetworkAttachmentDefinition configurations are compatible. To do so, verify that each
+node can `ping` OSD pods via the public network.
+
+Use the [toolbox](../../Troubleshooting/ceph-toolbox.md) or the
+[kubectl plugin](../../Troubleshooting/kubectl-plugin.md) to list OSD IPs.
+
+The example below uses
+the kubectl plugin, and the OSD public network is 192.168.20.0/24.
+
+```console
+$ kubectl rook-ceph ceph osd dump | grep 'osd\.'
+osd.0 up   in  weight 1 up_from 7 up_thru 0 down_at 0 last_clean_interval [0,0) [v2:192.168.20.19:6800/213587265,v1:192.168.20.19:6801/213587265] [v2:192.168.30.1:6800/213587265,v1:192.168.30.1:6801/213587265] exists,up 7ebbc19a-d45a-4b12-8fef-0f9423a59e78
+osd.1 up   in  weight 1 up_from 24 up_thru 24 down_at 20 last_clean_interval [8,23) [v2:192.168.20.20:6800/3144257456,v1:192.168.20.20:6801/3144257456] [v2:192.168.30.2:6804/3145257456,v1:192.168.30.2:6805/3145257456] exists,up 146b27da-d605-4138-9748-65603ed0dfa5
+osd.2 up   in  weight 1 up_from 21 up_thru 0 down_at 20 last_clean_interval [18,20) [v2:192.168.20.21:6800/1809748134,v1:192.168.20.21:6801/1809748134] [v2:192.168.30.3:6804/1810748134,v1:192.168.30.3:6805/1810748134] exists,up ff3d6592-634e-46fd-a0e4-4fe9fafc0386
+```
+
+Now check that each node (NODE) can reach OSDs over the public network:
+
+```console
+$ ssh user@NODE
+$ user@NODE $> ping -c3 192.168.20.19
+# [truncated, successful output]
+$ user@NODE $> ping -c3 192.168.20.20
+# [truncated, successful output]
+$ user@NODE $> ping -c3 192.168.20.21
+# [truncated, successful output]
+```
+
+If any node does not get a successful `ping` to a running OSD, it is not safe to proceed. A problem
+may arise here for many reasons. Some reasons include: the host may not be properly attached to the
+Multus public network, the public NetworkAttachmentDefinition may not be properly configured to
+route back to the host, the host may have a firewall rule blocking the connection in either
+direction, or the network switch may have a firewall rule blocking the connection. Diagnose and fix
+the issue, then return to **Step 1**.
+
+**Step 6**
+
+If the above check succeeds for all nodes, proceed with the
+[Disabling Holder Pods](#disabling-holder-pods) steps below.
+
+### Disabling Holder Pods
+
+**Step 1**
+
+If any CephClusters have Multus enabled (`network.provider: "multus"`), follow the
+[Disabling Holder Pods with Multus](#disabling-holder-pods-with-multus)
+steps above before continuing.
+
+**Step 2**
+
+Begin by setting `CSI_DISABLE_HOLDER_PODS: "true"`. If `CSI_ENABLE_HOST_NETWORK` is set to
+`"false"`, also set this value to `"true"` at the same time.
+
+After this, `csi-*plugin-*` pods will restart, and `csi-*plugin-holder-*` pods will remain running.
+
+**Step 3**
+
+Check that CSI pods are using the correct host networking configuration using the example below as
+guidance (in the example, `CSI_ENABLE_HOST_NETWORK` is `"true"`):
+
+```console
+$ kubectl -n rook-ceph get -o yaml daemonsets.apps csi-rbdplugin | grep -i hostnetwork
+      hostNetwork: true
+$ kubectl -n rook-ceph get -o yaml daemonsets.apps csi-cephfsplugin | grep -i hostnetwork
+      hostNetwork: true
+$ kubectl -n rook-ceph get -o yaml daemonsets.apps csi-nfsplugin | grep -i hostnetwork
+      hostNetwork: true
+```
+
+**Step 4**
+
+At this stage, PVCs for running applications are still using the holder pods. These PVCs must be
+migrated from the holder to the new network. Follow the below process to do so.
+
+For each node in the Kubernetes cluster:
+
+1. `cordon` and `drain` the node
+2. Wait for all pods to drain
+3. Delete all `csi-*plugin-holder*` pods on the node (a new holder will take it's place)
+4. `uncordon` and `undrain` the node
+5. Wait for the node to be rehydrated and stable
+6. Proceed to the next node
+
+**Step 5**
+
+After this process is done for all Kubernetes nodes, it is safe to delete the `csi-*plugin-holder*`
+daemonsets.
+
+Delete the holder daemonsets using the example below as guidance:
+
+```console
+$ kubectl -n rook-ceph get daemonset -o name | grep plugin-holder
+daemonset.apps/csi-cephfsplugin-holder-my-cluster
+daemonset.apps/csi-rbdplugin-holder-my-cluster
+
+$ kubectl -n rook-ceph delete daemonset.apps/csi-cephfsplugin-holder-my-cluster
+daemonset.apps "csi-cephfsplugin-holder-my-cluster" deleted
+
+$ kubectl -n rook-ceph delete daemonset.apps/csi-rbdplugin-holder-my-cluster
+daemonset.apps "csi-rbdplugin-holder-my-cluster" deleted
+```
+
+**Step 6**
+
+The migration is now complete! Congratulations!
diff --git a/Documentation/CRDs/specification.md b/Documentation/CRDs/specification.md
index e0b015109..dd6949d64 100644
--- a/Documentation/CRDs/specification.md
+++ b/Documentation/CRDs/specification.md
@@ -10959,17 +10959,6 @@ string
 </tr>
 <tr>
 <td>
-<code>default</code><br/>
-<em>
-bool
-</em>
-</td>
-<td>
-<p>Sets given placement as default. Only one placement in the list can be marked as default.</p>
-</td>
-</tr>
-<tr>
-<td>
 <code>metadataPoolName</code><br/>
 <em>
 string
diff --git a/Documentation/Helm-Charts/operator-chart.md b/Documentation/Helm-Charts/operator-chart.md
index 63ec4f542..a78ddc1ad 100644
--- a/Documentation/Helm-Charts/operator-chart.md
+++ b/Documentation/Helm-Charts/operator-chart.md
@@ -84,6 +84,7 @@ The following table lists the configurable parameters of the rook-operator chart
 | `csi.csiRBDPluginVolumeMount` | The volume mounts of the CephCSI RBD plugin DaemonSet | `nil` |
 | `csi.csiRBDProvisionerResource` | CEPH CSI RBD provisioner resource requirement list csi-omap-generator resources will be applied only if `enableOMAPGenerator` is set to `true` | see values.yaml |
 | `csi.disableCsiDriver` | Disable the CSI driver. | `"false"` |
+| `csi.disableHolderPods` | Deprecation note: Rook uses "holder" pods to allow CSI to connect to the multus public network without needing hosts to the network. Holder pods are being removed. See issue for details: https://github.com/rook/rook/issues/13055. New Rook deployments should set this to "true". | `true` |
 | `csi.enableCSIEncryption` | Enable Ceph CSI PVC encryption support | `false` |
 | `csi.enableCSIHostNetwork` | Enable host networking for CSI CephFS and RBD nodeplugins. This may be necessary in some network configurations where the SDN does not provide access to an external cluster or there is significant drop in read/write performance | `true` |
 | `csi.enableCephfsDriver` | Enable Ceph CSI CephFS driver | `true` |
diff --git a/Documentation/Storage-Configuration/Advanced/key-management-system.md b/Documentation/Storage-Configuration/Advanced/key-management-system.md
index 098ebcc28..841beb39b 100644
--- a/Documentation/Storage-Configuration/Advanced/key-management-system.md
+++ b/Documentation/Storage-Configuration/Advanced/key-management-system.md
@@ -21,7 +21,7 @@ The `security` section contains settings related to encryption of the cluster.
         * `schedule`: the schedule, written in [cron format](https://en.wikipedia.org/wiki/Cron), with which key rotation [CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/) is created, default value is `"@weekly"`.
 
 !!! note
-    Currently key rotation is supported when the Key Encryption Keys are stored in a Kubernetes Secret or Vault KMS.
+    Currently key rotation is only supported for the default type, where the Key Encryption Keys are stored in a Kubernetes Secret.
 
 Supported KMS providers:
 
diff --git a/Documentation/Storage-Configuration/Object-Storage-RGW/ceph-object-bucket-claim.md b/Documentation/Storage-Configuration/Object-Storage-RGW/ceph-object-bucket-claim.md
index a119b4c71..c2be34c68 100644
--- a/Documentation/Storage-Configuration/Object-Storage-RGW/ceph-object-bucket-claim.md
+++ b/Documentation/Storage-Configuration/Object-Storage-RGW/ceph-object-bucket-claim.md
@@ -32,8 +32,6 @@ spec:
   additionalConfig: [6]
     maxObjects: "1000"
     maxSize: "2G"
-    bucketMaxObjects: "3000"
-    bucketMaxSize: "4G"
 ```
 
 1. `name` of the `ObjectBucketClaim`. This name becomes the name of the Secret and ConfigMap.
@@ -47,10 +45,8 @@ If both `bucketName` and `generateBucketName` are blank or omitted then the stor
 5. `storageClassName` which defines the StorageClass which contains the names of the bucket provisioner, the object-store and specifies the bucket retention policy.
 6. `additionalConfig` is an optional list of key-value pairs used to define attributes specific to the bucket being provisioned by this OBC. This information is typically tuned to a particular bucket provisioner and may limit application portability. Options supported:
 
-    * `maxObjects`: The maximum number of objects in the bucket as a quota on the user account automatically created for the bucket.
-    * `maxSize`: The maximum size of the bucket as a quota on the user account automatically created for the bucket. Please note minimum recommended value is 4K.
-    * `bucketMaxObjects`: The maximum number of objects in the bucket as an individual bucket quota. This is useful when the bucket is shared among multiple users.
-    * `bucketMaxSize`: The maximum size of the bucket as an individual bucket quota.
+    * `maxObjects`: The maximum number of objects in the bucket
+    * `maxSize`: The maximum size of the bucket, please note minimum recommended value is 4K.
 
 ### OBC Custom Resource after Bucket Provisioning
 
diff --git a/Documentation/Storage-Configuration/Object-Storage-RGW/object-storage.md b/Documentation/Storage-Configuration/Object-Storage-RGW/object-storage.md
index 10123bef9..3ada73c7e 100644
--- a/Documentation/Storage-Configuration/Object-Storage-RGW/object-storage.md
+++ b/Documentation/Storage-Configuration/Object-Storage-RGW/object-storage.md
@@ -201,15 +201,14 @@ Object Storage API allows users to override where bucket data will be stored dur
 To enable this feature, configure `poolPlacements` representing a list of possible bucket data locations.
 Each `poolPlacement` must have:
 
-* a **unique** `name` to refer to it in `<LocationConstraint>` or `X-Storage-Policy`. Name `default-placement` is reserved and can be used **only** if placement also marked as `default`.
-* **optional** `default` flag to use given placement by default, meaning that it will be used if no location constraint is provided. Only one placement in the list can be marked as default.
+* a **unique** `name` to refer to it in `<LocationConstraint>` or `X-Storage-Policy`. A placement with reserved name `default` will be used by default if no location constraint is provided.
 * `dataPoolName` and `metadataPoolName` representing object data and metadata locations. In Rook, these data locations are backed by `CephBlockPool`. `poolPlacements` and `storageClasses` specs refer pools by name. This means that all pools should be defined in advance. Similarly to [sharedPools](#create-local-object-stores-with-shared-pools), the same pool can be reused across multiple ObjectStores and/or poolPlacements/storageClasses because of RADOS namespaces. Here, each pool will be namespaced with `<object store name>.<placement name>.<pool type>` key.
 * **optional** `dataNonECPoolName` - extra pool for data that cannot use erasure coding (ex: multi-part uploads). If not set, `metadataPoolName` will be used.
 * **optional** list of placement `storageClasses`. Classes defined per placement, which means that even classes of `default` placement will be available only within this placement and not others. Each placement will automatically have default storage class named `STANDARD`. `STANDARD` class always points to placement `dataPoolName` and cannot be removed or redefined. Each storage class must have:
     * `name` (unique within placement). RGW allows arbitrary name for StorageClasses, however some clients/libs insist on AWS names so it is recommended to use one of the valid `x-amz-storage-class` values for better compatibility: `STANDARD | REDUCED_REDUNDANCY | STANDARD_IA | ONEZONE_IA | INTELLIGENT_TIERING | GLACIER | DEEP_ARCHIVE | OUTPOSTS | GLACIER_IR | SNOW | EXPRESS_ONEZONE`. See [AWS docs](https://aws.amazon.com/s3/storage-classes/).
     * `dataPoolName` - overrides placement data pool when this class is selected by user.
 
-Example: Configure `CephObjectStore` with `default` placement `us` pools and placement `europe` pointing to pools in corresponding geographies. These geographical locations are only an example. Placement name can be arbitrary and could reflect the backing pool's replication factor, device class, or failure domain. This example also  defines storage class `REDUCED_REDUNDANCY` for each placement.
+Example: Configure `CephObjectStore` with `default` placement pointing to `us` pools and placement `europe` pointing to pools in corresponding geographies. These geographical locations are only an example. Placement name can be arbitrary and could reflect the backing pool's replication factor, device class, or failure domain. This example also  defines storage class `REDUCED_REDUNDANCY` for each placement.
 
 ```yaml
 apiVersion: ceph.rook.io/v1
@@ -223,8 +222,7 @@ spec:
     instances: 1
   sharedPools:
     poolPlacements:
-    - name: us
-      default: true
+    - name: default
       metadataPoolName: "us-data-pool"
       dataPoolName: "us-meta-pool"
       storageClasses:
diff --git a/Documentation/Storage-Configuration/ceph-teardown.md b/Documentation/Storage-Configuration/ceph-teardown.md
index 6196854ed..b207efc47 100644
--- a/Documentation/Storage-Configuration/ceph-teardown.md
+++ b/Documentation/Storage-Configuration/ceph-teardown.md
@@ -205,4 +205,3 @@ This cleanup is supported only for the following custom resources:
 | --------                             | ------- |
 | CephFilesystemSubVolumeGroup         | CSI stored RADOS OMAP details for pvc/volumesnapshots, subvolume snapshots, subvolume clones, subvolumes |
 | CephBlockPoolRadosNamespace          | Images and snapshots in the RADOS namespace|
-| CephBlockPool                        | Images and snapshots in the BlockPool|
diff --git a/Documentation/Upgrade/rook-upgrade.md b/Documentation/Upgrade/rook-upgrade.md
index 9d6bc6e6b..e9197240f 100644
--- a/Documentation/Upgrade/rook-upgrade.md
+++ b/Documentation/Upgrade/rook-upgrade.md
@@ -49,12 +49,22 @@ those releases.
     official releases. Builds from the master branch can have functionality changed or removed at any
     time without compatibility support and without prior notice.
 
-## Breaking changes in v1.16
+## Breaking changes in v1.15
+
+* Rook has deprecated CSI network "holder" pods.
+    If there are pods named `csi-*plugin-holder-*` in the Rook operator namespace, see the
+    [detailed documentation](../CRDs/Cluster/network-providers.md#holder-pod-deprecation)
+    to disable them. This deprecation process is required before upgrading to the future Rook v1.16.
+
+* Ceph COSI driver images have been updated. This impacts existing COSI Buckets, BucketClaims, and
+    BucketAccesses. Update existing clusters following the guide
+    [here](https://github.com/rook/rook/discussions/14297).
+
+* CephObjectStore, CephObjectStoreUser, and OBC endpoint behavior has changed when CephObjectStore
+    `spec.hosting` configurations are set. Use the new `spec.hosting.advertiseEndpoint` config to
+    define required behavior as
+    [documented](../Storage-Configuration/Object-Storage-RGW/object-storage.md#object-store-endpoint).
 
-* Rook fully deprecated CSI "holder" pods in Rook v1.16. Any Rook v1.15 CephCluster with
-    `csi-*plugin-holder-*` pods present in the Rook operator namespace must follow holder pod
-    removal migration steps outlined in
-    [v1.15 documentation](https://rook.io/docs/rook/v1.15/CRDs/Cluster/network-providers/?h=depre#holder-pod-deprecation).
 
 ## Considerations
 
@@ -101,6 +111,10 @@ Also update optional resources like Prometheus monitoring noted more fully in th
 If Rook is installed via the Helm chart, Helm will handle some details of the upgrade itself.
 The upgrade steps in this guide will clarify what Helm handles automatically.
 
+!!! important
+    If there are pods named `csi-*plugin-holder-*` in the Rook operator namespace, set the new
+    config `csi.disableHolderPods: false` in the values.yaml before upgrading to v1.15.
+
 The `rook-ceph` helm chart upgrade performs the Rook upgrade.
 The `rook-ceph-cluster` helm chart upgrade performs a [Ceph upgrade](./ceph-upgrade.md) if the Ceph image is updated.
 The `rook-ceph` chart should be upgraded before `rook-ceph-cluster`, so the latest operator has the opportunity to update
@@ -248,3 +262,10 @@ This cluster is finished:
 At this point, the Rook operator should be running version `rook/ceph:v1.15.0`.
 
 Verify the CephCluster health using the [health verification doc](health-verification.md).
+
+### **6. Disable holder pods**
+
+Rook has deprecated CSI network "holder" pods. If there are pods named
+`csi-*plugin-holder-*` in the Rook operator namespace, see the
+[detailed documentation](../CRDs/Cluster/network-providers.md#holder-pod-deprecation)
+to disable them. This deprecation process is required before upgrading to the future Rook v1.16.
diff --git a/cmd/rook/ceph/cleanup.go b/cmd/rook/ceph/cleanup.go
index 0a2dea1fd..f158619d9 100644
--- a/cmd/rook/ceph/cleanup.go
+++ b/cmd/rook/ceph/cleanup.go
@@ -62,12 +62,6 @@ var cleanUpRadosNamespaceCmd = &cobra.Command{
 	Short: "Starts the cleanup process for a CephBlockPoolRadosNamespace",
 }
 
-var cleanUpBlockPoolCmd = &cobra.Command{
-	// the subcommand matches CRD kind of the custom resource to be cleaned up
-	Use:   "CephBlockPool",
-	Short: "Starts the cleanup process for a CephBlockPool",
-}
-
 func init() {
 	cleanUpHostCmd.Flags().StringVar(&dataDirHostPath, "data-dir-host-path", "", "dataDirHostPath on the node")
 	cleanUpHostCmd.Flags().StringVar(&namespaceDir, "namespace-dir", "", "dataDirHostPath on the node")
@@ -80,12 +74,11 @@ func init() {
 	flags.SetFlagsFromEnv(cleanUpHostCmd.Flags(), rook.RookEnvVarPrefix)
 	flags.SetFlagsFromEnv(cleanUpSubVolumeGroupCmd.Flags(), rook.RookEnvVarPrefix)
 
-	cleanUpCmd.AddCommand(cleanUpHostCmd, cleanUpSubVolumeGroupCmd, cleanUpRadosNamespaceCmd, cleanUpBlockPoolCmd)
+	cleanUpCmd.AddCommand(cleanUpHostCmd, cleanUpSubVolumeGroupCmd, cleanUpRadosNamespaceCmd)
 
 	cleanUpHostCmd.RunE = startHostCleanUp
 	cleanUpSubVolumeGroupCmd.RunE = startSubVolumeGroupCleanUp
 	cleanUpRadosNamespaceCmd.RunE = startRadosNamespaceCleanup
-	cleanUpBlockPoolCmd.RunE = startBlockPoolCleanup
 }
 
 func startHostCleanUp(cmd *cobra.Command, args []string) error {
@@ -181,25 +174,3 @@ func startRadosNamespaceCleanup(cmd *cobra.Command, args []string) error {
 
 	return nil
 }
-
-func startBlockPoolCleanup(cmd *cobra.Command, args []string) error {
-	rook.SetLogLevel()
-	rook.LogStartupInfo(cleanUpRadosNamespaceCmd.Flags())
-
-	ctx := cmd.Context()
-	context := createContext()
-	namespace := os.Getenv(k8sutil.PodNamespaceEnvVar)
-	clusterInfo := client.AdminClusterInfo(ctx, namespace, "")
-
-	poolName := os.Getenv(opcontroller.CephBlockPoolNameEnv)
-	if poolName == "" {
-		rook.TerminateFatal(fmt.Errorf("cephblockpool name is not available in the pod environment variables"))
-	}
-
-	err := cleanup.BlockPoolCleanup(context, clusterInfo, poolName)
-	if err != nil {
-		rook.TerminateFatal(fmt.Errorf("failed to cleanup cephBlockPool %q resource %v", poolName, err))
-	}
-
-	return nil
-}
diff --git a/deploy/charts/rook-ceph/templates/configmap.yaml b/deploy/charts/rook-ceph/templates/configmap.yaml
index 98b99741d..c29dbcd74 100644
--- a/deploy/charts/rook-ceph/templates/configmap.yaml
+++ b/deploy/charts/rook-ceph/templates/configmap.yaml
@@ -38,6 +38,7 @@ data:
   CSI_ENABLE_ENCRYPTION: {{ .Values.csi.enableCSIEncryption | quote }}
   CSI_ENABLE_OMAP_GENERATOR: {{ .Values.csi.enableOMAPGenerator | quote }}
   CSI_ENABLE_HOST_NETWORK: {{ .Values.csi.enableCSIHostNetwork | quote }}
+  CSI_DISABLE_HOLDER_PODS: {{ .Values.csi.disableHolderPods | quote }}
   CSI_ENABLE_METADATA: {{ .Values.csi.enableMetadata | quote }}
   CSI_ENABLE_VOLUME_GROUP_SNAPSHOT: {{ .Values.csi.enableVolumeGroupSnapshot | quote }}
 {{- if .Values.csi.csiDriverNamePrefix }}
diff --git a/deploy/charts/rook-ceph/templates/resources.yaml b/deploy/charts/rook-ceph/templates/resources.yaml
index 77ff73089..12c2d1ffb 100644
--- a/deploy/charts/rook-ceph/templates/resources.yaml
+++ b/deploy/charts/rook-ceph/templates/resources.yaml
@@ -12383,9 +12383,6 @@ spec:
                             description: The data pool used to store ObjectStore objects data.
                             minLength: 1
                             type: string
-                          default:
-                            description: Sets given placement as default. Only one placement in the list can be marked as default.
-                            type: boolean
                           metadataPoolName:
                             description: The metadata pool used to store ObjectStore bucket index.
                             minLength: 1
@@ -12423,7 +12420,6 @@ spec:
                             type: array
                         required:
                           - dataPoolName
-                          - default
                           - metadataPoolName
                           - name
                         type: object
@@ -13309,9 +13305,6 @@ spec:
                             description: The data pool used to store ObjectStore objects data.
                             minLength: 1
                             type: string
-                          default:
-                            description: Sets given placement as default. Only one placement in the list can be marked as default.
-                            type: boolean
                           metadataPoolName:
                             description: The metadata pool used to store ObjectStore bucket index.
                             minLength: 1
@@ -13349,7 +13342,6 @@ spec:
                             type: array
                         required:
                           - dataPoolName
-                          - default
                           - metadataPoolName
                           - name
                         type: object
diff --git a/deploy/charts/rook-ceph/values.yaml b/deploy/charts/rook-ceph/values.yaml
index 5b48fc868..7e3d47bb3 100644
--- a/deploy/charts/rook-ceph/values.yaml
+++ b/deploy/charts/rook-ceph/values.yaml
@@ -88,6 +88,10 @@ csi:
   # in some network configurations where the SDN does not provide access to an external cluster or
   # there is significant drop in read/write performance
   enableCSIHostNetwork: true
+  # -- Deprecation note: Rook uses "holder" pods to allow CSI to connect to the multus public network
+  # without needing hosts to the network. Holder pods are being removed. See issue for details:
+  # https://github.com/rook/rook/issues/13055. New Rook deployments should set this to "true".
+  disableHolderPods: true
   # -- Enable Snapshotter in CephFS provisioner pod
   enableCephfsSnapshotter: true
   # -- Enable Snapshotter in NFS provisioner pod
@@ -636,7 +640,7 @@ enforceHostNetwork: false
 disableDeviceHotplug: false
 
 # -- The revision history limit for all pods created by Rook. If blank, the K8s default is 10.
-revisionHistoryLimit:
+revisionHistoryLimit: 
 
 # -- Blacklist certain disks according to the regex provided.
 discoverDaemonUdev:
diff --git a/deploy/examples/crds.yaml b/deploy/examples/crds.yaml
index b10029784..1ffc720b8 100644
--- a/deploy/examples/crds.yaml
+++ b/deploy/examples/crds.yaml
@@ -12374,9 +12374,6 @@ spec:
                             description: The data pool used to store ObjectStore objects data.
                             minLength: 1
                             type: string
-                          default:
-                            description: Sets given placement as default. Only one placement in the list can be marked as default.
-                            type: boolean
                           metadataPoolName:
                             description: The metadata pool used to store ObjectStore bucket index.
                             minLength: 1
@@ -12414,7 +12411,6 @@ spec:
                             type: array
                         required:
                           - dataPoolName
-                          - default
                           - metadataPoolName
                           - name
                         type: object
@@ -13297,9 +13293,6 @@ spec:
                             description: The data pool used to store ObjectStore objects data.
                             minLength: 1
                             type: string
-                          default:
-                            description: Sets given placement as default. Only one placement in the list can be marked as default.
-                            type: boolean
                           metadataPoolName:
                             description: The metadata pool used to store ObjectStore bucket index.
                             minLength: 1
@@ -13337,7 +13330,6 @@ spec:
                             type: array
                         required:
                           - dataPoolName
-                          - default
                           - metadataPoolName
                           - name
                         type: object
diff --git a/deploy/examples/create-external-cluster-resources.py b/deploy/examples/create-external-cluster-resources.py
index d059a1456..268176c4f 100644
--- a/deploy/examples/create-external-cluster-resources.py
+++ b/deploy/examples/create-external-cluster-resources.py
@@ -377,7 +377,7 @@ class RadosJSON:
             "--rgw-tls-cert-path",
             default="",
             required=False,
-            help="RADOS Gateway endpoint TLS certificate (or intermediate signing certificate)",
+            help="RADOS Gateway endpoint TLS certificate",
         )
         output_group.add_argument(
             "--rgw-skip-tls",
diff --git a/deploy/examples/operator-openshift.yaml b/deploy/examples/operator-openshift.yaml
index 239b75b16..876a029f0 100644
--- a/deploy/examples/operator-openshift.yaml
+++ b/deploy/examples/operator-openshift.yaml
@@ -139,6 +139,11 @@ data:
   # there is significant drop in read/write performance.
   # CSI_ENABLE_HOST_NETWORK: "true"
 
+  # Deprecation note: Rook uses "holder" pods to allow CSI to connect to the multus public network
+  # without needing hosts to the network. Holder pods are being removed. See issue for details:
+  # https://github.com/rook/rook/issues/13055. New Rook deployments should set this to "true".
+  CSI_DISABLE_HOLDER_PODS: "true"
+
   # Set logging level for cephCSI containers maintained by the cephCSI.
   # Supported values from 0 to 5. 0 for general useful logs, 5 for trace level verbosity.
   # CSI_LOG_LEVEL: "0"
diff --git a/deploy/examples/operator.yaml b/deploy/examples/operator.yaml
index 1a90a5193..0533051ff 100644
--- a/deploy/examples/operator.yaml
+++ b/deploy/examples/operator.yaml
@@ -51,6 +51,11 @@ data:
   # there is significant drop in read/write performance.
   # CSI_ENABLE_HOST_NETWORK: "true"
 
+  # Deprecation note: Rook uses "holder" pods to allow CSI to connect to the multus public network
+  # without needing hosts to the network. Holder pods are being removed. See issue for details:
+  # https://github.com/rook/rook/issues/13055. New Rook deployments should set this to "true".
+  CSI_DISABLE_HOLDER_PODS: "true"
+
   # Set to true to enable adding volume metadata on the CephFS subvolume and RBD images.
   # Not all users might be interested in getting volume/snapshot details as metadata on CephFS subvolume and RBD images.
   # Hence enable metadata is false by default.
diff --git a/design/ceph/multus-network.md b/design/ceph/multus-network.md
index 901904683..045cba88b 100644
--- a/design/ceph/multus-network.md
+++ b/design/ceph/multus-network.md
@@ -93,6 +93,50 @@ network.
 The design for mitigating the issue is to add a new DaemonSet that will own the network for all CephFS mounts
 as well as RBD mapped devices. The `csi-{cephfs,rbd}plugin` DaemonSet are left untouched.
 
+#### New "holder" DaemonSet
+The Rook-Ceph Operator's CSI controller creates a `csi-plugin-holder` DaemonSet configured to use the
+`network.selectors.public` network specified for the CephCluster. This DaemonSet runs on all the
+nodes along side the `csi-{cephfs,rbd}plugin` DaemonSet.
+
+This Pod should only be stopped and restarted when a node is stopped so that volume operations do
+not become blocked. The Rook-Ceph Operator's CSI controller should set the DaemonSet's update
+strategy to `OnDelete` so that the pods do not get deleted if the DaemonSet is updated while also
+ensuring that the pods will be updated on the next node reboot (or node drain).
+
+The new holder DaemonSet only contains a single container called `holder`, the container
+responsible for pinning the network for filesystem mounts and mapped block devices. It is used as a passthrough by
+the Ceph-CSI plugin pod which when mounting or mapping will use the network namespace of that holder
+pod.
+
+One task of the Holder container is to pass the required PID into the `csi-plugin` pod. To
+solve this, the container will create a symlink to an equivalent of `/proc/$$/ns/net` in the
+`hostPath` volume shared with the `csi-plugin` pod. Then it will sleep forever.
+In order to support multiple Ceph-CSI consuming multiple Ceph clusters, the name of the symlink should be based on the Ceph
+cluster FSID. The complete name of the symlink can be decided during the implementation phase of the
+design.
+That symlink name will be stored in the Ceph-CSI configmap with a key name that we will define
+during the implementation.
+
+#### Interactions between components
+When the Ceph-CSI plugin mounts or maps a block device it will use the network namespace of the
+Holder pod. This is achieved by using `nsenter` to enter the network namespace of the Holder pod.
+Some changes are expected as part of the mounting/mapping method in today's `csi-plugin` pod.
+Instead of invoking `rbd map ...`, invoke `nsenter --net=<net
+ns file of pause in long-running pod> rbd map ...` (and same for `mount -t ceph ...`).  The `csi-plugin`
+pod is already privileged, it already adds `CAP_SYS_ADMIN`. The only missing piece is to expose the host
+PID namespace with `hostPID: true` on the `csi-cephfsplugin` DaemonSet (RBD already has it).
+
+Cleaning up the symlink is not a requirement nor a big issue. Once the Holder container stops, its
+network namespace goes away with it. So at worst, we end up with a broken symlink.
+
+When a new node is added, both DaemonSets are started, and the
+process described above occurs on the node.
+
+The initial implementation of this design is limited to supporting a single CephCluster with
+Multus.
+Until we stop using HostNetwork entirely it is impossible to support multiple CephClusters with and
+without Multus enabled.
+
 ## Accepted proposal
 
 So far, the team has decided to go with the [whereabouts](https://github.com/dougbtv/whereabouts) IPAM.
diff --git a/pkg/apis/ceph.rook.io/v1/types.go b/pkg/apis/ceph.rook.io/v1/types.go
index 633b9153c..61812212b 100755
--- a/pkg/apis/ceph.rook.io/v1/types.go
+++ b/pkg/apis/ceph.rook.io/v1/types.go
@@ -1563,9 +1563,6 @@ type PoolPlacementSpec struct {
 	// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9._/-]+$`
 	Name string `json:"name"`
 
-	// Sets given placement as default. Only one placement in the list can be marked as default.
-	Default bool `json:"default"`
-
 	// The metadata pool used to store ObjectStore bucket index.
 	// +kubebuilder:validation:Required
 	// +kubebuilder:validation:MinLength=1
diff --git a/pkg/daemon/ceph/cleanup/radosnamespace.go b/pkg/daemon/ceph/cleanup/radosnamespace.go
index a7381b0d1..1ebb6f09a 100644
--- a/pkg/daemon/ceph/cleanup/radosnamespace.go
+++ b/pkg/daemon/ceph/cleanup/radosnamespace.go
@@ -17,8 +17,6 @@ limitations under the License.
 package cleanup
 
 import (
-	"fmt"
-
 	"github.com/pkg/errors"
 	"github.com/rook/rook/pkg/clusterd"
 	"github.com/rook/rook/pkg/daemon/ceph/client"
@@ -28,67 +26,46 @@ import (
 func RadosNamespaceCleanup(context *clusterd.Context, clusterInfo *client.ClusterInfo, poolName, radosNamespace string) error {
 	logger.Infof("starting clean up of CephBlockPoolRadosNamespace %q resources in cephblockpool %q", radosNamespace, poolName)
 
-	err := cleanupImages(context, clusterInfo, poolName, radosNamespace)
-	if err != nil {
-		logger.Errorf("failed to clean up CephBlockPoolRadosNamespace %q resources in cephblockpool %q", radosNamespace, poolName)
-		return err
-	}
-
-	logger.Infof("successfully cleaned up CephBlockPoolRadosNamespace %q resources in cephblockpool %q", radosNamespace, poolName)
-	return nil
-}
-
-func cleanupImages(context *clusterd.Context, clusterInfo *client.ClusterInfo, poolName, radosNamespace string) error {
-	msg := fmt.Sprintf("cephblockpool %q", poolName)
-	if radosNamespace != "" {
-		msg = fmt.Sprintf("%s in rados namespace %q", msg, radosNamespace)
-	}
 	images, err := cephclient.ListImagesInRadosNamespace(context, clusterInfo, poolName, radosNamespace)
 	if err != nil {
-		return errors.Wrapf(err, "failed to list images in %s", msg)
+		return errors.Wrapf(err, "failed to list images in cephblockpool %q in rados namespace %q", poolName, radosNamespace)
 	}
 
 	var retErr error
 	for _, image := range images {
 		snaps, err := cephclient.ListSnapshotsInRadosNamespace(context, clusterInfo, poolName, image.Name, radosNamespace)
 		if err != nil {
-			retErr = errors.Wrapf(err, "failed to list snapshots for the image %q in %s", image.Name, msg)
+			retErr = errors.Wrapf(err, "failed to list snapshots for the image %q in cephblockpool %q in rados namespace %q.", image.Name, poolName, radosNamespace)
 			logger.Error(retErr)
 		}
 
 		for _, snap := range snaps {
 			err := cephclient.DeleteSnapshotInRadosNamespace(context, clusterInfo, poolName, image.Name, snap.Name, radosNamespace)
 			if err != nil {
-				retErr = errors.Wrapf(err, "failed to delete snapshot %q of the image %q in %s", snap.Name, image.Name, msg)
+				retErr = errors.Wrapf(err, "failed to delete snapshot %q of the image %q in cephblockpool %q in rados namespace %q.", snap.Name, image.Name, poolName, radosNamespace)
 				logger.Error(retErr)
 			} else {
-				logger.Infof("successfully deleted snapshot %q of image %q in %s", snap.Name, image.Name, msg)
+				logger.Infof("successfully deleted snapshot %q of image %q in cephblockpool %q in rados namespace %q", snap.Name, image.Name, poolName, radosNamespace)
 			}
 		}
 
 		err = cephclient.MoveImageToTrashInRadosNamespace(context, clusterInfo, poolName, image.Name, radosNamespace)
 		if err != nil {
-			retErr = errors.Wrapf(err, "failed to move image %q to trash in %s", image.Name, msg)
+			retErr = errors.Wrapf(err, "failed to move image %q to trash in cephblockpool %q in rados namespace %q.", image.Name, poolName, radosNamespace)
 			logger.Error(retErr)
 		}
 		err = cephclient.DeleteImageFromTrashInRadosNamespace(context, clusterInfo, poolName, image.ID, radosNamespace)
 		if err != nil {
-			retErr = errors.Wrapf(err, "failed to add task to remove image %q from trash in %s", image.Name, msg)
+			retErr = errors.Wrapf(err, "failed to add task to remove image %q from trash in cephblockpool %q in rados namespace %q.", image.Name, poolName, radosNamespace)
 			logger.Error(retErr)
 		}
 	}
-	return retErr
-}
-
-func BlockPoolCleanup(context *clusterd.Context, clusterInfo *client.ClusterInfo, poolName string) error {
-	logger.Infof("starting clean up of CephBlockPool %q resource", poolName)
 
-	err := cleanupImages(context, clusterInfo, poolName, "")
-	if err != nil {
-		logger.Errorf("failed to clean up CephBlockPool %q resource", poolName)
-		return err
+	if retErr != nil {
+		logger.Errorf("failed to clean up CephBlockPoolRadosNamespace %q resources in cephblockpool %q", radosNamespace, poolName)
+		return retErr
 	}
 
-	logger.Infof("successfully cleaned up CephBlockPool %q resource", poolName)
+	logger.Infof("successfully cleaned up CephBlockPoolRadosNamespace %q resources in cephblockpool %q", radosNamespace, poolName)
 	return nil
 }
diff --git a/pkg/daemon/ceph/client/image.go b/pkg/daemon/ceph/client/image.go
index 8ea707d9c..f16fa2334 100644
--- a/pkg/daemon/ceph/client/image.go
+++ b/pkg/daemon/ceph/client/image.go
@@ -85,10 +85,7 @@ func ListImagesInRadosNamespace(context *clusterd.Context, clusterInfo *ClusterI
 // ListSnapshotsInRadosNamespace lists all the snapshots created for an image in a cephblockpool in a given rados namespace
 func ListSnapshotsInRadosNamespace(context *clusterd.Context, clusterInfo *ClusterInfo, poolName, imageName, namespace string) ([]CephBlockImageSnapshot, error) {
 	snapshots := []CephBlockImageSnapshot{}
-	args := []string{"snap", "ls", getImageSpec(imageName, poolName)}
-	if namespace != "" {
-		args = append(args, "--namespace", namespace)
-	}
+	args := []string{"snap", "ls", getImageSpec(imageName, poolName), "--namespace", namespace}
 	cmd := NewRBDCommand(context, clusterInfo, args)
 	cmd.JsonOutput = true
 	buf, err := cmd.Run()
@@ -104,10 +101,7 @@ func ListSnapshotsInRadosNamespace(context *clusterd.Context, clusterInfo *Clust
 
 // DeleteSnapshotInRadosNamespace deletes a image snapshot created in block pool in a given rados namespace
 func DeleteSnapshotInRadosNamespace(context *clusterd.Context, clusterInfo *ClusterInfo, poolName, imageName, snapshot, namespace string) error {
-	args := []string{"snap", "rm", getImageSnapshotSpec(poolName, imageName, snapshot)}
-	if namespace != "" {
-		args = append(args, "--namespace", namespace)
-	}
+	args := []string{"snap", "rm", getImageSnapshotSpec(poolName, imageName, snapshot), "--namespace", namespace}
 	cmd := NewRBDCommand(context, clusterInfo, args)
 	_, err := cmd.Run()
 	if err != nil {
@@ -118,10 +112,7 @@ func DeleteSnapshotInRadosNamespace(context *clusterd.Context, clusterInfo *Clus
 
 // MoveImageToTrashInRadosNamespace moves the cephblockpool image to trash in the rados namespace
 func MoveImageToTrashInRadosNamespace(context *clusterd.Context, clusterInfo *ClusterInfo, poolName, imageName, namespace string) error {
-	args := []string{"trash", "mv", getImageSpec(imageName, poolName)}
-	if namespace != "" {
-		args = append(args, "--namespace", namespace)
-	}
+	args := []string{"trash", "mv", getImageSpec(imageName, poolName), "--namespace", namespace}
 	cmd := NewRBDCommand(context, clusterInfo, args)
 	_, err := cmd.Run()
 	if err != nil {
@@ -277,12 +268,8 @@ func getImageSpec(name, poolName string) string {
 }
 
 func getImageSpecInRadosNamespace(poolName, namespace, imageID string) string {
-	if namespace == "" {
-		return fmt.Sprintf("%s/%s", poolName, imageID)
-	}
 	return fmt.Sprintf("%s/%s/%s", poolName, namespace, imageID)
 }
-
 func getImageSnapshotSpec(poolName, imageName, snapshot string) string {
 	return fmt.Sprintf("%s/%s@%s", poolName, imageName, snapshot)
 }
diff --git a/pkg/daemon/ceph/osd/kms/kms.go b/pkg/daemon/ceph/osd/kms/kms.go
index 24763f002..de555e425 100644
--- a/pkg/daemon/ceph/osd/kms/kms.go
+++ b/pkg/daemon/ceph/osd/kms/kms.go
@@ -238,25 +238,6 @@ func (c *Config) UpdateSecret(secretName, secretValue string) error {
 
 		return nil
 	}
-	if c.IsVault() {
-		// Store the secret in Vault
-		v, err := InitVault(c.ClusterInfo.Context, c.context, c.ClusterInfo.Namespace, c.clusterSpec.Security.KeyManagementService.ConnectionDetails)
-		if err != nil {
-			return errors.Wrap(err, "failed to init vault kms")
-		}
-		k := buildVaultKeyContext(c.clusterSpec.Security.KeyManagementService.ConnectionDetails)
-		// Build Secret
-		secretName = GenerateOSDEncryptionSecretName(secretName)
-		data := make(map[string]interface{})
-		data[secretName] = secretValue
-
-		_, err = v.PutSecret(secretName, data, k)
-		if err != nil {
-			return errors.Wrapf(err, "failed to put secret %q in vault kms", secretName)
-		}
-
-		return nil
-	}
 
 	return errors.Errorf("update secret is not supported for the %q KMS", c.Provider)
 }
diff --git a/pkg/operator/ceph/cluster/cephstatus.go b/pkg/operator/ceph/cluster/cephstatus.go
index 7f7e86a1a..8c8bfe207 100644
--- a/pkg/operator/ceph/cluster/cephstatus.go
+++ b/pkg/operator/ceph/cluster/cephstatus.go
@@ -314,8 +314,11 @@ func (c *cephStatusChecker) getRookPodsOnNode(node string) ([]v1.Pod, error) {
 	appLabels := []string{
 		"csi-rbdplugin-provisioner",
 		"csi-rbdplugin",
+		"csi-rbdplugin-holder",
+		"csi-cephfsplugin-holder",
 		"csi-cephfsplugin-provisioner",
 		"csi-cephfsplugin",
+		"csi-nfsplugin-holder",
 		"csi-nfsplugin-provisioner",
 		"csi-nfsplugin",
 		"rook-ceph-operator",
diff --git a/pkg/operator/ceph/cluster/mon/spec.go b/pkg/operator/ceph/cluster/mon/spec.go
index 430c2dbdb..161d77e20 100644
--- a/pkg/operator/ceph/cluster/mon/spec.go
+++ b/pkg/operator/ceph/cluster/mon/spec.go
@@ -58,9 +58,6 @@ func (c *Cluster) getLabels(monConfig *monConfig, canary, includeNewLabels bool)
 		if monConfig.Zone != "" {
 			labels["zone"] = monConfig.Zone
 		}
-		if !canary {
-			labels["mon_daemon"] = "true"
-		}
 	}
 
 	return labels
diff --git a/pkg/operator/ceph/cluster/osd/key_rotation.go b/pkg/operator/ceph/cluster/osd/key_rotation.go
index ae10ca7b6..6401a8eb4 100644
--- a/pkg/operator/ceph/cluster/osd/key_rotation.go
+++ b/pkg/operator/ceph/cluster/osd/key_rotation.go
@@ -137,12 +137,6 @@ func (c *Cluster) getKeyRotationPodTemplateSpec(osdProps osdProperties, osd OSDI
 		devices = append(devices, encryptionBlockDestinationCopy(devicesBasePath, bluestoreWalName))
 	}
 
-	if c.spec.Security.KeyManagementService.IsVaultKMS() {
-		volumeTLS, volumeMountTLS := kms.VaultVolumeAndMount(c.spec.Security.KeyManagementService.ConnectionDetails, "")
-		volumes = append(volumes, volumeTLS)
-		volumeMounts = append(volumeMounts, volumeMountTLS)
-	}
-
 	keyRotationContainer, err := c.getKeyRotationContainer(osdProps, volumeMounts, devices)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to generate key rotation container")
diff --git a/pkg/operator/ceph/cluster/osd/spec.go b/pkg/operator/ceph/cluster/osd/spec.go
index 4506d5249..f1ab7516b 100644
--- a/pkg/operator/ceph/cluster/osd/spec.go
+++ b/pkg/operator/ceph/cluster/osd/spec.go
@@ -867,13 +867,10 @@ func (c *Cluster) getActivateOSDInitContainer(configDir, namespace, osdID string
 
 	adminKeyringVol, adminKeyringVolMount := cephkey.Volume().Admin(), cephkey.VolumeMount().Admin()
 	volume = append(volume, adminKeyringVol)
-	udevVolume := v1.Volume{Name: "udev", VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/run/udev"}}}
-	volume = append(volume, udevVolume)
 	volMounts := []v1.VolumeMount{
 		{Name: activateOSDVolumeName, MountPath: activateOSDMountPathID},
 		{Name: "devices", MountPath: "/dev"},
 		{Name: k8sutil.ConfigOverrideName, ReadOnly: true, MountPath: opconfig.EtcCephDir},
-		{Name: "udev", MountPath: "/run/udev"},
 		adminKeyringVolMount,
 	}
 
diff --git a/pkg/operator/ceph/csi/controller.go b/pkg/operator/ceph/csi/controller.go
index e2fc9e39f..31c9d7924 100644
--- a/pkg/operator/ceph/csi/controller.go
+++ b/pkg/operator/ceph/csi/controller.go
@@ -52,15 +52,23 @@ const (
 
 // ReconcileCSI reconciles a ceph-csi driver
 type ReconcileCSI struct {
-	scheme           *runtime.Scheme
-	client           client.Client
-	context          *clusterd.Context
-	opManagerContext context.Context
-	opConfig         opcontroller.OperatorConfig
+	scheme             *runtime.Scheme
+	client             client.Client
+	context            *clusterd.Context
+	opManagerContext   context.Context
+	opConfig           opcontroller.OperatorConfig
+	clustersWithHolder []ClusterDetail
 	// the first cluster CR which will determine some settings for the csi driver
 	firstCephCluster *cephv1.ClusterSpec
 }
 
+// ClusterDetail is a struct that holds the information of a cluster, it knows its internals (like
+// FSID through clusterInfo) and the Kubernetes object that represents it (like the CephCluster CRD)
+type ClusterDetail struct {
+	cluster     *cephv1.CephCluster
+	clusterInfo *cephclient.ClusterInfo
+}
+
 // Add creates a new Ceph CSI Controller and adds it to the Manager. The Manager will set fields on the Controller
 // and Start it when the Manager is Started.
 func Add(mgr manager.Manager, context *clusterd.Context, opManagerContext context.Context, opConfig opcontroller.OperatorConfig) error {
@@ -70,11 +78,12 @@ func Add(mgr manager.Manager, context *clusterd.Context, opManagerContext contex
 // newReconciler returns a new reconcile.Reconciler
 func newReconciler(mgr manager.Manager, context *clusterd.Context, opManagerContext context.Context, opConfig opcontroller.OperatorConfig) reconcile.Reconciler {
 	return &ReconcileCSI{
-		scheme:           mgr.GetScheme(),
-		client:           mgr.GetClient(),
-		context:          context,
-		opConfig:         opConfig,
-		opManagerContext: opManagerContext,
+		scheme:             mgr.GetScheme(),
+		client:             mgr.GetClient(),
+		context:            context,
+		opConfig:           opConfig,
+		opManagerContext:   opManagerContext,
+		clustersWithHolder: []ClusterDetail{},
 	}
 }
 
@@ -241,6 +250,22 @@ func (r *ReconcileCSI) reconcile(request reconcile.Request) (reconcile.Result, e
 	}
 	CustomCSICephConfigExists = exists
 
+	csiHostNetworkEnabled, err := strconv.ParseBool(k8sutil.GetValue(r.opConfig.Parameters, "CSI_ENABLE_HOST_NETWORK", "true"))
+	if err != nil {
+		return reconcile.Result{}, errors.Wrap(err, "failed to parse value for 'CSI_ENABLE_HOST_NETWORK'")
+	}
+
+	csiDisableHolders, err := strconv.ParseBool(k8sutil.GetValue(r.opConfig.Parameters, "CSI_DISABLE_HOLDER_PODS", "false"))
+	if err != nil {
+		return reconcile.Result{}, errors.Wrap(err, "failed to parse value for 'CSI_DISABLE_HOLDER_PODS'")
+	}
+
+	// begin each reconcile with the assumption that holder pods won't be deployed
+	// the loop below will determine with each reconcile if they need deployed
+	// without this, holder pods won't be removed unless the operator is restarted
+	r.clustersWithHolder = []ClusterDetail{}
+	holderEnabled = false
+
 	for i, cluster := range cephClusters.Items {
 		if !cluster.DeletionTimestamp.IsZero() {
 			logger.Debugf("ceph cluster %q is being deleting, no need to reconcile the csi driver", request.NamespacedName)
@@ -270,14 +295,29 @@ func (r *ReconcileCSI) reconcile(request reconcile.Request) (reconcile.Result, e
 		}
 		clusterInfo.OwnerInfo = k8sutil.NewOwnerInfo(&cephClusters.Items[i], r.scheme)
 
-		// ensure any remaining holder-related configs are cleared
-		holderEnabled = false
+		// is holder enabled for this cluster?
+		thisHolderEnabled := (!csiHostNetworkEnabled || cluster.Spec.Network.IsMultus()) && !csiDisableHolders
+
+		// Do we have a multus cluster or csi host network disabled?
+		// If so deploy the plugin holder with the fsid attached
+		if thisHolderEnabled {
+			logger.Debugf("cluster %q: deploying the ceph-csi plugin holder", cluster.Name)
+			r.clustersWithHolder = append(r.clustersWithHolder, ClusterDetail{cluster: &cephClusters.Items[i], clusterInfo: clusterInfo})
+
+			// holder pods are enabled globally if any cluster needs a holder pod
+			holderEnabled = true
+		} else {
+			logger.Debugf("cluster %q: not deploying the ceph-csi plugin holder", request.NamespacedName)
+		}
+
+		// if holder pods were disabled, the controller needs to update the configmap for each
+		// cephcluster to remove the net namespace file path
 		err = reconcileSaveCSIDriverOptions(r.context.Clientset, cluster.Namespace, clusterInfo)
 		if err != nil {
 			return opcontroller.ImmediateRetryResult, errors.Wrapf(err, "failed to update CSI driver options for cluster %q", cluster.Name)
 		}
 
-		// disable Rook-managed CSI drivers if CSI operator is enabled
+		// Skip the new CSI-operator creation when holder pod is enabled until multus support is added in the CSI operator
 		if EnableCSIOperator() {
 			logger.Info("disabling csi-driver since EnableCSIOperator is true")
 			err := r.stopDrivers(serverVersion)
diff --git a/pkg/operator/ceph/csi/spec.go b/pkg/operator/ceph/csi/spec.go
index 59af7f601..de30e74a8 100644
--- a/pkg/operator/ceph/csi/spec.go
+++ b/pkg/operator/ceph/csi/spec.go
@@ -21,6 +21,7 @@ import (
 	_ "embed"
 	"fmt"
 	"path"
+	"strconv"
 	"strings"
 	"time"
 
@@ -29,6 +30,7 @@ import (
 	opcontroller "github.com/rook/rook/pkg/operator/ceph/controller"
 	"github.com/rook/rook/pkg/operator/k8sutil"
 	"github.com/rook/rook/pkg/operator/k8sutil/cmdreporter"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/pkg/errors"
 	apps "k8s.io/api/apps/v1"
@@ -36,8 +38,11 @@ import (
 	k8scsi "k8s.io/api/storage/v1beta1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/version"
 	"k8s.io/client-go/kubernetes"
+
+	cephcsi "github.com/ceph/ceph-csi/api/deploy/kubernetes"
 )
 
 type Param struct {
@@ -110,6 +115,15 @@ type templateParam struct {
 	Namespace string
 }
 
+type driverDetails struct {
+	name           string
+	fullName       string
+	holderTemplate string
+	toleration     string
+	nodeAffinity   string
+	resource       string
+}
+
 var (
 	CSIParam Param
 
@@ -150,6 +164,8 @@ var (
 	// Local package template path for RBD
 	//go:embed template/rbd/csi-rbdplugin.yaml
 	RBDPluginTemplatePath string
+	//go:embed template/rbd/csi-rbdplugin-holder.yaml
+	RBDPluginHolderTemplatePath string
 	//go:embed template/rbd/csi-rbdplugin-provisioner-dep.yaml
 	RBDProvisionerDepTemplatePath string
 	//go:embed template/rbd/csi-rbdplugin-svc.yaml
@@ -158,6 +174,8 @@ var (
 	// Local package template path for CephFS
 	//go:embed template/cephfs/csi-cephfsplugin.yaml
 	CephFSPluginTemplatePath string
+	//go:embed template/cephfs/csi-cephfsplugin-holder.yaml
+	CephFSPluginHolderTemplatePath string
 	//go:embed template/cephfs/csi-cephfsplugin-provisioner-dep.yaml
 	CephFSProvisionerDepTemplatePath string
 	//go:embed template/cephfs/csi-cephfsplugin-svc.yaml
@@ -168,6 +186,8 @@ var (
 	NFSPluginTemplatePath string
 	//go:embed template/nfs/csi-nfsplugin-provisioner-dep.yaml
 	NFSProvisionerDepTemplatePath string
+	//go:embed template/nfs/csi-nfsplugin-holder.yaml
+	NFSPluginHolderTemplatePath string
 
 	//go:embed template/csi-logrotate-sidecar.yaml
 	LogrotateTemplatePath string
@@ -313,6 +333,8 @@ func (r *ReconcileCSI) startDrivers(ver *version.Info, ownerInfo *k8sutil.OwnerI
 		csiDriverobj                                                                    v1CsiDriver
 	)
 
+	enabledDrivers := make([]driverDetails, 0)
+
 	tp := templateParam{
 		Param:     CSIParam,
 		Namespace: r.opConfig.OperatorNamespace,
@@ -369,6 +391,14 @@ func (r *ReconcileCSI) startDrivers(ver *version.Info, ownerInfo *k8sutil.OwnerI
 			}
 			rbdService.Namespace = r.opConfig.OperatorNamespace
 		}
+		enabledDrivers = append(enabledDrivers, driverDetails{
+			name:           RBDDriverShortName,
+			fullName:       RBDDriverName,
+			holderTemplate: RBDPluginHolderTemplatePath,
+			nodeAffinity:   rbdPluginNodeAffinityEnv,
+			toleration:     rbdPluginTolerationsEnv,
+			resource:       rbdPluginResource,
+		})
 	}
 	if EnableCephFS {
 		tp.CsiComponentName = nodePlugin
@@ -399,6 +429,14 @@ func (r *ReconcileCSI) startDrivers(ver *version.Info, ownerInfo *k8sutil.OwnerI
 			}
 			cephfsService.Namespace = r.opConfig.OperatorNamespace
 		}
+		enabledDrivers = append(enabledDrivers, driverDetails{
+			name:           CephFSDriverShortName,
+			fullName:       CephFSDriverName,
+			holderTemplate: CephFSPluginHolderTemplatePath,
+			nodeAffinity:   cephFSPluginNodeAffinityEnv,
+			toleration:     cephFSPluginTolerationsEnv,
+			resource:       cephFSPluginResource,
+		})
 	}
 
 	if EnableNFS {
@@ -421,6 +459,15 @@ func (r *ReconcileCSI) startDrivers(ver *version.Info, ownerInfo *k8sutil.OwnerI
 			applyLogrotateSidecar(&nfsProvisionerDeployment.Spec.Template, "csi-nfs-deployment-log-collector", LogrotateTemplatePath, tp)
 		}
 		nfsProvisionerDeployment.Spec.Template.Spec.HostNetwork = opcontroller.EnforceHostNetwork()
+
+		enabledDrivers = append(enabledDrivers, driverDetails{
+			name:           NFSDriverShortName,
+			fullName:       NFSDriverName,
+			holderTemplate: NFSPluginHolderTemplatePath,
+			nodeAffinity:   nfsPluginNodeAffinityEnv,
+			toleration:     nfsPluginTolerationsEnv,
+			resource:       nfsPluginResource,
+		})
 	}
 
 	// get common provisioner tolerations and node affinity
@@ -431,6 +478,13 @@ func (r *ReconcileCSI) startDrivers(ver *version.Info, ownerInfo *k8sutil.OwnerI
 	pluginTolerations := getToleration(r.opConfig.Parameters, pluginTolerationsEnv, []corev1.Toleration{})
 	pluginNodeAffinity := getNodeAffinity(r.opConfig.Parameters, pluginNodeAffinityEnv, &corev1.NodeAffinity{})
 
+	// Deploy the CSI Holder DaemonSet if Multus is enabled or
+	// EnableCSIHostNetwork is disabled.
+	err = r.configureHolders(enabledDrivers, tp, pluginTolerations, pluginNodeAffinity)
+	if err != nil {
+		return errors.Wrap(err, "failed to configure holder")
+	}
+
 	if rbdPlugin != nil {
 		// get RBD plugin tolerations and node affinity, defaults to common tolerations and node affinity if not specified
 		rbdPluginTolerations := getToleration(r.opConfig.Parameters, rbdPluginTolerationsEnv, pluginTolerations)
@@ -451,6 +505,9 @@ func (r *ReconcileCSI) startDrivers(ver *version.Info, ownerInfo *k8sutil.OwnerI
 		if err != nil {
 			return errors.Wrapf(err, "failed to apply network config to rbd plugin daemonset %q", rbdPlugin.Name)
 		}
+		if holderEnabled {
+			rbdPlugin.Spec.Template.Spec.HostNetwork = false
+		}
 		err = k8sutil.CreateDaemonSet(r.opManagerContext, r.opConfig.OperatorNamespace, r.context.Clientset, rbdPlugin)
 		if err != nil {
 			return errors.Wrapf(err, "failed to start rbdplugin daemonset %q", rbdPlugin.Name)
@@ -520,6 +577,11 @@ func (r *ReconcileCSI) startDrivers(ver *version.Info, ownerInfo *k8sutil.OwnerI
 		if err != nil {
 			return errors.Wrapf(err, "failed to apply network config to cephfs plugin daemonset %q", cephfsPlugin.Name)
 		}
+		if holderEnabled {
+			cephfsPlugin.Spec.Template.Spec.HostNetwork = false
+			// HostPID is used to communicate with the network namespace
+			cephfsPlugin.Spec.Template.Spec.HostPID = true
+		}
 
 		err = k8sutil.CreateDaemonSet(r.opManagerContext, r.opConfig.OperatorNamespace, r.context.Clientset, cephfsPlugin)
 		if err != nil {
@@ -590,6 +652,11 @@ func (r *ReconcileCSI) startDrivers(ver *version.Info, ownerInfo *k8sutil.OwnerI
 		if err != nil {
 			return errors.Wrapf(err, "failed to apply network config to nfs plugin daemonset %q", nfsPlugin.Name)
 		}
+		if holderEnabled {
+			nfsPlugin.Spec.Template.Spec.HostNetwork = false
+			// HostPID is used to communicate with the network namespace
+			nfsPlugin.Spec.Template.Spec.HostPID = true
+		}
 
 		err = k8sutil.CreateDaemonSet(r.opManagerContext, r.opConfig.OperatorNamespace, r.context.Clientset, nfsPlugin)
 		if err != nil {
@@ -798,6 +865,166 @@ func (r *ReconcileCSI) validateCSIVersion(ownerInfo *k8sutil.OwnerInfo) (*CephCS
 	return version, nil
 }
 
+func (r *ReconcileCSI) configureHolders(enabledDrivers []driverDetails, tp templateParam, pluginTolerations []corev1.Toleration, pluginNodeAffinity *corev1.NodeAffinity) error {
+	for _, cluster := range r.clustersWithHolder {
+		for _, driver := range enabledDrivers {
+			err := r.configureHolder(driver, cluster, tp, pluginTolerations, pluginNodeAffinity)
+			if err != nil {
+				return errors.Wrapf(err, "failed to configure holder %q for %q/%q", driver.name, cluster.cluster.Name, cluster.cluster.Namespace)
+			}
+		}
+	}
+
+	return nil
+}
+
+func (r *ReconcileCSI) configureHolder(driver driverDetails, c ClusterDetail, tp templateParam, pluginTolerations []corev1.Toleration, pluginNodeAffinity *corev1.NodeAffinity) error {
+	cephPluginHolder, err := templateToDaemonSet("cephpluginholder", driver.holderTemplate, tp)
+	if err != nil {
+		return errors.Wrapf(err, "failed to load ceph %q plugin holder template", driver.fullName)
+	}
+
+	// As the plugin holder daemonset is created in the operator namespace, we
+	// need to set the owner reference to the cluster namespace only if the
+	// operator and cluster are created in same namespace so that the
+	// plugin holder daemonset is deleted when the cluster is deleted.
+	if r.opConfig.OperatorNamespace == c.cluster.Namespace {
+		err = c.clusterInfo.OwnerInfo.SetControllerReference(cephPluginHolder)
+		if err != nil {
+			return errors.Wrapf(err, "failed to set owner reference to plugin holder %q", driver.fullName)
+		}
+	}
+	holderPluginTolerations := getToleration(r.opConfig.Parameters, driver.toleration, pluginTolerations)
+	holderPluginNodeAffinity := getNodeAffinity(r.opConfig.Parameters, driver.nodeAffinity, pluginNodeAffinity)
+	// apply driver's plugin tolerations and node affinity
+	applyToPodSpec(&cephPluginHolder.Spec.Template.Spec, holderPluginNodeAffinity, holderPluginTolerations)
+
+	// apply resource request and limit from corresponding plugin container
+	applyResourcesToContainers(r.opConfig.Parameters, driver.resource, &cephPluginHolder.Spec.Template.Spec)
+
+	// Append the CEPH_CLUSTER_NAMESPACE env var so that the main container can use it to create the network
+	// namespace symlink to the Kubelet plugin directory
+	cephPluginHolder.Spec.Template.Spec.Containers[0].Env = append(
+		cephPluginHolder.Spec.Template.Spec.Containers[0].Env,
+		corev1.EnvVar{
+			Name:  "CEPH_CLUSTER_NAMESPACE",
+			Value: c.cluster.Namespace,
+		},
+	)
+
+	// Append the driver name so that the symlink file goes into the right location on the
+	// kubelet plugin directory
+	cephPluginHolder.Spec.Template.Spec.Containers[0].Env = append(
+		cephPluginHolder.Spec.Template.Spec.Containers[0].Env,
+		corev1.EnvVar{
+			Name:  "ROOK_CEPH_CSI_DRIVER_NAME",
+			Value: driver.fullName,
+		},
+	)
+
+	// Make the DS name unique per Ceph cluster
+	cephPluginHolder.Name = fmt.Sprintf("%s-%s", cephPluginHolder.Name, c.cluster.Name)
+	cephPluginHolder.Spec.Template.Name = cephPluginHolder.Name
+	cephPluginHolder.Spec.Template.Spec.Containers[0].Name = fmt.Sprintf("%s-%s", cephPluginHolder.Spec.Template.Spec.Containers[0].Name, c.cluster.Name)
+
+	// Add default labels
+	k8sutil.AddRookVersionLabelToDaemonSet(cephPluginHolder)
+
+	// If multus is enabled, add the multus plugin label
+	if c.cluster.Spec.Network.IsMultus() {
+		// Apply Multus annotations to daemonset spec
+		err = k8sutil.ApplyMultus(c.cluster.GetNamespace(), &c.cluster.Spec.Network, &cephPluginHolder.Spec.Template.ObjectMeta)
+		if err != nil {
+			return errors.Wrapf(err, "failed to apply multus configuration for holder %q in cluster %q", cephPluginHolder.Name, c.cluster.Namespace)
+		}
+	}
+	// Finally create the DaemonSet
+	_, err = r.context.Clientset.AppsV1().DaemonSets(r.opConfig.OperatorNamespace).Create(r.opManagerContext, cephPluginHolder, metav1.CreateOptions{})
+	if err != nil {
+		if kerrors.IsAlreadyExists(err) {
+			_, err = r.context.Clientset.AppsV1().DaemonSets(r.opConfig.OperatorNamespace).Update(r.opManagerContext, cephPluginHolder, metav1.UpdateOptions{})
+			if err != nil {
+				return errors.Wrapf(err, "failed to update ceph plugin holder daemonset %q", cephPluginHolder.Name)
+			}
+			logger.Debugf("holder %q already exists for cluster %q, updating it, restart holder pods to take effect of update", cephPluginHolder.Name, c.cluster.Namespace)
+		} else {
+			return errors.Wrapf(err, "failed to start ceph plugin holder daemonset %q", cephPluginHolder.Name)
+		}
+	}
+
+	clusterConfigEntry := &CSIClusterConfigEntry{
+		ClusterInfo: cephcsi.ClusterInfo{
+			Monitors: MonEndpoints(c.clusterInfo.Monitors, c.cluster.Spec.RequireMsgr2()),
+			RBD:      cephcsi.RBD{},
+			CephFS: cephcsi.CephFS{
+				FuseMountOptions:   c.clusterInfo.CSIDriverSpec.CephFS.FuseMountOptions,
+				KernelMountOptions: c.clusterInfo.CSIDriverSpec.CephFS.KernelMountOptions,
+			},
+			NFS: cephcsi.NFS{},
+			ReadAffinity: cephcsi.ReadAffinity{
+				Enabled:             c.clusterInfo.CSIDriverSpec.ReadAffinity.Enabled,
+				CrushLocationLabels: c.clusterInfo.CSIDriverSpec.ReadAffinity.CrushLocationLabels,
+			},
+		},
+	}
+
+	netNamespaceFilePath := generateNetNamespaceFilePath(CSIParam.KubeletDirPath, driver.fullName, c.cluster.Namespace)
+	if driver.name == RBDDriverShortName {
+		clusterConfigEntry.RBD.NetNamespaceFilePath = netNamespaceFilePath
+	}
+	if driver.name == CephFSDriverShortName {
+		clusterConfigEntry.CephFS.NetNamespaceFilePath = netNamespaceFilePath
+	}
+	if driver.name == NFSDriverShortName {
+		clusterConfigEntry.NFS.NetNamespaceFilePath = netNamespaceFilePath
+	}
+
+	// Save the path of the network namespace file for ceph-csi to use
+	clusterId := c.cluster.Namespace // cluster ID is same as cluster namespace for CephClusters
+	err = SaveClusterConfig(r.context.Clientset, clusterId, c.cluster.Namespace, c.clusterInfo, clusterConfigEntry)
+	if err != nil {
+		return errors.Wrapf(err, "failed to save cluster config for csi holder %q", driver.fullName)
+	}
+	return nil
+}
+
+func GenerateNetNamespaceFilePath(ctx context.Context, client client.Client, clusterNamespace, opNamespace, driverName string) (string, error) {
+	var driverSuffix string
+	opNamespaceName := types.NamespacedName{Name: opcontroller.OperatorSettingConfigMapName, Namespace: opNamespace}
+	opConfig := &corev1.ConfigMap{}
+	err := client.Get(ctx, opNamespaceName, opConfig)
+	if err != nil && !kerrors.IsNotFound(err) {
+		return "", errors.Wrap(err, "failed to get operator's configmap")
+	}
+
+	// net namespace file path is empty string if holder pods are disabled
+	csiDisableHolders, err := strconv.ParseBool(k8sutil.GetValue(opConfig.Data, "CSI_DISABLE_HOLDER_PODS", "false"))
+	if err != nil {
+		return "", errors.Wrap(err, "failed to parse value for 'CSI_DISABLE_HOLDER_PODS'")
+	}
+	if csiDisableHolders {
+		return "", nil
+	}
+
+	switch driverName {
+	case RBDDriverShortName:
+		driverSuffix = rbdDriverSuffix
+	case CephFSDriverShortName:
+		driverSuffix = cephFSDriverSuffix
+	default:
+		return "", errors.Errorf("unsupported driver name %q", driverName)
+	}
+
+	kubeletDirPath := k8sutil.GetValue(opConfig.Data, "ROOK_CSI_KUBELET_DIR_PATH", DefaultKubeletDirPath)
+	driverFullName := fmt.Sprintf("%s.%s", opNamespace, driverSuffix)
+
+	return generateNetNamespaceFilePath(kubeletDirPath, driverFullName, clusterNamespace), nil
+}
+
+func generateNetNamespaceFilePath(kubeletDirPath, driverFullName, clusterNamespace string) string {
+	return fmt.Sprintf("%s/plugins/%s/%s.net.ns", kubeletDirPath, driverFullName, clusterNamespace)
+}
+
 func validateCSIDriverNamePrefix(ctx context.Context, clientset kubernetes.Interface, namespace, driverNamePrefix string) error {
 	if EnableRBD {
 		rbdDriverNamePrefix, err := getCSIDriverNamePrefixFromDeployment(ctx, clientset, namespace, csiRBDProvisioner, "csi-rbdplugin")
@@ -879,5 +1106,5 @@ func getPrefixFromArg(arg string) (string, bool) {
 }
 
 func EnableCSIOperator() bool {
-	return enableCSIOperator
+	return enableCSIOperator && !IsHolderEnabled()
 }
diff --git a/pkg/operator/ceph/csi/template/cephfs/csi-cephfsplugin-holder.yaml b/pkg/operator/ceph/csi/template/cephfs/csi-cephfsplugin-holder.yaml
new file mode 100644
index 000000000..f29659b29
--- /dev/null
+++ b/pkg/operator/ceph/csi/template/cephfs/csi-cephfsplugin-holder.yaml
@@ -0,0 +1,67 @@
+# This DaemonSet is only active when the cluster is deployed with Multus enabled or EnableCSIHostNetwork
+# is set to false. Hopefully in the future it will be part of every Ceph-CSI deployment. Its role is to
+# "hold" the Ceph Filesystem mounts that are created by the CSI driver. Essentially, it exposes its
+# network namespace into the Kubelet plugin directly and Ceph-CSI picks it up. When mounting, the Ceph-CSI
+# driver will use the network namespace of the DaemonSet instead of its own (the plugin pod). This pod
+# is not expected to be updated nor restarted unless the node reboots.
+kind: DaemonSet
+apiVersion: apps/v1
+metadata:
+  name: csi-cephfsplugin-holder
+  namespace: {{ .Namespace }}
+spec:
+  selector:
+    matchLabels:
+      app: csi-cephfsplugin-holder
+  updateStrategy:
+    # DO NOT CHANGE THE STRATEGY
+    # The pod is not supposed to be restarted on updates, it should never receive updates too.
+    # If it does then the connectivity to the Ceph cluster will be lost and the client I/O will
+    # fail.
+    type: OnDelete
+  template:
+    metadata:
+      labels:
+        app: csi-cephfsplugin-holder
+        {{ range $key, $value := .CSICephFSPodLabels }}
+        {{ $key }}: "{{ $value }}"
+        {{ end }}
+    spec:
+      # HostPID is needed to expose the correct process ID network namespace and not the process namespace
+      hostPID: true
+      securityContext: {}
+      serviceAccountName: rook-csi-cephfs-plugin-sa
+      {{ if .PluginPriorityClassName }}
+      priorityClassName: {{ .PluginPriorityClassName }}
+      {{ end }}
+      containers:
+        - name: csi-cephfsplugin-holder
+          # This is necessary for the Bidirectional mount propagation
+          securityContext:
+            privileged: true
+            capabilities:
+              drop: ["ALL"]
+          image: {{ .CSIPluginImage }}
+          command:
+            - "/bin/sh"
+            - "-c"
+          # the third $ allows the "$$" to be interpreted so it can return the current bash process ID
+          args:
+            - "ln
+              --force
+              --symbolic
+              --verbose
+              /proc/$$$/ns/net
+              {{ .KubeletDirPath }}/plugins/$ROOK_CEPH_CSI_DRIVER_NAME/$CEPH_CLUSTER_NAMESPACE.net.ns
+              &&
+              sleep
+              infinity"
+          volumeMounts:
+            - name: plugin-mount-dir
+              mountPath: "{{ .KubeletDirPath }}/plugins"
+              mountPropagation: "Bidirectional"
+      volumes:
+        - name: plugin-mount-dir
+          hostPath:
+            path: "{{ .KubeletDirPath }}/plugins"
+            type: Directory
diff --git a/pkg/operator/ceph/csi/template/nfs/csi-nfsplugin-holder.yaml b/pkg/operator/ceph/csi/template/nfs/csi-nfsplugin-holder.yaml
new file mode 100644
index 000000000..240d4b6b5
--- /dev/null
+++ b/pkg/operator/ceph/csi/template/nfs/csi-nfsplugin-holder.yaml
@@ -0,0 +1,67 @@
+# This DaemonSet is only active when the cluster is deployed with Multus enabled or EnableCSIHostNetwork
+# is set to false. Hopefully in the future it will be part of every Ceph-CSI deployment. Its role is to
+# "hold" the Ceph Filesystem mounts that are created by the CSI driver. Essentially, it exposes its
+# network namespace into the Kubelet plugin directly and Ceph-CSI picks it up. When mounting, the Ceph-CSI
+# driver will use the network namespace of the DaemonSet instead of its own (the plugin pod). This pod
+# is not expected to be updated nor restarted unless the node reboots.
+kind: DaemonSet
+apiVersion: apps/v1
+metadata:
+  name: csi-nfsplugin-holder
+  namespace: {{ .Namespace }}
+spec:
+  selector:
+    matchLabels:
+      app: csi-nfsplugin-holder
+  updateStrategy:
+    # DO NOT CHANGE THE STRATEGY
+    # The pod is not supposed to be restarted on updates, it should never receive updates too.
+    # If it does then the connectivity to the Ceph cluster will be lost and the client I/O will
+    # fail.
+    type: OnDelete
+  template:
+    metadata:
+      labels:
+        app: csi-nfsplugin-holder
+        {{ range $key, $value := .CSINFSPodLabels }}
+        {{ $key }}: "{{ $value }}"
+        {{ end }}
+    spec:
+      # HostPID is needed to expose the correct process ID network namespace and not the process namespace
+      hostPID: true
+      securityContext: {}
+      serviceAccountName: rook-csi-nfs-plugin-sa
+      {{ if .PluginPriorityClassName }}
+      priorityClassName: {{ .PluginPriorityClassName }}
+      {{ end }}
+      containers:
+        - name: csi-nfsplugin-holder
+          # This is necessary for the Bidirectional mount propagation
+          securityContext:
+            privileged: true
+            capabilities:
+              drop: ["ALL"]
+          image: {{ .CSIPluginImage }}
+          command:
+            - "/bin/sh"
+            - "-c"
+          # the third $ allows the "$$" to be interpreted so it can return the current bash process ID
+          args:
+            - "ln
+              --force
+              --symbolic
+              --verbose
+              /proc/$$$/ns/net
+              {{ .KubeletDirPath }}/plugins/$ROOK_CEPH_CSI_DRIVER_NAME/$CEPH_CLUSTER_NAMESPACE.net.ns
+              &&
+              sleep
+              infinity"
+          volumeMounts:
+            - name: plugin-mount-dir
+              mountPath: "{{ .KubeletDirPath }}/plugins"
+              mountPropagation: "Bidirectional"
+      volumes:
+        - name: plugin-mount-dir
+          hostPath:
+            path: "{{ .KubeletDirPath }}/plugins"
+            type: Directory
diff --git a/pkg/operator/ceph/csi/template/rbd/csi-rbdplugin-holder.yaml b/pkg/operator/ceph/csi/template/rbd/csi-rbdplugin-holder.yaml
new file mode 100644
index 000000000..c8f11c5cc
--- /dev/null
+++ b/pkg/operator/ceph/csi/template/rbd/csi-rbdplugin-holder.yaml
@@ -0,0 +1,67 @@
+# This DaemonSet is only active when the cluster is deployed with Multus enabled or EnableCSIHostNetwork
+# is set to false. Hopefully in the future it will be part of every Ceph-CSI deployment. Its role is
+# to "hold" the network RBD block mapping that are created by the CSI driver. Essentially, it exposes
+# its network namespace into the Kubelet plugin directly and Ceph-CSI picks it up. When mapping an RBD
+# image, the Ceph-CSI driver will use the network namespace of the DaemonSet instead of its own (the plugin pod).
+# This pod is not expected to be updated nor restarted unless the node reboots.
+kind: DaemonSet
+apiVersion: apps/v1
+metadata:
+  name: csi-rbdplugin-holder
+  namespace: {{ .Namespace }}
+spec:
+  selector:
+    matchLabels:
+      app: csi-rbdplugin-holder
+  updateStrategy:
+    # DO NOT CHANGE THE STRATEGY
+    # The pod is not supposed to be restarted on updates, it should never receive updates too.
+    # If it does then the connectivity to the Ceph cluster will be lost and the client I/O will
+    # fail.
+    type: OnDelete
+  template:
+    metadata:
+      labels:
+        app: csi-rbdplugin-holder
+        {{ range $key, $value := .CSIRBDPodLabels }}
+        {{ $key }}: "{{ $value }}"
+        {{ end }}
+    spec:
+      # HostPID is needed to expose the correct process ID network namespace and not the process namespace
+      hostPID: true
+      securityContext: {}
+      serviceAccountName: rook-csi-rbd-plugin-sa
+      {{ if .PluginPriorityClassName }}
+      priorityClassName: {{ .PluginPriorityClassName }}
+      {{ end }}
+      containers:
+        - name: csi-rbdplugin-holder
+          # This is necessary for the Bidirectional mount propagation
+          securityContext:
+            privileged: true
+            capabilities:
+              drop: ["ALL"]
+          image: {{ .CSIPluginImage }}
+          command:
+            - "/bin/sh"
+            - "-c"
+          # the third $ allows the "$$" to be interpreted so it can return the current bash process ID
+          args:
+            - "ln
+              --force
+              --symbolic
+              --verbose
+              /proc/$$$/ns/net
+              {{ .KubeletDirPath }}/plugins/$ROOK_CEPH_CSI_DRIVER_NAME/$CEPH_CLUSTER_NAMESPACE.net.ns
+              &&
+              sleep
+              infinity"
+          volumeMounts:
+            - name: plugin-mount-dir
+              mountPath: "{{ .KubeletDirPath }}/plugins"
+              mountPropagation: "Bidirectional"
+      volumes:
+        - name: plugin-mount-dir
+          hostPath:
+            path: "{{ .KubeletDirPath }}/plugins"
+            type: Directory
diff --git a/pkg/operator/ceph/file/subvolumegroup/controller.go b/pkg/operator/ceph/file/subvolumegroup/controller.go
index d74abe149..5e7cb80f7 100644
--- a/pkg/operator/ceph/file/subvolumegroup/controller.go
+++ b/pkg/operator/ceph/file/subvolumegroup/controller.go
@@ -311,7 +311,15 @@ func (r *ReconcileCephFilesystemSubVolumeGroup) updateClusterConfig(cephFilesyst
 		},
 	}
 
-	csiClusterConfigEntry.CephFS.NetNamespaceFilePath = ""
+	// If the cluster has Multus enabled we need to append the network namespace of the driver's
+	// holder DaemonSet in the csi configmap
+	if cephCluster.Spec.Network.IsMultus() {
+		netNamespaceFilePath, err := csi.GenerateNetNamespaceFilePath(r.opManagerContext, r.client, cephCluster.Namespace, r.opConfig.OperatorNamespace, csi.CephFSDriverShortName)
+		if err != nil {
+			return errors.Wrap(err, "failed to generate cephfs net namespace file path")
+		}
+		csiClusterConfigEntry.CephFS.NetNamespaceFilePath = netNamespaceFilePath
+	}
 
 	err := csi.SaveClusterConfig(r.context.Clientset, buildClusterID(cephFilesystemSubVolumeGroup), cephCluster.Namespace, r.clusterInfo, &csiClusterConfigEntry)
 	if err != nil {
diff --git a/pkg/operator/ceph/object/bucket/provisioner.go b/pkg/operator/ceph/object/bucket/provisioner.go
index 074905625..831362102 100644
--- a/pkg/operator/ceph/object/bucket/provisioner.go
+++ b/pkg/operator/ceph/object/bucket/provisioner.go
@@ -57,10 +57,8 @@ type Provisioner struct {
 }
 
 type additionalConfigSpec struct {
-	maxObjects       *int64
-	maxSize          *int64
-	bucketMaxObjects *int64
-	bucketMaxSize    *int64
+	maxObjects *int64
+	maxSize    *int64
 }
 
 var _ apibkt.Provisioner = &Provisioner{}
@@ -580,20 +578,6 @@ func (p *Provisioner) setAdditionalSettings(options *apibkt.BucketOptions) error
 		return errors.Wrap(err, "failed to process additionalConfig")
 	}
 
-	err = p.setUserQuota(additionalConfig)
-	if err != nil {
-		return errors.Wrap(err, "failed to set user quota")
-	}
-
-	err = p.setBucketQuota(additionalConfig)
-	if err != nil {
-		return errors.Wrap(err, "failed to set bucket quota")
-	}
-
-	return nil
-}
-
-func (p *Provisioner) setUserQuota(additionalConfig *additionalConfigSpec) error {
 	liveQuota, err := p.adminOpsClient.GetUserQuota(p.clusterInfo.Context, admin.QuotaSpec{UID: p.cephUserName})
 	if err != nil {
 		return errors.Wrapf(err, "failed to fetch user %q", p.cephUserName)
@@ -644,59 +628,6 @@ func (p *Provisioner) setUserQuota(additionalConfig *additionalConfigSpec) error
 	return nil
 }
 
-func (p *Provisioner) setBucketQuota(additionalConfig *additionalConfigSpec) error {
-	bucket, err := p.adminOpsClient.GetBucketInfo(p.clusterInfo.Context, admin.Bucket{Bucket: p.bucketName})
-	if err != nil {
-		return errors.Wrapf(err, "failed to fetch bucket %q", p.bucketName)
-	}
-	liveQuota := bucket.BucketQuota
-
-	// Copy only the fields that are actively managed by the provisioner to
-	// prevent passing back undesirable combinations of fields.  It is
-	// known to be problematic to set both MaxSize and MaxSizeKB.
-	currentQuota := admin.QuotaSpec{
-		Enabled:    liveQuota.Enabled,
-		MaxObjects: liveQuota.MaxObjects,
-		MaxSize:    liveQuota.MaxSize,
-	}
-	targetQuota := currentQuota
-
-	// enable or disable quota for user
-	quotaEnabled := (additionalConfig.bucketMaxObjects != nil) || (additionalConfig.bucketMaxSize != nil)
-
-	targetQuota.Enabled = &quotaEnabled
-
-	if additionalConfig.bucketMaxObjects != nil {
-		targetQuota.MaxObjects = additionalConfig.bucketMaxObjects
-	} else if currentQuota.MaxObjects != nil && *currentQuota.MaxObjects >= 0 {
-		// if the existing value is already negative, we don't want to change it
-		var objects int64 = -1
-		targetQuota.MaxObjects = &objects
-	}
-
-	if additionalConfig.bucketMaxSize != nil {
-		targetQuota.MaxSize = additionalConfig.bucketMaxSize
-	} else if currentQuota.MaxSize != nil && *currentQuota.MaxSize >= 0 {
-		// if the existing value is already negative, we don't want to change it
-		var size int64 = -1
-		targetQuota.MaxSize = &size
-	}
-
-	diff := cmp.Diff(currentQuota, targetQuota)
-	if diff != "" {
-		logger.Debugf("Quota for bucket %q has changed. diff:%s", p.bucketName, diff)
-		// UID & Bucket are not set in the QuotaSpec returned by GetBucketInfo()
-		targetQuota.UID = p.cephUserName
-		targetQuota.Bucket = p.bucketName
-		err = p.adminOpsClient.SetIndividualBucketQuota(p.clusterInfo.Context, targetQuota)
-		if err != nil {
-			return errors.Wrapf(err, "failed to set bucket %q quota enabled=%v %+v", p.bucketName, quotaEnabled, additionalConfig)
-		}
-	}
-
-	return nil
-}
-
 func (p *Provisioner) setTlsCaCert() error {
 	objStore, err := p.getObjectStore()
 	if err != nil {
diff --git a/pkg/operator/ceph/object/bucket/util.go b/pkg/operator/ceph/object/bucket/util.go
index de1936d26..fe75c9e0d 100644
--- a/pkg/operator/ceph/object/bucket/util.go
+++ b/pkg/operator/ceph/object/bucket/util.go
@@ -106,20 +106,6 @@ func additionalConfigSpecFromMap(config map[string]string) (*additionalConfigSpe
 		}
 	}
 
-	if _, ok := config["bucketMaxObjects"]; ok {
-		spec.bucketMaxObjects, err = quanityToInt64(config["bucketMaxObjects"])
-		if err != nil {
-			return nil, errors.Wrapf(err, "failed to parse bucketMaxObjects quota")
-		}
-	}
-
-	if _, ok := config["bucketMaxSize"]; ok {
-		spec.bucketMaxSize, err = quanityToInt64(config["bucketMaxSize"])
-		if err != nil {
-			return nil, errors.Wrapf(err, "failed to parse bucketMaxSize quota")
-		}
-	}
-
 	return &spec, nil
 }
 
diff --git a/pkg/operator/ceph/object/objectstore.go b/pkg/operator/ceph/object/objectstore.go
index e2e79304a..63f28dbed 100644
--- a/pkg/operator/ceph/object/objectstore.go
+++ b/pkg/operator/ceph/object/objectstore.go
@@ -800,8 +800,7 @@ func ConfigureSharedPoolsForZone(objContext *Context, sharedPools cephv1.ObjectS
 	if err != nil {
 		return err
 	}
-	defaultPlacement := getDefaultPlacementName(sharedPools)
-	zoneGroupUpdated, err := adjustZoneGroupPlacementTargets(zoneGroupConfig, zoneUpdated, defaultPlacement)
+	zoneGroupUpdated, err := adjustZoneGroupPlacementTargets(zoneGroupConfig, zoneUpdated)
 	if err != nil {
 		return err
 	}
@@ -1402,10 +1401,10 @@ func ValidateObjectStorePoolsConfig(metadataPool, dataPool cephv1.PoolSpec, shar
 		return err
 	}
 	if !EmptyPool(dataPool) && sharedPools.DataPoolName != "" {
-		return fmt.Errorf("invalidObjStorePoolConfig: object store dataPool and sharedPools.dataPool=%s are mutually exclusive. Only one of them can be set.", sharedPools.DataPoolName)
+		return fmt.Errorf("invalidObjStorePoolCofig: object store dataPool and sharedPools.dataPool=%s are mutually exclusive. Only one of them can be set.", sharedPools.DataPoolName)
 	}
 	if !EmptyPool(metadataPool) && sharedPools.MetadataPoolName != "" {
-		return fmt.Errorf("invalidObjStorePoolConfig: object store metadataPool and sharedPools.metadataPool=%s are mutually exclusive. Only one of them can be set.", sharedPools.MetadataPoolName)
+		return fmt.Errorf("invalidObjStorePoolCofig: object store metadataPool and sharedPools.metadataPool=%s are mutually exclusive. Only one of them can be set.", sharedPools.MetadataPoolName)
 	}
 	return nil
 }
diff --git a/pkg/operator/ceph/object/s3-handlers.go b/pkg/operator/ceph/object/s3-handlers.go
index 13eccb826..a40614c70 100644
--- a/pkg/operator/ceph/object/s3-handlers.go
+++ b/pkg/operator/ceph/object/s3-handlers.go
@@ -199,18 +199,12 @@ func (s *S3Agent) DeleteObjectInBucket(bucketname string, key string) (bool, err
 
 func BuildTransportTLS(tlsCert []byte, insecure bool) *http.Transport {
 	//nolint:gosec // is enabled only for testing
-	tlsConfig := &tls.Config{InsecureSkipVerify: insecure}
-	var caCertPool *x509.CertPool
-	var err error
-	caCertPool, err = x509.SystemCertPool()
-	if err != nil {
-		logger.Warningf("failed to load system cert pool; continuing without loading system certs")
-		caCertPool = x509.NewCertPool() // start with empty cert pool instead
-	}
+	tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12, InsecureSkipVerify: insecure}
 	if len(tlsCert) > 0 {
+		caCertPool := x509.NewCertPool()
 		caCertPool.AppendCertsFromPEM(tlsCert)
+		tlsConfig.RootCAs = caCertPool
 	}
-	tlsConfig.RootCAs = caCertPool
 
 	return &http.Transport{
 		TLSClientConfig: tlsConfig,
diff --git a/pkg/operator/ceph/object/shared_pools.go b/pkg/operator/ceph/object/shared_pools.go
index b447cdf46..7627a08a8 100644
--- a/pkg/operator/ceph/object/shared_pools.go
+++ b/pkg/operator/ceph/object/shared_pools.go
@@ -13,13 +13,14 @@ import (
 )
 
 const (
+	defaultPlacementName           = "default"
 	defaultPlacementCephConfigName = "default-placement"
 	defaultPlacementStorageClass   = "STANDARD"
 )
 
 func IsNeedToCreateObjectStorePools(sharedPools cephv1.ObjectSharedPoolsSpec) bool {
 	for _, pp := range sharedPools.PoolPlacements {
-		if pp.Default {
+		if pp.Name == defaultPlacementName {
 			// No need to create pools. External pools from default placement will be used
 			return false
 		}
@@ -32,25 +33,14 @@ func IsNeedToCreateObjectStorePools(sharedPools cephv1.ObjectSharedPoolsSpec) bo
 }
 
 func validatePoolPlacements(placements []cephv1.PoolPlacementSpec) error {
-	hasDefault := false
 	names := make(map[string]struct{}, len(placements))
 	for _, p := range placements {
-		if hasDefault && p.Default {
-			return fmt.Errorf("invalidObjStorePoolConfig: only one placement can be set as default")
-		}
-		hasDefault = hasDefault || p.Default
-
 		if _, ok := names[p.Name]; ok {
-			return fmt.Errorf("invalidObjStorePoolConfig: invalid placement %s: placement names must be unique", p.Name)
+			return fmt.Errorf("invalidObjStorePoolCofig: invalid placement %s: placement names must be unique", p.Name)
 		}
-
-		if p.Name == defaultPlacementCephConfigName && !p.Default {
-			return fmt.Errorf("invalidObjStorePoolConfig: placement with name %s must be marked as default", defaultPlacementCephConfigName)
-		}
-
 		names[p.Name] = struct{}{}
 		if err := validatePoolPlacementStorageClasses(p.StorageClasses); err != nil {
-			return fmt.Errorf("invalidObjStorePoolConfig: invalid placement %s: %w", p.Name, err)
+			return fmt.Errorf("invalidObjStorePoolCofig: invalid placement %s: %w", p.Name, err)
 		}
 	}
 	return nil
@@ -89,7 +79,6 @@ func adjustZonePlacementPools(zone map[string]interface{}, spec cephv1.ObjectSha
 
 	fromSpec := toZonePlacementPools(spec, name)
 
-	defaultPlacementName := getDefaultPlacementName(spec)
 	inConfig := map[string]struct{}{}
 	idxToRemove := map[int]struct{}{}
 	for i, p := range placements {
@@ -102,7 +91,7 @@ func adjustZonePlacementPools(zone map[string]interface{}, spec cephv1.ObjectSha
 			return nil, fmt.Errorf("unable to get pool placement name for zone %s: %w", name, err)
 		}
 		// check if placement should be removed
-		if _, inSpec := fromSpec[placementID]; !inSpec && placementID != defaultPlacementName {
+		if _, inSpec := fromSpec[placementID]; !inSpec && placementID != defaultPlacementCephConfigName {
 			// remove placement if it is not in spec, but don't remove default placement
 			idxToRemove[i] = struct{}{}
 			continue
@@ -162,18 +151,9 @@ func adjustZonePlacementPools(zone map[string]interface{}, spec cephv1.ObjectSha
 	return zone, nil
 }
 
-func getDefaultPlacementName(spec cephv1.ObjectSharedPoolsSpec) string {
-	for _, p := range spec.PoolPlacements {
-		if p.Default {
-			return p.Name
-		}
-	}
-	return defaultPlacementCephConfigName
-}
-
 func getDefaultMetadataPool(spec cephv1.ObjectSharedPoolsSpec) string {
 	for _, p := range spec.PoolPlacements {
-		if p.Default {
+		if p.Name == defaultPlacementName {
 			return p.MetadataPoolName
 		}
 	}
@@ -185,10 +165,12 @@ func toZonePlacementPools(spec cephv1.ObjectSharedPoolsSpec, ns string) map[stri
 	hasDefault := false
 	res := make(map[string]ZonePlacementPool, len(spec.PoolPlacements)+1)
 	for _, pp := range spec.PoolPlacements {
-		if pp.Default {
+		name := pp.Name
+		if pp.Name == defaultPlacementName {
 			hasDefault = true
+			name = defaultPlacementCephConfigName
 		}
-		res[pp.Name] = toZonePlacementPool(pp, ns)
+		res[name] = toZonePlacementPool(pp, ns)
 	}
 	if !hasDefault && spec.DataPoolName != "" && spec.MetadataPoolName != "" {
 		// set shared pools as default if no default placement was provided
@@ -215,7 +197,10 @@ func toZonePlacementPools(spec cephv1.ObjectSharedPoolsSpec, ns string) map[stri
 }
 
 func toZonePlacementPool(spec cephv1.PoolPlacementSpec, ns string) ZonePlacementPool {
-	placementNS := ns + "." + spec.Name
+	placementNS := ns
+	if spec.Name != defaultPlacementName {
+		placementNS += "." + spec.Name
+	}
 	// The extra pool is for omap data for multi-part uploads, so we use
 	// the metadata pool instead of the data pool.
 	nonECPool := spec.MetadataPoolName + ":" + placementNS + ".data.non-ec"
@@ -233,12 +218,15 @@ func toZonePlacementPool(spec cephv1.PoolPlacementSpec, ns string) ZonePlacement
 					DataPool: spec.DataPoolName + ":" + placementNS + ".data",
 				},
 			},
-			// Workaround: 'radosgw-admin set zone json' command sets incorrect default value for placement inline_data field.
+			// Workaround: radosgw-admin set zone json command sets incorrect default value for placement inline_data field.
 			// So we should set default value (true) explicitly.
 			// See: https://tracker.ceph.com/issues/67933
 			InlineData: true,
 		},
 	}
+	if res.Key == defaultPlacementName {
+		res.Key = defaultPlacementCephConfigName
+	}
 	for _, v := range spec.StorageClasses {
 		res.Val.StorageClasses[v.Name] = ZonePlacementStorageClass{
 			DataPool: v.DataPoolName + ":" + ns + "." + v.Name,
@@ -247,7 +235,7 @@ func toZonePlacementPool(spec cephv1.PoolPlacementSpec, ns string) ZonePlacement
 	return res
 }
 
-func adjustZoneGroupPlacementTargets(group, zone map[string]interface{}, defaultPlacement string) (map[string]interface{}, error) {
+func adjustZoneGroupPlacementTargets(group, zone map[string]interface{}) (map[string]interface{}, error) {
 	name, err := getObjProperty[string](group, "name")
 	if err != nil {
 		return nil, fmt.Errorf("unable to get zonegroup name: %w", err)
@@ -259,7 +247,7 @@ func adjustZoneGroupPlacementTargets(group, zone map[string]interface{}, default
 		return nil, fmt.Errorf("unable to deep copy config for zonegroup %s: %w", name, err)
 	}
 
-	_, err = updateObjProperty(group, defaultPlacement, "default_placement")
+	_, err = updateObjProperty(group, defaultPlacementCephConfigName, "default_placement")
 	if err != nil {
 		return nil, fmt.Errorf("unable to set default_placement for zonegroup %s: %w", name, err)
 	}
diff --git a/pkg/operator/ceph/pool/controller.go b/pkg/operator/ceph/pool/controller.go
index 33ac9b428..015d59dea 100644
--- a/pkg/operator/ceph/pool/controller.go
+++ b/pkg/operator/ceph/pool/controller.go
@@ -76,7 +76,6 @@ type ReconcileCephBlockPool struct {
 	blockPoolContexts map[string]*blockPoolHealth
 	opManagerContext  context.Context
 	recorder          record.EventRecorder
-	opConfig          opcontroller.OperatorConfig
 }
 
 type blockPoolHealth struct {
@@ -88,11 +87,11 @@ type blockPoolHealth struct {
 // Add creates a new CephBlockPool Controller and adds it to the Manager. The Manager will set fields on the Controller
 // and Start it when the Manager is Started.
 func Add(mgr manager.Manager, context *clusterd.Context, opManagerContext context.Context, opConfig opcontroller.OperatorConfig) error {
-	return add(opManagerContext, mgr, newReconciler(mgr, context, opManagerContext, opConfig))
+	return add(opManagerContext, mgr, newReconciler(mgr, context, opManagerContext))
 }
 
 // newReconciler returns a new reconcile.Reconciler
-func newReconciler(mgr manager.Manager, context *clusterd.Context, opManagerContext context.Context, opConfig opcontroller.OperatorConfig) reconcile.Reconciler {
+func newReconciler(mgr manager.Manager, context *clusterd.Context, opManagerContext context.Context) reconcile.Reconciler {
 	return &ReconcileCephBlockPool{
 		client:            mgr.GetClient(),
 		scheme:            mgr.GetScheme(),
@@ -100,7 +99,6 @@ func newReconciler(mgr manager.Manager, context *clusterd.Context, opManagerCont
 		blockPoolContexts: make(map[string]*blockPoolHealth),
 		opManagerContext:  opManagerContext,
 		recorder:          mgr.GetEventRecorderFor("rook-" + controllerName),
-		opConfig:          opConfig,
 	}
 }
 
@@ -247,12 +245,6 @@ func (r *ReconcileCephBlockPool) reconcile(request reconcile.Request) (reconcile
 		logger.Infof("deleting pool %q", poolSpec.Name)
 		err = deletePool(r.context, clusterInfo, &poolSpec)
 		if err != nil {
-			if opcontroller.ForceDeleteRequested(cephBlockPool.GetAnnotations()) {
-				cleanupErr := r.cleanup(cephBlockPool, &cephCluster)
-				if cleanupErr != nil {
-					return reconcile.Result{}, *cephBlockPool, errors.Wrapf(cleanupErr, "failed to create clean up job for ceph blockpool %q", cephBlockPool.Name)
-				}
-			}
 			return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrapf(err, "failed to delete pool %q. ", cephBlockPool.Name)
 		}
 
@@ -381,20 +373,6 @@ func (r *ReconcileCephBlockPool) reconcileCreatePool(clusterInfo *cephclient.Clu
 	return reconcile.Result{}, nil
 }
 
-func (r *ReconcileCephBlockPool) cleanup(cephblockpool *cephv1.CephBlockPool, cephCluster *cephv1.CephCluster) error {
-	logger.Infof("starting cleanup of the ceph resources for CephBlockPool %q in k8s namespace %q", cephblockpool.Name, cephblockpool.Namespace)
-	cleanupConfig := map[string]string{
-		opcontroller.CephBlockPoolNameEnv: cephblockpool.Name,
-	}
-	cleanup := opcontroller.NewResourceCleanup(cephblockpool, cephCluster, r.opConfig.Image, cleanupConfig)
-	jobName := k8sutil.TruncateNodeNameForJob("cleanup-cephblockpool-%s", cephblockpool.Name)
-	err := cleanup.StartJob(r.clusterInfo.Context, r.context.Clientset, jobName)
-	if err != nil {
-		return errors.Wrapf(err, "failed to run clean up job to clean the cephblockpool %q", cephblockpool.Name)
-	}
-	return nil
-}
-
 // Create the pool
 func createPool(context *clusterd.Context, clusterInfo *cephclient.ClusterInfo, clusterSpec *cephv1.ClusterSpec, p *cephv1.NamedPoolSpec) error {
 	// Set the application name to rbd by default, but override later for special pools
diff --git a/pkg/operator/ceph/pool/radosnamespace/controller.go b/pkg/operator/ceph/pool/radosnamespace/controller.go
index 790fd6f61..7bae0b595 100644
--- a/pkg/operator/ceph/pool/radosnamespace/controller.go
+++ b/pkg/operator/ceph/pool/radosnamespace/controller.go
@@ -326,7 +326,14 @@ func (r *ReconcileCephBlockPoolRadosNamespace) updateClusterConfig(cephBlockPool
 		},
 	}
 
-	csiClusterConfigEntry.RBD.NetNamespaceFilePath = ""
+	if cephCluster.Spec.Network.IsMultus() {
+		// Build the network namespace config to be injected into the csi config
+		netNamespaceFilePath, err := csi.GenerateNetNamespaceFilePath(r.opManagerContext, r.client, cephCluster.Namespace, r.opConfig.OperatorNamespace, csi.RBDDriverShortName)
+		if err != nil {
+			return errors.Wrap(err, "failed to generate rbd net namespace file path")
+		}
+		csiClusterConfigEntry.RBD.NetNamespaceFilePath = netNamespaceFilePath
+	}
 
 	// Save cluster config in the csi config map
 	err := csi.SaveClusterConfig(r.context.Clientset, buildClusterID(cephBlockPoolRadosNamespace), cephCluster.Namespace, r.clusterInfo, &csiClusterConfigEntry)


Test patch:
diff --git a/.github/workflows/canary-integration-test.yml b/.github/workflows/canary-integration-test.yml
index 3b493eeff..55dd34b0a 100644
--- a/.github/workflows/canary-integration-test.yml
+++ b/.github/workflows/canary-integration-test.yml
@@ -25,7 +25,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -334,7 +334,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -412,7 +412,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
       - name: consider debugging
@@ -463,7 +463,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -512,7 +512,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -570,7 +570,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -622,7 +622,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -679,7 +679,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -755,7 +755,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -807,7 +807,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -862,7 +862,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -930,7 +930,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -984,7 +984,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -1048,7 +1048,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -1085,7 +1085,6 @@ jobs:
           yq write -i tests/manifests/test-cluster-on-pvc-encrypted.yaml "spec.storage.storageClassDeviceSets[0].count" 2
           yq write -i tests/manifests/test-cluster-on-pvc-encrypted.yaml "spec.storage.storageClassDeviceSets[0].volumeClaimTemplates[0].spec.resources.requests.storage" 6Gi
           kubectl create -f tests/manifests/test-cluster-on-pvc-encrypted.yaml
-          kubectl patch -n rook-ceph cephcluster rook-ceph --type merge -p '{"spec":{"security":{"keyRotation":{"enabled": true, "schedule":"*/1 * * * *"}}}}'
           yq merge --inplace --arrays append tests/manifests/test-object.yaml tests/manifests/test-kms-vault-spec-token-auth.yaml
           yq write -i tests/manifests/test-object.yaml "spec.security.kms.connectionDetails.VAULT_BACKEND_PATH" rook/ver2
           kubectl create -f tests/manifests/test-object.yaml
@@ -1106,9 +1105,6 @@ jobs:
           tests/scripts/deploy-validate-vault.sh validate_osd
           sudo lsblk
 
-      - name: wait and verify key rotation
-        run: tests/scripts/deploy-validate-vault.sh validate_key_rotation rook/ver1
-
       - name: validate rgw vault kv
         run: |
           tests/scripts/deploy-validate-vault.sh validate_rgw
@@ -1138,7 +1134,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -1174,7 +1170,6 @@ jobs:
           yq write -i tests/manifests/test-cluster-on-pvc-encrypted.yaml "spec.storage.storageClassDeviceSets[0].count" 2
           yq write -i tests/manifests/test-cluster-on-pvc-encrypted.yaml "spec.storage.storageClassDeviceSets[0].volumeClaimTemplates[0].spec.resources.requests.storage" 6Gi
           kubectl create -f tests/manifests/test-cluster-on-pvc-encrypted.yaml
-          kubectl patch -n rook-ceph cephcluster rook-ceph --type merge -p '{"spec":{"security":{"keyRotation":{"enabled": true, "schedule":"*/1 * * * *"}}}}'
           tests/scripts/github-action-helper.sh deploy_manifest_with_local_build deploy/examples/toolbox.yaml
 
       - name: wait for prepare pod
@@ -1191,9 +1186,6 @@ jobs:
           tests/scripts/deploy-validate-vault.sh validate_osd
           sudo lsblk
 
-      - name: wait and verify key rotation
-        run: KUBERNETES_AUTH=true tests/scripts/deploy-validate-vault.sh validate_key_rotation rook/ver1
-
       - name: collect common logs
         if: always()
         uses: ./.github/workflows/collect-logs
@@ -1208,7 +1200,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -1265,7 +1257,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -1524,7 +1516,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -1619,7 +1611,7 @@ jobs:
       matrix:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -1639,9 +1631,8 @@ jobs:
           ibm-service-api-key: ${{ secrets.IBM_SERVICE_API_KEY }}
           github-token: ${{ secrets.GITHUB_TOKEN }}
           artifact-name: ${{ github.job }}-${{ matrix.ceph-image }}
-          ceph-image: ${{ matrix.ceph-image }}
 
-  multus-public-and-cluster:
+  multus-cluster-network:
     runs-on: ubuntu-22.04
     if: "!contains(github.event.pull_request.labels.*.name, 'skip-ci')"
     strategy:
@@ -1649,7 +1640,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -1658,50 +1649,45 @@ jobs:
         with:
           use-tmate: ${{ secrets.USE_TMATE }}
 
+      - name: setup golang
+        uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
+        with:
+          go-version: "1.22"
+
       - name: setup cluster resources
         uses: ./.github/workflows/canary-test-config
 
       - name: set Ceph version in CephCluster manifest
-        run: tests/scripts/github-action-helper.sh replace_ceph_image "deploy/examples/cluster-multus-test.yaml" "${{ matrix.ceph-image }}"
-
-      - name: Setup multus
-        run: ./tests/scripts/multus/setup-multus.sh
+        run: tests/scripts/github-action-helper.sh replace_ceph_image  "deploy/examples/cluster-test.yaml" "${{ matrix.ceph-image }}"
 
-      - name: Set up multus prerequisite host routing
-        run: kubectl create -f tests/scripts/multus/host-cfg-ds.yaml
+      - name: allow holder pod deployment
+        run: sed -i "s|CSI_DISABLE_HOLDER_PODS|# CSI_DISABLE_HOLDER_PODS|g" "deploy/examples/operator.yaml"
 
-      - name: create cluster prerequisites
-        run: tests/scripts/github-action-helper.sh create_cluster_prerequisites
-
-      - name: Install public and cluster NADs in rook-ceph namespace
-        run: kubectl create -f tests/scripts/multus/rook-ceph-public-cluster-nads.yaml
+      - name: validate-yaml
+        run: tests/scripts/github-action-helper.sh validate_yaml
 
       - name: use local disk and create partitions for osds
         run: |
           tests/scripts/github-action-helper.sh use_local_disk
           tests/scripts/github-action-helper.sh create_partitions_for_osds
 
-      - name: deploy cluster
-        run: |
-          tests/scripts/github-action-helper.sh deploy_manifest_with_local_build deploy/examples/operator.yaml
-
-          export BLOCK="$(tests/scripts/github-action-helper.sh find_extra_block_dev)"
-          yq write -i deploy/examples/cluster-multus-test.yaml "spec.storage.deviceFilter" "${BLOCK}"
-          kubectl create -f deploy/examples/cluster-multus-test.yaml
-
-          tests/scripts/github-action-helper.sh deploy_toolbox
+      - name: deploy multus
+        run: tests/scripts/github-action-helper.sh deploy_multus
 
-          kubectl create -f deploy/examples/filesystem-test.yaml
-          kubectl create -f deploy/examples/nfs-test.yaml
+      - name: deploy multus cluster
+        run: tests/scripts/github-action-helper.sh deploy_multus_cluster
 
       - name: wait for prepare pod
         run: tests/scripts/github-action-helper.sh wait_for_prepare_pod 2
 
       - name: wait for ceph to be ready
-        run: tests/scripts/github-action-helper.sh wait_for_ceph_to_be_ready osd 2
+        run: IS_POD_NETWORK=true IS_MULTUS=true tests/scripts/github-action-helper.sh wait_for_ceph_to_be_ready osd 2
+
+      - name: wait for ceph-csi configmap to be updated with network namespace
+        run: tests/scripts/github-action-helper.sh wait_for_ceph_csi_configmap_to_be_updated
 
       - name: wait for cephnfs to be ready
-        run: tests/scripts/github-action-helper.sh wait_for_ceph_to_be_ready nfs 1
+        run: IS_POD_NETWORK=true IS_MULTUS=true tests/scripts/github-action-helper.sh wait_for_ceph_to_be_ready nfs 1
 
       - name: check multus connections
         run: tests/scripts/github-action-helper.sh test_multus_connections
@@ -1722,6 +1708,64 @@ jobs:
           name: ${{ github.job }}-${{ matrix.ceph-image }}
           additional-namespace: kube-system
 
+  csi-hostnetwork-disabled:
+    runs-on: ubuntu-22.04
+    if: "!contains(github.event.pull_request.labels.*.name, 'skip-ci')"
+    strategy:
+      matrix:
+        ceph-image: ${{ fromJson(inputs.ceph_images) }}
+    steps:
+      - name: checkout
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
+        with:
+          fetch-depth: 0
+
+      - name: consider debugging
+        uses: ./.github/workflows/tmate_debug
+        with:
+          use-tmate: ${{ secrets.USE_TMATE }}
+
+      - name: setup cluster resources
+        uses: ./.github/workflows/canary-test-config
+
+      - name: allow holder pod deployment
+        run: sed -i "s|CSI_DISABLE_HOLDER_PODS|# CSI_DISABLE_HOLDER_PODS|g" "deploy/examples/operator.yaml"
+
+      - name: set Ceph version in CephCluster manifest
+        run: tests/scripts/github-action-helper.sh replace_ceph_image  "deploy/examples/cluster-test.yaml" "${{ matrix.ceph-image }}"
+
+      - name: use local disk and create partitions for osds
+        run: |
+          tests/scripts/github-action-helper.sh use_local_disk
+          tests/scripts/github-action-helper.sh create_partitions_for_osds
+
+      - name: deploy CSI hostNetworking disabled cluster
+        run: tests/scripts/github-action-helper.sh deploy_csi_hostnetwork_disabled_cluster
+
+      - name: wait for prepare pod
+        run: tests/scripts/github-action-helper.sh wait_for_prepare_pod 2
+
+      - name: wait for ceph to be ready
+        run: IS_POD_NETWORK=true tests/scripts/github-action-helper.sh wait_for_ceph_to_be_ready osd 2
+
+      - name: wait for ceph-csi configmap to be updated with network namespace
+        run: tests/scripts/github-action-helper.sh wait_for_ceph_csi_configmap_to_be_updated
+
+      - name: test ceph-csi-rbd plugin restart
+        run: tests/scripts/github-action-helper.sh test_csi_rbd_workload
+
+      - name: test ceph-csi-cephfs plugin restart
+        run: tests/scripts/github-action-helper.sh test_csi_cephfs_workload
+
+      - name: test ceph-csi-nfs plugin restart
+        run: tests/scripts/github-action-helper.sh test_csi_nfs_workload
+
+      - name: collect common logs
+        if: always()
+        uses: ./.github/workflows/collect-logs
+        with:
+          name: ${{ github.job }}-${{ matrix.ceph-image }}
+
   two-object-one-zone:
     runs-on: ubuntu-22.04
     if: "!contains(github.event.pull_request.labels.*.name, 'skip-ci')"
@@ -1730,7 +1774,7 @@ jobs:
         ceph-image: ${{ fromJson(inputs.ceph_images) }}
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/integration-test-helm-suite.yaml b/.github/workflows/integration-test-helm-suite.yaml
index 962f98f8d..f00ccdc79 100644
--- a/.github/workflows/integration-test-helm-suite.yaml
+++ b/.github/workflows/integration-test-helm-suite.yaml
@@ -31,7 +31,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/integration-test-keystone-auth-suite.yaml b/.github/workflows/integration-test-keystone-auth-suite.yaml
index 24da4dd58..014652a5c 100644
--- a/.github/workflows/integration-test-keystone-auth-suite.yaml
+++ b/.github/workflows/integration-test-keystone-auth-suite.yaml
@@ -31,7 +31,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/integration-test-mgr-suite.yaml b/.github/workflows/integration-test-mgr-suite.yaml
index cf8623cba..916feff75 100644
--- a/.github/workflows/integration-test-mgr-suite.yaml
+++ b/.github/workflows/integration-test-mgr-suite.yaml
@@ -30,7 +30,7 @@ jobs:
         kubernetes-versions: ["v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/integration-test-multi-cluster-suite.yaml b/.github/workflows/integration-test-multi-cluster-suite.yaml
index 71c73b018..cc1f24252 100644
--- a/.github/workflows/integration-test-multi-cluster-suite.yaml
+++ b/.github/workflows/integration-test-multi-cluster-suite.yaml
@@ -31,7 +31,7 @@ jobs:
         kubernetes-versions: ["v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/integration-test-object-suite.yaml b/.github/workflows/integration-test-object-suite.yaml
index be07cdb0f..d3980eed0 100644
--- a/.github/workflows/integration-test-object-suite.yaml
+++ b/.github/workflows/integration-test-object-suite.yaml
@@ -31,7 +31,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/integration-test-smoke-suite.yaml b/.github/workflows/integration-test-smoke-suite.yaml
index 0ccf3fe1a..d64bcdf01 100644
--- a/.github/workflows/integration-test-smoke-suite.yaml
+++ b/.github/workflows/integration-test-smoke-suite.yaml
@@ -31,7 +31,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/integration-test-upgrade-suite.yaml b/.github/workflows/integration-test-upgrade-suite.yaml
index 0c88af1a2..99051c6f1 100644
--- a/.github/workflows/integration-test-upgrade-suite.yaml
+++ b/.github/workflows/integration-test-upgrade-suite.yaml
@@ -31,7 +31,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -75,7 +75,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/integration-tests-on-release.yaml b/.github/workflows/integration-tests-on-release.yaml
index e4fb4c5c3..32bd9039d 100644
--- a/.github/workflows/integration-tests-on-release.yaml
+++ b/.github/workflows/integration-tests-on-release.yaml
@@ -24,7 +24,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.28.12", "v1.29.7", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -64,7 +64,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.28.12", "v1.29.7", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -105,7 +105,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.28.12", "v1.29.7", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -143,7 +143,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.28.12", "v1.29.7", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -181,7 +181,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.28.12", "v1.29.7", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
@@ -222,7 +222,7 @@ jobs:
         kubernetes-versions: ["v1.26.15", "v1.31.0"]
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
index 704862473..3167fd743 100644
--- a/.github/workflows/unit-test.yml
+++ b/.github/workflows/unit-test.yml
@@ -37,11 +37,11 @@ jobs:
     if: "!contains(github.event.pull_request.labels.*.name, 'skip-ci')"
     steps:
       - name: checkout
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+      - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:
           go-version: "1.22"
 
diff --git a/pkg/daemon/ceph/cleanup/radosnamespace_test.go b/pkg/daemon/ceph/cleanup/radosnamespace_test.go
index 6010982e4..eb6d7aca2 100644
--- a/pkg/daemon/ceph/cleanup/radosnamespace_test.go
+++ b/pkg/daemon/ceph/cleanup/radosnamespace_test.go
@@ -90,56 +90,3 @@ func TestRadosNamespace(t *testing.T) {
 		assert.NoError(t, err)
 	})
 }
-
-func TestBlockPoolCleanup(t *testing.T) {
-	clusterInfo := cephclient.AdminTestClusterInfo("mycluster")
-	poolName := "test-pool"
-
-	t.Run("no images in blockpool", func(t *testing.T) {
-		executor := &exectest.MockExecutor{}
-		executor.MockExecuteCommandWithOutput = func(command string, args ...string) (string, error) {
-			logger.Infof("Command: %s %v", command, args)
-			if args[0] == "ls" && args[1] == "-l" {
-				assert.Equal(t, poolName, args[2])
-				return "", nil
-			}
-			return "", errors.New("unknown command")
-		}
-		context := &clusterd.Context{Executor: executor}
-		err := BlockPoolCleanup(context, clusterInfo, poolName)
-		assert.NoError(t, err)
-	})
-
-	t.Run("images with snapshots available in blockpool", func(t *testing.T) {
-		executor := &exectest.MockExecutor{}
-		executor.MockExecuteCommandWithOutput = func(command string, args ...string) (string, error) {
-			logger.Infof("Command: %s %v", command, args)
-			// list all subvolumes in subvolumegroup
-			if args[0] == "ls" && args[1] == "-l" {
-				assert.Equal(t, poolName, args[2])
-				return mockImageLSResponse, nil
-			}
-			if args[0] == "snap" && args[1] == "ls" {
-				assert.Equal(t, "test-pool/csi-vol-136268e8-5386-4453-a6bd-9dca381d187d", args[2])
-				return mockSnapshotsResponse, nil
-			}
-			if args[0] == "snap" && args[1] == "rm" {
-				assert.Equal(t, "test-pool/csi-vol-136268e8-5386-4453-a6bd-9dca381d187d@snap1", args[2])
-				return "", nil
-			}
-			if args[0] == "trash" && args[1] == "mv" {
-				assert.Equal(t, "test-pool/csi-vol-136268e8-5386-4453-a6bd-9dca381d187d", args[2])
-				return "", nil
-			}
-			if args[0] == "rbd" && args[1] == "task" && args[2] == "add" && args[3] == "trash" {
-				// pool-name/image-id
-				assert.Equal(t, "test-pool/16e35cfa56a7", args[5])
-				return "", nil
-			}
-			return "", errors.New("unknown command")
-		}
-		context := &clusterd.Context{Executor: executor}
-		err := BlockPoolCleanup(context, clusterInfo, poolName)
-		assert.NoError(t, err)
-	})
-}
diff --git a/pkg/operator/ceph/cluster/cephstatus_test.go b/pkg/operator/ceph/cluster/cephstatus_test.go
index 183a4b4c5..d464f3a52 100644
--- a/pkg/operator/ceph/cluster/cephstatus_test.go
+++ b/pkg/operator/ceph/cluster/cephstatus_test.go
@@ -324,10 +324,13 @@ func TestGetRookPodsOnNode(t *testing.T) {
 		{"app": "rook-ceph-osd"},
 		{"app": "csi-rbdplugin-provisioner"},
 		{"app": "csi-rbdplugin"},
+		{"app": "csi-rbdplugin-holder"},
 		{"app": "csi-cephfsplugin-provisioner"},
 		{"app": "csi-cephfsplugin"},
+		{"app": "csi-cephfsplugin-holder"},
 		{"app": "csi-nfsplugin-provisioner"},
 		{"app": "csi-nfsplugin"},
+		{"app": "csi-nfsplugin-holder"},
 		{"app": "rook-ceph-operator"},
 		{"app": "rook-ceph-crashcollector"},
 		{"app": "rook-ceph-mgr"},
@@ -363,7 +366,7 @@ func TestGetRookPodsOnNode(t *testing.T) {
 	pods, err := c.getRookPodsOnNode("node0")
 	assert.NoError(t, err)
 	// A pod is having two matching labels and its returned only once
-	assert.Equal(t, 14, len(pods))
+	assert.Equal(t, 17, len(pods))
 
 	podNames := []string{}
 	for _, pod := range pods {
diff --git a/pkg/operator/ceph/cluster/osd/spec_test.go b/pkg/operator/ceph/cluster/osd/spec_test.go
index f04495fb4..5f5a9579b 100644
--- a/pkg/operator/ceph/cluster/osd/spec_test.go
+++ b/pkg/operator/ceph/cluster/osd/spec_test.go
@@ -143,10 +143,10 @@ func testPodDevices(t *testing.T, dataDir, deviceName string, allDevices bool) {
 	assert.Equal(t, corev1.RestartPolicyAlways, deployment.Spec.Template.Spec.RestartPolicy)
 	assert.Equal(t, "my-priority-class", deployment.Spec.Template.Spec.PriorityClassName)
 	if devMountNeeded && len(dataDir) > 0 {
-		assert.Equal(t, 10, len(deployment.Spec.Template.Spec.Volumes))
+		assert.Equal(t, 9, len(deployment.Spec.Template.Spec.Volumes))
 	}
 	if devMountNeeded && len(dataDir) == 0 {
-		assert.Equal(t, 10, len(deployment.Spec.Template.Spec.Volumes))
+		assert.Equal(t, 9, len(deployment.Spec.Template.Spec.Volumes))
 	}
 	if !devMountNeeded && len(dataDir) > 0 {
 		assert.Equal(t, 1, len(deployment.Spec.Template.Spec.Volumes))
@@ -164,7 +164,7 @@ func testPodDevices(t *testing.T, dataDir, deviceName string, allDevices bool) {
 	initCont := deployment.Spec.Template.Spec.InitContainers[0]
 	assert.Equal(t, "quay.io/ceph/ceph:v15", initCont.Image)
 	assert.Equal(t, "activate", initCont.Name)
-	assert.Equal(t, 5, len(initCont.VolumeMounts))
+	assert.Equal(t, 4, len(initCont.VolumeMounts))
 	initCont = deployment.Spec.Template.Spec.InitContainers[1]
 	assert.Equal(t, "quay.io/ceph/ceph:v15", initCont.Image)
 	assert.Equal(t, "expand-bluefs", initCont.Name)
diff --git a/pkg/operator/ceph/csi/controller_test.go b/pkg/operator/ceph/csi/controller_test.go
index b1998bab4..2860c6d67 100644
--- a/pkg/operator/ceph/csi/controller_test.go
+++ b/pkg/operator/ceph/csi/controller_test.go
@@ -264,10 +264,12 @@ func TestCephCSIController(t *testing.T) {
 
 		ds, err := c.Clientset.AppsV1().DaemonSets(namespace).List(ctx, metav1.ListOptions{})
 		assert.NoError(t, err)
-		assert.Equal(t, 2, len(ds.Items), ds)
+		assert.Equal(t, 4, len(ds.Items), ds)
 		assert.Equal(t, "csi-cephfsplugin", ds.Items[0].Name)
-		assert.Equal(t, "csi-rbdplugin", ds.Items[1].Name)
-		assert.Equal(t, ``, ds.Items[1].Spec.Template.Annotations["k8s.v1.cni.cncf.io/networks"], ds.Items[1].Spec.Template.Annotations)
+		assert.Equal(t, "csi-cephfsplugin-holder-rook-ceph", ds.Items[1].Name)
+		assert.Equal(t, "csi-rbdplugin", ds.Items[2].Name)
+		assert.Equal(t, "csi-rbdplugin-holder-rook-ceph", ds.Items[3].Name)
+		assert.Equal(t, `[{"name":"public-net","namespace":"rook-ceph"}]`, ds.Items[1].Spec.Template.Annotations["k8s.v1.cni.cncf.io/networks"], ds.Items[1].Spec.Template.Annotations)
 
 		assert.Equal(t, []string{namespace}, saveCSIDriverOptionsCalledForClusterNS)
 	})
diff --git a/pkg/operator/ceph/csi/operator_config_test.go b/pkg/operator/ceph/csi/operator_config_test.go
index 33f10c2a5..4beb6a9a5 100644
--- a/pkg/operator/ceph/csi/operator_config_test.go
+++ b/pkg/operator/ceph/csi/operator_config_test.go
@@ -46,6 +46,7 @@ func TestReconcileCSI_createOrUpdateOperatorConfig(t *testing.T) {
 		opConfig: opcontroller.OperatorConfig{
 			OperatorNamespace: "test",
 		},
+		clustersWithHolder: []ClusterDetail{},
 	}
 	cluster := &cephv1.CephCluster{
 		ObjectMeta: metav1.ObjectMeta{
diff --git a/pkg/operator/ceph/csi/operator_driver_test.go b/pkg/operator/ceph/csi/operator_driver_test.go
index 647cd6fa2..c1c0c2831 100644
--- a/pkg/operator/ceph/csi/operator_driver_test.go
+++ b/pkg/operator/ceph/csi/operator_driver_test.go
@@ -48,6 +48,7 @@ func TestReconcileCSI_createOrUpdateDriverResources(t *testing.T) {
 		opConfig: opcontroller.OperatorConfig{
 			OperatorNamespace: "test",
 		},
+		clustersWithHolder: []ClusterDetail{},
 	}
 	cluster := &cephv1.CephCluster{
 		ObjectMeta: metav1.ObjectMeta{
diff --git a/pkg/operator/ceph/csi/spec_test.go b/pkg/operator/ceph/csi/spec_test.go
index 603ec9f46..5e75fdfbf 100644
--- a/pkg/operator/ceph/csi/spec_test.go
+++ b/pkg/operator/ceph/csi/spec_test.go
@@ -21,13 +21,117 @@ import (
 	_ "embed"
 	"testing"
 
+	cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
+	rookclient "github.com/rook/rook/pkg/client/clientset/versioned/fake"
+	"github.com/rook/rook/pkg/clusterd"
+	cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
+	opcontroller "github.com/rook/rook/pkg/operator/ceph/controller"
+	"github.com/rook/rook/pkg/operator/k8sutil"
+	testop "github.com/rook/rook/pkg/operator/test"
 	"github.com/stretchr/testify/assert"
 	apps "k8s.io/api/apps/v1"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
 	kfake "k8s.io/client-go/kubernetes/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
+func TestReconcileCSI_configureHolders(t *testing.T) {
+	r := &ReconcileCSI{
+		context: &clusterd.Context{
+			Clientset:     testop.New(t, 1),
+			RookClientset: rookclient.NewSimpleClientset(),
+		},
+		opManagerContext:   context.TODO(),
+		opConfig:           opcontroller.OperatorConfig{},
+		clustersWithHolder: []ClusterDetail{},
+	}
+
+	t.Run("no clusters, noop", func(t *testing.T) {
+		err := r.configureHolders([]driverDetails{}, templateParam{}, []v1.Toleration{}, nil)
+		assert.NoError(t, err)
+	})
+
+	t.Run("one multus cluster", func(t *testing.T) {
+		r.opConfig.OperatorNamespace = "rook-ceph"
+		driverDetails := []driverDetails{
+			{
+				name:           "rbd",
+				fullName:       "rbd.csi.ceph.com",
+				holderTemplate: CephFSPluginHolderTemplatePath,
+				nodeAffinity:   cephFSPluginNodeAffinityEnv,
+				toleration:     cephFSPluginTolerationsEnv,
+			},
+		}
+		tp := templateParam{
+			Param:     CSIParam,
+			Namespace: r.opConfig.OperatorNamespace,
+		}
+
+		r.clustersWithHolder = []ClusterDetail{
+			{
+				cluster: &cephv1.CephCluster{
+					ObjectMeta: metav1.ObjectMeta{Namespace: "rook-ceph", Name: "my-cluster"},
+					Spec:       cephv1.ClusterSpec{},
+				},
+				clusterInfo: &cephclient.ClusterInfo{
+					Monitors:  map[string]*cephclient.MonInfo{"a": {Name: "a", Endpoint: "10.0.0.1:6789"}},
+					OwnerInfo: cephclient.NewMinimumOwnerInfoWithOwnerRef()},
+			},
+		}
+
+		t.Setenv(k8sutil.PodNamespaceEnvVar, "rook-ceph")
+		_, err := r.context.Clientset.CoreV1().ConfigMaps("rook-ceph").Create(r.opManagerContext, &v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: ConfigName, Namespace: "rook-ceph"}, Data: map[string]string{}}, metav1.CreateOptions{})
+		assert.NoError(t, err)
+
+		err = r.configureHolders(driverDetails, tp, []v1.Toleration{}, nil)
+		assert.NoError(t, err)
+	})
+}
+
+func TestGenerateNetNamespaceFilePath(t *testing.T) {
+	ctx := context.TODO()
+
+	t.Run("generate with no op configmap available and non supported driver name", func(t *testing.T) {
+		client := fake.NewClientBuilder().Build()
+		netNsFilePath, err := GenerateNetNamespaceFilePath(ctx, client, "rook-ceph", "rook-ceph", "foo")
+		assert.Error(t, err)
+		assert.Empty(t, "", netNsFilePath)
+	})
+
+	t.Run("generate with no op configmap available for rbd", func(t *testing.T) {
+		client := fake.NewClientBuilder().Build()
+		netNsFilePath, err := GenerateNetNamespaceFilePath(ctx, client, "rook-ceph", "rook-ceph", "rbd")
+		assert.NoError(t, err)
+		assert.Equal(t, "/var/lib/kubelet/plugins/rook-ceph.rbd.csi.ceph.com/rook-ceph.net.ns", netNsFilePath)
+	})
+
+	t.Run("generate with no op configmap available for cephfs", func(t *testing.T) {
+		client := fake.NewClientBuilder().Build()
+		netNsFilePath, err := GenerateNetNamespaceFilePath(ctx, client, "rook-ceph", "rook-ceph", "cephfs")
+		assert.NoError(t, err)
+		assert.Equal(t, "/var/lib/kubelet/plugins/rook-ceph.cephfs.csi.ceph.com/rook-ceph.net.ns", netNsFilePath)
+	})
+
+	t.Run("generate with op configmap for cephfs", func(t *testing.T) {
+		opCm := &v1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      opcontroller.OperatorSettingConfigMapName,
+				Namespace: "rook-ceph",
+			},
+			Data: map[string]string{"ROOK_CSI_KUBELET_DIR_PATH": "/foo"},
+		}
+		object := []runtime.Object{
+			opCm,
+		}
+		client := fake.NewClientBuilder().WithRuntimeObjects(object...).Build()
+		netNsFilePath, err := GenerateNetNamespaceFilePath(ctx, client, "rook-ceph", "rook-ceph", "cephfs")
+		assert.NoError(t, err)
+		assert.Equal(t, "/foo/plugins/rook-ceph.cephfs.csi.ceph.com/rook-ceph.net.ns", netNsFilePath)
+	})
+}
+
 func Test_getCSIDriverNamePrefixFromDeployment(t *testing.T) {
 	namespace := "test"
 	deployment := func(name, containerName, drivernameSuffix string) *apps.Deployment {
diff --git a/pkg/operator/ceph/object/bucket/provisioner_test.go b/pkg/operator/ceph/object/bucket/provisioner_test.go
index 467b0a54f..e9af29131 100644
--- a/pkg/operator/ceph/object/bucket/provisioner_test.go
+++ b/pkg/operator/ceph/object/bucket/provisioner_test.go
@@ -40,11 +40,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-const (
-	userPath   = "rgw.test/admin/user"
-	bucketPath = "rgw.test/admin/bucket"
-)
-
 func TestPopulateDomainAndPort(t *testing.T) {
 	ctx := context.TODO()
 	store := "test-store"
@@ -147,42 +142,40 @@ func TestQuanityToInt64(t *testing.T) {
 }
 
 func TestProvisioner_setAdditionalSettings(t *testing.T) {
-	newProvisioner := func(t *testing.T, getResult *map[string]string, getSeen, putSeen *map[string][]string) *Provisioner {
+	newProvisioner := func(t *testing.T, getUserResult string, putValsSeen *[]string) *Provisioner {
 		mockClient := &object.MockClient{
 			MockDo: func(req *http.Request) (*http.Response, error) {
-				path := req.URL.Path
-
 				// t.Logf("HTTP req: %#v", req)
-				t.Logf("HTTP %s: %s %s", req.Method, path, req.URL.RawQuery)
-
-				statusCode := 200
-				if _, ok := (*getResult)[path]; !ok {
-					// path not configured
-					statusCode = 500
+				t.Logf("HTTP %s: %s %s", req.Method, req.URL.Path, req.URL.RawQuery)
+
+				assert.Contains(t, req.URL.RawQuery, "&uid=bob")
+
+				if req.Method == http.MethodGet {
+					if req.URL.Path == "my.endpoint.net/admin/user" {
+						statusCode := 200
+						if getUserResult == "" {
+							statusCode = 500
+						}
+						return &http.Response{
+							StatusCode: statusCode,
+							Body:       io.NopCloser(bytes.NewReader([]byte(getUserResult))),
+						}, nil
+					}
 				}
-				responseBody := []byte(`[]`)
-
-				switch method := req.Method; method {
-				case http.MethodGet:
-					(*getSeen)[path] = append((*getSeen)[path], req.URL.RawQuery)
-					responseBody = []byte((*getResult)[path])
-				case http.MethodPut:
-					if (*putSeen)[path] == nil {
-						(*putSeen)[path] = []string{}
+				if req.Method == http.MethodPut {
+					if req.URL.Path == "my.endpoint.net/admin/user" {
+						*putValsSeen = append(*putValsSeen, req.URL.RawQuery)
+						return &http.Response{
+							StatusCode: 200,
+							Body:       io.NopCloser(bytes.NewReader([]byte(`[]`))),
+						}, nil
 					}
-					(*putSeen)[path] = append((*putSeen)[path], req.URL.RawQuery)
-				default:
-					panic(fmt.Sprintf("unexpected request: %q. method %q. path %q", req.URL.RawQuery, req.Method, path))
 				}
-
-				return &http.Response{
-					StatusCode: statusCode,
-					Body:       io.NopCloser(bytes.NewReader(responseBody)),
-				}, nil
+				panic(fmt.Sprintf("unexpected request: %q. method %q. path %q", req.URL.RawQuery, req.Method, req.URL.Path))
 			},
 		}
 
-		adminClient, err := admin.New("rgw.test", "accesskey", "secretkey", mockClient)
+		adminClient, err := admin.New("my.endpoint.net", "accesskey", "secretkey", mockClient)
 		assert.NoError(t, err)
 
 		p := &Provisioner{
@@ -196,16 +189,12 @@ func TestProvisioner_setAdditionalSettings(t *testing.T) {
 		return p
 	}
 
-	t.Run("user and bucket quota should remain disabled", func(t *testing.T) {
-		getResult := map[string]string{
-			userPath:   `{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
-			bucketPath: `{"bucket_quota":{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}}`,
-		}
-		getSeen := map[string][]string{}
-		putSeen := map[string][]string{}
-
-		p := newProvisioner(t, &getResult, &getSeen, &putSeen)
-		p.setBucketName("bob")
+	t.Run("quota should remain disabled", func(t *testing.T) {
+		putValsSeen := []string{}
+		p := newProvisioner(t,
+			`{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
+			&putValsSeen,
+		)
 
 		err := p.setAdditionalSettings(&apibkt.BucketOptions{
 			ObjectBucketClaim: &v1alpha1.ObjectBucketClaim{
@@ -215,26 +204,15 @@ func TestProvisioner_setAdditionalSettings(t *testing.T) {
 			},
 		})
 		assert.NoError(t, err)
-
-		assert.Len(t, getSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("uid=bob", getSeen[userPath]), 1)
-		assert.Len(t, getSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("bucket=bob", getSeen[bucketPath]), 1)
-
-		assert.Len(t, putSeen[userPath], 0)   // user quota should not be touched
-		assert.Len(t, putSeen[bucketPath], 0) // bucket quota should not be touched
+		assert.Len(t, putValsSeen, 0)
 	})
 
-	t.Run("user and bucket quota should be disabled", func(t *testing.T) {
-		getResult := map[string]string{
-			userPath:   `{"enabled":true,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":2}`,
-			bucketPath: `{"owner": "bob","bucket_quota":{"enabled":true,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":3}}`,
-		}
-		getSeen := map[string][]string{}
-		putSeen := map[string][]string{}
-
-		p := newProvisioner(t, &getResult, &getSeen, &putSeen)
-		p.setBucketName("bob")
+	t.Run("quota should be disabled", func(t *testing.T) {
+		putValsSeen := []string{}
+		p := newProvisioner(t,
+			`{"enabled":true,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":2}`,
+			&putValsSeen,
+		)
 
 		err := p.setAdditionalSettings(&apibkt.BucketOptions{
 			ObjectBucketClaim: &v1alpha1.ObjectBucketClaim{
@@ -245,27 +223,17 @@ func TestProvisioner_setAdditionalSettings(t *testing.T) {
 		})
 		assert.NoError(t, err)
 
-		assert.Len(t, getSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("uid=bob", getSeen[userPath]), 1)
-		assert.Len(t, getSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("bucket=bob", getSeen[bucketPath]), 1)
-
-		assert.Len(t, putSeen[userPath], 1)
-		assert.Equal(t, 1, numberOfCallsWithValue("enabled=false", putSeen[userPath]))
-		assert.Len(t, putSeen[bucketPath], 1)
-		assert.Equal(t, 1, numberOfCallsWithValue("enabled=false", putSeen[bucketPath]))
+		// quota should be disabled, and that's it
+		assert.Len(t, putValsSeen, 1)
+		assert.Equal(t, 1, numberOfPutsWithValue(`enabled=false`, putValsSeen))
 	})
 
-	t.Run("user maxSize quota should be enabled", func(t *testing.T) {
-		getResult := map[string]string{
-			userPath:   `{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
-			bucketPath: `{"bucket_quota":{"enabled":false,"check_on_raw":false,"max_size":-1,"max_size_kb":0,"max_objects":-1}}`,
-		}
-		getSeen := map[string][]string{}
-		putSeen := map[string][]string{}
-
-		p := newProvisioner(t, &getResult, &getSeen, &putSeen)
-		p.setBucketName("bob")
+	t.Run("maxSize quota should be enabled", func(t *testing.T) {
+		putValsSeen := []string{}
+		p := newProvisioner(t,
+			`{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
+			&putValsSeen,
+		)
 
 		err := p.setAdditionalSettings(&apibkt.BucketOptions{
 			ObjectBucketClaim: &v1alpha1.ObjectBucketClaim{
@@ -278,219 +246,104 @@ func TestProvisioner_setAdditionalSettings(t *testing.T) {
 		})
 		assert.NoError(t, err)
 
-		assert.Len(t, getSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("uid=bob", getSeen[userPath]), 1)
-		assert.Len(t, getSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("bucket=bob", getSeen[bucketPath]), 1)
+		assert.Zero(t, numberOfPutsWithValue(`enabled=false`, putValsSeen)) // no puts should disable
 
-		assert.Len(t, putSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("enabled=true", putSeen[userPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-size=2", putSeen[userPath]), 1)
-		assert.Len(t, putSeen[bucketPath], 0) // bucket quota should not be touched
-	})
-
-	t.Run("user maxObjects quota should be enabled", func(t *testing.T) {
-		getResult := map[string]string{
-			userPath:   `{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
-			bucketPath: `{"bucket_quota":{"enabled":false,"check_on_raw":false,"max_size":-1,"max_size_kb":0,"max_objects":-1}}`,
-		}
-		getSeen := map[string][]string{}
-		putSeen := map[string][]string{}
+		assert.NotEmpty(t, putWithValue(`enabled=true`, putValsSeen)) // at least one put enables
 
-		p := newProvisioner(t, &getResult, &getSeen, &putSeen)
-		p.setBucketName("bob")
+		// there is only one time that max-size is set, and it's to the right value
+		assert.Equal(t, 1, numberOfPutsWithValue(`max-size`, putValsSeen))
+		assert.NotEmpty(t, putWithValue(`max-size=2`, putValsSeen))
 
-		err := p.setAdditionalSettings(&apibkt.BucketOptions{
-			ObjectBucketClaim: &v1alpha1.ObjectBucketClaim{
-				Spec: v1alpha1.ObjectBucketClaimSpec{
-					AdditionalConfig: map[string]string{
-						"maxObjects": "2",
-					},
-				},
-			},
-		})
-		assert.NoError(t, err)
-
-		assert.Len(t, getSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("uid=bob", getSeen[userPath]), 1)
-		assert.Len(t, getSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("bucket=bob", getSeen[bucketPath]), 1)
-
-		assert.Len(t, putSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("enabled=true", putSeen[userPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-objects=2", putSeen[userPath]), 1)
-		assert.Len(t, putSeen[bucketPath], 0) // bucket quota should not be touched
 	})
 
-	t.Run("user maxObjects and maxSize quotas should be enabled", func(t *testing.T) {
-		getResult := map[string]string{
-			userPath:   `{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
-			bucketPath: `{"bucket_quota":{"enabled":false,"check_on_raw":false,"max_size":-1,"max_size_kb":0,"max_objects":-1}}`,
-		}
-		getSeen := map[string][]string{}
-		putSeen := map[string][]string{}
-
-		p := newProvisioner(t, &getResult, &getSeen, &putSeen)
-		p.setBucketName("bob")
+	t.Run("maxObjects quota should be enabled", func(t *testing.T) {
+		putValsSeen := []string{}
+		p := newProvisioner(t,
+			`{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
+			&putValsSeen,
+		)
 
 		err := p.setAdditionalSettings(&apibkt.BucketOptions{
 			ObjectBucketClaim: &v1alpha1.ObjectBucketClaim{
 				Spec: v1alpha1.ObjectBucketClaimSpec{
 					AdditionalConfig: map[string]string{
 						"maxObjects": "2",
-						"maxSize":    "3",
 					},
 				},
 			},
 		})
 		assert.NoError(t, err)
 
-		assert.Len(t, getSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("uid=bob", getSeen[userPath]), 1)
-		assert.Len(t, getSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("bucket=bob", getSeen[bucketPath]), 1)
+		assert.Zero(t, numberOfPutsWithValue(`enabled=false`, putValsSeen)) // no puts should disable
 
-		assert.Len(t, putSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("enabled=true", putSeen[userPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-objects=2", putSeen[userPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-size=3", putSeen[userPath]), 1)
-		assert.Len(t, putSeen[bucketPath], 0) // bucket quota should not be touched
-	})
-
-	t.Run("user quotas are enabled and need updated enabled", func(t *testing.T) {
-		getResult := map[string]string{
-			userPath:   `{"enabled":true,"check_on_raw":false,"max_size":1,"max_size_kb":0,"max_objects":1}`,
-			bucketPath: `{"bucket_quota":{"enabled":false,"check_on_raw":false,"max_size":-1,"max_size_kb":0,"max_objects":-1}}`,
-		}
-		getSeen := map[string][]string{}
-		putSeen := map[string][]string{}
+		assert.NotEmpty(t, putWithValue(`enabled=true`, putValsSeen)) // at least one put enables
 
-		p := newProvisioner(t, &getResult, &getSeen, &putSeen)
-		p.setBucketName("bob")
+		// there is only one time max-objects is set, and it's to the right value
+		assert.NotEmpty(t, putWithValue(`max-objects=2`, putValsSeen))
+		assert.Equal(t, 1, numberOfPutsWithValue(`max-objects`, putValsSeen))
 
-		err := p.setAdditionalSettings(&apibkt.BucketOptions{
-			ObjectBucketClaim: &v1alpha1.ObjectBucketClaim{
-				Spec: v1alpha1.ObjectBucketClaimSpec{
-					AdditionalConfig: map[string]string{
-						"maxObjects": "12",
-						"maxSize":    "13",
-					},
-				},
-			},
-		})
-		assert.NoError(t, err)
-
-		assert.Len(t, getSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("uid=bob", getSeen[userPath]), 1)
-		assert.Len(t, getSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("bucket=bob", getSeen[bucketPath]), 1)
-
-		assert.Len(t, putSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("enabled=true", putSeen[userPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-objects=12", putSeen[userPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-size=13", putSeen[userPath]), 1)
-		assert.Len(t, putSeen[bucketPath], 0) // bucket quota should not be touched
 	})
 
-	t.Run("bucket maxObjects quota should be enabled", func(t *testing.T) {
-		getResult := map[string]string{
-			userPath:   `{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
-			bucketPath: `{"bucket_quota":{"enabled":false,"check_on_raw":false,"max_size":-1,"max_size_kb":0,"max_objects":-1}}`,
-		}
-		getSeen := map[string][]string{}
-		putSeen := map[string][]string{}
-
-		p := newProvisioner(t, &getResult, &getSeen, &putSeen)
-		p.setBucketName("bob")
+	t.Run("maxObjects and maxSize quotas should be enabled", func(t *testing.T) {
+		putValsSeen := []string{}
+		p := newProvisioner(t,
+			`{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
+			&putValsSeen,
+		)
 
 		err := p.setAdditionalSettings(&apibkt.BucketOptions{
 			ObjectBucketClaim: &v1alpha1.ObjectBucketClaim{
 				Spec: v1alpha1.ObjectBucketClaimSpec{
 					AdditionalConfig: map[string]string{
-						"bucketMaxObjects": "4",
+						"maxSize":    "2",
+						"maxObjects": "3",
 					},
 				},
 			},
 		})
 		assert.NoError(t, err)
 
-		assert.Len(t, getSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("uid=bob", getSeen[userPath]), 1)
-		assert.Len(t, getSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("bucket=bob", getSeen[bucketPath]), 1)
+		assert.Zero(t, numberOfPutsWithValue(`enabled=false`, putValsSeen)) // no puts should disable
 
-		assert.Len(t, putSeen[userPath], 0) // user quota should not be touched
-		assert.Len(t, putSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("enabled=true", putSeen[bucketPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-objects=4", putSeen[bucketPath]), 1)
-	})
-
-	t.Run("bucket maxSize quota should be enabled", func(t *testing.T) {
-		getResult := map[string]string{
-			userPath:   `{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
-			bucketPath: `{"bucket_quota":{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}}`,
-		}
-		getSeen := map[string][]string{}
-		putSeen := map[string][]string{}
-
-		p := newProvisioner(t, &getResult, &getSeen, &putSeen)
-		p.setBucketName("bob")
-
-		err := p.setAdditionalSettings(&apibkt.BucketOptions{
-			ObjectBucketClaim: &v1alpha1.ObjectBucketClaim{
-				Spec: v1alpha1.ObjectBucketClaimSpec{
-					AdditionalConfig: map[string]string{
-						"bucketMaxSize": "5",
-					},
-				},
-			},
-		})
-		assert.NoError(t, err)
+		assert.NotEmpty(t, putWithValue(`enabled=true`, putValsSeen)) // at least one put enables
 
-		assert.Len(t, getSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("uid=bob", getSeen[userPath]), 1)
-		assert.Len(t, getSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("bucket=bob", getSeen[bucketPath]), 1)
+		// there is only one time that max-size is set, and it's to the right value
+		assert.Equal(t, 1, numberOfPutsWithValue(`max-size`, putValsSeen))
+		assert.NotEmpty(t, putWithValue(`max-size=2`, putValsSeen))
 
-		assert.Len(t, putSeen[userPath], 0) // user quota should not be touched
-		assert.Len(t, putSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("enabled=true", putSeen[bucketPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-size=5", putSeen[bucketPath]), 1)
+		// there is only one time max-objects is set, and it's to the right value
+		assert.NotEmpty(t, putWithValue(`max-objects=3`, putValsSeen))
+		assert.Equal(t, 1, numberOfPutsWithValue(`max-objects`, putValsSeen))
 	})
 
-	t.Run("bucket quotas are enabled and need updated enabled", func(t *testing.T) {
-		getResult := map[string]string{
-			userPath:   `{"enabled":false,"check_on_raw":false,"max_size":-1024,"max_size_kb":0,"max_objects":-1}`,
-			bucketPath: `{"bucket_quota":{"enabled":true,"check_on_raw":false,"max_size":5,"max_size_kb":0,"max_objects":4}}`,
-		}
-		getSeen := map[string][]string{}
-		putSeen := map[string][]string{}
-
-		p := newProvisioner(t, &getResult, &getSeen, &putSeen)
-		p.setBucketName("bob")
+	t.Run("quotas are enabled and need updated enabled", func(t *testing.T) {
+		putValsSeen := []string{}
+		p := newProvisioner(t,
+			`{"enabled":true,"check_on_raw":false,"max_size":1,"max_size_kb":0,"max_objects":1}`,
+			&putValsSeen,
+		)
 
 		err := p.setAdditionalSettings(&apibkt.BucketOptions{
 			ObjectBucketClaim: &v1alpha1.ObjectBucketClaim{
 				Spec: v1alpha1.ObjectBucketClaimSpec{
 					AdditionalConfig: map[string]string{
-						"bucketMaxObjects": "14",
-						"bucketMaxSize":    "15",
+						"maxSize":    "2",
+						"maxObjects": "3",
 					},
 				},
 			},
 		})
 		assert.NoError(t, err)
 
-		assert.Len(t, getSeen[userPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("uid=bob", getSeen[userPath]), 1)
-		assert.Len(t, getSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("bucket=bob", getSeen[bucketPath]), 1)
+		assert.Zero(t, numberOfPutsWithValue(`enabled=false`, putValsSeen)) // no puts should disable
+
+		// there is only one time that max-size is set, and it's to the right value
+		assert.Equal(t, 1, numberOfPutsWithValue(`max-size`, putValsSeen))
+		assert.NotEmpty(t, putWithValue(`max-size=2`, putValsSeen))
 
-		assert.Len(t, putSeen[userPath], 0) // user quota should not be touched
-		assert.Len(t, putSeen[bucketPath], 1)
-		assert.Equal(t, numberOfCallsWithValue("enabled=true", putSeen[bucketPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-objects=14", putSeen[bucketPath]), 1)
-		assert.Equal(t, numberOfCallsWithValue("max-size=15", putSeen[bucketPath]), 1)
+		// there is only one time max-objects is set, and it's to the right value
+		assert.NotEmpty(t, putWithValue(`max-objects=3`, putValsSeen))
+		assert.Equal(t, 1, numberOfPutsWithValue(`max-objects`, putValsSeen))
 	})
 }
 
@@ -514,21 +367,9 @@ func TestProvisioner_additionalConfigSpecFromMap(t *testing.T) {
 		var i int64 = 3
 		assert.Equal(t, additionalConfigSpec{maxSize: &i}, *spec)
 	})
-
-	t.Run("bucketMaxObjects field should be set", func(t *testing.T) {
-		spec, err := additionalConfigSpecFromMap(map[string]string{"bucketMaxObjects": "4"})
-		assert.NoError(t, err)
-		assert.Equal(t, additionalConfigSpec{bucketMaxObjects: &(&struct{ i int64 }{4}).i}, *spec)
-	})
-
-	t.Run("bucketMaxSize field should be set", func(t *testing.T) {
-		spec, err := additionalConfigSpecFromMap(map[string]string{"bucketMaxSize": "5"})
-		assert.NoError(t, err)
-		assert.Equal(t, additionalConfigSpec{bucketMaxSize: &(&struct{ i int64 }{5}).i}, *spec)
-	})
 }
 
-func numberOfCallsWithValue(substr string, strs []string) int {
+func numberOfPutsWithValue(substr string, strs []string) int {
 	count := 0
 	for _, s := range strs {
 		if strings.Contains(s, substr) {
@@ -537,3 +378,12 @@ func numberOfCallsWithValue(substr string, strs []string) int {
 	}
 	return count
 }
+
+func putWithValue(substr string, strs []string) string {
+	for _, s := range strs {
+		if strings.Contains(s, substr) {
+			return s
+		}
+	}
+	return ""
+}
diff --git a/pkg/operator/ceph/object/objectstore_test.go b/pkg/operator/ceph/object/objectstore_test.go
index ee5aa4c83..4662349a5 100644
--- a/pkg/operator/ceph/object/objectstore_test.go
+++ b/pkg/operator/ceph/object/objectstore_test.go
@@ -152,7 +152,7 @@ const (
   "notif_pool": "%[1]s:store-a.log.notif"
 }`
 
-	objectZonegroupJsonTempl = `{
+	objectZonegroupJson = `{
     "id": "610c9e3d-19e7-40b0-9f88-03319c4bc65a",
     "name": "store-a",
     "api_name": "test",
@@ -174,14 +174,14 @@ const (
     ],
     "placement_targets": [
         {
-            "name": "%[1]s",
+            "name": "default-placement",
             "tags": [],
             "storage_classes": [
                 "STANDARD"
             ]
         }
     ],
-    "default_placement": "%[1]s",
+    "default_placement": "default-placement",
     "realm_id": "29e28253-be54-4581-90dd-206020d2fcdd",
     "sync_policy": {
         "groups": []
@@ -229,7 +229,6 @@ func TestReconcileRealm(t *testing.T) {
 
 func TestConfigureStoreWithSharedPools(t *testing.T) {
 	sharedMetaPoolAlreadySet, sharedDataPoolAlreadySet := "", ""
-	defaultPlacement := defaultPlacementCephConfigName
 	zoneGetCalled := false
 	zoneSetCalled := false
 	zoneGroupGetCalled := false
@@ -274,7 +273,7 @@ func TestConfigureStoreWithSharedPools(t *testing.T) {
 		} else if args[0] == "zonegroup" {
 			if args[1] == "get" {
 				zoneGroupGetCalled = true
-				return fmt.Sprintf(objectZonegroupJsonTempl, defaultPlacement), nil
+				return objectZonegroupJson, nil
 			} else if args[1] == "set" {
 				zoneGroupSetCalled = true
 				for _, arg := range args {
@@ -288,7 +287,7 @@ func TestConfigureStoreWithSharedPools(t *testing.T) {
 					}
 					return string(inBytes), nil
 				}
-				return fmt.Sprintf(objectZonegroupJsonTempl, defaultPlacement), nil
+				return objectZonegroupJson, nil
 			}
 		}
 		return "", errors.Errorf("unexpected ceph command %q", args)
@@ -335,14 +334,12 @@ func TestConfigureStoreWithSharedPools(t *testing.T) {
 		sharedPools := cephv1.ObjectSharedPoolsSpec{
 			PoolPlacements: []cephv1.PoolPlacementSpec{
 				{
-					Name:             "default",
-					Default:          true,
+					Name:             defaultPlacementName,
 					MetadataPoolName: "test-meta",
 					DataPoolName:     "test-data",
 				},
 			},
 		}
-		defaultPlacement = "default"
 		err := ConfigureSharedPoolsForZone(context, sharedPools)
 		assert.NoError(t, err)
 		assert.True(t, zoneGetCalled)
@@ -357,7 +354,6 @@ func TestConfigureStoreWithSharedPools(t *testing.T) {
 			MetadataPoolName: "test-meta",
 			DataPoolName:     "test-data",
 		}
-		defaultPlacement = defaultPlacementCephConfigName
 		sharedMetaPoolAlreadySet, sharedDataPoolAlreadySet = "test-meta", "test-data"
 		zoneGetCalled = false
 		zoneSetCalled = false
@@ -374,8 +370,7 @@ func TestConfigureStoreWithSharedPools(t *testing.T) {
 		sharedPools := cephv1.ObjectSharedPoolsSpec{
 			PoolPlacements: []cephv1.PoolPlacementSpec{
 				{
-					Name:             "default",
-					Default:          true,
+					Name:             defaultPlacementName,
 					MetadataPoolName: "test-meta",
 					DataPoolName:     "test-data",
 				},
diff --git a/pkg/operator/ceph/object/s3-handlers_test.go b/pkg/operator/ceph/object/s3-handlers_test.go
index f7f20fe0c..308575397 100644
--- a/pkg/operator/ceph/object/s3-handlers_test.go
+++ b/pkg/operator/ceph/object/s3-handlers_test.go
@@ -53,7 +53,7 @@ func TestNewS3Agent(t *testing.T) {
 		insecure := true
 		s3Agent, err := newS3Agent(accessKey, secretKey, endpoint, debug, nil, insecure)
 		assert.NoError(t, err)
-		assert.NotNil(t, s3Agent.Client.Config.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs) // still includes sys certs
+		assert.Nil(t, s3Agent.Client.Config.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs)
 		assert.True(t, s3Agent.Client.Config.HTTPClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify)
 		assert.False(t, *s3Agent.Client.Config.DisableSSL)
 	})
diff --git a/pkg/operator/ceph/object/shared_pools_test.go b/pkg/operator/ceph/object/shared_pools_test.go
index f40306141..4ef29275f 100644
--- a/pkg/operator/ceph/object/shared_pools_test.go
+++ b/pkg/operator/ceph/object/shared_pools_test.go
@@ -24,7 +24,6 @@ func Test_validatePoolPlacements(t *testing.T) {
 				placements: []cephv1.PoolPlacementSpec{
 					{
 						Name:              "name1",
-						Default:           true,
 						MetadataPoolName:  "", // handled by CRD validation
 						DataPoolName:      "", // handled by CRD validation
 						DataNonECPoolName: "", // handled by CRD validation
@@ -32,7 +31,6 @@ func Test_validatePoolPlacements(t *testing.T) {
 					},
 					{
 						Name:              "name2",
-						Default:           false,
 						MetadataPoolName:  "", // handled by CRD validation
 						DataPoolName:      "", // handled by CRD validation
 						DataNonECPoolName: "", // handled by CRD validation
@@ -64,78 +62,6 @@ func Test_validatePoolPlacements(t *testing.T) {
 			},
 			wantErr: true,
 		},
-		{
-			name: "invalid: more than one default placement",
-			args: args{
-				placements: []cephv1.PoolPlacementSpec{
-					{
-						Name:              "one",
-						Default:           true,
-						MetadataPoolName:  "", // handled by CRD validation
-						DataPoolName:      "", // handled by CRD validation
-						DataNonECPoolName: "", // handled by CRD validation
-						StorageClasses:    []cephv1.PlacementStorageClassSpec{},
-					},
-					{
-						Name:              "two",
-						Default:           true,
-						MetadataPoolName:  "", // handled by CRD validation
-						DataPoolName:      "", // handled by CRD validation
-						DataNonECPoolName: "", // handled by CRD validation
-						StorageClasses:    []cephv1.PlacementStorageClassSpec{},
-					},
-				},
-			},
-			wantErr: true,
-		},
-		{
-			name: "invalid: non-default placement with reserved name",
-			args: args{
-				placements: []cephv1.PoolPlacementSpec{
-					{
-						Name:              defaultPlacementCephConfigName,
-						Default:           false,
-						MetadataPoolName:  "", // handled by CRD validation
-						DataPoolName:      "", // handled by CRD validation
-						DataNonECPoolName: "", // handled by CRD validation
-						StorageClasses:    []cephv1.PlacementStorageClassSpec{},
-					},
-					{
-						Name:              "two",
-						Default:           true,
-						MetadataPoolName:  "", // handled by CRD validation
-						DataPoolName:      "", // handled by CRD validation
-						DataNonECPoolName: "", // handled by CRD validation
-						StorageClasses:    []cephv1.PlacementStorageClassSpec{},
-					},
-				},
-			},
-			wantErr: true,
-		},
-		{
-			name: "valid: default placement with reserved name",
-			args: args{
-				placements: []cephv1.PoolPlacementSpec{
-					{
-						Name:              defaultPlacementCephConfigName,
-						Default:           true,
-						MetadataPoolName:  "", // handled by CRD validation
-						DataPoolName:      "", // handled by CRD validation
-						DataNonECPoolName: "", // handled by CRD validation
-						StorageClasses:    []cephv1.PlacementStorageClassSpec{},
-					},
-					{
-						Name:              "two",
-						Default:           false,
-						MetadataPoolName:  "", // handled by CRD validation
-						DataPoolName:      "", // handled by CRD validation
-						DataNonECPoolName: "", // handled by CRD validation
-						StorageClasses:    []cephv1.PlacementStorageClassSpec{},
-					},
-				},
-			},
-			wantErr: false,
-		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -239,8 +165,7 @@ func TestIsNeedToCreateObjectStorePools(t *testing.T) {
 					PreserveRadosNamespaceDataOnDelete: false,
 					PoolPlacements: []cephv1.PoolPlacementSpec{
 						{
-							Name:              "name",
-							Default:           true,
+							Name:              "default",
 							MetadataPoolName:  "", // handled by CRD validation
 							DataPoolName:      "", // handled by CRD validation
 							DataNonECPoolName: "", // handled by CRD validation
@@ -296,8 +221,7 @@ func TestIsNeedToCreateObjectStorePools(t *testing.T) {
 					PreserveRadosNamespaceDataOnDelete: false,
 					PoolPlacements: []cephv1.PoolPlacementSpec{
 						{
-							Name:              "default",
-							Default:           false,
+							Name:              "fast",
 							MetadataPoolName:  "", // handled by CRD validation
 							DataPoolName:      "", // handled by CRD validation
 							DataNonECPoolName: "", // handled by CRD validation
@@ -343,8 +267,7 @@ func Test_getDefaultMetadataPool(t *testing.T) {
 							StorageClasses:    []cephv1.PlacementStorageClassSpec{},
 						},
 						{
-							Name:              "some_name_2",
-							Default:           true,
+							Name:              defaultPlacementName,
 							MetadataPoolName:  "meta2",
 							DataPoolName:      "data2",
 							DataNonECPoolName: "data-non-ec2",
@@ -371,8 +294,7 @@ func Test_getDefaultMetadataPool(t *testing.T) {
 							StorageClasses:    []cephv1.PlacementStorageClassSpec{},
 						},
 						{
-							Name:              "some_name_2",
-							Default:           true,
+							Name:              defaultPlacementName,
 							MetadataPoolName:  "meta2",
 							DataPoolName:      "data2",
 							DataNonECPoolName: "data-non-ec2",
@@ -392,8 +314,7 @@ func Test_getDefaultMetadataPool(t *testing.T) {
 					PreserveRadosNamespaceDataOnDelete: false,
 					PoolPlacements: []cephv1.PoolPlacementSpec{
 						{
-							Name:              "default",
-							Default:           false,
+							Name:              "some_name",
 							MetadataPoolName:  "meta1",
 							DataPoolName:      "data1",
 							DataNonECPoolName: "data-non-ec1",
@@ -413,8 +334,7 @@ func Test_getDefaultMetadataPool(t *testing.T) {
 					PreserveRadosNamespaceDataOnDelete: false,
 					PoolPlacements: []cephv1.PoolPlacementSpec{
 						{
-							Name:              "default",
-							Default:           false,
+							Name:              "some_name",
 							MetadataPoolName:  "meta1",
 							DataPoolName:      "data1",
 							DataNonECPoolName: "data-non-ec1",
@@ -449,8 +369,7 @@ func Test_toZonePlacementPool(t *testing.T) {
 			name: "map default placement without non-ec to config",
 			args: args{
 				spec: cephv1.PoolPlacementSpec{
-					Name:              defaultPlacementCephConfigName,
-					Default:           true,
+					Name:              defaultPlacementName,
 					MetadataPoolName:  "meta",
 					DataPoolName:      "data",
 					DataNonECPoolName: "",
@@ -466,11 +385,11 @@ func Test_toZonePlacementPool(t *testing.T) {
 			want: ZonePlacementPool{
 				Key: defaultPlacementCephConfigName,
 				Val: ZonePlacementPoolVal{
-					DataExtraPool: "meta:ns.default-placement.data.non-ec",
-					IndexPool:     "meta:ns.default-placement.index",
+					DataExtraPool: "meta:ns.data.non-ec",
+					IndexPool:     "meta:ns.index",
 					StorageClasses: map[string]ZonePlacementStorageClass{
 						defaultPlacementStorageClass: {
-							DataPool: "data:ns.default-placement.data",
+							DataPool: "data:ns.data",
 						},
 						"REDUCED_REDUNDANCY": {
 							DataPool: "reduced:ns.REDUCED_REDUNDANCY",
@@ -484,8 +403,7 @@ func Test_toZonePlacementPool(t *testing.T) {
 			name: "map default placement to config",
 			args: args{
 				spec: cephv1.PoolPlacementSpec{
-					Name:              "fast",
-					Default:           true,
+					Name:              defaultPlacementName,
 					MetadataPoolName:  "meta",
 					DataPoolName:      "data",
 					DataNonECPoolName: "repl",
@@ -499,13 +417,13 @@ func Test_toZonePlacementPool(t *testing.T) {
 				ns: "ns",
 			},
 			want: ZonePlacementPool{
-				Key: "fast",
+				Key: defaultPlacementCephConfigName,
 				Val: ZonePlacementPoolVal{
-					DataExtraPool: "repl:ns.fast.data.non-ec",
-					IndexPool:     "meta:ns.fast.index",
+					DataExtraPool: "repl:ns.data.non-ec",
+					IndexPool:     "meta:ns.index",
 					StorageClasses: map[string]ZonePlacementStorageClass{
 						defaultPlacementStorageClass: {
-							DataPool: "data:ns.fast.data",
+							DataPool: "data:ns.data",
 						},
 						"REDUCED_REDUNDANCY": {
 							DataPool: "reduced:ns.REDUCED_REDUNDANCY",
@@ -515,12 +433,37 @@ func Test_toZonePlacementPool(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "map default placement without extra SC to config",
+			args: args{
+				spec: cephv1.PoolPlacementSpec{
+					Name:              defaultPlacementName,
+					MetadataPoolName:  "meta",
+					DataPoolName:      "data",
+					DataNonECPoolName: "repl",
+					StorageClasses:    []cephv1.PlacementStorageClassSpec{},
+				},
+				ns: "ns",
+			},
+			want: ZonePlacementPool{
+				Key: defaultPlacementCephConfigName,
+				Val: ZonePlacementPoolVal{
+					DataExtraPool: "repl:ns.data.non-ec",
+					IndexPool:     "meta:ns.index",
+					StorageClasses: map[string]ZonePlacementStorageClass{
+						defaultPlacementStorageClass: {
+							DataPool: "data:ns.data",
+						},
+					},
+					InlineData: true,
+				},
+			},
+		},
 		{
 			name: "map non-default placement without non-ec to config",
 			args: args{
 				spec: cephv1.PoolPlacementSpec{
 					Name:              "placement",
-					Default:           false,
 					MetadataPoolName:  "meta",
 					DataPoolName:      "data",
 					DataNonECPoolName: "",
@@ -555,7 +498,6 @@ func Test_toZonePlacementPool(t *testing.T) {
 			args: args{
 				spec: cephv1.PoolPlacementSpec{
 					Name:              "placement",
-					Default:           false,
 					MetadataPoolName:  "meta",
 					DataPoolName:      "data",
 					DataNonECPoolName: "repl",
@@ -638,8 +580,7 @@ func Test_toZonePlacementPools(t *testing.T) {
 					PreserveRadosNamespaceDataOnDelete: false,
 					PoolPlacements: []cephv1.PoolPlacementSpec{
 						{
-							Name:              "some_name",
-							Default:           true,
+							Name:              defaultPlacementName,
 							MetadataPoolName:  "meta1",
 							DataPoolName:      "data1",
 							DataNonECPoolName: "data-non-ec",
@@ -655,14 +596,14 @@ func Test_toZonePlacementPools(t *testing.T) {
 				ns: "rgw-instance",
 			},
 			want: map[string]ZonePlacementPool{
-				"some_name": {
-					Key: "some_name",
+				defaultPlacementCephConfigName: {
+					Key: defaultPlacementCephConfigName,
 					Val: ZonePlacementPoolVal{
-						DataExtraPool: "data-non-ec:rgw-instance.some_name.data.non-ec",
-						IndexPool:     "meta1:rgw-instance.some_name.index",
+						DataExtraPool: "data-non-ec:rgw-instance.data.non-ec",
+						IndexPool:     "meta1:rgw-instance.index",
 						StorageClasses: map[string]ZonePlacementStorageClass{
 							defaultPlacementStorageClass: {
-								DataPool: "data1:rgw-instance.some_name.data",
+								DataPool: "data1:rgw-instance.data",
 							},
 							"REDUCED_REDUNDANCY": {
 								DataPool: "reduced:rgw-instance.REDUCED_REDUNDANCY",
@@ -1021,8 +962,7 @@ func Test_adjustZoneDefaultPools(t *testing.T) {
 					PreserveRadosNamespaceDataOnDelete: false,
 					PoolPlacements: []cephv1.PoolPlacementSpec{
 						{
-							Name:             "some_name",
-							Default:          true,
+							Name:             defaultPlacementName,
 							MetadataPoolName: "meta-pool",
 							DataPoolName:     "data-pool",
 						},
@@ -1478,9 +1418,8 @@ func Test_adjustZonePlacementPools(t *testing.T) {
 
 func Test_adjustZoneGroupPlacementTargets(t *testing.T) {
 	type args struct {
-		zone             string
-		groupBefore      string
-		defaultPlacement string
+		zone        string
+		groupBefore string
 	}
 	tests := []struct {
 		name        string
@@ -1492,7 +1431,6 @@ func Test_adjustZoneGroupPlacementTargets(t *testing.T) {
 		{
 			name: "nothing changed",
 			args: args{
-				defaultPlacement: defaultPlacementCephConfigName,
 				groupBefore: `{
     "id": "610c9e3d-19e7-40b0-9f88-03319c4bc65a",
     "name": "test",
@@ -1554,7 +1492,6 @@ func Test_adjustZoneGroupPlacementTargets(t *testing.T) {
 		{
 			name: "default changed",
 			args: args{
-				defaultPlacement: defaultPlacementCephConfigName,
 				groupBefore: `{
     "id": "610c9e3d-19e7-40b0-9f88-03319c4bc65a",
     "name": "test",
@@ -1616,7 +1553,6 @@ func Test_adjustZoneGroupPlacementTargets(t *testing.T) {
 		{
 			name: "storage class added",
 			args: args{
-				defaultPlacement: defaultPlacementCephConfigName,
 				groupBefore: `{
     "id": "610c9e3d-19e7-40b0-9f88-03319c4bc65a",
     "name": "test",
@@ -1681,7 +1617,6 @@ func Test_adjustZoneGroupPlacementTargets(t *testing.T) {
 		{
 			name: "placement added",
 			args: args{
-				defaultPlacement: defaultPlacementCephConfigName,
 				groupBefore: `{
     "id": "610c9e3d-19e7-40b0-9f88-03319c4bc65a",
     "name": "test",
@@ -1762,7 +1697,6 @@ func Test_adjustZoneGroupPlacementTargets(t *testing.T) {
 		{
 			name: "placement and sc removed",
 			args: args{
-				defaultPlacement: defaultPlacementCephConfigName,
 				groupBefore: `{
     "id": "610c9e3d-19e7-40b0-9f88-03319c4bc65a",
     "name": "test",
@@ -1835,7 +1769,7 @@ func Test_adjustZoneGroupPlacementTargets(t *testing.T) {
 			srcGroup := map[string]interface{}{}
 			err = json.Unmarshal([]byte(tt.args.groupBefore), &srcGroup)
 			assert.NoError(t, err)
-			changedGroup, err := adjustZoneGroupPlacementTargets(srcGroup, zj, tt.args.defaultPlacement)
+			changedGroup, err := adjustZoneGroupPlacementTargets(srcGroup, zj)
 
 			orig := map[string]interface{}{}
 			jErr := json.Unmarshal([]byte(tt.args.groupBefore), &orig)
@@ -1935,76 +1869,3 @@ func Test_createPlacementTargetsFromZonePoolPlacements(t *testing.T) {
 		})
 	}
 }
-
-func Test_getDefaultPlacementName(t *testing.T) {
-	type args struct {
-		spec cephv1.ObjectSharedPoolsSpec
-	}
-	tests := []struct {
-		name string
-		args args
-		want string
-	}{
-		{
-			name: "no default placement set in spec",
-			args: args{
-				spec: cephv1.ObjectSharedPoolsSpec{
-					PoolPlacements: []cephv1.PoolPlacementSpec{
-						{
-							Name:    "one",
-							Default: false,
-						},
-						{
-							Name:    "two",
-							Default: false,
-						},
-					},
-				},
-			},
-			want: defaultPlacementCephConfigName,
-		},
-		{
-			name: "first placement set as defaultu in spec",
-			args: args{
-				spec: cephv1.ObjectSharedPoolsSpec{
-					PoolPlacements: []cephv1.PoolPlacementSpec{
-						{
-							Name:    "one",
-							Default: true,
-						},
-						{
-							Name:    "two",
-							Default: false,
-						},
-					},
-				},
-			},
-			want: "one",
-		},
-		{
-			name: "second placement set as default in spec",
-			args: args{
-				spec: cephv1.ObjectSharedPoolsSpec{
-					PoolPlacements: []cephv1.PoolPlacementSpec{
-						{
-							Name:    "one",
-							Default: false,
-						},
-						{
-							Name:    "two",
-							Default: true,
-						},
-					},
-				},
-			},
-			want: "two",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := getDefaultPlacementName(tt.args.spec); got != tt.want {
-				t.Errorf("getDefaultPlacementName() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
diff --git a/tests/framework/installer/ceph_installer.go b/tests/framework/installer/ceph_installer.go
index 0e2287190..a02757fcd 100644
--- a/tests/framework/installer/ceph_installer.go
+++ b/tests/framework/installer/ceph_installer.go
@@ -265,12 +265,7 @@ func (h *CephInstaller) CreateCephCluster() error {
 }
 
 func (h *CephInstaller) waitForCluster() error {
-	monWaitLabel := "app=rook-ceph-mon,mon_daemon=true"
-	if h.Manifests.Settings().RookVersion == Version1_14 {
-		// TODO: Remove this when upgrade test is from v1.15 since v1.14 does not have the mon_daemon label
-		monWaitLabel = "app=rook-ceph-mon"
-	}
-	if err := h.k8shelper.WaitForPodCount(monWaitLabel, h.settings.Namespace, h.settings.Mons); err != nil {
+	if err := h.k8shelper.WaitForPodCount("app=rook-ceph-mon", h.settings.Namespace, h.settings.Mons); err != nil {
 		return err
 	}
 
diff --git a/tests/framework/utils/k8s_helper.go b/tests/framework/utils/k8s_helper.go
index d7ebb81f3..efa2dba1a 100644
--- a/tests/framework/utils/k8s_helper.go
+++ b/tests/framework/utils/k8s_helper.go
@@ -341,9 +341,6 @@ func (k8sh *K8sHelper) WaitForPodCount(label, namespace string, count int) error
 
 		if len(pods.Items) >= count {
 			logger.Infof("found %d pods with label %s", count, label)
-			for _, pod := range pods.Items {
-				logger.Infof("pod found includes %q with status %q", pod.Name, pod.Status.Phase)
-			}
 			return nil
 		}
 
diff --git a/tests/integration/ceph_object_test.go b/tests/integration/ceph_object_test.go
index db55cd39e..d51dd59b4 100644
--- a/tests/integration/ceph_object_test.go
+++ b/tests/integration/ceph_object_test.go
@@ -19,11 +19,10 @@ package integration
 import (
 	"context"
 	"encoding/json"
-	"strings"
+	"k8s.io/apimachinery/pkg/api/errors"
 	"testing"
 	"time"
 
-	"github.com/kube-object-storage/lib-bucket-provisioner/pkg/apis/objectbucket.io/v1alpha1"
 	cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
 	"github.com/rook/rook/pkg/daemon/ceph/client"
 	rgw "github.com/rook/rook/pkg/operator/ceph/object"
@@ -33,7 +32,6 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/suite"
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -238,7 +236,7 @@ func testObjectStoreOperations(s *suite.Suite, helper *clients.TestClient, k8sh
 			assert.Equal(t, ObjBody, read)
 		})
 
-		t.Run("user quota enforcement", func(t *testing.T) {
+		t.Run("quota enforcement", func(t *testing.T) {
 			_, poErr := s3client.PutObjectInBucket(bucketname, ObjBody, ObjectKey2, contentType)
 			assert.Nil(t, poErr)
 			logger.Infof("Testing the max object limit")
@@ -246,7 +244,7 @@ func testObjectStoreOperations(s *suite.Suite, helper *clients.TestClient, k8sh
 			assert.Error(t, poErr)
 		})
 
-		t.Run("update user quota limits", func(t *testing.T) {
+		t.Run("update quota limits", func(t *testing.T) {
 			poErr := helper.BucketClient.UpdateObc(obcName, bucketStorageClassName, bucketname, newMaxObject, true)
 			assert.Nil(t, poErr)
 			updated := utils.Retry(20, 2*time.Second, "OBC is updated", func() bool {
@@ -271,161 +269,6 @@ func testObjectStoreOperations(s *suite.Suite, helper *clients.TestClient, k8sh
 		})
 	})
 
-	// this test deviates from the others in this package in that it does not
-	// rely on kubectl and uses the k8s api directly
-	t.Run("OBC bucket quota enforcement", func(t *testing.T) {
-		bucketName := "bucket-quota-test"
-		var obName string
-		var s3client *rgw.S3Agent
-
-		t.Run("create obc with bucketMaxObjects", func(t *testing.T) {
-			newObc := v1alpha1.ObjectBucketClaim{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      bucketName,
-					Namespace: namespace,
-				},
-				Spec: v1alpha1.ObjectBucketClaimSpec{
-					BucketName:       bucketName,
-					StorageClassName: bucketStorageClassName,
-					AdditionalConfig: map[string]string{
-						"bucketMaxObjects": "1",
-					},
-				},
-			}
-			_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(namespace).Create(ctx, &newObc, metav1.CreateOptions{})
-			assert.Nil(t, err)
-			obcBound := utils.Retry(20, 2*time.Second, "OBC is Bound", func() bool {
-				liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(namespace).Get(ctx, bucketName, metav1.GetOptions{})
-				if err != nil {
-					return false
-				}
-
-				return liveObc.Status.Phase == v1alpha1.ObjectBucketClaimStatusPhaseBound
-			})
-			assert.True(t, obcBound)
-
-			// wait until obc is Bound to lookup the ob name
-			obc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(namespace).Get(ctx, bucketName, metav1.GetOptions{})
-			assert.Nil(t, err)
-			obName = obc.Spec.ObjectBucketName
-
-			obBound := utils.Retry(20, 2*time.Second, "OB is Bound", func() bool {
-				liveOb, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
-				if err != nil {
-					return false
-				}
-
-				return liveOb.Status.Phase == v1alpha1.ObjectBucketStatusPhaseBound
-			})
-			assert.True(t, obBound)
-
-			// verify that bucketMaxObjects is set
-			assert.True(t, obc.Spec.AdditionalConfig["bucketMaxObjects"] == "1")
-
-			// as the tests are running external to k8s, the internal svc can't be used
-			labelSelector := "rgw=" + storeName
-			services, err := k8sh.Clientset.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{LabelSelector: labelSelector})
-			assert.Nil(t, err)
-			assert.Equal(t, 1, len(services.Items))
-			s3endpoint := services.Items[0].Spec.ClusterIP + ":80"
-
-			secret, err := k8sh.Clientset.CoreV1().Secrets(namespace).Get(ctx, bucketName, metav1.GetOptions{})
-			assert.Nil(t, err)
-			s3AccessKey := string(secret.Data["AWS_ACCESS_KEY_ID"])
-			s3SecretKey := string(secret.Data["AWS_SECRET_ACCESS_KEY"])
-
-			if objectStore.Spec.IsTLSEnabled() {
-				s3client, err = rgw.NewInsecureS3Agent(s3AccessKey, s3SecretKey, s3endpoint, true)
-			} else {
-				s3client, err = rgw.NewS3Agent(s3AccessKey, s3SecretKey, s3endpoint, true, nil)
-			}
-			assert.Nil(t, err)
-			logger.Infof("endpoint (%s) Accesskey (%s) secret (%s)", s3endpoint, s3AccessKey, s3SecretKey)
-		})
-
-		t.Run("bucketMaxObjects quota is enforced", func(t *testing.T) {
-			// first object should succeed
-			_, err := s3client.PutObjectInBucket(bucketName, ObjBody, ObjectKey1, contentType)
-			assert.Nil(t, err)
-			// second object should fail as bucket quota is 1
-			_, err = s3client.PutObjectInBucket(bucketName, ObjBody, ObjectKey2, contentType)
-			assert.Error(t, err)
-			// cleanup bucket
-			_, err = s3client.DeleteObjectInBucket(bucketName, ObjectKey1)
-			assert.Nil(t, err)
-			_, err = s3client.DeleteObjectInBucket(bucketName, ObjectKey2)
-			assert.Nil(t, err)
-		})
-
-		t.Run("change quota to bucketMaxSize", func(t *testing.T) {
-			obc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(namespace).Get(ctx, bucketName, metav1.GetOptions{})
-			assert.Nil(t, err)
-
-			obc.Spec.AdditionalConfig = map[string]string{"bucketMaxSize": "4Ki"}
-
-			_, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(namespace).Update(ctx, obc, metav1.UpdateOptions{})
-			assert.Nil(t, err)
-			obcBound := utils.Retry(20, 2*time.Second, "OBC is Bound", func() bool {
-				liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(namespace).Get(ctx, bucketName, metav1.GetOptions{})
-				if err != nil {
-					return false
-				}
-
-				return liveObc.Status.Phase == v1alpha1.ObjectBucketClaimStatusPhaseBound
-			})
-			assert.True(t, obcBound)
-
-			// wait until obc is Bound to lookup the ob name
-			obc, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(namespace).Get(ctx, bucketName, metav1.GetOptions{})
-			assert.Nil(t, err)
-			obName = obc.Spec.ObjectBucketName
-
-			obBound := utils.Retry(20, 2*time.Second, "OB is Bound", func() bool {
-				liveOb, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
-				if err != nil {
-					return false
-				}
-
-				return liveOb.Status.Phase == v1alpha1.ObjectBucketStatusPhaseBound
-			})
-			assert.True(t, obBound)
-
-			// verify that bucketMaxSize is set
-			assert.True(t, obc.Spec.AdditionalConfig["bucketMaxSize"] == "4Ki")
-		})
-
-		t.Run("bucketMaxSize quota is enforced", func(t *testing.T) {
-			// first ~3KiB Object should succeed
-			_, err := s3client.PutObjectInBucket(bucketName, strings.Repeat("1", 3072), ObjectKey1, contentType)
-			assert.Nil(t, err)
-			// second ~2KiB Object should fail as bucket quota is is 4KiB
-			_, err = s3client.PutObjectInBucket(bucketName, strings.Repeat("2", 2048), ObjectKey2, contentType)
-			assert.Error(t, err)
-			// cleanup bucket
-			_, err = s3client.DeleteObjectInBucket(bucketName, ObjectKey1)
-			assert.Nil(t, err)
-			_, err = s3client.DeleteObjectInBucket(bucketName, ObjectKey2)
-			assert.Nil(t, err)
-		})
-
-		t.Run("delete bucket", func(t *testing.T) {
-			err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(namespace).Delete(ctx, bucketName, metav1.DeleteOptions{})
-			assert.Nil(t, err)
-
-			absent := utils.Retry(20, 2*time.Second, "OBC is absent", func() bool {
-				_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(namespace).Get(ctx, bucketName, metav1.GetOptions{})
-				return err != nil
-			})
-			assert.True(t, absent)
-
-			absent = utils.Retry(20, 2*time.Second, "OB is absent", func() bool {
-				_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
-				return err != nil
-			})
-			assert.True(t, absent)
-		})
-	})
-
 	t.Run("Regression check: OBC does not revert to Pending phase", func(t *testing.T) {
 		// A bug exists in older versions of lib-bucket-provisioner that will revert a bucket and claim
 		// back to "Pending" phase after being created and initially "Bound" by looping infinitely in
diff --git a/tests/scripts/deploy-validate-vault.sh b/tests/scripts/deploy-validate-vault.sh
index b0003c0e8..df793a07e 100755
--- a/tests/scripts/deploy-validate-vault.sh
+++ b/tests/scripts/deploy-validate-vault.sh
@@ -276,32 +276,6 @@ function validate_osd_secret {
   fi
 }
 
-function validate_key_rotation() {
-  local backend_path=$1
-  pvc_name=$(kubectl get pvc -n rook-ceph -l ceph.rook.io/setIndex=0 -o jsonpath='{.items[0].metadata.name}')
-  key_name="rook-ceph-osd-encryption-key-$pvc_name"
-  cmd="vault kv get -format=json $backend_path/$key_name"
-  old_key=$(kubectl exec vault-0 -- sh -c "$cmd" | jq -r ".data.\"$key_name\"")
-  local new_key
-  runtime=180
-  endtime=$((SECONDS + runtime))
-  while [ $SECONDS -le $endtime ]; do
-    echo "Time Now: $(date +%H:%M:%S)"
-    new_key=$(kubectl exec vault-0 -- sh -c "$cmd" | jq -r ".data.\"$key_name\"")
-
-    if [ "$old_key" != "$new_key" ]; then
-      echo "encryption passphrase is successfully rotated"
-      exit 0
-    fi
-
-    echo "encryption passphrase is not rotated, sleeping for 10 seconds"
-    sleep 10
-  done
-
-  echo "encryption passphrase is not rotated"
-  exit 1
-}
-
 ########
 # MAIN #
 ########
@@ -316,9 +290,6 @@ validate_osd)
 validate_rgw)
   validate_rgw_deployment
   ;;
-validate_key_rotation)
-  validate_key_rotation "$2"
-  ;;
 *)
   echo "invalid action $ACTION" >&2
   exit 1
diff --git a/tests/scripts/github-action-helper.sh b/tests/scripts/github-action-helper.sh
index 558c315a5..c2735d260 100755
--- a/tests/scripts/github-action-helper.sh
+++ b/tests/scripts/github-action-helper.sh
@@ -615,6 +615,58 @@ function create_helm_tag() {
   docker tag "${build_image}" "rook/ceph:${helm_tag}"
 }
 
+function deploy_multus() {
+  # download the multus daemonset, and remove mem and cpu limits that cause it to crash on minikube
+  curl https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/master/deployments/multus-daemonset-thick.yml |
+    sed -e 's/cpu: /# cpu: /g' -e 's/memory: /# memory: /g' |
+    kubectl apply -f -
+
+  # install whereabouts
+  kubectl apply \
+    -f https://raw.githubusercontent.com/k8snetworkplumbingwg/whereabouts/master/doc/crds/daemonset-install.yaml \
+    -f https://github.com/k8snetworkplumbingwg/whereabouts/raw/master/doc/crds/whereabouts.cni.cncf.io_ippools.yaml \
+    -f https://github.com/k8snetworkplumbingwg/whereabouts/raw/master/doc/crds/whereabouts.cni.cncf.io_overlappingrangeipreservations.yaml
+
+  # create the rook-ceph namespace if it doesn't exist, the NAD will go in this namespace
+  kubectl create namespace rook-ceph || true
+
+  # install network attachment definitions
+  IFACE="eth0" # the runner has eth0 so we don't need any heuristics to find the interface
+  kubectl apply -f - <<EOF
+---
+apiVersion: k8s.cni.cncf.io/v1
+kind: NetworkAttachmentDefinition
+metadata:
+  name: public-net
+  namespace: rook-ceph
+  labels:
+  annotations:
+spec:
+  config: '{ "cniVersion": "0.3.0", "type": "macvlan", "master": "$IFACE", "mode": "bridge", "ipam": { "type": "whereabouts", "range": "192.168.20.0/24" } }'
+---
+apiVersion: k8s.cni.cncf.io/v1
+kind: NetworkAttachmentDefinition
+metadata:
+  name: cluster-net
+  namespace: rook-ceph
+  labels:
+  annotations:
+spec:
+  config: '{ "cniVersion": "0.3.0", "type": "macvlan", "master": "$IFACE", "mode": "bridge", "ipam": { "type": "whereabouts", "range": "192.168.21.0/24" } }'
+EOF
+}
+
+function deploy_multus_cluster() {
+  cd "${REPO_DIR}/deploy/examples"
+  sed -i 's/.*ROOK_CSI_ENABLE_NFS:.*/  ROOK_CSI_ENABLE_NFS: \"true\"/g' operator.yaml
+  deploy_manifest_with_local_build operator.yaml
+  deploy_toolbox
+  sed -i "s|#deviceFilter:|deviceFilter: $(block_dev_basename)|g" cluster-multus-test.yaml
+  kubectl create -f cluster-multus-test.yaml
+  kubectl create -f filesystem-test.yaml
+  kubectl create -f nfs-test.yaml
+}
+
 function test_multus_connections() {
   EXEC='kubectl -n rook-ceph exec -t deploy/rook-ceph-tools -- ceph --connect-timeout 10'
   # each OSD should exist on both public and cluster network
diff --git a/tests/scripts/multus/default-public-cluster-nads.yaml b/tests/scripts/multus/default-public-cluster-nads.yaml
index 2747741cc..28ccd184a 100644
--- a/tests/scripts/multus/default-public-cluster-nads.yaml
+++ b/tests/scripts/multus/default-public-cluster-nads.yaml
@@ -22,8 +22,6 @@ spec:
       }
     }
 ---
-# IPv6 address here (with IPv4 above) makes this unsuitable for CephCluster use, but it is okay for
-# testing validation tool
 apiVersion: k8s.cni.cncf.io/v1
 kind: NetworkAttachmentDefinition
 metadata:
diff --git a/tests/scripts/multus/host-cfg-ds.yaml b/tests/scripts/multus/host-cfg-ds.yaml
index 9fbedcfa3..113db80e9 100644
--- a/tests/scripts/multus/host-cfg-ds.yaml
+++ b/tests/scripts/multus/host-cfg-ds.yaml
@@ -64,6 +64,5 @@ spec:
               sleep infinity
           resources: {}
           securityContext:
-            privileged: true
             capabilities:
               add: ["NET_ADMIN"]
diff --git a/tests/scripts/multus/rook-ceph-public-cluster-nads.yaml b/tests/scripts/multus/rook-ceph-public-cluster-nads.yaml
deleted file mode 100644
index a45eb7224..000000000
--- a/tests/scripts/multus/rook-ceph-public-cluster-nads.yaml
+++ /dev/null
@@ -1,43 +0,0 @@
----
-apiVersion: k8s.cni.cncf.io/v1
-kind: NetworkAttachmentDefinition
-metadata:
-  name: public-net
-  namespace: rook-ceph
-  labels:
-  annotations:
-spec:
-  config: |
-    {
-      "cniVersion": "0.3.1",
-      "type": "macvlan",
-      "master": "eth0",
-      "mode": "bridge",
-      "ipam": {
-        "type": "whereabouts",
-        "range": "192.168.20.0/24",
-        "routes": [
-          {"dst": "192.168.29.0/24"}
-        ]
-      }
-    }
----
-apiVersion: k8s.cni.cncf.io/v1
-kind: NetworkAttachmentDefinition
-metadata:
-  name: cluster-net
-  namespace: rook-ceph
-  labels:
-  annotations:
-spec:
-  config: |
-    {
-      "cniVersion": "0.3.1",
-      "type": "macvlan",
-      "master": "eth0",
-      "mode": "bridge",
-      "ipam": {
-        "type": "whereabouts",
-        "range": "192.168.21.0/24"
-      }
-    }
diff --git a/tests/scripts/validate_cluster.sh b/tests/scripts/validate_cluster.sh
index eeac1344b..091e83caa 100755
--- a/tests/scripts/validate_cluster.sh
+++ b/tests/scripts/validate_cluster.sh
@@ -103,10 +103,25 @@ function test_demo_pool {
 
 function test_csi {
   timeout 360 bash -x <<-'EOF'
-    until [[ "$(kubectl -n rook-ceph get pods --field-selector=status.phase=Running|grep -c ^csi-)" -eq 6 ]]; do
-      echo "waiting for csi pods to be ready"
-      sleep 5
-    done
+    echo $IS_POD_NETWORK
+    echo $IS_MULTUS
+    if [ -z "$IS_POD_NETWORK" ]; then
+      until [[ "$(kubectl -n rook-ceph get pods --field-selector=status.phase=Running|grep -c ^csi-)" -eq 6 ]]; do
+        echo "waiting for csi pods to be ready"
+        sleep 5
+      done
+    else
+      until [[ "$(kubectl -n rook-ceph get pods --field-selector=status.phase=Running|grep -c ^csi-)" -eq 9 ]]; do
+        echo "waiting for csi pods to be ready with multus or pod networking"
+        sleep 5
+      done
+    fi
+    if [ -n "$IS_MULTUS" ]; then
+      echo "verifying csi holder interfaces (multus ones must be present)"
+      kubectl -n rook-ceph exec -t ds/csi-rbdplugin-holder-my-cluster -- grep eth0 /proc/net/dev
+      kubectl -n rook-ceph exec -t ds/csi-cephfsplugin-holder-my-cluster -- grep eth0 /proc/net/dev
+      kubectl -n rook-ceph exec -t ds/csi-nfsplugin-holder-my-cluster -- grep eth0 /proc/net/dev
+    fi
 EOF
 }