diff --git a/Makefile b/Makefile
index e248382..66affff 100644
--- a/Makefile
+++ b/Makefile
@@ -18,8 +18,11 @@ CONTAINER_REGISTRY ?= ghcr.io/kubestellar/kubeflex
 # latest tag
 LATEST_TAG ?= $(shell git describe --tags $(git rev-list --tags --max-count=1))
 
-# Image URL to use all building/pushing image targets
-IMG ?= ghcr.io/kubestellar/kubeflex/manager:latest
+KO_DOCKER_REPO ?= ko.local
+IMAGE_TAG ?= $(shell git rev-parse --short HEAD)
+CMD_NAME ?= manager
+IMG ?= ${KO_DOCKER_REPO}/${CMD_NAME}:${IMAGE_TAG}
+
 # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
 ENVTEST_K8S_VERSION = 1.26.1
 
@@ -195,6 +198,19 @@ chart: manifests kustomize
 	@mkdir -p chart/crds
 	$(KUSTOMIZE) build config/crd > chart/crds/crds.yaml
 
+.PHONY: ko-local-build
+ko-local-build:
+	KO_DOCKER_REPO=${KO_DOCKER_REPO} ko build -B ./cmd/${CMD_NAME} -t ${IMAGE_TAG} --platform linux/${ARCH}
+
+# this is used for local testing
+.PHONY: kind-load-image
+kind-load-image:
+	kind load docker-image ${IMG} --name kubeflex
+
+.PHONY: install-local-chart
+install-local-chart: chart kind-load-image
+	helm upgrade --install --create-namespace -n kubeflex-system kubeflex-operator ./chart
+
 ##@ Build Dependencies
 
 ## Location to install dependencies to
diff --git a/api/v1alpha1/conditions_test.go b/api/v1alpha1/conditions_test.go
index e7930c2..083a94c 100644
--- a/api/v1alpha1/conditions_test.go
+++ b/api/v1alpha1/conditions_test.go
@@ -99,5 +99,5 @@ func generateCondition(ctype ConditionType, reason ConditionReason, message stri
 }
 
 func addTime(t time.Duration) metav1.Time {
-	return metav1.NewTime(time.Now().Add(2 * time.Hour))
+	return metav1.NewTime(time.Now().Add(t))
 }
