Repository: kubernetes/kubernetes (language: go)

Issue:
Remove gitRepo volume type
### What would you like to be added?

I'd like to remove gitRepo volume types. It's been deprecated for 6 years now and if a giant foot gun. 

In a recent blog a researcher has exploited it to get remote code execution(RCE). 

https://irsl.medium.com/sneaky-write-hook-git-clone-to-root-on-k8s-node-e38236205d54


### Why is this needed?

To prevent folks from using a deprecated and dangerous volume type. 


Discussion / hints:
This issue is currently awaiting triage.

If a SIG or subproject determines this is a relevant issue, they will accept it by applying the `triage/accepted` label and provide further guidance.

The `triage/accepted` label can be added by org members by writing `/triage accepted` in a comment.


<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
/sig-security
/sig security
/sig storage
+1. FYI we discussed about removing the gitRepo volume provisioner in the past (for context - https://docs.google.com/document/d/1-8KEG8AjAgKznS9NFm3qWqkGyCHmvU6HVl0sk5hwoAE/edit#heading=h.9ealp5t409p1). As a first step to restrict the use, we could also evaluate adding support in PSA to disallow creation of gitRepo volume for Baseline/Restricted namespaces [1].

[1] - https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline
I'd definitely agree that this should be removed, or at the very least moved to not be enabled by default. PSS baseline would be good to.

Having a situation where anyone with create pod automatically gets code execution on the underlying node as root is .... not the best.
We should remove the gitRepo volume provisioner or at least disable it by default. Adding a restriction in the Pod Security Standards baseline would be good too. Letting anyone who can create a pod run code as root on the node is very risky. 

+1 for removal, and putting a forbid into PSS baseline (like hostpath is today) while that happens.
+1 for removing gitRepo if possible

Huge +1 to forbidding gitRepo in baseline
Copying my comment from https://github.com/kubernetes/kubernetes/issues/126041:

> It looks like completely removing the API/code would have been considered a backwards incompatible change which k8s avoids, and so only a deprecation notice was created and the code was left as is see #63445 (comment)
> 
> We have been able to get rid of other in-tree plugins by keeping the API and implementing "CSI migration" which enables the implementation to be removed and redirect to new CSI drivers. In this case there is no "CSI" migration -- instead the recommendation is to use emptyDir + initContainers.
> 
> To remove risk from the unmaintained code -- I suggest that we keep the API, but remove the implementation. (We did exactly this with Flocker, for example, see #111618)


cc @thockin

somewhat git-sync adjacent.
I'm in favor of a controlled way to remove this, but there *has* to be a way to burn down use safely. Starting with an opt-in gate that disables the plugin, then planning and communicating how to flip that to opt-out safely is a much more controlled way to allow this to roll out.
> Starting with an opt-in gate that disables the plugin, then planning and communicating how to flip that to opt-out safely is a much more controlled way to allow this to roll out.

This, we should outline:
- A concrete "this feature gate will be removed in release 1.X"
- This is what you can use instead (e.g. git-sync)

And communicate it widely ahead of time, not just in release notes. With a way to opt-back in to support for N releases. 
We don't want people to stop upgrading Kubernetes because we abruptly dropped functionality.

At the minimum we should also be creating a kubernetes.io blogpost and planning to work with contributor comms on that and posting to social media etc. cc @kubernetes/contributor-comms 


EDIT: while long deprecated, it has been sitting deprecated since 2018 / v1.11 https://github.com/kubernetes/kubernetes/pull/63445 ... so it would be very easy to be using this while not at all being aware of impending about removal, similar to [perma-beta](https://github.com/kubernetes/enhancements/blob/master/keps/sig-architecture/1635-prevent-permabeta/README.md)
How about we block it in PSS baseline for now and work on a plan to remove it with opt-in in 1.32?
I don't think it makes sense to block in baseline... that profile forbids _inherently_ host-level and escalating things. I don't think gitRepo volumes fall in that bucket.
It looks like there are ways to disable hooks (e.g. config `core.hooksPath=/dev/null`, but I have not manually tested that) which would have LESS, but not NO impact.  We could add that as an optional field in gitRepo and then security-conscious folks could demand it be set, while still allowing repos to be cloned?
Regarding removal, see also the discussion in https://github.com/kubernetes/kubernetes/pull/126084#issuecomment-2231579005


> I don't think it makes sense to block in baseline... that profile forbids inherently host-level and escalating things. I don't think gitRepo volumes fall in that bucket.

Isn't it effectively at this point? Unless we're able to prevent these hooks ...


> It looks like there are ways to disable hooks (e.g. config core.hooksPath=/dev/null, but I have not manually tested that) which would have LESS, but not NO impact. We could add that as an optional field in gitRepo and then security-conscious folks could demand it be set, while still allowing repos to be cloned?

Don't we have precedent with breaking behavioral changes for volumes to mitigate serious vulns?

https://github.com/kubernetes/kubernetes/issues/60814

> **Fix impact:**
> Secret, configMap, downwardAPI and projected volumes will be mounted as read-only volumes. Applications that attempt to write to these volumes will receive read-only filesystem errors. Previously, applications were allowed to make changes to these volumes, but those changes were reverted at an arbitrary interval by the system. Applications should be re-configured to write derived files to another location.
Ah, we do have https://github.com/kubernetes/kubernetes/pull/124531 (and cherry picks)
Should the removal be part of 1.33? [on behalf of SRC]
Yes please! To liggitt@'s point we should atleast consider adding an opt-in that allows users to turn this off in 1.33. (I am happy to drive that change). In addition we can update documentation and provide alternatives to gitRepo.
I posted somewhere a VAP config that people can/should use to deny gitRepo (will post it again below).

> Should the removal be part of 1.33? [on behalf of SRC]

Do we think this is SO critical that we violate our own breaking-changes rule?

Easy: Add a new optional field in `gitRepo` to disable hooks.  Alpha-gated in 33, GA, default-off in 34.
  Pro: not a breaking change
  Con: opt-in

Medium: Add a new optional field in `gitRepo` to disable hooks.  Alpha-gated in 33, GA default-on in 34.
  Pro: secure by default
  Con: breaking change for users of hooks
  Rebuttal: it's hard to imagine a correct use of hooks

Hard: Nullify the `gitRepo` functionality entirely.  (it's been deprecated since 1.11)
  Pro: secure by default
  Con: breaking change for all `gitRepo` users


deny-gitrepo-vap.yaml:
```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "deny-git-repo-volumes"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE"]
      resources:   ["pods"]
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["podtemplates", "replicationcontrollers"]
    - apiGroups:   ["apps"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["daemonsets", "deployments", "replicasets", "statefulsets"]
    - apiGroups:   ["batch"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["jobs", "cronjobs"]
  validations:
    - expression: |
        (object.kind == "Pod" &&
            object.spec.?volumes.orValue([]).all(v, !has(v.gitRepo))) ||
        (object.kind == "PodTemplate" &&
            object.template.spec.?volumes.orValue([]).all(v, !has(v.gitRepo))) ||
        (["DaemonSet","Deployment","ReplicaSet",'StatefulSet','Job'].filter(kind, object.kind == kind) != [] &&
            object.spec.template.spec.?volumes.orValue([]).all(v, !has(v.gitRepo))) ||
        (object.kind == "CronJob" &&
            object.spec.jobTemplate.spec.template.spec.?volumes.orValue([]).all(v, !has(v.gitRepo)))
      message: "gitRepo volumes are forbidden"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "deny-git-repo-volumes"
spec:
  policyName: "deny-git-repo-volumes"
  validationActions: [Deny]
  # empty matchResources means match all
```


I think a way for a cluster admin to neuter gitRepo volume support would be in bounds, similar to how other volume types had disable gates. We definitely cannot drop the API fields, and I haven't thought through whether the volume handler should be disabled by default or not (my inclination is always to avoid breaking changes that will bite distracted admins that just upgrade without scrutinizing release notes)
Regarding dropping fields entirely: see https://kubernetes.io/docs/reference/using-api/deprecation-policy/
Note: I am not proposing to remove fields.  At most we would make them mean nothing.

> a way for a cluster admin to neuter gitRepo volume support

Does the VAP I provided above satisfy that?  I think it does, as long as "opt in to security" is acceptable.  And if that is acceptable, I don't know if we need a change to the `gitRepo` API/impl to block hooks (it's opt-in either way).

If we want secure-by-default, that's a breaking change however we do it.  We can do varying levels of hoop-jumping to re-allow usage.
- trivially restored: Add a field to `gitRepo` requiring end-users to opt-in to hooks.
  - Pro: easy for an impacted user to fix themselves
  - Con: easy for an attacker to circumvent
- possible to restore by admin: Add a kubelet config which tells it not to run hooks for `gitRepo`
  - Pro: hard for attacker to circumvent
  - Con: requires changes on each node, enablement is whole node
- very hard to restore by admin: Add an API server config which tells it not to run hooks
  - Pro: whole cluster
  - Con: probably better to have the VAP above
- impossible to restore: Disable git hooks in `gitRepo`, no way to re-enable
  - Pro: no API surface, no circumvention
  - Con: no way to restore
- a step too far: turn `gitRepo` into an actual no-op
  - Pro: no way around it
  - Con: no way around it 

The question is: Do we think this is dire enough to break people?

I think the hole is pretty nasty, personally, and may be worth it.  I have a hard time coming up with a legitimate use of git hooks that run in root context, `gitRepo` has been deprecated for years, and there is a workable alternative in `git-sync`.  

I am pretty sure there are users of `gitRepo` volumes, so breaking them entirely seems a step too far, but breaking the specific hooks functionality might be OK (even without an option to restore it).  Obviously it would be a gate, so we'd have a way back.  We might want to consider accelerating the announcment-and-wait cycle.
Could we write a `ValidatingAdmissionWebhook` similar to `DenyServiceExternalIP` that prevents this? I think having this built in than slapped on is better.
@vinayakankugoyal What is "this" in "denies this"?

The VAP I wrote above denies all use of gitRepo, but it's opt-in.  Cluster admin has to take the action of installing it.

In looking at it, I think I forgot to properly handle "replicationcontrollers" which just goes to show how easy it is to get wrong :)
Sorry that I wasn't clear earlier. "this" meant a way to block the use of gitRepo in the cluster.
I am not a huge fan of using VAP for this. If we deem gitRepo to be dangerous then we should provide a way to outright disable it using an APIServer flag because that option is more auditable. Its much easier to check if the AdmissionPlugin "DenyGitRepo" is enabled than to check if a VAP exists that blocks gitRepo volumes.
I don't have a strong feeling, but:

* Do we want dozens of bespoke admission plugins?  This is what VAP is for.
* If we do that, cluster providers need to re-expose it through their infra APIs - more work
* If we do that, there's no way to ask the cluster "is gitRepo blocked" except to try creating a pod
* VAP can be enabled or disabled more granularly 
This is announced as targeting future removal in the 1.32 release notes - https://github.com/kubernetes/website/pull/48666/files#r1871864868

Until we've actually decided that, we should not announce removal
cc @xing-yang @saad-ali @msau42 @jsafrane


> This is announced as targeting future removal in the 1.32 release notes - https://github.com/kubernetes/website/pull/48666/files#r1871864868
> 
> Until we've actually decided that, we should not announce removal

We (SIG Storage) should have coordinated that better with the folks in this thread before it went in (sorry about that).

We discussed this in SIG Storage. Our thought process is:

1. There is no one actively maintaining git-repo.
2. There are security concerns with the plugin
3. Our [public docs](https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo) have a warning about deprecation(`The gitRepo volume type is deprecated...`) and there is a warning on the resource.
5. There is a viable alternative emptyDir+initContainer
6. Disabling hooks feels like playing "whack a mole" with security issues that emerge. 
7. We have "removed" other in-tree plugins successfully in the past.
8. There is a non-trivial cost to continuing to fix the gitRepo code.

So from a SIG Storage POV -- we would prefer to remove the gitRepo plugin altogether. Note that "remove" here means removing the business logic not the API (which is still a breaking change, of course). 

The idea is to get the removal notice in the release notes ASAP, so that we can actually remove it in some future release.

If API reviewers disagree with that (or want more time to discuss this) we will undo the [removal notice](https://github.com/kubernetes/website/pull/48666/files#r1871864868). 

Please let us know.
https://github.com/kubernetes/website/pull/48666/files#r1871943622

"""
we've discussed this at SIG Storage meetings and agreed to remove this
"""

This is relatively unprecedented and I don't think this is a decision to be made lightly or unilaterally.  It sets precedent for the project.  Unfortunately, I have a thing I must be at during this week's sig-arch meeting.  I will try to come for some of it.

I personally DO NOT think we should announce the removal without broader agreement and boundaries - what makes this OK and the next one not?
I put it on sig-arch agenda
sig-arch is this Thursday. The proposed release notes and announced removal of gitRepo would hit the 1.32.0 release this Wednesday.

I mostly agree with Tim's comment that removal is not a decision to be made lightly or unilaterally.

There is *some* precedent around migration of cloud provider volume plugins to CSI, and removal of obsolete volume drivers, but this doesn't seem to fall into that category.

As mentioned earlier, I'm in favor of a way for cluster admins to neuter the gitRepo plugin, and we could discuss what the default should be in future minors, but I don't think we're ready to announce future *removal*.
for the CSI volume plugins, we were sooooo conservative, we still have portworx that is to be removed in 1.33... we can't rush here!

So +1 to what @thockin and @liggitt said.
Based on your input, we'll go ahead and rollback the removal notice.

Rollback is in this PR: https://github.com/kubernetes/website/pull/48962
Quick notes from sig-arch discussion:

Preference for a way to disable it while allowing cluster providers (Openshift, GKE, etc) to offer a way to enable it if they need to, based on their own data.  I read that as a built-in admission controller (or maybe a pre-installed VAP?) that prevents either using `gitRepo` or perhaps just prevents using hooks.  This would be a break for people who use gitRepo (or just those who use hooks).  Providers can look at their respective fleets for how many users of `gitRepo` exist.  They cannot easily analyze how many of those `gitRepo` users rely on hooks.

Preference for on-by-default, since this is a significant potential exposure.

IMO, if we are doing a breaking change, we need to scope it as small as possible.  I do not love this feature, but there are lots of features in k8s I don't like, and we're not considering ripping those out.  :)

Here's my revised proposal(s):

* Add a field to `gitRepo` called `enableInsecureHooks bool` or something like that.  Default to false.
* Either:
  a) Add a built-in admission control that rejects workloads that set `gitRepo.enableInsecureHooks: true` with either:
    1) On by default + relnote about disabling it if needed
    2) Relnote that cluster providers are STRONGLY ENCOURAGED to enable it
  b) Publish a VAP that rejects workloads that set `gitRepo.enableInsecureHooks: true` with a relnote that cluster providers are STRONGLY ENCOURAGED to preinstall it.

