diff --git a/.golangci.yml b/.golangci.yml
index 0cb83fc5046f..fa41d79de378 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -108,6 +108,9 @@ linters-settings:
         alias: addonsv1alpha4
       - pkg: sigs.k8s.io/cluster-api/exp/addons/api/v1beta1
         alias: addonsv1
+      # CAPI exp runtime
+      - pkg: sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1
+        alias: runtimev1
       # CAPD
       - pkg: sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1alpha3
         alias: infrav1alpha3
diff --git a/Makefile b/Makefile
index d4f625e18f52..b20bb56b865c 100644
--- a/Makefile
+++ b/Makefile
@@ -179,7 +179,7 @@ help:  # Display this help
 ALL_GENERATE_MODULES = core kubeadm-bootstrap kubeadm-control-plane
 
 .PHONY: generate
-generate: ## Run all generate-manifests-*, generate-go-deepcopy-* and generate-go-conversions-* targets
+generate: ## Run all generate-manifests-*, generate-go-deepcopy-*, generate-go-conversions-* targets
 	$(MAKE) generate-modules generate-manifests generate-go-deepcopy generate-go-conversions
 	$(MAKE) -C $(CAPD_DIR) generate
 
@@ -196,6 +196,7 @@ generate-manifests-core: $(CONTROLLER_GEN) $(KUSTOMIZE) ## Generate manifests e.
 		paths=./$(EXP_DIR)/internal/controllers/... \
 		paths=./$(EXP_DIR)/addons/api/... \
 		paths=./$(EXP_DIR)/addons/internal/controllers/... \
+		paths=./$(EXP_DIR)/runtime/api/... \
 		crd:crdVersions=v1 \
 		rbac:roleName=manager-role \
 		output:crd:dir=./config/crd/bases \
@@ -243,6 +244,7 @@ generate-go-deepcopy-core: $(CONTROLLER_GEN) ## Generate deepcopy go code for co
 		paths=./api/... \
 		paths=./$(EXP_DIR)/api/... \
 		paths=./$(EXP_DIR)/addons/api/... \
+		paths=./$(EXP_DIR)/runtime/api/... \
 		paths=./cmd/clusterctl/... \
 		paths=./internal/test/builder/...
 
@@ -799,7 +801,7 @@ $(CONVERSION_GEN): # Build conversion-gen from tools folder.
 	GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(CONVERSION_GEN_PKG) $(CONVERSION_GEN_BIN) $(CONVERSION_GEN_VER)
 
 $(CONVERSION_VERIFIER): $(TOOLS_DIR)/go.mod # Build conversion-verifier from tools folder.
-	cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/conversion-verifier sigs.k8s.io/cluster-api/hack/tools/conversion-verifier
+	cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/$(CONVERSION_VERIFIER_BIN) sigs.k8s.io/cluster-api/hack/tools/conversion-verifier
 
 $(GOTESTSUM): # Build gotestsum from tools folder.
 	GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GOTESTSUM_PKG) $(GOTESTSUM_BIN) $(GOTESTSUM_VER)