diff --git a/api/v1alpha1/controlplane_types.go b/api/v1alpha1/controlplane_types.go
index 1503e96..c45a943 100644
--- a/api/v1alpha1/controlplane_types.go
+++ b/api/v1alpha1/controlplane_types.go
@@ -22,10 +22,18 @@ import (
 
 // ControlPlaneSpec defines the desired state of ControlPlane
 type ControlPlaneSpec struct {
-	Type               ControlPlaneType  `json:"type,omitempty"`
-	Backend            BackendDBType     `json:"backend,omitempty"`
-	PostCreateHook     *string           `json:"postCreateHook,omitempty"`
-	PostCreateHookVars map[string]string `json:"postCreateHookVars,omitempty"`
+	Type    ControlPlaneType `json:"type,omitempty"`
+	Backend BackendDBType    `json:"backend,omitempty"`
+	// bootstrapSecretRef contains a reference to the kubeconfig used to bootstrap adoption of
+	// an external cluster
+	// +optional
+	BootstrapSecretRef *BootstrapSecretReference `json:"bootstrapSecretRef,omitempty"`
+	// tokenExpirationSeconds is the expiration time for generated auth token
+	// +optional
+	// +kubebuilder:default:=31536000
+	TokenExpirationSeconds *int64            `json:"tokenExpirationSeconds,omitempty"`
+	PostCreateHook         *string           `json:"postCreateHook,omitempty"`
+	PostCreateHookVars     map[string]string `json:"postCreateHookVars,omitempty"`
 }
 
 // ControlPlaneStatus defines the observed state of ControlPlane
@@ -71,7 +79,7 @@ const (
 	BackendDBTypeDedicated BackendDBType = "dedicated"
 )
 
-// +kubebuilder:validation:Enum=k8s;ocm;vcluster;host
+// +kubebuilder:validation:Enum=k8s;ocm;vcluster;host;external
 type ControlPlaneType string
 
 const (
@@ -79,6 +87,7 @@ const (
 	ControlPlaneTypeOCM      ControlPlaneType = "ocm"
 	ControlPlaneTypeVCluster ControlPlaneType = "vcluster"
 	ControlPlaneTypeHost     ControlPlaneType = "host"
+	ControlPlaneTypeExternal ControlPlaneType = "external"
 )
 
 // We do not use ObjectReference as its use is discouraged in favor of a locally defined type.
@@ -90,12 +99,25 @@ type SecretReference struct {
 	// `name` is the name of the secret.
 	// Required
 	Name string `json:"name"`
-	// Required
+	// +optional
 	Key string `json:"key"`
 	// Required
 	InClusterKey string `json:"inClusterKey"`
 }
 
+// We do not use ObjectReference as its use is discouraged in favor of a locally defined type.
+// See ObjectReference in https://github.com/kubernetes/api/blob/master/core/v1/types.go
+type BootstrapSecretReference struct {
+	// `namespace` is the namespace of the secret.
+	// Required
+	Namespace string `json:"namespace"`
+	// `name` is the name of the secret.
+	// Required
+	Name string `json:"name"`
+	// Required
+	InClusterKey string `json:"inClusterKey"`
+}
+
 func init() {
 	SchemeBuilder.Register(&ControlPlane{}, &ControlPlaneList{})
 }
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index fddaad0..bbbf0d8 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -24,6 +24,21 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BootstrapSecretReference) DeepCopyInto(out *BootstrapSecretReference) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapSecretReference.
+func (in *BootstrapSecretReference) DeepCopy() *BootstrapSecretReference {
+	if in == nil {
+		return nil
+	}
+	out := new(BootstrapSecretReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ControlPlane) DeepCopyInto(out *ControlPlane) {
 	*out = *in
@@ -103,6 +118,16 @@ func (in *ControlPlaneList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ControlPlaneSpec) DeepCopyInto(out *ControlPlaneSpec) {
 	*out = *in
+	if in.BootstrapSecretRef != nil {
+		in, out := &in.BootstrapSecretRef, &out.BootstrapSecretRef
+		*out = new(BootstrapSecretReference)
+		**out = **in
+	}
+	if in.TokenExpirationSeconds != nil {
+		in, out := &in.TokenExpirationSeconds, &out.TokenExpirationSeconds
+		*out = new(int64)
+		**out = **in
+	}
 	if in.PostCreateHook != nil {
 		in, out := &in.PostCreateHook, &out.PostCreateHook
 		*out = new(string)
diff --git a/chart/crds/crds.yaml b/chart/crds/crds.yaml
index 31dfefc..27fcd49 100644
--- a/chart/crds/crds.yaml
+++ b/chart/crds/crds.yaml
@@ -59,18 +59,48 @@ spec:
                 - shared
                 - dedicated
                 type: string
+              bootstrapSecretRef:
+                description: |-
+                  bootstrapSecretRef contains a reference to the kubeconfig used to bootstrap adoption of
+                  an external cluster
+                properties:
+                  inClusterKey:
+                    description: Required
+                    type: string
+                  name:
+                    description: |-
+                      `name` is the name of the secret.
+                      Required
+                    type: string
+                  namespace:
+                    description: |-
+                      `namespace` is the namespace of the secret.
+                      Required
+                    type: string
+                required:
+                - inClusterKey
+                - name
+                - namespace
+                type: object
               postCreateHook:
                 type: string
               postCreateHookVars:
                 additionalProperties:
                   type: string
                 type: object
+              tokenExpirationSeconds:
+                default: 31536000
+                description: tokenExpirationSeconds is the expiration time for generated
+                  auth token
+                format: int64
+                type: integer
               type:
                 enum:
                 - k8s
                 - ocm
                 - vcluster
                 - host
+                - external
                 type: string
             type: object
           status:
@@ -119,7 +149,6 @@ spec:
                     description: Required
                     type: string
                   key:
-                    description: Required
                     type: string
                   name:
                     description: |-
@@ -133,7 +162,6 @@ spec:
                     type: string
                 required:
                 - inClusterKey
-                - key
                 - name
                 - namespace
                 type: object
@@ -253,7 +281,6 @@ spec:
                     description: Required
                     type: string
                   key:
-                    description: Required
                     type: string
                   name:
                     description: |-
@@ -267,7 +294,6 @@ spec:
                     type: string
                 required:
                 - inClusterKey
-                - key
                 - name
                 - namespace
                 type: object
diff --git a/chart/templates/operator.yaml b/chart/templates/operator.yaml
index bac33d9..d0b5b90 100644
--- a/chart/templates/operator.yaml
+++ b/chart/templates/operator.yaml
@@ -591,7 +591,6 @@ spec:
         - --secure-listen-address=0.0.0.0:8443
         - --upstream=http://127.0.0.1:8080/
         - --logtostderr=true
-        - --v={{.Values.verbosity}}
         image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1
         name: kube-rbac-proxy
         ports:
@@ -614,12 +613,13 @@ spec:
         - --health-probe-bind-address=:8081
         - --metrics-bind-address=127.0.0.1:8080
         - --leader-elect
+        - --zap-log-level={{max (.Values.verbosity | default 2 | int) 1}}
         env:
         - name: HELM_CONFIG_HOME
           value: /tmp
         - name: HELM_CACHE_HOME
           value: /tmp
-        image: ghcr.io/kubestellar/kubeflex/manager:latest
+        image: to-be-replaced-by-ci
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
diff --git a/cmd/kflex/adopt/adopt.go b/cmd/kflex/adopt/adopt.go
new file mode 100644
index 0000000..32a284d
--- /dev/null
+++ b/cmd/kflex/adopt/adopt.go
@@ -0,0 +1,255 @@
+/*
+Copyright 2024 The KubeStellar 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 adopt
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/url"
+	"os"
+	"sync"
+
+	"path/filepath"
+
+	homedir "github.com/mitchellh/go-homedir"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/clientcmd/api"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1"
+	"github.com/kubestellar/kubeflex/cmd/kflex/common"
+	cont "github.com/kubestellar/kubeflex/cmd/kflex/ctx"
+	kfclient "github.com/kubestellar/kubeflex/pkg/client"
+	"github.com/kubestellar/kubeflex/pkg/util"
+)
+
+type CPAdopt struct {
+	common.CP
+	AdoptedKubeconfig             string
+	AdoptedContext                string
+	AdoptedURLOverride            string
+	AdoptedTokenExpirationSeconds int64
+}
+
+// Adopt a control plane from another cluster
+func (c *CPAdopt) Adopt(hook string, hookVars []string, chattyStatus bool) {
+	done := make(chan bool)
+	var wg sync.WaitGroup
+	cx := cont.CPCtx{}
+	cx.Context(chattyStatus, false, false, false)
+
+	controlPlaneType := tenancyv1alpha1.ControlPlaneTypeExternal
+	util.PrintStatus(fmt.Sprintf("Adopting control plane %s of type %s ...", c.Name, controlPlaneType), done, &wg, chattyStatus)
+
+	bootstrapKubeconfig := getBootstrapKubeconfig(c)
+
+	cl, err := kfclient.GetClient(c.Kubeconfig)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error getting kubeflex client: %v\n", err)
+		os.Exit(1)
+	}
+
+	clientsetp, err := kfclient.GetClientSet(c.Kubeconfig)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error getting clientset: %v\n", err)
+		os.Exit(1)
+	}
+
+	if err := applyAdoptedBootstrapSecret(clientsetp, c.Name, bootstrapKubeconfig, c.AdoptedContext, c.AdoptedURLOverride); err != nil {
+		fmt.Fprintf(os.Stderr, "error creating adopted cluster kubeconfig: %v\n", err)
+		os.Exit(1)
+	}
+
+	cp, err := common.GenerateControlPlane(c.Name, string(controlPlaneType), "", hook, hookVars, &c.AdoptedTokenExpirationSeconds)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "error generating control plane object: %v\n", err)
+		os.Exit(1)
+	}
+
+	if err := cl.Create(context.TODO(), cp, &client.CreateOptions{}); err != nil {
+		fmt.Fprintf(os.Stderr, "Error creating ControlPlane object: %v\n", err)
+		os.Exit(1)
+	}
+
+	done <- true
+	wg.Wait()
+}
+
+func applyAdoptedBootstrapSecret(clientset *kubernetes.Clientset, cpName, bootstrapKubeconfig, contextName, adoptedURLOverride string) error {
+	// Load the kubeconfig from file
+	config, err := clientcmd.LoadFromFile(bootstrapKubeconfig)
+	if err != nil {
+		return fmt.Errorf("failed to load kubeconfig file %s: %w", bootstrapKubeconfig, err)
+	}
+
+	// Retrieve the specified context
+	context, exists := config.Contexts[contextName]
+	if !exists {
+		return fmt.Errorf("context %s not found in the kubeconfig", contextName)
+	}
+
+	// Retrieve the associated cluster
+	cluster, exists := config.Clusters[context.Cluster]
+	if !exists {
+		return fmt.Errorf("cluster %s not found for context %s", context.Cluster, contextName)
+	}
+
+	// Construct a new kubeConfig object
+	kubeConfig := api.NewConfig()
+
+	kubeConfig.Clusters[context.Cluster] = cluster
+
+	kubeConfig.Clusters[context.Cluster].Server = cluster.Server
+
+	if adoptedURLOverride != "" {
+		if err := isValidServerURL(adoptedURLOverride); err != nil {
+			return fmt.Errorf("invalid server endpoint %s. Please provide a valid value with the `url-override` option", adoptedURLOverride)
+		}
+		kubeConfig.Clusters[context.Cluster].Server = adoptedURLOverride
+	}
+
+	if authInfo, exists := config.AuthInfos[context.AuthInfo]; exists {
+		kubeConfig.AuthInfos[contextName] = authInfo
+	} else {
+		return fmt.Errorf("authInfo %s not found for context %s", context.AuthInfo, contextName)
+	}
+
+	kubeConfig.Contexts[contextName] = &api.Context{
+		Cluster:    context.Cluster,
+		AuthInfo:   contextName,
+		Extensions: context.Extensions,
+		Namespace:  context.Namespace,
+	}
+	kubeConfig.CurrentContext = contextName
+
+	newKubeConfig, err := clientcmd.Write(*kubeConfig)
+	if err != nil {
+		return fmt.Errorf("failed to serialize the new kubeconfig: %w", err)
+	}
+
+	return createOrUpdateSecret(clientset, cpName, newKubeConfig)
+}
+
+func createOrUpdateSecret(clientset *kubernetes.Clientset, cpName string, kubeconfig []byte) error {
+
+	bootstrapSecretName := util.GenerateBootstrapSecretName(cpName)
+
+	// Define the kubeconfig secret
+	kubeConfigSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      bootstrapSecretName,
+			Namespace: util.SystemNamespace,
+		},
+		Type: corev1.SecretTypeOpaque,
+		Data: map[string][]byte{util.KubeconfigSecretKeyInCluster: kubeconfig},
+	}
+
+	// Try to create the secret
+	if _, err := clientset.CoreV1().Secrets(util.SystemNamespace).Create(context.TODO(), kubeConfigSecret, metav1.CreateOptions{}); err != nil {
+		// Check if the error is because the secret already exists
+		if apierrors.IsAlreadyExists(err) {
+			// Retrieve the existing secret
+			existingSecret, getErr := clientset.CoreV1().Secrets(util.SystemNamespace).Get(context.TODO(), bootstrapSecretName, metav1.GetOptions{})
+			if getErr != nil {
+				return fmt.Errorf("failed to fetch existing secret %s in namespace %s: %w", util.AdminConfSecret, bootstrapSecretName, getErr)
+			}
+
+			// Update the data of the existing secret
+			existingSecret.Data = kubeConfigSecret.Data
+
+			// Update the secret with new data
+			if _, updateErr := clientset.CoreV1().Secrets(util.SystemNamespace).Update(context.TODO(), existingSecret, metav1.UpdateOptions{}); updateErr != nil {
+				return fmt.Errorf("failed to update existing secret %s in namespace %s: %w", bootstrapSecretName, util.SystemNamespace, updateErr)
+			}
+		} else {
+			return fmt.Errorf("failed to create secret %s in namespace %s: %w", bootstrapSecretName, util.SystemNamespace, err)
+		}
+	}
+
+	return nil
+}
+
+// check if the current server URL in the adopted cluster kubeconfig is a valid URL
+// and it not using a local address, which would not work in a container
+func isValidServerURL(serverURL string) error {
+	// Parse the URL
+	parsedURL, err := url.Parse(serverURL)
+	if err != nil {
+		return fmt.Errorf("invalid URL: %w", err)
+	}
+
+	// Ensure the URL scheme is either http or https
+	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
+		return errors.New("URL must start with http:// or https://")
+	}
+
+	// Ensure the host is non-empty
+	if parsedURL.Host == "" {
+		return errors.New("URL must have a host part")
+	}
+
+	// Reject URLs with user information (i.e., username or password)
+	if parsedURL.User != nil {
+		return errors.New("URL must not contain user info")
+	}
+
+	// Reject URLs containing query parameters
+	if parsedURL.RawQuery != "" {
+		return errors.New("URL must not contain query parameters")
+	}
+
+	// Reject URLs containing fragments
+	if parsedURL.Fragment != "" {
+		return errors.New("URL must not contain fragments")
+	}
+
+	localAddresses := []string{"127.0.0.1", "localhost", "::1"}
+	for _, addr := range localAddresses {
+		if parsedURL.Host == addr {
+			return fmt.Errorf("URL must not use addresses in %v", localAddresses)
+		}
+	}
+	return nil
+}
+
+func getBootstrapKubeconfig(c *CPAdopt) string {
+	if c.AdoptedKubeconfig != "" {
+		return c.AdoptedKubeconfig
+	}
+	if c.Kubeconfig != "" {
+		return c.Kubeconfig
+	}
+	return getKubeConfigFromEnv()
+}
+
+func getKubeConfigFromEnv() string {
+	kubeconfig := os.Getenv("KUBECONFIG")
+	if kubeconfig == "" {
+		home, err := homedir.Dir()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Error finding home directory: %v\n", err)
+			os.Exit(1)
+		}
+		kubeconfig = filepath.Join(home, ".kube", "config")
+	}
+	return kubeconfig
+}
diff --git a/cmd/kflex/common/cp.go b/cmd/kflex/common/cp.go
index c3097db..d3975b0 100644
--- a/cmd/kflex/common/cp.go
+++ b/cmd/kflex/common/cp.go
@@ -18,6 +18,13 @@ package common
 
 import (
 	"context"
+	"fmt"
+	"strings"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1"
+	"github.com/kubestellar/kubeflex/pkg/util"
 )
 
 type CP struct {
@@ -25,3 +32,60 @@ type CP struct {
 	Kubeconfig string
 	Name       string
 }
+
+func GenerateControlPlane(name, controlPlaneType, backendType, hook string, hookVars []string, tokenExpirationSeconds *int64) (*tenancyv1alpha1.ControlPlane, error) {
+	cp := &tenancyv1alpha1.ControlPlane{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+		Spec: tenancyv1alpha1.ControlPlaneSpec{
+			Type:                   tenancyv1alpha1.ControlPlaneType(controlPlaneType),
+			Backend:                tenancyv1alpha1.BackendDBType(backendType),
+			TokenExpirationSeconds: tokenExpirationSeconds,
+		},
+	}
+	if hook != "" {
+		cp.Spec.PostCreateHook = &hook
+		var err error
+		cp.Spec.PostCreateHookVars, err = convertToMap(hookVars)
+		if err != nil {
+			return nil, err
+		}
+	}
+	if controlPlaneType == string(tenancyv1alpha1.ControlPlaneTypeExternal) {
+		cp.Spec.BootstrapSecretRef = &tenancyv1alpha1.BootstrapSecretReference{
+			Name:         util.GenerateBootstrapSecretName(name),
+			Namespace:    util.SystemNamespace,
+			InClusterKey: util.KubeconfigSecretKeyInCluster,
+		}
+	}
+	return cp, nil
+}
+
+func convertToMap(pairs []string) (map[string]string, error) {
+	params := make(map[string]string)
+
+	for _, pair := range pairs {
+		// Ensure the pair is not empty
+		if pair == "" {
+			continue
+		}
+
+		// Split the pair into key and value using "=" as the delimiter
+		split := strings.SplitN(pair, "=", 2)
+		if len(split) != 2 {
+			return nil, fmt.Errorf("unexpected expression %q. Must be in the form 'key=value'", pair)
+		}
+
+		key := strings.TrimSpace(split[0])
+		value := strings.TrimSpace(split[1])
+
+		if key == "" {
+			return nil, fmt.Errorf("invalid key in expression %q", pair)
+		}
+
+		params[key] = value
+	}
+
+	return params, nil
+}
diff --git a/cmd/kflex/create/create.go b/cmd/kflex/create/create.go
index 3866cc7..5bf5a5d 100644
--- a/cmd/kflex/create/create.go
+++ b/cmd/kflex/create/create.go
@@ -20,11 +20,9 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"strings"
 	"sync"
 
 	tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/kubestellar/kubeflex/cmd/kflex/common"
@@ -45,14 +43,17 @@ func (c *CPCreate) Create(controlPlaneType, backendType, hook string, hookVars [
 	cx := cont.CPCtx{}
 	cx.Context(chattyStatus, false, false, false)
 
-	clp, err := kfclient.GetClient(c.Kubeconfig)
+	cl, err := kfclient.GetClient(c.Kubeconfig)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Error getting client: %v\n", err)
 		os.Exit(1)
 	}
-	cl := *clp
 
-	cp := c.generateControlPlane(controlPlaneType, backendType, hook, hookVars)
+	cp, err := common.GenerateControlPlane(c.Name, controlPlaneType, backendType, hook, hookVars, nil)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "error generating control plane object: %v\n", err)
+		os.Exit(1)
+	}
 
 	util.PrintStatus(fmt.Sprintf("Creating new control plane %s of type %s ...", c.Name, controlPlaneType), done, &wg, chattyStatus)
 	if err := cl.Create(context.TODO(), cp, &client.CreateOptions{}); err != nil {
@@ -104,33 +105,3 @@ func (c *CPCreate) Create(controlPlaneType, backendType, hook string, hookVars [
 
 	wg.Wait()
 }
-
-func (c *CPCreate) generateControlPlane(controlPlaneType, backendType, hook string, hookVars []string) *tenancyv1alpha1.ControlPlane {
-	cp := &tenancyv1alpha1.ControlPlane{
-		ObjectMeta: v1.ObjectMeta{
-			Name: c.Name,
-		},
-		Spec: tenancyv1alpha1.ControlPlaneSpec{
-			Type:    tenancyv1alpha1.ControlPlaneType(controlPlaneType),
-			Backend: tenancyv1alpha1.BackendDBType(backendType),
-		},
-	}
-	if hook != "" {
-		cp.Spec.PostCreateHook = &hook
-		cp.Spec.PostCreateHookVars = convertToMap(hookVars)
-	}
-	return cp
-}
-
-func convertToMap(pairs []string) map[string]string {
-	params := make(map[string]string)
-
-	for _, pair := range pairs {
-		split := strings.SplitN(pair, "=", 2)
-		if len(split) == 2 {
-			params[split[0]] = split[1]
-		}
-	}
-
-	return params
-}
diff --git a/cmd/kflex/ctx/ctx.go b/cmd/kflex/ctx/ctx.go
index 138696c..7e37845 100644
--- a/cmd/kflex/ctx/ctx.go
+++ b/cmd/kflex/ctx/ctx.go
@@ -29,7 +29,7 @@ import (
 	kfclient "github.com/kubestellar/kubeflex/pkg/client"
 	"github.com/kubestellar/kubeflex/pkg/kubeconfig"
 	"github.com/kubestellar/kubeflex/pkg/util"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/tools/clientcmd/api"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
@@ -124,15 +124,14 @@ func (c *CPCtx) Context(chattyStatus, failIfNone, overwriteExistingCtx, setCurre
 }
 
 func (c *CPCtx) loadAndMergeFromServer(kconfig *api.Config) error {
-	kfcClientp, err := kfclient.GetClient(c.Kubeconfig)
+	kfcClient, err := kfclient.GetClient(c.Kubeconfig)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Error getting kf client: %s\n", err)
 		os.Exit(1)
 	}
-	kfcClient := *kfcClientp
 
 	cp := &tenancyv1alpha1.ControlPlane{
-		ObjectMeta: v1.ObjectMeta{
+		ObjectMeta: metav1.ObjectMeta{
 			Name: c.CP.Name,
 		},
 	}
diff --git a/cmd/kflex/delete/delete.go b/cmd/kflex/delete/delete.go
index fa4188c..cf68e52 100644
--- a/cmd/kflex/delete/delete.go
+++ b/cmd/kflex/delete/delete.go
@@ -23,7 +23,7 @@ import (
 	"sync"
 
 	tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -39,7 +39,11 @@ type CPDelete struct {
 
 func (c *CPDelete) Delete(chattyStatus bool) {
 	done := make(chan bool)
-	cp := c.generateControlPlane()
+	cp := &tenancyv1alpha1.ControlPlane{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: c.Name,
+		},
+	}
 	var wg sync.WaitGroup
 
 	util.PrintStatus(fmt.Sprintf("Deleting control plane %s...", c.Name), done, &wg, chattyStatus)
@@ -63,12 +67,11 @@ func (c *CPDelete) Delete(chattyStatus bool) {
 		os.Exit(1)
 	}
 
-	kfcClientp, err := kfclient.GetClient(c.Kubeconfig)
+	kfcClient, err := kfclient.GetClient(c.Kubeconfig)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Error getting kf client: %s\n", err)
 		os.Exit(1)
 	}
-	kfcClient := *kfcClientp
 
 	if err := kfcClient.Delete(context.TODO(), cp, &client.DeleteOptions{}); err != nil {
 		fmt.Fprintf(os.Stderr, "Error deleting instance: %s\n", err)
@@ -88,11 +91,3 @@ func (c *CPDelete) Delete(chattyStatus bool) {
 	done <- true
 	wg.Wait()
 }
-
-func (c *CPDelete) generateControlPlane() *tenancyv1alpha1.ControlPlane {
-	return &tenancyv1alpha1.ControlPlane{
-		ObjectMeta: v1.ObjectMeta{
-			Name: c.Name,
-		},
-	}
-}
diff --git a/cmd/kflex/main.go b/cmd/kflex/main.go
index af54f87..cc1b87b 100644
--- a/cmd/kflex/main.go
+++ b/cmd/kflex/main.go
@@ -26,6 +26,7 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/go-logr/zapr"
 	tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1"
+	"github.com/kubestellar/kubeflex/cmd/kflex/adopt"
 	"github.com/kubestellar/kubeflex/cmd/kflex/common"
 	cr "github.com/kubestellar/kubeflex/cmd/kflex/create"
 	cont "github.com/kubestellar/kubeflex/cmd/kflex/ctx"
@@ -53,6 +54,10 @@ var hookVars []string
 var hostContainer string
 var overwriteExistingCtx bool
 var setCurrentCtxAsHosting bool
+var adoptedKubeconfig string
+var adoptedContext string
+var adoptedURLOverride string
+var adoptedTokenExpirationSeconds int64
 
 // defaults
 const BKTypeDefault = string(tenancyv1alpha1.BackendDBTypeShared)
@@ -116,7 +121,7 @@ var initCmd = &cobra.Command{
 }
 
 var createCmd = &cobra.Command{
-	Use:   "create",
+	Use:   "create <name>",
 	Short: "Create a control plane instance",
 	Long: `Create a control plane instance and switches the Kubeconfig context to
 	        the current instance`,
@@ -140,8 +145,31 @@ var createCmd = &cobra.Command{
 	},
 }
 
+var adoptCmd = &cobra.Command{
+	Use:   "adopt <name>",
+	Short: "Adopt a control plane from an external cluster",
+	Long: `Adopt a control plane from an external cluster and switches the Kubeconfig context to
+	        the current instance`,
+	Args: cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		cp := adopt.CPAdopt{
+			CP: common.CP{
+				Ctx:        createContext(),
+				Name:       args[0],
+				Kubeconfig: kubeconfig,
+			},
+			AdoptedKubeconfig:             adoptedKubeconfig,
+			AdoptedContext:                adoptedContext,
+			AdoptedURLOverride:            adoptedURLOverride,
+			AdoptedTokenExpirationSeconds: adoptedTokenExpirationSeconds,
+		}
+		// create passing the control plane type and backend type
+		cp.Adopt(Hook, hookVars, chattyStatus)
+	},
+}
+
 var deleteCmd = &cobra.Command{
-	Use:   "delete",
+	Use:   "delete <name>",
 	Short: "Delete a control plane instance",
 	Long: `Delete a control plane instance and switches the context back to
 	        the hosting cluster context`,
@@ -182,10 +210,10 @@ var ctxCmd = &cobra.Command{
 }
 
 func init() {
-	versionCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to kubeconfig file")
+	versionCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to the kubeconfig file for the KubeFlex hosting cluster. If not specified, and $KUBECONFIG is set, it uses the value in $KUBECONFIG, otherwise it falls back to ${HOME}/.kube/configg")
 	versionCmd.Flags().BoolVarP(&chattyStatus, "chatty-status", "s", true, "chatty status indicator")
 
-	initCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to kubeconfig file")
+	initCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to the kubeconfig file for the KubeFlex hosting cluster. If not specified, and $KUBECONFIG is set, it uses the value in $KUBECONFIG, otherwise it falls back to ${HOME}/.kube/config")
 	initCmd.Flags().IntVarP(&verbosity, "verbosity", "v", 0, "log level") // TODO - figure out how to inject verbosity
 	initCmd.Flags().BoolVarP(&createkind, "create-kind", "c", false, "Create and configure a kind cluster for installing Kubeflex")
 	initCmd.Flags().StringVarP(&domain, "domain", "d", "localtest.me", "domain for FQDN")
@@ -193,7 +221,7 @@ func init() {
 	initCmd.Flags().IntVarP(&externalPort, "externalPort", "p", 9443, "external port used by ingress")
 	initCmd.Flags().BoolVarP(&chattyStatus, "chatty-status", "s", true, "chatty status indicator")
 
-	createCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to kubeconfig file")
+	createCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to the kubeconfig file for the KubeFlex hosting cluster. If not specified, it defaults to the value set in $KUBECONFIG, and if $KUBECONFIG is not set, it falls back to ${HOME}/.kube/config.")
 	createCmd.Flags().IntVarP(&verbosity, "verbosity", "v", 0, "log level") // TODO - figure out how to inject verbosity
 	createCmd.Flags().StringVarP(&CType, "type", "t", "", "type of control plane: k8s|ocm|vcluster")
 	createCmd.Flags().StringVarP(&BkType, "backend-type", "b", "", "backend DB sharing: shared|dedicated")
@@ -201,11 +229,21 @@ func init() {
 	createCmd.Flags().BoolVarP(&chattyStatus, "chatty-status", "s", true, "chatty status indicator")
 	createCmd.Flags().StringArrayVarP(&hookVars, "set", "e", []string{}, "set post create hook variables, in the form name=value ")
 
-	deleteCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to kubeconfig file")
+	adoptCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to the kubeconfig file for the KubeFlex hosting cluster. If not specified, it defaults to the value set in $KUBECONFIG, and if $KUBECONFIG is not set, it falls back to ${HOME}/.kube/config.")
+	adoptCmd.Flags().IntVarP(&verbosity, "verbosity", "v", 0, "log level") // TODO - figure out how to inject verbosity
+	adoptCmd.Flags().StringVarP(&Hook, "postcreate-hook", "p", "", "name of post create hook to run")
+	adoptCmd.Flags().BoolVarP(&chattyStatus, "chatty-status", "s", true, "chatty status indicator")
+	adoptCmd.Flags().StringArrayVarP(&hookVars, "set", "e", []string{}, "set post create hook variables, in the form name=value ")
+	adoptCmd.Flags().StringVarP(&adoptedKubeconfig, "adopted-kubeconfig", "a", "", "path to the kubeconfig file for the adopted cluster. If unspecified, it uses the default Kubeconfig")
+	adoptCmd.Flags().StringVarP(&adoptedContext, "adopted-context", "c", "", "path to adopted cluster context in adopted kubeconfig")
+	adoptCmd.Flags().StringVarP(&adoptedURLOverride, "url-override", "u", "", "URL overrride for adopted cluster. Required when cluster address uses local host address, e.g. `https://127.0.0.1` ")
+	adoptCmd.Flags().Int64VarP(&adoptedTokenExpirationSeconds, "expiration-seconds", "x", 86400*365, "adopted token expiration in seconds. Default is one year.")
+
+	deleteCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to the kubeconfig file for the KubeFlex hosting cluster. If not specified, it defaults to the value set in $KUBECONFIG, and if $KUBECONFIG is not set, it falls back to ${HOME}/.kube/config.")
 	deleteCmd.Flags().IntVarP(&verbosity, "verbosity", "v", 0, "log level") // TODO - figure out how to inject verbosity
 	deleteCmd.Flags().BoolVarP(&chattyStatus, "chatty-status", "s", true, "chatty status indicator")
 
-	ctxCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to kubeconfig file")
+	ctxCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "path to the kubeconfig file for the KubeFlex hosting cluster. If not specified, it defaults to the value set in $KUBECONFIG, and if $KUBECONFIG is not set, it falls back to ${HOME}/.kube/config.")
 	ctxCmd.Flags().IntVarP(&verbosity, "verbosity", "v", 0, "log level") // TODO - figure out how to inject verbosity
 	ctxCmd.Flags().BoolVarP(&chattyStatus, "chatty-status", "s", true, "chatty status indicator")
 	ctxCmd.Flags().BoolVarP(&overwriteExistingCtx, "overwrite-existing-context", "o", false, "Overwrite of hosting cluster context with new control plane context")
@@ -214,6 +252,7 @@ func init() {
 	rootCmd.AddCommand(versionCmd)
 	rootCmd.AddCommand(initCmd)
 	rootCmd.AddCommand(createCmd)
+	rootCmd.AddCommand(adoptCmd)
 	rootCmd.AddCommand(deleteCmd)
 	rootCmd.AddCommand(ctxCmd)
 }
diff --git a/config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml b/config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml
index a7b30a4..8904b4c 100644
--- a/config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml
+++ b/config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml
@@ -60,18 +60,48 @@ spec:
                 - shared
                 - dedicated
                 type: string
+              bootstrapSecretRef:
+                description: |-
+                  bootstrapSecretRef contains a reference to the kubeconfig used to bootstrap adoption of
+                  an external cluster
+                properties:
+                  inClusterKey:
+                    description: Required
+                    type: string
+                  name:
+                    description: |-
+                      `name` is the name of the secret.
+                      Required
+                    type: string
+                  namespace:
+                    description: |-
+                      `namespace` is the namespace of the secret.
+                      Required
+                    type: string
+                required:
+                - inClusterKey
+                - name
+                - namespace
+                type: object
               postCreateHook:
                 type: string
               postCreateHookVars:
                 additionalProperties:
                   type: string
                 type: object
+              tokenExpirationSeconds:
+                default: 31536000
+                description: tokenExpirationSeconds is the expiration time for generated
+                  auth token
+                format: int64
+                type: integer
               type:
                 enum:
                 - k8s
                 - ocm
                 - vcluster
                 - host
+                - external
                 type: string
             type: object
           status:
@@ -120,7 +150,6 @@ spec:
                     description: Required
                     type: string
                   key:
-                    description: Required
                     type: string
                   name:
                     description: |-
@@ -134,7 +163,6 @@ spec:
                     type: string
                 required:
                 - inClusterKey
-                - key
                 - name
                 - namespace
                 type: object
diff --git a/config/crd/bases/tenancy.kflex.kubestellar.org_postcreatehooks.yaml b/config/crd/bases/tenancy.kflex.kubestellar.org_postcreatehooks.yaml
index 75cebae..448e71b 100644
--- a/config/crd/bases/tenancy.kflex.kubestellar.org_postcreatehooks.yaml
+++ b/config/crd/bases/tenancy.kflex.kubestellar.org_postcreatehooks.yaml
@@ -105,7 +105,6 @@ spec:
                     description: Required
                     type: string
                   key:
-                    description: Required
                     type: string
                   name:
                     description: |-
@@ -119,7 +118,6 @@ spec:
                     type: string
                 required:
                 - inClusterKey
-                - key
                 - name
                 - namespace
                 type: object
diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml
index 6c662d8..0898972 100644
--- a/config/default/manager_auth_proxy_patch.yaml
+++ b/config/default/manager_auth_proxy_patch.yaml
@@ -46,7 +46,6 @@ spec:
         - "--secure-listen-address=0.0.0.0:8443"
         - "--upstream=http://127.0.0.1:8080/"
         - "--logtostderr=true"
-        - "--v=0"
         ports:
         - containerPort: 8443
           protocol: TCP
diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml
index 9fc6767..a3ac480 100644
--- a/config/manager/kustomization.yaml
+++ b/config/manager/kustomization.yaml
@@ -5,5 +5,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 images:
 - name: controller
-  newName: ghcr.io/kubestellar/kubeflex/manager
-  newTag: latest
+  newName: ko.local/manager
+  newTag: d80da22
diff --git a/docs/users.md b/docs/users.md
index 40ff969..9aa777e 100644
--- a/docs/users.md
+++ b/docs/users.md
@@ -280,6 +280,7 @@ clusters registration and support for the [`ManifestWork` API](https://open-clus
 - vcluster: this is based on the [vcluster project](https://www.vcluster.com) and provides the ability to create pods in the hosting namespace of the hosting cluster.
 - host: this control plane type exposes the underlying hosting cluster with the same control plane abstraction
 used by the other control plane types.
+- external: this control plane type represents an existing cluster that was not created by KubeFlex and is not the KubeFlex hosting cluster.
 
 ## Control Plane Backends
 
@@ -314,6 +315,82 @@ To create a control plane of type `host` run the command:
 kflex create cp4 --type host
 ```
 
+To create a control plane of type `external` with the required options, run the command: 
+
+```shell
+kflex adopt --adopted-context <kubeconfig-context-of-external-cluster> cp5
+```
+
+*Important*: This command generates a secret containing a long-lived token for accessing 
+the external cluster within the namespace associated with the control plane. The secret is automatically 
+removed when the associated control plane is deleted.
+
+### Creating a control plane of type `external` with the API
+
+To create a control plane of type `external` with the API, you need to provide 
+first a **bootstrap secret** containing a bootstrap Kubeconfig for accessing the external cluster.
+The bootstrap Kubeconfig is used by the KubeFlex controllers to generate a long-lived
+token for accessing the external cluster.  The bootstrap kubeconfig is required to have only one context, 
+so given a Kubeconfig for the external cluster `$EXTERNAL_KUBECONFIG` with context for the external
+cluster `$EXTERNAL_CONTEXT` you can generate the `$BOOTSTRAP_KUBECONFIG` with the command:
+
+```shell
+kubectl --kubeconfig=$EXTERNAL_KUBECONFIG config view --minify --flatten \
+--context=$EXTERNAL_CONTEXT > $BOOTSTRAP_KUBECONFIG
+```
+
+If the Kubeconfig for your external cluster uses a loopback address for the server URL, you
+need to follow these [steps](#determining-the-endpoint-for-an-external-cluster-using-loopback-address) 
+to determine the address to use for `cluster.server` in the Kubeconfig and set that value in
+the file referenced by`$BOOTSTRAP_KUBECONFIG` created in the previous step. If the address is the value of `$INTERNAL_ADDRESS` then you can update the bootstrap Kubeconfig as follows:
+
+```shell
+# e.g. INTERNAL_ADDRESS=https://ext1-control-plane:6443
+kubectl --kubeconfig=$BOOTSTRAP_KUBECONFIG config set-cluster $(kubectl --kubeconfig=$BOOTSTRAP_KUBECONFIG config current-context) --server=$INTERNAL_ADDRESS
+```
+
+At this point, you can create the bootstrap secret with the command:
+
+```shell
+CP_NAME=ext1
+kubectl create secret generic ${CP_NAME}-bootstrap --from-file=kubeconfig-incluster=$BOOTSTRAP_KUBECONFIG --namespace kubeflex-system
+```
+where `${CP_NAME}` is the name of the control plane to create.
+
+*Important*: once the KubeFlex controller generates a long-lived token, it removes the bootstrap secret.
+
+Finally, you can create the new control plane of type "external" applying the following yaml:
+
+```shell
+kubectl apply -f - <<EOF
+apiVersion: tenancy.kflex.kubestellar.org/v1alpha1
+kind: ControlPlane
+metadata:
+  name: ${CP_NAME}
+spec:
+  type: external
+  bootstrapSecretRef:
+    inClusterKey: kubeconfig-incluster
+    name: ${CP_NAME}-bootstrap
+    namespace: kubeflex-system
+EOF
+```
+You can verify that the control plane has been created correctly with the command:
+
+```shell
+$ kubectl get cps
+NAME   SYNCED   READY   TYPE       AGE
+ext1   True     True    external   5s
+```
+
+and check that the secret with the long-lived token has been created in `${CP_NAME}-system`:
+
+```shell
+$ kubectl get secrets -n ${CP_NAME}-system
+NAME               TYPE     DATA   AGE
+admin-kubeconfig   Opaque   1      4m47s
+```
+
 ## Working with an OCM control plane
 
 Let's create an OCM control plane:
@@ -522,6 +599,65 @@ vcluster-0                                          2/2     Running   0
 
 The nginx pod is the one with the name `nginx-x-default-x-vcluster`.
 
+## Working with an external control plane
+
+In this section, we will show an example of creating an external control plane to adopt
+a kind cluster named `ext1`. This example supposes that the external cluster `ext1` 
+and the KubeFlex hosting cluster are on the same docker network.
+
+### Determining the endpoint for an external cluster using loopback address
+
+This is a common scenario when adopting kind or k3d. For clusters using the 
+default `kind` docker network, execute the following command to 
+check the DNS name of the external cluster `ext1` on the docker network:
+
+```shell
+docker inspect ext1-control-plane | jq '.[].NetworkSettings.Networks.kind.DNSNames' 
+```
+
+The output will show something similar to the following:
+
+```shell
+[
+  "ext1-control-plane",
+  "79540574c3c7"
+]
+```
+
+The endpoint for the adopted cluster should then be set to `https://ext1-control-plane:6443`. Note that
+the port `6443` is a default value used by kind.
+
+If you're not utilizing the default `kind` network, you'll need to make sure that the external cluster `ext1` 
+and the KubeFlex hosting cluster are on the same docker network. 
+
+```shelll
+docker inspect ext1-control-plane | jq '.[].NetworkSettings.Networks | keys[]'
+docker inspect kubeflex-control-plane | jq '.[].NetworkSettings.Networks | keys[]'
+```
+
+### Adopting the external cluster
+
+To set up the external cluster ext1 as a control plane named cpe, use the following command:
+
+```shell
+$ kflex adopt --adopted-context kind-ext1 --url-override https://ext1-control-plane:6443 ext1
+```
+
+Explanation of command parameters:
+
+- `--adopted-context kind-ext1`:
+    This specifies the context name, kind-ext1, for the ext1 cluster. Ensure that this context is correctly set in your current kubeconfig file.``
+
+- `--url-override https://ext1-control-plane:6443`:
+    This parameter sets the endpoint URL for the external control plane. It's crucial to use this option when the server URL in the existing kubeconfig uses a local loopback address, which is common for kind or k3d servers running on your local machine. Here, replace https://ext1-control-plane:6443 with the actual endpoint you have determined for your external control plane in the previous step.
+
+- `ext1`: 
+   This is the name of the new control plane.    
+
+### External clusters with reachable network address
+
+If the network address of the external cluster's API server in the bootstrap Kubeconfig is accessible by the controllers operating within the KubeFlex hosting cluster, there is no need to specify a `url-override`.
+
 ## Post-create hooks
 
 With post-create hooks you can automate applying kubernetes templates on the hosting cluster or on
diff --git a/internal/controller/controlplane_controller.go b/internal/controller/controlplane_controller.go
index 59648a0..631b5ab 100644
--- a/internal/controller/controlplane_controller.go
+++ b/internal/controller/controlplane_controller.go
@@ -33,6 +33,7 @@ import (
 	clog "sigs.k8s.io/controller-runtime/pkg/log"
 
 	tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1"
+	"github.com/kubestellar/kubeflex/pkg/reconcilers/external"
 	"github.com/kubestellar/kubeflex/pkg/reconcilers/host"
 	"github.com/kubestellar/kubeflex/pkg/reconcilers/k8s"
 	"github.com/kubestellar/kubeflex/pkg/reconcilers/ocm"
@@ -154,6 +155,9 @@ func (r *ControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request
 	case tenancyv1alpha1.ControlPlaneTypeHost:
 		reconciler := host.New(r.Client, r.Scheme, r.Version, r.ClientSet, r.DynamicClient)
 		return reconciler.Reconcile(ctx, hcp)
+	case tenancyv1alpha1.ControlPlaneTypeExternal:
+		reconciler := external.New(r.Client, r.Scheme, r.Version, r.ClientSet, r.DynamicClient)
+		return reconciler.Reconcile(ctx, hcp)
 	default:
 		return ctrl.Result{}, fmt.Errorf("unsupported control plane type: %s", hcp.Spec.Type)
 	}
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 0d9305e..93e7a34 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -46,7 +46,7 @@ func GetClientSet(kubeconfig string) (*kubernetes.Clientset, error) {
 	return clientset, nil
 }
 
-func GetClient(kubeconfig string) (*client.Client, error) {
+func GetClient(kubeconfig string) (client.Client, error) {
 	config, err := getConfig(kubeconfig)
 	if err != nil {
 		return nil, err
@@ -69,7 +69,7 @@ func GetClient(kubeconfig string) (*client.Client, error) {
 	if err != nil {
 		return nil, fmt.Errorf("error creating client: %s", err)
 	}
-	return &c, nil
+	return c, nil
 }
 
 func GetOpendShiftSecClient(kubeconfig string) (*versioned.Clientset, error) {
diff --git a/pkg/kubeconfig/kubeconfig.go b/pkg/kubeconfig/kubeconfig.go
index 3039834..3504af7 100644
--- a/pkg/kubeconfig/kubeconfig.go
+++ b/pkg/kubeconfig/kubeconfig.go
@@ -22,6 +22,7 @@ import (
 	"time"
 
 	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/fields"
 	"k8s.io/apimachinery/pkg/util/wait"
@@ -141,6 +142,36 @@ func WatchForSecretCreation(clientset kubernetes.Clientset, controlPlaneName, se
 	return nil
 }
 
+func WaitForNamespaceReady(ctx context.Context, clientset kubernetes.Interface, controlPlaneName string) error {
+	namespace := util.GenerateNamespaceFromControlPlaneName(controlPlaneName)
+
+	err := wait.PollUntilContextTimeout(
+		ctx,
+		2*time.Second,
+		2*time.Minute,
+		true,
+		func(context.Context) (bool, error) {
+			ns, err := clientset.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
+			if errors.IsNotFound(err) {
+				return false, nil // Retry if namespace is not found
+			} else if err != nil {
+				return false, fmt.Errorf("error checking namespace status: %v", err)
+			}
+
+			if ns.Status.Phase == v1.NamespaceActive {
+				return true, nil // Namespace is ready
+			}
+
+			return false, nil // Continue waiting
+		},
+	)
+
+	if err != nil {
+		return fmt.Errorf("timed out waiting for namespace %s to be ready: %v", namespace, err)
+	}
+	return nil
+}
+
 func adjustConfigKeys(config *clientcmdapi.Config, cpName, controlPlaneType string) {
 	switch controlPlaneType {
 	case string(tenancyv1alpha1.ControlPlaneTypeOCM):
diff --git a/pkg/reconcilers/external/kubeconfig.go b/pkg/reconcilers/external/kubeconfig.go
new file mode 100644
index 0000000..485d697
--- /dev/null
+++ b/pkg/reconcilers/external/kubeconfig.go
@@ -0,0 +1,301 @@
+/*
+Copyright 2024 The KubeStellar 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 external
+
+import (
+	"context"
+	"fmt"
+
+	authenticationv1 "k8s.io/api/authentication/v1"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	v1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/clientcmd/api"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	clog "sigs.k8s.io/controller-runtime/pkg/log"
+
+	tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1"
+	"github.com/kubestellar/kubeflex/pkg/util"
+)
+
+const (
+	adoptedClusterSAName      = "kubeflex"
+	adoptedClusterSANamespace = "kube-system"
+)
+
+var (
+	defaultAdoptedTokenExpirationSeconds int64 = 365 * 86400
+)
+
+func (r *ExternalReconciler) ReconcileKubeconfigFromBootstrapSecret(ctx context.Context, hcp *tenancyv1alpha1.ControlPlane) error {
+
+	// do not reconcile if kubeconfig secret is already present
+	if r.IsKubeconfigSecretPresent(ctx, *hcp) {
+		return nil
+	}
+
+	bootstrapApiConfig, err := getKubeconfigFromBootstrapSecret(r.Client, ctx, hcp)
+	if err != nil {
+		return err
+	}
+
+	bootstrapRestConfig, err := clientcmd.NewDefaultClientConfig(*bootstrapApiConfig, &clientcmd.ConfigOverrides{}).ClientConfig()
+	if err != nil {
+		return err
+	}
+
+	aClientset, err := kubernetes.NewForConfig(bootstrapRestConfig)
+	if err != nil {
+		return err
+	}
+
+	if err = reconcileAdoptedClusterRoleBinding(ctx, aClientset, adoptedClusterSAName, adoptedClusterSANamespace); err != nil {
+		return fmt.Errorf("error creating ClusterRoleBinding on the adopted cluster: %v", err)
+	}
+
+	if err = reconcileAdoptedServiceAccount(ctx, aClientset.CoreV1().ServiceAccounts(adoptedClusterSANamespace), adoptedClusterSAName); err != nil {
+		return fmt.Errorf("error creating ServiceAccount on the adopted cluster: %v", err)
+	}
+
+	bearerToken, err := requestTokenWithExpiration(ctx, aClientset.CoreV1().ServiceAccounts(adoptedClusterSANamespace), adoptedClusterSAName, hcp.Spec.TokenExpirationSeconds)
+	if err != nil {
+		return fmt.Errorf("error requesting token from the adopted cluster: %v", err)
+	}
+
+	newKubeConfig, err := createNewKubeConfig(bootstrapApiConfig, bearerToken, hcp)
+	if err != nil {
+		return fmt.Errorf("error creating adopted cluster kubeconfig: %v", err)
+	}
+
+	if err := r.ReconcileKubeconfigSecret(ctx, *hcp, newKubeConfig); err != nil {
+		return fmt.Errorf("error creating kubeconfig secret: %v", err)
+	}
+
+	return deleteBootstrapSecret(ctx, r.Client, hcp)
+}
+
+func getKubeconfigFromBootstrapSecret(crClient client.Client, ctx context.Context, hcp *tenancyv1alpha1.ControlPlane) (*api.Config, error) {
+
+	if hcp.Spec.BootstrapSecretRef == nil {
+		return nil, fmt.Errorf("bootstrapSecretRef must be present in the control plane")
+	}
+
+	bootstrapSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      hcp.Spec.BootstrapSecretRef.Name,
+			Namespace: hcp.Spec.BootstrapSecretRef.Namespace,
+		},
+	}
+
+	err := crClient.Get(ctx, client.ObjectKeyFromObject(bootstrapSecret), bootstrapSecret, &client.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	key := util.DefaultString(hcp.Spec.BootstrapSecretRef.InClusterKey, util.KubeconfigSecretKeyInCluster)
+
+	kconfigBytes := bootstrapSecret.Data[key]
+	if kconfigBytes == nil {
+		return nil, fmt.Errorf("kubeconfig not found in bootstrap secret for key %s", key)
+	}
+
+	return clientcmd.Load(kconfigBytes)
+}
+
+func reconcileAdoptedClusterRoleBinding(ctx context.Context, clientset *kubernetes.Clientset, saName, saNamespace string) error {
+	name := saName + "-clusterrolebinding"
+
+	// Check if the ClusterRoleBinding already exists
+	_, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, name, metav1.GetOptions{})
+	if err == nil {
+		return nil
+	}
+	if !apierrors.IsNotFound(err) {
+		return err
+	}
+
+	// Define new ClusterRoleBinding
+	clusterRoleBinding := &rbacv1.ClusterRoleBinding{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+		RoleRef: rbacv1.RoleRef{
+			APIGroup: "rbac.authorization.k8s.io",
+			Kind:     "ClusterRole",
+			Name:     "cluster-admin",
+		},
+		Subjects: []rbacv1.Subject{
+			{
+				Kind:      "ServiceAccount",
+				Name:      saName,
+				Namespace: saNamespace,
+			},
+		},
+	}
+
+	// Create the ClusterRoleBinding
+	_, err = clientset.RbacV1().ClusterRoleBindings().Create(ctx, clusterRoleBinding, metav1.CreateOptions{})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func reconcileAdoptedServiceAccount(ctx context.Context, saClient v1.ServiceAccountInterface, saName string) error {
+
+	// Check if the ServiceAccount already exists
+	_, err := saClient.Get(ctx, saName, metav1.GetOptions{})
+	if err == nil {
+		return nil
+	}
+	if !apierrors.IsNotFound(err) {
+		return err
+	}
+
+	// Define a new ServiceAccount
+	sa := &corev1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: saName,
+		},
+	}
+
+	// Create the ServiceAccount
+	_, err = saClient.Create(ctx, sa, metav1.CreateOptions{})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func requestTokenWithExpiration(ctx context.Context, saClient v1.ServiceAccountInterface, saName string, expirationSeconds *int64) (string, error) {
+	if expirationSeconds == nil {
+		expirationSeconds = &defaultAdoptedTokenExpirationSeconds
+	}
+
+	tokenRequest := &authenticationv1.TokenRequest{
+		Spec: authenticationv1.TokenRequestSpec{
+			Audiences:         []string{},
+			ExpirationSeconds: expirationSeconds,
+		},
+	}
+
+	tokenResponse, err := saClient.CreateToken(ctx, saName, tokenRequest, metav1.CreateOptions{})
+	if err != nil {
+		return "", err
+	}
+	return tokenResponse.Status.Token, nil
+}
+
+func createNewKubeConfig(bootstrapConfig *api.Config, token string, hcp *tenancyv1alpha1.ControlPlane) ([]byte, error) {
+	context := bootstrapConfig.Contexts[bootstrapConfig.CurrentContext]
+
+	cluster := bootstrapConfig.Clusters[context.Cluster]
+	if cluster == nil {
+		return nil, fmt.Errorf("invalid cluster name %s for adoped cluster", context.Cluster)
+	}
+
+	kubeConfig := api.NewConfig()
+
+	kubeConfig.Clusters[context.Cluster] = cluster
+
+	kubeConfig.AuthInfos[hcp.Name] = &api.AuthInfo{
+		Token: token,
+	}
+
+	kubeConfig.Contexts[hcp.Name] = &api.Context{
+		Cluster:    context.Cluster,
+		AuthInfo:   hcp.Name,
+		Namespace:  context.Namespace,
+		Extensions: context.Extensions,
+	}
+	kubeConfig.CurrentContext = hcp.Name
+
+	newKubeConfig, err := clientcmd.Write(*kubeConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	return newKubeConfig, nil
+}
+
+func (r *ExternalReconciler) ReconcileKubeconfigSecret(ctx context.Context, cp tenancyv1alpha1.ControlPlane, kubeconfig []byte) error {
+	namespace := util.GenerateNamespaceFromControlPlaneName(cp.Name)
+
+	// create kubeconfig secret
+	kubeConfigSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      util.AdminConfSecret,
+			Namespace: namespace,
+		},
+		Type: corev1.SecretTypeOpaque,
+		Data: map[string][]byte{util.KubeconfigSecretKeyInCluster: kubeconfig},
+	}
+
+	// Attempt to get the existing kubeconfig secret
+	err := r.Client.Get(ctx, client.ObjectKeyFromObject(kubeConfigSecret), kubeConfigSecret)
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			// Set controller reference on new kubeconfig secret
+			if setErr := controllerutil.SetControllerReference(&cp, kubeConfigSecret, r.Scheme); setErr != nil {
+				return setErr
+			}
+			// Create the kubeconfig secret as it does not exist
+			return r.Client.Create(ctx, kubeConfigSecret)
+		}
+		return err
+	}
+
+	// Update the existing kubeconfig secret
+	return r.Client.Update(ctx, kubeConfigSecret)
+}
+
+func deleteBootstrapSecret(ctx context.Context, crClient client.Client, hcp *tenancyv1alpha1.ControlPlane) error {
+	_ = clog.FromContext(ctx)
+
+	bootstrapSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      hcp.Spec.BootstrapSecretRef.Name,
+			Namespace: hcp.Spec.BootstrapSecretRef.Namespace,
+		},
+	}
+	return crClient.Delete(ctx, bootstrapSecret)
+}
+
+func (r *ExternalReconciler) IsKubeconfigSecretPresent(ctx context.Context, cp tenancyv1alpha1.ControlPlane) bool {
+	namespace := util.GenerateNamespaceFromControlPlaneName(cp.Name)
+
+	kubeConfigSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      util.AdminConfSecret,
+			Namespace: namespace,
+		},
+	}
+
+	// Attempt to get the existing kubeconfig secret
+	if err := r.Client.Get(ctx, client.ObjectKeyFromObject(kubeConfigSecret), kubeConfigSecret); err == nil {
+		return true
+	}
+
+	return false
+}
diff --git a/pkg/reconcilers/external/reconciler.go b/pkg/reconcilers/external/reconciler.go
new file mode 100644
index 0000000..1695c24
--- /dev/null
+++ b/pkg/reconcilers/external/reconciler.go
@@ -0,0 +1,69 @@
+/*
+Copyright 2024 The KubeStellar 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 external
+
+import (
+	"context"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/kubernetes"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	tenancyv1alpha1 "github.com/kubestellar/kubeflex/api/v1alpha1"
+	"github.com/kubestellar/kubeflex/pkg/reconcilers/shared"
+	"github.com/kubestellar/kubeflex/pkg/util"
+)
+
+// ExternalReconciler reconciles an "external" ControlPlane to adopt an external cluster with the ControlPlane abstraction
+type ExternalReconciler struct {
+	*shared.BaseReconciler
+}
+
+func New(cl client.Client, scheme *runtime.Scheme, version string, clientSet *kubernetes.Clientset, dynamicClient *dynamic.DynamicClient) *ExternalReconciler {
+	return &ExternalReconciler{
+		BaseReconciler: &shared.BaseReconciler{
+			Client:        cl,
+			Scheme:        scheme,
+			ClientSet:     clientSet,
+			DynamicClient: dynamicClient,
+		},
+	}
+}
+
+func (r *ExternalReconciler) Reconcile(ctx context.Context, hcp *tenancyv1alpha1.ControlPlane) (ctrl.Result, error) {
+
+	if err := r.BaseReconciler.ReconcileNamespace(ctx, hcp); err != nil {
+		return r.UpdateStatusForSyncingError(hcp, err)
+	}
+
+	if err := r.ReconcileKubeconfigFromBootstrapSecret(ctx, hcp); err != nil {
+		return r.UpdateStatusForSyncingError(hcp, err)
+	}
+
+	r.UpdateStatusWithSecretRef(hcp, util.AdminConfSecret, util.KubeconfigSecretKeyDefault, util.KubeconfigSecretKeyInCluster)
+
+	if hcp.Spec.PostCreateHook != nil &&
+		tenancyv1alpha1.HasConditionAvailable(hcp.Status.Conditions) {
+		if err := r.ReconcileUpdatePostCreateHook(ctx, hcp); err != nil {
+			return r.UpdateStatusForSyncingError(hcp, err)
+		}
+	}
+
+	return r.UpdateStatusForSyncingSuccess(ctx, hcp)
+}
diff --git a/pkg/reconcilers/host/secret.go b/pkg/reconcilers/host/secret.go
index f940e84..0488094 100644
--- a/pkg/reconcilers/host/secret.go
+++ b/pkg/reconcilers/host/secret.go
@@ -120,7 +120,7 @@ func (r *HostReconciler) getServiceAccountToken(ctx context.Context, hcp *tenanc
 		},
 	}
 
-	if err := r.Client.Get(context.TODO(), client.ObjectKeyFromObject(saSecret), saSecret, &client.GetOptions{}); err != nil {
+	if err := r.Client.Get(ctx, client.ObjectKeyFromObject(saSecret), saSecret, &client.GetOptions{}); err != nil {
 		return nil, nil, err
 	}
 
diff --git a/pkg/util/status_check.go b/pkg/util/status_check.go
index 01f3b02..f4cd712 100644
--- a/pkg/util/status_check.go
+++ b/pkg/util/status_check.go
@@ -135,9 +135,8 @@ func IsAPIServerDeploymentReady(log logr.Logger, c client.Client, hcp tenancyv1a
 	}
 
 	switch hcp.Spec.Type {
-
-	case tenancyv1alpha1.ControlPlaneTypeHost:
-		// host is always available
+	case tenancyv1alpha1.ControlPlaneTypeHost, tenancyv1alpha1.ControlPlaneTypeExternal:
+		// host or external is always available
 		return true, nil
 	case tenancyv1alpha1.ControlPlaneTypeVCluster:
 		s := &v1.StatefulSet{
@@ -176,7 +175,7 @@ func IsAPIServerDeploymentReady(log logr.Logger, c client.Client, hcp tenancyv1a
 			return true, nil
 		}
 	default:
-		log.Info("control plane type not supported", "type", hcp.Spec.Type)
+		log.Error(fmt.Errorf("control plane type not supported"), "type", hcp.Spec.Type)
 		return false, nil
 	}
 
diff --git a/pkg/util/util.go b/pkg/util/util.go
index 6c99068..55802f7 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -146,3 +146,14 @@ func ZeroFields(obj runtime.Object) runtime.Object {
 
 	return zeroed
 }
+
+func DefaultString(value, defaultValue string) string {
+	if value == "" {
+		return defaultValue
+	}
+	return value
+}
+
+func GenerateBootstrapSecretName(cpName string) string {
+	return fmt.Sprintf("%s-bootstrap", cpName)
+}