Having a field in the API allows `gitRepo` users who do not need hooks to keep working, while allowing those who need to hooks to self-identify, and locked-down providers (e.g. do not allow SSH to nodes) can prevent hooks entirely. 

VAP has the advantage of being an "initial setting" that providers impose, but cluster admins can change (either partially or entirely) *without* needing providers to expose it through their own API.  I don't think we have any real precedent for this specific pattern, but we know many providers run their own webhooks or other admission controls.  It has the disadvantage of requiring providers to take action to be secure.

Built-in admission control has the advantage and disadvantage of being on by default, and requires providers to expose an knob through their own APIs to change the behavior, and such a change is for the whole cluster (unless we also offer per-node control).

I lean towards VAP, myself.  It seems like a useful pattern to have "recommended but optional" policies that can be installed by providers and changed by admins.

Thoughts?
@saad-ali @msau42 since this sort of rolls up to sig-storage, is there anyone who can help with impl?  It should not be very  much work, but having someone who could do it would help me a ton.

Someone who wants to build up to org membership, perhaps?  Or someone who wants to build their contributions up more?  I imagine this is a small KEP, especially if we add a field.
@thockin I'd be more than happy to help with impl and driving the KEP. 
Awesome.  Can we talk either next week or after the holidays?  If we are going to come up with a KEP, we need to move fast.  Talking to some folks today, even more options were presented:

1) Builtin-admission as described above
2) Pre-installed VAP as described above
3) NEW IDEA: disable `gitRepo.allowInsecureHooks` in the baseline PSP.  Pro: simple!
4) NEW IDEA: go through the act of CSI migration. Pro: removes gitRepo from kubelet entirely.  Con: longer process.
@thockin

I started a doc to capture some of the ideas discussed here and in sg-arch. Eventually we can use it  for the KEP.

https://docs.google.com/document/d/1sUx65kAm1EMXpAhPycMApoNDTKAQR2vGP7yz88jKDSE/edit?usp=sharing
Thanks Tim for the follow up on this and @vinayakankugoyal for volunteering to move this forward. 
We'll work with Vinayak to move the KEP forward.
Just a quick question on the current status of security exposure via `gitrepo` volumes. My understanding was that after the fixes put in place for [CVE-2024-10220](https://github.com/kubernetes/kubernetes/issues/128885) there wasn't currently a risk of users with rights to create pods being able to execute commands on the node as root.

Reading the discussions on this, is there still a way for someone creating a pod to use this to get command execution via git hooks? I was guessing there might be as we're discussing the idea of having a feature to allow cluster-admins to disable git hooks. But I wasn't sure if that's a current exposure or just something we're worried about being possible in the future if another vulnerability is discovered.
I think this is the "real" resolution to that CVE.  Running git as kubelet is a large an unnecessary vector for attacks.  There are alternatives which are superior in pretty much every way.  Rather than play whack-a-mole forever, the KEP proposes to rip the bandaid off.
@thockin oh yeah I agree in terms of removing the plugin altogether, that's definitely (IMO) the right way to go. The bit I was a bit confused about was the `enableInsecureHooks` flag option. That made me think that in the current setup it's still possible for someone creating a pod to get the Kubelet to run a hook script (after the patch for the CVE-2024-10220).

I'm not that experienced with git hooks, so wasn't sure if that was a possibility or I was missing something :)
I don't think there was a patch, this is the reaction to the CVE (plus the VAP that was published).  Did I miss a patch?
https://github.com/kubernetes/kubernetes/pull/124531 and backports addressed the specific issue of directory depth nesting to address that CVE. They did not prevent use of repos with hooks.
Ahh, right.  Thank you 
@liggitt Just checking, my understanding of git hooks was that they're not copied from source repositories during a git clone operation, they're purely local, with the only work-around being a git config setting `hooksPath` that specifies them at a different location so that they can be stored and transmitted as part of a clone operation. 