diff --git a/config/crd/bases/runtime.cluster.x-k8s.io_extensionconfigs.yaml b/config/crd/bases/runtime.cluster.x-k8s.io_extensionconfigs.yaml
new file mode 100644
index 000000000000..fb4fc882a542
--- /dev/null
+++ b/config/crd/bases/runtime.cluster.x-k8s.io_extensionconfigs.yaml
@@ -0,0 +1,248 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.8.0
+  creationTimestamp: null
+  name: extensionconfigs.runtime.cluster.x-k8s.io
+spec:
+  group: runtime.cluster.x-k8s.io
+  names:
+    categories:
+    - cluster-api
+    kind: ExtensionConfig
+    listKind: ExtensionConfigList
+    plural: extensionconfigs
+    shortNames:
+    - ext
+    singular: extensionconfig
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: Time duration since creation of ExtensionConfig
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: ExtensionConfig is the Schema for the ExtensionConfig API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: ExtensionConfigSpec is the desired state of the ExtensionConfig
+            properties:
+              clientConfig:
+                description: ClientConfig defines how to communicate with ExtensionHandlers.
+                properties:
+                  caBundle:
+                    description: CABundle is a PEM encoded CA bundle which will be
+                      used to validate the ExtensionHandler's server certificate.
+                    format: byte
+                    type: string
+                  service:
+                    description: "Service is a reference to the Kubernetes service
+                      for the ExtensionHandler. Either `service` or `url` must be
+                      specified. \n If the ExtensionHandler is running within a cluster,
+                      then you should use `service`."
+                    properties:
+                      name:
+                        description: Name is the name of the service.
+                        type: string
+                      namespace:
+                        description: Namespace is the namespace of the service.
+                        type: string
+                      path:
+                        description: Path is an optional URL path which will be sent
+                          in any request to this service. If a path is set it will
+                          be used as prefix and the hook-specific path will be appended.
+                        type: string
+                      port:
+                        description: Port is the port on the service that hosting
+                          ExtensionHandler. Default to 8443. `port` should be a valid
+                          port number (1-65535, inclusive).
+                        format: int32
+                        type: integer
+                    required:
+                    - name
+                    - namespace
+                    type: object
+                  url:
+                    description: "URL gives the location of the ExtensionHandler,
+                      in standard URL form (`scheme://host:port/path`). Exactly one
+                      of `url` or `service` must be specified. \n The `host` should
+                      not refer to a service running in the cluster; use the `service`
+                      field instead. \n The scheme should be \"https\"; the URL should
+                      begin with \"https://\". \"http\" is supported for insecure
+                      development purposes only. \n A path is optional, and if present
+                      may be any string permissible in a URL. If a path is set it
+                      will be used as prefix and the hook-specific path will be appended.
+                      \n Attempting to use a user or basic auth e.g. \"user:password@\"
+                      is not allowed. Fragments (\"#...\") and query parameters (\"?...\")
+                      are not allowed either."
+                    type: string
+                type: object
+              namespaceSelector:
+                description: NamespaceSelector decides whether to run the webhook
+                  on an object based on whether the namespace for that object matches
+                  the selector. Default to the empty LabelSelector, which matches
+                  everything.
+                properties:
+                  matchExpressions:
+                    description: matchExpressions is a list of label selector requirements.
+                      The requirements are ANDed.
+                    items:
+                      description: A label selector requirement is a selector that
+                        contains values, a key, and an operator that relates the key
+                        and values.
+                      properties:
+                        key:
+                          description: key is the label key that the selector applies
+                            to.
+                          type: string
+                        operator:
+                          description: operator represents a key's relationship to
+                            a set of values. Valid operators are In, NotIn, Exists
+                            and DoesNotExist.
+                          type: string
+                        values:
+                          description: values is an array of string values. If the
+                            operator is In or NotIn, the values array must be non-empty.
+                            If the operator is Exists or DoesNotExist, the values
+                            array must be empty. This array is replaced during a strategic
+                            merge patch.
+                          items:
+                            type: string
+                          type: array
+                      required:
+                      - key
+                      - operator
+                      type: object
+                    type: array
+                  matchLabels:
+                    additionalProperties:
+                      type: string
+                    description: matchLabels is a map of {key,value} pairs. A single
+                      {key,value} in the matchLabels map is equivalent to an element
+                      of matchExpressions, whose key field is "key", the operator
+                      is "In", and the values array contains only "value". The requirements
+                      are ANDed.
+                    type: object
+                type: object
+            required:
+            - clientConfig
+            type: object
+          status:
+            description: ExtensionConfigStatus is the current state of the ExtensionConfig
+            properties:
+              conditions:
+                description: Conditions define the current service state of the ExtensionConfig.
+                items:
+                  description: Condition defines an observation of a Cluster API resource
+                    operational state.
+                  properties:
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status
+                        to another. This should be when the underlying condition changed.
+                        If that is not known, then using the time when the API field
+                        changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: A human readable message indicating details about
+                        the transition. This field may be empty.
+                      type: string
+                    reason:
+                      description: The reason for the condition's last transition
+                        in CamelCase. The specific API may choose whether or not this
+                        field is considered a guaranteed API. This field may not be
+                        empty.
+                      type: string
+                    severity:
+                      description: Severity provides an explicit classification of
+                        Reason code, so the users or machines can immediately understand
+                        the current situation and act accordingly. The Severity field
+                        MUST be set only when Status=False.
+                      type: string
+                    status:
+                      description: Status of the condition, one of True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type of condition in CamelCase or in foo.example.com/CamelCase.
+                        Many .condition.type values are consistent across resources
+                        like Available, but because arbitrary conditions can be useful
+                        (see .node.status.conditions), the ability to deconflict is
+                        important.
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - status
+                  - type
+                  type: object
+                type: array
+              handlers:
+                description: Handlers defines the current ExtensionHandlers supported
+                  by an Extension.
+                items:
+                  description: ExtensionHandler specifies the details of a handler
+                    for a particular runtime hook registered by an Extension server.
+                  properties:
+                    failurePolicy:
+                      description: FailurePolicy defines how failures in calls to
+                        the ExtensionHandler should be handled by a client. Defaults
+                        to Fail if not set.
+                      type: string
+                    name:
+                      description: Name is the unique name of the ExtensionHandler.
+                      type: string
+                    requestHook:
+                      description: RequestHook defines the versioned runtime hook
+                        which this ExtensionHandler serves.
+                      properties:
+                        apiVersion:
+                          description: APIVersion is the Version of the Hook.
+                          type: string
+                        hook:
+                          description: Hook is the name of the hook.
+                          type: string
+                      required:
+                      - apiVersion
+                      - hook
+                      type: object
+                    timeoutSeconds:
+                      description: TimeoutSeconds defines the timeout duration for
+                        client calls to the ExtensionHandler.
+                      format: int32
+                      type: integer
+                  required:
+                  - name
+                  - requestHook
+                  type: object
+                type: array
+                x-kubernetes-list-map-keys:
+                - name
+                x-kubernetes-list-type: map
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml
index 9a928eca182b..8590dfa92ad1 100644
--- a/config/manager/manager.yaml
+++ b/config/manager/manager.yaml
@@ -22,7 +22,7 @@ spec:
         args:
         - "--leader-elect"
         - "--metrics-bind-addr=localhost:8080"
-        - "--feature-gates=MachinePool=${EXP_MACHINE_POOL:=false},ClusterResourceSet=${EXP_CLUSTER_RESOURCE_SET:=false},ClusterTopology=${CLUSTER_TOPOLOGY:=false}"
+        - "--feature-gates=MachinePool=${EXP_MACHINE_POOL:=false},ClusterResourceSet=${EXP_CLUSTER_RESOURCE_SET:=false},ClusterTopology=${CLUSTER_TOPOLOGY:=false},RuntimeSDK=${EXP_RUNTIME_SDK:=false}"
         image: controller:latest
         name: manager
         ports:
diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index 4e9b44e71ce9..08143916d7c0 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -159,6 +159,28 @@ webhooks:
     resources:
     - clusterclasses
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  - v1beta1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /mutate-runtime-cluster-x-k8s-io-v1beta1-extensionconfig
+  failurePolicy: Fail
+  matchPolicy: Equivalent
+  name: default.extensionconfig.runtime.addons.cluster.x-k8s.io
+  rules:
+  - apiGroups:
+    - runtime.cluster.x-k8s.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - extensionconfigs
+  sideEffects: None
 - admissionReviewVersions:
   - v1
   - v1beta1
@@ -344,6 +366,28 @@ webhooks:
     resources:
     - clusterclasses
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  - v1beta1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-runtime-cluster-x-k8s-io-v1beta1-extensionconfig
+  failurePolicy: Fail
+  matchPolicy: Equivalent
+  name: validation.extensionconfig.runtime.cluster.x-k8s.io
+  rules:
+  - apiGroups:
+    - runtime.cluster.x-k8s.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - extensionconfigs
+  sideEffects: None
 - admissionReviewVersions:
   - v1
   - v1beta1
diff --git a/exp/runtime/api/v1alpha1/doc.go b/exp/runtime/api/v1alpha1/doc.go
new file mode 100644
index 000000000000..d7778dadc9ba
--- /dev/null
+++ b/exp/runtime/api/v1alpha1/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package v1alpha1 contains the v1alpha1 implementation of ExtensionConfig.
+package v1alpha1
diff --git a/exp/runtime/api/v1alpha1/extensionconfig_types.go b/exp/runtime/api/v1alpha1/extensionconfig_types.go
new file mode 100644
index 000000000000..49829ec0a36d
--- /dev/null
+++ b/exp/runtime/api/v1alpha1/extensionconfig_types.go
@@ -0,0 +1,198 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+)
+
+// ANCHOR: ExtensionConfigSpec
+
+// ExtensionConfigSpec defines the desired state of ExtensionConfig.
+type ExtensionConfigSpec struct {
+	// ClientConfig defines how to communicate with ExtensionHandlers.
+	ClientConfig ClientConfig `json:"clientConfig"`
+
+	// NamespaceSelector decides whether to run the webhook on an object based
+	// on whether the namespace for that object matches the selector.
+	// Default to the empty LabelSelector, which matches everything.
+	// +optional
+	NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
+}
+
+// ClientConfig contains the information to make a client
+// connection with an ExtensionHandler.
+type ClientConfig struct {
+	// URL gives the location of the ExtensionHandler, in standard URL form
+	// (`scheme://host:port/path`). Exactly one of `url` or `service`
+	// must be specified.
+	//
+	// The `host` should not refer to a service running in the cluster; use
+	// the `service` field instead.
+	//
+	// The scheme should be "https"; the URL should begin with "https://".
+	// "http" is supported for insecure development purposes only.
+	//
+	// A path is optional, and if present may be any string permissible in
+	// a URL. If a path is set it will be used as prefix and the hook-specific
+	// path will be appended.
+	//
+	// Attempting to use a user or basic auth e.g. "user:password@" is not
+	// allowed. Fragments ("#...") and query parameters ("?...") are not
+	// allowed either.
+	//
+	// +optional
+	URL *string `json:"url,omitempty"`
+
+	// Service is a reference to the Kubernetes service for the ExtensionHandler.
+	// Either `service` or `url` must be specified.
+	//
+	// If the ExtensionHandler is running within a cluster, then you should use `service`.
+	//
+	// +optional
+	Service *ServiceReference `json:"service,omitempty"`
+
+	// CABundle is a PEM encoded CA bundle which will be used to validate the ExtensionHandler's server certificate.
+	// +optional
+	CABundle []byte `json:"caBundle,omitempty"`
+}
+
+// ServiceReference holds a reference to a Kubernetes Service.
+type ServiceReference struct {
+	// Namespace is the namespace of the service.
+	Namespace string `json:"namespace"`
+
+	// Name is the name of the service.
+	Name string `json:"name"`
+
+	// Path is an optional URL path which will be sent in any request to
+	// this service. If a path is set it will be used as prefix and the hook-specific
+	// path will be appended.
+	// +optional
+	Path *string `json:"path,omitempty"`
+
+	// Port is the port on the service that hosting ExtensionHandler.
+	// Default to 8443.
+	// `port` should be a valid port number (1-65535, inclusive).
+	// +optional
+	Port *int32 `json:"port,omitempty"`
+}
+
+// ANCHOR_END: ExtensionConfigSpec
+
+// ANCHOR: ExtensionConfigStatus
+
+// ExtensionConfigStatus defines the observed state of ExtensionConfig.
+type ExtensionConfigStatus struct {
+	// Handlers defines the current ExtensionHandlers supported by an Extension.
+	// +optional
+	// +listType=map
+	// +listMapKey=name
+	Handlers []ExtensionHandler `json:"handlers,omitempty"`
+
+	// Conditions define the current service state of the ExtensionConfig.
+	// +optional
+	Conditions clusterv1.Conditions `json:"conditions,omitempty"`
+}
+
+// ExtensionHandler specifies the details of a handler for a particular runtime hook registered by an Extension server.
+type ExtensionHandler struct {
+	// Name is the unique name of the ExtensionHandler.
+	Name string `json:"name"`
+
+	// RequestHook defines the versioned runtime hook which this ExtensionHandler serves.
+	RequestHook GroupVersionHook `json:"requestHook"`
+
+	// TimeoutSeconds defines the timeout duration for client calls to the ExtensionHandler.
+	// +optional
+	TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"`
+
+	// FailurePolicy defines how failures in calls to the ExtensionHandler should be handled by a client.
+	// Defaults to Fail if not set.
+	// +optional
+	FailurePolicy *FailurePolicy `json:"failurePolicy,omitempty"`
+}
+
+// GroupVersionHook defines the runtime hook when the ExtensionHandler is called.
+type GroupVersionHook struct {
+	// APIVersion is the Version of the Hook.
+	APIVersion string `json:"apiVersion"`
+
+	// Hook is the name of the hook.
+	Hook string `json:"hook"`
+}
+
+// FailurePolicy specifies a failure policy that defines how unrecognized errors from the admission endpoint are handled.
+type FailurePolicy string
+
+const (
+	// FailurePolicyIgnore means that an error calling the extension is ignored.
+	FailurePolicyIgnore FailurePolicy = "Ignore"
+
+	// FailurePolicyFail means that an error calling the extension is propagated as an error.
+	FailurePolicyFail FailurePolicy = "Fail"
+)
+
+// ANCHOR_END: ExtensionConfigStatus
+
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:path=extensionconfigs,shortName=ext,scope=Namespaced,categories=cluster-api
+// +kubebuilder:subresource:status
+// +kubebuilder:storageversion
+// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of ExtensionConfig"
+
+// ExtensionConfig is the Schema for the ExtensionConfig API.
+type ExtensionConfig struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	// ExtensionConfigSpec is the desired state of the ExtensionConfig
+	Spec ExtensionConfigSpec `json:"spec,omitempty"`
+
+	// ExtensionConfigStatus is the current state of the ExtensionConfig
+	Status ExtensionConfigStatus `json:"status,omitempty"`
+}
+
+// GetConditions returns the set of conditions for this object.
+func (e *ExtensionConfig) GetConditions() clusterv1.Conditions {
+	return e.Status.Conditions
+}
+
+// SetConditions sets the conditions on this object.
+func (e *ExtensionConfig) SetConditions(conditions clusterv1.Conditions) {
+	e.Status.Conditions = conditions
+}
+
+// +kubebuilder:object:root=true
+
+// ExtensionConfigList contains a list of ExtensionConfig.
+type ExtensionConfigList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []ExtensionConfig `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&ExtensionConfig{}, &ExtensionConfigList{})
+}
+
+const (
+	// RuntimeExtensionDiscovered is a condition set on an ExtensionConfig object once it has been discovered by the Runtime SDK client.
+	RuntimeExtensionDiscovered clusterv1.ConditionType = "Discovered"
+)
diff --git a/exp/runtime/api/v1alpha1/groupversion_info.go b/exp/runtime/api/v1alpha1/groupversion_info.go
new file mode 100644
index 000000000000..44ac3d706817
--- /dev/null
+++ b/exp/runtime/api/v1alpha1/groupversion_info.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// +kubebuilder:object:generate=true
+// +groupName=runtime.cluster.x-k8s.io
+
+package v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+	// GroupVersion is group version used to register these objects.
+	GroupVersion = schema.GroupVersion{Group: "runtime.cluster.x-k8s.io", Version: "v1beta1"}
+
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+	// AddToScheme adds the types in this group-version to the given scheme.
+	AddToScheme = SchemeBuilder.AddToScheme
+)
diff --git a/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go b/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 000000000000..a87475e6fb21
--- /dev/null
+++ b/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,233 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/cluster-api/api/v1beta1"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClientConfig) DeepCopyInto(out *ClientConfig) {
+	*out = *in
+	if in.URL != nil {
+		in, out := &in.URL, &out.URL
+		*out = new(string)
+		**out = **in
+	}
+	if in.Service != nil {
+		in, out := &in.Service, &out.Service
+		*out = new(ServiceReference)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.CABundle != nil {
+		in, out := &in.CABundle, &out.CABundle
+		*out = make([]byte, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientConfig.
+func (in *ClientConfig) DeepCopy() *ClientConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(ClientConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExtensionConfig) DeepCopyInto(out *ExtensionConfig) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConfig.
+func (in *ExtensionConfig) DeepCopy() *ExtensionConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(ExtensionConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ExtensionConfig) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExtensionConfigList) DeepCopyInto(out *ExtensionConfigList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]ExtensionConfig, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConfigList.
+func (in *ExtensionConfigList) DeepCopy() *ExtensionConfigList {
+	if in == nil {
+		return nil
+	}
+	out := new(ExtensionConfigList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ExtensionConfigList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExtensionConfigSpec) DeepCopyInto(out *ExtensionConfigSpec) {
+	*out = *in
+	in.ClientConfig.DeepCopyInto(&out.ClientConfig)
+	if in.NamespaceSelector != nil {
+		in, out := &in.NamespaceSelector, &out.NamespaceSelector
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConfigSpec.
+func (in *ExtensionConfigSpec) DeepCopy() *ExtensionConfigSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ExtensionConfigSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExtensionConfigStatus) DeepCopyInto(out *ExtensionConfigStatus) {
+	*out = *in
+	if in.Handlers != nil {
+		in, out := &in.Handlers, &out.Handlers
+		*out = make([]ExtensionHandler, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make(v1beta1.Conditions, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConfigStatus.
+func (in *ExtensionConfigStatus) DeepCopy() *ExtensionConfigStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(ExtensionConfigStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExtensionHandler) DeepCopyInto(out *ExtensionHandler) {
+	*out = *in
+	out.RequestHook = in.RequestHook
+	if in.TimeoutSeconds != nil {
+		in, out := &in.TimeoutSeconds, &out.TimeoutSeconds
+		*out = new(int32)
+		**out = **in
+	}
+	if in.FailurePolicy != nil {
+		in, out := &in.FailurePolicy, &out.FailurePolicy
+		*out = new(FailurePolicy)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionHandler.
+func (in *ExtensionHandler) DeepCopy() *ExtensionHandler {
+	if in == nil {
+		return nil
+	}
+	out := new(ExtensionHandler)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GroupVersionHook) DeepCopyInto(out *GroupVersionHook) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupVersionHook.
+func (in *GroupVersionHook) DeepCopy() *GroupVersionHook {
+	if in == nil {
+		return nil
+	}
+	out := new(GroupVersionHook)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServiceReference) DeepCopyInto(out *ServiceReference) {
+	*out = *in
+	if in.Path != nil {
+		in, out := &in.Path, &out.Path
+		*out = new(string)
+		**out = **in
+	}
+	if in.Port != nil {
+		in, out := &in.Port, &out.Port
+		*out = new(int32)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceReference.
+func (in *ServiceReference) DeepCopy() *ServiceReference {
+	if in == nil {
+		return nil
+	}
+	out := new(ServiceReference)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/feature/feature.go b/feature/feature.go
index 354e4bf40826..79ab03fc305d 100644
--- a/feature/feature.go
+++ b/feature/feature.go
@@ -45,6 +45,11 @@ const (
 	// alpha: v0.4
 	ClusterTopology featuregate.Feature = "ClusterTopology"
 
+	// RuntimeSDK is a feature gate for the Runtime hooks and extensions functionality.
+	//
+	// alpha: v1.2
+	RuntimeSDK featuregate.Feature = "RuntimeSDK"
+
 	// KubeadmBootstrapFormatIgnition is a feature gate for the Ignition bootstrap format
 	// functionality.
 	//
@@ -64,4 +69,5 @@ var defaultClusterAPIFeatureGates = map[featuregate.Feature]featuregate.FeatureS
 	ClusterResourceSet:             {Default: true, PreRelease: featuregate.Beta},
 	ClusterTopology:                {Default: false, PreRelease: featuregate.Alpha},
 	KubeadmBootstrapFormatIgnition: {Default: false, PreRelease: featuregate.Alpha},
+	RuntimeSDK:                     {Default: false, PreRelease: featuregate.Alpha},
 }
diff --git a/internal/webhooks/runtime/doc.go b/internal/webhooks/runtime/doc.go
new file mode 100644
index 000000000000..6bf9f066c8ef
--- /dev/null
+++ b/internal/webhooks/runtime/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package runtime contains the webhook implementation for runtime ExtensionConfig.
+package runtime
diff --git a/internal/webhooks/runtime/extensionconfig_webhook.go b/internal/webhooks/runtime/extensionconfig_webhook.go
new file mode 100644
index 000000000000..0d7e879210ff
--- /dev/null
+++ b/internal/webhooks/runtime/extensionconfig_webhook.go
@@ -0,0 +1,102 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package runtime
+
+import (
+	"context"
+	"fmt"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+
+	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
+	"sigs.k8s.io/cluster-api/feature"
+)
+
+// ExtensionConfig is the webhook for runtimev1.ExtensionConfig.
+type ExtensionConfig struct{}
+
+func (webhook *ExtensionConfig) SetupWebhookWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&runtimev1.ExtensionConfig{}).
+		WithDefaulter(webhook).
+		WithValidator(webhook).
+		Complete()
+}
+
+// +kubebuilder:webhook:verbs=create;update,path=/validate-runtime-cluster-x-k8s-io-v1beta1-extensionconfig,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=runtime.cluster.x-k8s.io,resources=extensionconfigs,versions=v1beta1,name=validation.extensionconfig.runtime.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
+// +kubebuilder:webhook:verbs=create;update,path=/mutate-runtime-cluster-x-k8s-io-v1beta1-extensionconfig,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=runtime.cluster.x-k8s.io,resources=extensionconfigs,versions=v1beta1,name=default.extensionconfig.runtime.addons.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
+
+var _ webhook.CustomValidator = &ExtensionConfig{}
+var _ webhook.CustomDefaulter = &ExtensionConfig{}
+
+// Default implements webhook.Defaulter so a webhook will be registered for the type.
+func (webhook *ExtensionConfig) Default(ctx context.Context, obj runtime.Object) error {
+	extensionConfig, ok := obj.(*runtimev1.ExtensionConfig)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", obj))
+	}
+	// Default NamespaceSelector to an empty LabelSelector, which matches everything, if not set.
+	if extensionConfig.Spec.NamespaceSelector == nil {
+		extensionConfig.Spec.NamespaceSelector = &metav1.LabelSelector{}
+	}
+	return nil
+}
+
+// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
+func (webhook *ExtensionConfig) ValidateCreate(ctx context.Context, obj runtime.Object) error {
+	extensionConfig, ok := obj.(*runtimev1.ExtensionConfig)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", obj))
+	}
+	return webhook.validate(ctx, nil, extensionConfig)
+}
+
+// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
+func (webhook *ExtensionConfig) ValidateUpdate(ctx context.Context, old, updated runtime.Object) error {
+	oldExtensionConfig, ok := old.(*runtimev1.ExtensionConfig)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", old))
+	}
+	newExtensionConfig, ok := updated.(*runtimev1.ExtensionConfig)
+	if !ok {
+		return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", updated))
+	}
+	return webhook.validate(ctx, oldExtensionConfig, newExtensionConfig)
+}
+
+// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
+func (webhook *ExtensionConfig) validate(_ context.Context, _, _ *runtimev1.ExtensionConfig) error {
+	// NOTE: ExtensionConfig is behind the RuntimeSDK feature gate flag; the web hook
+	// must prevent creating and updating objects in case the feature flag is disabled.
+	if !feature.Gates.Enabled(feature.RuntimeSDK) {
+		return field.Forbidden(
+			field.NewPath("spec"),
+			"can be set only if the RuntimeSDK feature flag is enabled",
+		)
+	}
+	return nil
+}
+
+// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
+func (webhook *ExtensionConfig) ValidateDelete(_ context.Context, _ runtime.Object) error {
+	return nil
+}
diff --git a/internal/webhooks/runtime/extensionconfig_webhook_test.go b/internal/webhooks/runtime/extensionconfig_webhook_test.go
new file mode 100644
index 000000000000..3e17dde9ae6c
--- /dev/null
+++ b/internal/webhooks/runtime/extensionconfig_webhook_test.go
@@ -0,0 +1,102 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package runtime
+
+import (
+	"context"
+	"testing"
+
+	. "github.com/onsi/gomega"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	utilfeature "k8s.io/component-base/featuregate/testing"
+	"k8s.io/utils/pointer"
+
+	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
+	"sigs.k8s.io/cluster-api/feature"
+)
+
+var (
+	fakeScheme = runtime.NewScheme()
+)
+
+func init() {
+	_ = runtimev1.AddToScheme(fakeScheme)
+}
+
+func TestExtensionConfigValidationFeatureGated(t *testing.T) {
+	extension := &runtimev1.ExtensionConfig{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-extension",
+		},
+		Spec: runtimev1.ExtensionConfigSpec{
+			ClientConfig: runtimev1.ClientConfig{
+				URL: pointer.String("https://extension-address.com"),
+			},
+		},
+	}
+	updatedExtension := extension.DeepCopy()
+	updatedExtension.Spec.ClientConfig.URL = pointer.StringPtr("https://a-new-extension-address.com")
+	tests := []struct {
+		name        string
+		new         *runtimev1.ExtensionConfig
+		old         *runtimev1.ExtensionConfig
+		featureGate bool
+		expectErr   bool
+	}{
+		{
+			name:        "creation should fail if feature flag is disabled",
+			new:         extension,
+			featureGate: false,
+			expectErr:   true,
+		},
+		{
+			name:        "update should fail if feature flag is disabled",
+			old:         extension,
+			new:         updatedExtension,
+			featureGate: false,
+			expectErr:   true,
+		},
+		{
+			name:        "creation should succeed if feature flag is enabled",
+			new:         extension,
+			featureGate: true,
+			expectErr:   false,
+		},
+		{
+			name:        "update should fail if feature flag is enabled",
+			old:         extension,
+			new:         updatedExtension,
+			featureGate: true,
+			expectErr:   false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, tt.featureGate)()
+			webhook := ExtensionConfig{}
+			g := NewWithT(t)
+			err := webhook.validate(context.TODO(), tt.old, tt.new)
+			if tt.expectErr {
+				g.Expect(err).To(HaveOccurred())
+				return
+			}
+			g.Expect(err).ToNot(HaveOccurred())
+		})
+	}
+}
diff --git a/main.go b/main.go
index 356e2596dd01..bae1953b25a1 100644
--- a/main.go
+++ b/main.go
@@ -55,7 +55,9 @@ import (
 	expv1alpha4 "sigs.k8s.io/cluster-api/exp/api/v1alpha4"
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
 	expcontrollers "sigs.k8s.io/cluster-api/exp/controllers"
+	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
 	"sigs.k8s.io/cluster-api/feature"
+	runtimev1webhooks "sigs.k8s.io/cluster-api/internal/webhooks/runtime"
 	"sigs.k8s.io/cluster-api/version"
 	"sigs.k8s.io/cluster-api/webhooks"
 )
@@ -105,6 +107,8 @@ func init() {
 	_ = addonsv1alpha4.AddToScheme(scheme)
 	_ = addonsv1.AddToScheme(scheme)
 
+	_ = runtimev1.AddToScheme(scheme)
+
 	// +kubebuilder:scaffold:scheme
 }
 
@@ -465,6 +469,13 @@ func setupWebhooks(mgr ctrl.Manager) {
 		setupLog.Error(err, "unable to create webhook", "webhook", "MachineHealthCheck")
 		os.Exit(1)
 	}
+
+	// NOTE: ExtensionConfig is behind the RuntimeSDK feature gate flag. The webhook will prevent creating or updating
+	// new objects if the feature flag is disabled.
+	if err := (&runtimev1webhooks.ExtensionConfig{}).SetupWebhookWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create webhook", "webhook", "ExtensionConfig")
+		os.Exit(1)
+	}
 }
 
 func concurrency(c int) controller.Options {
diff --git a/test/e2e/config/docker.yaml b/test/e2e/config/docker.yaml
index 8272eddbf93f..6598373f982d 100644
--- a/test/e2e/config/docker.yaml
+++ b/test/e2e/config/docker.yaml
@@ -219,6 +219,7 @@ variables:
   EXP_KUBEADM_BOOTSTRAP_FORMAT_IGNITION: "true"
   EXP_MACHINE_POOL: "true"
   CLUSTER_TOPOLOGY: "true"
+  EXP_RUNTIME_SDK: "true"
   # NOTE: INIT_WITH_BINARY and INIT_WITH_KUBERNETES_VERSION are only used by the clusterctl upgrade test to initialize
   # the management cluster to be upgraded.
   # NOTE: We test the latest release with a previous contract.