So I wasn't too clear if there's a case at the moment where the Kubelet could execute a hook script when setting up a gitRepo volume?

> [@liggitt](https://github.com/liggitt) Just checking, my understanding of git hooks was that they're not copied from source repositories during a git clone operation, they're purely local, with the only work-around being a git config setting `hooksPath` that specifies them at a different location so that they can be stored and transmitted as part of a clone operation.
> 
> So I wasn't too clear if there's a case at the moment where the Kubelet could execute a hook script when setting up a gitRepo volume?

I don't really know the details of git / hook / kubelet behavior, and what the directory level gap was that the previous fix closed, sorry.

Fix patch:
diff --git a/pkg/api/pod/warnings.go b/pkg/api/pod/warnings.go
index 3556f9d760323..ff8df957f25af 100644
--- a/pkg/api/pod/warnings.go
+++ b/pkg/api/pod/warnings.go
@@ -26,10 +26,12 @@ import (
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/validation"
 	"k8s.io/apimachinery/pkg/util/validation/field"
+	utilfeature "k8s.io/apiserver/pkg/util/feature"
 	nodeapi "k8s.io/kubernetes/pkg/api/node"
 	pvcutil "k8s.io/kubernetes/pkg/api/persistentvolumeclaim"
 	api "k8s.io/kubernetes/pkg/apis/core"
 	"k8s.io/kubernetes/pkg/apis/core/pods"
+	"k8s.io/kubernetes/pkg/features"
 )
 
 func GetWarningsForPod(ctx context.Context, pod, oldPod *api.Pod) []string {
@@ -142,7 +144,11 @@ func warningsForPodSpecAndMeta(fieldPath *field.Path, podSpec *api.PodSpec, meta
 			warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.11, non-functional in v1.16+", fieldPath.Child("spec", "volumes").Index(i).Child("photonPersistentDisk")))
 		}
 		if v.GitRepo != nil {
-			warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.11", fieldPath.Child("spec", "volumes").Index(i).Child("gitRepo")))
+			if !utilfeature.DefaultFeatureGate.Enabled(features.GitRepoVolumeDriver) {
+				warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.11, and disabled by default in v1.33+", fieldPath.Child("spec", "volumes").Index(i).Child("gitRepo")))
+			} else {
+				warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.11", fieldPath.Child("spec", "volumes").Index(i).Child("gitRepo")))
+			}
 		}
 		if v.ScaleIO != nil {
 			warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.16, non-functional in v1.22+", fieldPath.Child("spec", "volumes").Index(i).Child("scaleIO")))
diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go
index 5b9e4bdc79d25..806d95302adf3 100644
--- a/pkg/features/kube_features.go
+++ b/pkg/features/kube_features.go
@@ -264,6 +264,13 @@ const (
 	// Lock to default and remove after v1.22 based on user feedback that should be reflected in KEP #1972 update
 	ExecProbeTimeout featuregate.Feature = "ExecProbeTimeout"
 
+	// owner: @vinayakankugoyal @thockin
+	//
+	// Controls if the gitRepo volume plugin is supported or not.
+	// KEP #5040 disables the gitRepo volume plugin by default starting v1.33,
+	// this provides a way for users to override it.
+	GitRepoVolumeDriver featuregate.Feature = "GitRepoVolumeDriver"
+
 	// owner: @bobbypage
 	// Adds support for kubelet to detect node shutdown and gracefully terminate pods prior to the node being shutdown.
 	GracefulNodeShutdown featuregate.Feature = "GracefulNodeShutdown"
diff --git a/pkg/features/versioned_kube_features.go b/pkg/features/versioned_kube_features.go
index 7ea7c0e9fa517..887c81c4a4760 100644
--- a/pkg/features/versioned_kube_features.go
+++ b/pkg/features/versioned_kube_features.go
@@ -359,6 +359,11 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
 		{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
 	},
 
+	GitRepoVolumeDriver: {
+		{Version: version.MustParse("1.0"), Default: true, PreRelease: featuregate.GA},
+		{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Deprecated},
+	},
+
 	GracefulNodeShutdown: {
 		{Version: version.MustParse("1.20"), Default: false, PreRelease: featuregate.Alpha},
 		{Version: version.MustParse("1.21"), Default: true, PreRelease: featuregate.Beta},
diff --git a/pkg/volume/git_repo/git_repo.go b/pkg/volume/git_repo/git_repo.go
index 3a4bf850f5822..985be19cd52be 100644
--- a/pkg/volume/git_repo/git_repo.go
+++ b/pkg/volume/git_repo/git_repo.go
@@ -24,6 +24,8 @@ import (
 
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
+	utilfeature "k8s.io/apiserver/pkg/util/feature"
+	"k8s.io/kubernetes/pkg/features"
 	"k8s.io/kubernetes/pkg/volume"
 	volumeutil "k8s.io/kubernetes/pkg/volume/util"
 	"k8s.io/utils/exec"
@@ -169,6 +171,10 @@ func (b *gitRepoVolumeMounter) GetAttributes() volume.Attributes {
 
 // SetUp creates new directory and clones a git repo.
 func (b *gitRepoVolumeMounter) SetUp(mounterArgs volume.MounterArgs) error {
+	if !utilfeature.DefaultFeatureGate.Enabled(features.GitRepoVolumeDriver) {
+		return fmt.Errorf("git-repo volume plugin has been disabled; if necessary, it may be re-enabled by enabling the feature-gate `GitRepoVolumeDriver`")
+	}
+
 	return b.SetUpAt(b.GetPath(), mounterArgs)
 }
 


Test patch:
diff --git a/pkg/api/pod/warnings_test.go b/pkg/api/pod/warnings_test.go
index 0524c5188e66e..cc97b0f1a4cdd 100644
--- a/pkg/api/pod/warnings_test.go
+++ b/pkg/api/pod/warnings_test.go
@@ -26,7 +26,10 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/validation/field"
+	utilfeature "k8s.io/apiserver/pkg/util/feature"
+	featuregatetesting "k8s.io/component-base/featuregate/testing"
 	api "k8s.io/kubernetes/pkg/apis/core"
+	"k8s.io/kubernetes/pkg/features"
 	utilpointer "k8s.io/utils/pointer"
 )
 
@@ -148,10 +151,11 @@ func TestWarnings(t *testing.T) {
 	}
 	testName := "Test"
 	testcases := []struct {
-		name        string
-		template    *api.PodTemplateSpec
-		oldTemplate *api.PodTemplateSpec
-		expected    []string
+		name                  string
+		template              *api.PodTemplateSpec
+		oldTemplate           *api.PodTemplateSpec
+		gitRepoPluginDisabled bool
+		expected              []string
 	}{
 		{
 			name:     "null",
@@ -176,6 +180,16 @@ func TestWarnings(t *testing.T) {
 			},
 			expected: []string{`spec.volumes[0].gitRepo: deprecated in v1.11`},
 		},
+		{
+			name: "gitRepo plugin disabled",
+			template: &api.PodTemplateSpec{Spec: api.PodSpec{
+				Volumes: []api.Volume{
+					{Name: "s", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{}}},
+				}},
+			},
+			gitRepoPluginDisabled: true,
+			expected:              []string{`spec.volumes[0].gitRepo: deprecated in v1.11, and disabled by default in v1.33+`},
+		},
 		{
 			name: "scaleIO",
 			template: &api.PodTemplateSpec{Spec: api.PodSpec{
@@ -1786,6 +1800,7 @@ func TestWarnings(t *testing.T) {
 
 	for _, tc := range testcases {
 		t.Run("podspec_"+tc.name, func(t *testing.T) {
+			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.GitRepoVolumeDriver, !tc.gitRepoPluginDisabled)
 			var oldTemplate *api.PodTemplateSpec
 			if tc.oldTemplate != nil {
 				oldTemplate = tc.oldTemplate
@@ -1805,6 +1820,7 @@ func TestWarnings(t *testing.T) {
 		})
 
 		t.Run("pod_"+tc.name, func(t *testing.T) {
+			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.GitRepoVolumeDriver, !tc.gitRepoPluginDisabled)
 			var pod *api.Pod
 			if tc.template != nil {
 				pod = &api.Pod{
diff --git a/pkg/volume/git_repo/git_repo_test.go b/pkg/volume/git_repo/git_repo_test.go
index 5bcee89f9c9c2..bba41d32171ed 100644
--- a/pkg/volume/git_repo/git_repo_test.go
+++ b/pkg/volume/git_repo/git_repo_test.go
@@ -29,6 +29,9 @@ import (
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
+	utilfeature "k8s.io/apiserver/pkg/util/feature"
+	featuregatetesting "k8s.io/component-base/featuregate/testing"
+	"k8s.io/kubernetes/pkg/features"
 	"k8s.io/kubernetes/pkg/volume"
 	"k8s.io/kubernetes/pkg/volume/emptydir"
 	volumetest "k8s.io/kubernetes/pkg/volume/testing"
@@ -70,16 +73,19 @@ type expectedCommand struct {
 	dir string
 }
 
+type scenario struct {
+	name                  string
+	vol                   *v1.Volume
+	expecteds             []expectedCommand
+	isExpectedFailure     bool
+	gitRepoPluginDisabled bool
+}
+
 func TestPlugin(t *testing.T) {
 	gitURL := "https://github.com/kubernetes/kubernetes.git"
 	revision := "2a30ce65c5ab586b98916d83385c5983edd353a1"
 
-	scenarios := []struct {
-		name              string
-		vol               *v1.Volume
-		expecteds         []expectedCommand
-		isExpectedFailure bool
-	}{
+	scenarios := []scenario{
 		{
 			name: "target-dir",
 			vol: &v1.Volume{
@@ -106,7 +112,21 @@ func TestPlugin(t *testing.T) {
 					dir: "/target_dir",
 				},
 			},
-			isExpectedFailure: false,
+		},
+		{
+			name:                  "target-dir",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: gitURL,
+						Revision:   revision,
+						Directory:  "target_dir",
+					},
+				},
+			},
+			isExpectedFailure: true,
 		},
 		{
 			name: "target-dir-no-revision",
@@ -125,7 +145,20 @@ func TestPlugin(t *testing.T) {
 					dir: "",
 				},
 			},
-			isExpectedFailure: false,
+		},
+		{
+			name:                  "target-dir-no-revision",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: gitURL,
+						Directory:  "target_dir",
+					},
+				},
+			},
+			isExpectedFailure: true,
 		},
 		{
 			name: "only-git-clone",
@@ -143,7 +176,19 @@ func TestPlugin(t *testing.T) {
 					dir: "",
 				},
 			},
-			isExpectedFailure: false,
+		},
+		{
+			name:                  "only-git-clone",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: gitURL,
+					},
+				},
+			},
+			isExpectedFailure: true,
 		},
 		{
 			name: "no-target-dir",
@@ -171,7 +216,21 @@ func TestPlugin(t *testing.T) {
 					dir: "/kubernetes",
 				},
 			},
-			isExpectedFailure: false,
+		},
+		{
+			name:                  "no-target-dir",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: gitURL,
+						Revision:   revision,
+						Directory:  "",
+					},
+				},
+			},
+			isExpectedFailure: true,
 		},
 		{
 			name: "current-dir",
@@ -199,7 +258,21 @@ func TestPlugin(t *testing.T) {
 					dir: "",
 				},
 			},
-			isExpectedFailure: false,
+		},
+		{
+			name:                  "current-dir",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: gitURL,
+						Revision:   revision,
+						Directory:  ".",
+					},
+				},
+			},
+			isExpectedFailure: true,
 		},
 		{
 			name: "current-dir-mess",
@@ -227,7 +300,21 @@ func TestPlugin(t *testing.T) {
 					dir: "",
 				},
 			},
-			isExpectedFailure: false,
+		},
+		{
+			name:                  "current-dir-mess",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: gitURL,
+						Revision:   revision,
+						Directory:  "./.",
+					},
+				},
+			},
+			isExpectedFailure: true,
 		},
 		{
 			name: "invalid-repository",
@@ -241,6 +328,19 @@ func TestPlugin(t *testing.T) {
 			},
 			isExpectedFailure: true,
 		},
+		{
+			name:                  "invalid-repository",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: "--foo",
+					},
+				},
+			},
+			isExpectedFailure: true,
+		},
 		{
 			name: "invalid-revision",
 			vol: &v1.Volume{
@@ -254,6 +354,20 @@ func TestPlugin(t *testing.T) {
 			},
 			isExpectedFailure: true,
 		},
+		{
+			name:                  "invalid-revision",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: gitURL,
+						Revision:   "--bar",
+					},
+				},
+			},
+			isExpectedFailure: true,
+		},
 		{
 			name: "invalid-directory",
 			vol: &v1.Volume{
@@ -267,6 +381,20 @@ func TestPlugin(t *testing.T) {
 			},
 			isExpectedFailure: true,
 		},
+		{
+			name:                  "invalid-directory",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: gitURL,
+						Directory:  "-b",
+					},
+				},
+			},
+			isExpectedFailure: true,
+		},
 		{
 			name: "invalid-revision-directory-combo",
 			vol: &v1.Volume{
@@ -281,26 +409,40 @@ func TestPlugin(t *testing.T) {
 			},
 			isExpectedFailure: true,
 		},
+		{
+			name:                  "invalid-revision-directory-combo",
+			gitRepoPluginDisabled: true,
+			vol: &v1.Volume{
+				Name: "vol1",
+				VolumeSource: v1.VolumeSource{
+					GitRepo: &v1.GitRepoVolumeSource{
+						Repository: gitURL,
+						Revision:   "main",
+						Directory:  "foo/bar",
+					},
+				},
+			},
+			isExpectedFailure: true,
+		},
 	}
 
-	for _, scenario := range scenarios {
-		allErrs := doTestPlugin(scenario, t)
-		if len(allErrs) == 0 && scenario.isExpectedFailure {
-			t.Errorf("Unexpected success for scenario: %s", scenario.name)
-		}
-		if len(allErrs) > 0 && !scenario.isExpectedFailure {
-			t.Errorf("Unexpected failure for scenario: %s - %+v", scenario.name, allErrs)
-		}
+	for _, sc := range scenarios {
+		t.Run(fmt.Sprintf("%s/gitRepoPluginDisabled:%v", sc.name, sc.gitRepoPluginDisabled), func(t *testing.T) {
+			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.GitRepoVolumeDriver, !sc.gitRepoPluginDisabled)
+			allErrs := doTestPlugin(t, sc)
+			if len(allErrs) == 0 && sc.isExpectedFailure {
+				t.Errorf("Unexpected success for scenario: %s", sc.name)
+			}
+			if len(allErrs) > 0 && !sc.isExpectedFailure {
+				t.Errorf("Unexpected failure for scenario: %s - %+v", sc.name, allErrs)
+			}
+		})
+
 	}
 
 }
 
-func doTestPlugin(scenario struct {
-	name              string
-	vol               *v1.Volume
-	expecteds         []expectedCommand
-	isExpectedFailure bool
-}, t *testing.T) []error {
+func doTestPlugin(t *testing.T, sc scenario) []error {
 	allErrs := []error{}
 
 	plugMgr := volume.VolumePluginMgr{}
@@ -315,7 +457,7 @@ func doTestPlugin(scenario struct {
 		return allErrs
 	}
 	pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}}
-	mounter, err := plug.NewMounter(volume.NewSpecFromVolume(scenario.vol), pod)
+	mounter, err := plug.NewMounter(volume.NewSpecFromVolume(sc.vol), pod)
 
 	if err != nil {
 		allErrs = append(allErrs,
@@ -329,7 +471,7 @@ func doTestPlugin(scenario struct {
 	}
 
 	path := mounter.GetPath()
-	suffix := filepath.Join("pods/poduid/volumes/kubernetes.io~git-repo", scenario.vol.Name)
+	suffix := filepath.Join("pods/poduid/volumes/kubernetes.io~git-repo", sc.vol.Name)
 	if !strings.HasSuffix(path, suffix) {
 		allErrs = append(allErrs,
 			fmt.Errorf("got unexpected path: %s", path))
@@ -337,7 +479,7 @@ func doTestPlugin(scenario struct {
 	}
 
 	// Test setUp()
-	setUpErrs := doTestSetUp(scenario, mounter)
+	setUpErrs := doTestSetUp(sc, mounter)
 	allErrs = append(allErrs, setUpErrs...)
 
 	if _, err := os.Stat(path); err != nil {
@@ -353,7 +495,7 @@ func doTestPlugin(scenario struct {
 	}
 
 	// gitRepo volume should create its own empty wrapper path
-	podWrapperMetadataDir := fmt.Sprintf("%v/pods/poduid/plugins/kubernetes.io~empty-dir/wrapped_%v", rootDir, scenario.vol.Name)
+	podWrapperMetadataDir := fmt.Sprintf("%v/pods/poduid/plugins/kubernetes.io~empty-dir/wrapped_%v", rootDir, sc.vol.Name)
 
 	if _, err := os.Stat(podWrapperMetadataDir); err != nil {
 		if os.IsNotExist(err) {
@@ -392,13 +534,8 @@ func doTestPlugin(scenario struct {
 	return allErrs
 }
 
-func doTestSetUp(scenario struct {
-	name              string
-	vol               *v1.Volume
-	expecteds         []expectedCommand
-	isExpectedFailure bool
-}, mounter volume.Mounter) []error {
-	expecteds := scenario.expecteds
+func doTestSetUp(sc scenario, mounter volume.Mounter) []error {
+	expecteds := sc.expecteds
 	allErrs := []error{}
 
 	// Construct combined outputs from expected commands
diff --git a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml
index 0c9e20cc92475..8498d5da3277d 100644
--- a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml
+++ b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml
@@ -475,6 +475,16 @@
     lockToDefault: false
     preRelease: Alpha
     version: "1.32"
+- name: GitRepoVolumeDriver
+  versionedSpecs:
+  - default: true
+    lockToDefault: false
+    preRelease: GA
+    version: "1.0"
+  - default: false
+    lockToDefault: false
+    preRelease: Deprecated
+    version: "1.33"
 - name: GracefulNodeShutdown
   versionedSpecs:
   - default: false