Repository: crossplane/crossplane

Issue:
[v2] Don't allow cross-namespace `Usage` without a `by` resource
<!--
Thank you for helping to improve Crossplane!

Please be sure to search for open issues before raising a new one. We use issues
for bug reports and feature requests. Please find us at https://slack.crossplane.io
for questions, support, and discussion.
-->

### What happened?
<!--
Please let us know what behaviour you expected and how Crossplane diverged from
that behaviour.
-->

Currently a `Usage` can be created in one namespace to block a resource being deleted from another.

The use case for this is composing a resource into one namespace that depends on a resource in the other. (We know we'll probably need something like a [`ReferenceGrant`](https://gateway-api.sigs.k8s.io/geps/gep-709/#referencegrant) for this).

In the meantime you can currently create a `Usage` in one namespace with an `of` resource in another namespace and _no_ `by` resource at all. This probably shouldn't be allowed.


### How can we reproduce it?
<!--
Help us to reproduce your bug as succinctly and precisely as possible. Artifacts
such as example manifests or a script that triggers the issue are highly
appreciated!
-->

Create a `Usage` in one namespace with an `of` resource in another namespace and no `by` resource.

### What environment did it happen in?
Crossplane version: 

<!--
Include at least the version or commit of Crossplane you were running. Consider
also including your:

* Cloud provider or hardware configuration
* Kubernetes version (use `kubectl version`)
* Kubernetes distribution (e.g. Tectonic, GKE, OpenShift)
* OS (e.g. from /etc/os-release)
* Kernel (e.g. `uname -a`)
-->


Hints from issue comments:
@negz: I think implementing this is just a CEL validation rule away.

Solution patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d01cc5561..8826de9a3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -147,7 +147,7 @@ jobs:
         run: earthly --strict --remote-cache ghcr.io/crossplane/earthly-cache:${{ github.job }} +ci-codeql
 
       - name: Upload CodeQL Results to GitHub
-        uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3
+        uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3
         with:
           sarif_file: '_output/codeql/go.sarif'
 
@@ -173,7 +173,7 @@ jobs:
           output: 'trivy-results.sarif'
 
       - name: Upload Trivy Results to GitHub
-        uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3
+        uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3
         with:
           sarif_file: 'trivy-results.sarif'
 
diff --git a/.github/workflows/scan.yaml b/.github/workflows/scan.yaml
index ee52623a6..79ab4f70a 100644
--- a/.github/workflows/scan.yaml
+++ b/.github/workflows/scan.yaml
@@ -129,7 +129,7 @@ jobs:
           retention-days: 3
 
       - name: Upload Trivy Scan Results To GitHub Security Tab
-        uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3
+        uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3
         with:
           sarif_file: 'trivy-results.sarif'
           category: ${{ matrix.image }}:${{ env.tag }}
diff --git a/apis/apiextensions/v1/xrd_types.go b/apis/apiextensions/v1/xrd_types.go
index 20a10da61..f6757c99a 100644
--- a/apis/apiextensions/v1/xrd_types.go
+++ b/apis/apiextensions/v1/xrd_types.go
@@ -37,6 +37,7 @@ const (
 
 // CompositeResourceDefinitionSpec specifies the desired state of the definition.
 // +kubebuilder:validation:XValidation:rule="self.scope == 'LegacyCluster' || !has(self.claimNames)",message="Only LegacyCluster composite resources can offer claims"
+// +kubebuilder:validation:XValidation:rule="self.scope == 'LegacyCluster' || !has(self.connectionSecretKeys)",message="Only LegacyCluster composite resources support connection secrets"
 type CompositeResourceDefinitionSpec struct {
 	// Group specifies the API group of the defined composite resource.
 	// Composite resources are served under `/apis/<group>/...`. Must match the
@@ -76,9 +77,11 @@ type CompositeResourceDefinitionSpec struct {
 	// +kubebuilder:validation:XValidation:rule="!has(self.singular) || self.singular == self.singular.lowerAscii()",message="Singular name must be lowercase"
 	ClaimNames *extv1.CustomResourceDefinitionNames `json:"claimNames,omitempty"`
 
-	// ConnectionSecretKeys is the list of keys that will be exposed to the end
-	// user of the defined kind.
-	// If the list is empty, all keys will be published.
+	// ConnectionSecretKeys is the list of connection secret keys the
+	// defined XR can publish. If the list is empty, all keys will be
+	// published. If the list isn't empty, any connection secret keys that
+	// don't appear in the list will be filtered out. Only LegacyCluster XRs
+	// support connection secrets.
 	// +optional
 	ConnectionSecretKeys []string `json:"connectionSecretKeys,omitempty"`
 
diff --git a/apis/apiextensions/v2alpha1/xrd_types.go b/apis/apiextensions/v2alpha1/xrd_types.go
index b398b7bc8..96815bf94 100644
--- a/apis/apiextensions/v2alpha1/xrd_types.go
+++ b/apis/apiextensions/v2alpha1/xrd_types.go
@@ -36,6 +36,7 @@ const (
 
 // CompositeResourceDefinitionSpec specifies the desired state of the definition.
 // +kubebuilder:validation:XValidation:rule="!has(self.claimNames)",message="Claims aren't supported in apiextensions.crossplane.io/v2"
+// +kubebuilder:validation:XValidation:rule="!has(self.connectionSecretKeys)",message="XR connection secrets aren't supported in apiextensions.crossplane.io/v2"
 type CompositeResourceDefinitionSpec struct {
 	// Group specifies the API group of the defined composite resource.
 	// Composite resources are served under `/apis/<group>/...`. Must match the
@@ -58,12 +59,6 @@ type CompositeResourceDefinitionSpec struct {
 	// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
 	Scope CompositeResourceScope `json:"scope,omitempty"`
 
-	// ConnectionSecretKeys is the list of keys that will be exposed to the end
-	// user of the defined kind.
-	// If the list is empty, all keys will be published.
-	// +optional
-	ConnectionSecretKeys []string `json:"connectionSecretKeys,omitempty"`
-
 	// DefaultCompositionRef refers to the Composition resource that will be used
 	// in case no composition selector is given.
 	// +optional
@@ -121,6 +116,17 @@ type CompositeResourceDefinitionSpec struct {
 	// Deprecated: Claims aren't supported in apiextensions.crossplane.io/v2.
 	// +optional
 	DefaultCompositeDeletePolicy *xpv1.CompositeDeletePolicy `json:"defaultCompositeDeletePolicy,omitempty"`
+
+	// ConnectionSecretKeys is the list of connection secret keys the
+	// defined XR can publish. If the list is empty, all keys will be
+	// published. If the list isn't empty, any connection secret keys that
+	// don't appear in the list will be filtered out. Only LegacyCluster XRs
+	// support connection secrets.
+	//
+	// Deprecated: XR connection secrets aren't supported in
+	// apiextensions.crossplane.io/v2. Compose a secret instead.
+	// +optional
+	ConnectionSecretKeys []string `json:"connectionSecretKeys,omitempty"`
 }
 
 // A CompositionReference references a Composition.
diff --git a/apis/apiextensions/v2alpha1/zz_generated.deepcopy.go b/apis/apiextensions/v2alpha1/zz_generated.deepcopy.go
index f67d2dd42..2af34140f 100644
--- a/apis/apiextensions/v2alpha1/zz_generated.deepcopy.go
+++ b/apis/apiextensions/v2alpha1/zz_generated.deepcopy.go
@@ -106,11 +106,6 @@ func (in *CompositeResourceDefinitionList) DeepCopyObject() runtime.Object {
 func (in *CompositeResourceDefinitionSpec) DeepCopyInto(out *CompositeResourceDefinitionSpec) {
 	*out = *in
 	in.Names.DeepCopyInto(&out.Names)
-	if in.ConnectionSecretKeys != nil {
-		in, out := &in.ConnectionSecretKeys, &out.ConnectionSecretKeys
-		*out = make([]string, len(*in))
-		copy(*out, *in)
-	}
 	if in.DefaultCompositionRef != nil {
 		in, out := &in.DefaultCompositionRef, &out.DefaultCompositionRef
 		*out = new(CompositionReference)
@@ -153,6 +148,11 @@ func (in *CompositeResourceDefinitionSpec) DeepCopyInto(out *CompositeResourceDe
 		*out = new(v1.CompositeDeletePolicy)
 		**out = **in
 	}
+	if in.ConnectionSecretKeys != nil {
+		in, out := &in.ConnectionSecretKeys, &out.ConnectionSecretKeys
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeResourceDefinitionSpec.
diff --git a/apis/pkg/v1/package_runtime_types.go b/apis/pkg/v1/package_runtime_types.go
index 53074dac4..c9e064425 100644
--- a/apis/pkg/v1/package_runtime_types.go
+++ b/apis/pkg/v1/package_runtime_types.go
@@ -20,11 +20,6 @@ package v1
 // Only used by packages that uses a runtime, i.e. by providers and functions
 // but not for configurations.
 type PackageRuntimeSpec struct {
-	// ControllerConfigRef references a ControllerConfig resource that will be
-	// used to configure the packaged controller Deployment.
-	// Deprecated: Use RuntimeConfigReference instead.
-	// +optional
-	ControllerConfigReference *ControllerConfigReference `json:"controllerConfigRef,omitempty"`
 	// RuntimeConfigRef references a RuntimeConfig resource that will be used
 	// to configure the package runtime.
 	// +optional
@@ -48,13 +43,6 @@ type PackageRevisionRuntimeSpec struct {
 	TLSClientSecretName *string `json:"tlsClientSecretName,omitempty"`
 }
 
-// A ControllerConfigReference to a ControllerConfig resource that will be used
-// to configure the packaged controller Deployment.
-type ControllerConfigReference struct {
-	// Name of the ControllerConfig.
-	Name string `json:"name"`
-}
-
 // A RuntimeConfigReference to a runtime config resource that will be used
 // to configure the package runtime.
 type RuntimeConfigReference struct {
diff --git a/apis/pkg/v1/zz_generated.deepcopy.go b/apis/pkg/v1/zz_generated.deepcopy.go
index 4a823d917..2dc86c7df 100644
--- a/apis/pkg/v1/zz_generated.deepcopy.go
+++ b/apis/pkg/v1/zz_generated.deepcopy.go
@@ -177,21 +177,6 @@ func (in *ConfigurationStatus) DeepCopy() *ConfigurationStatus {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ControllerConfigReference) DeepCopyInto(out *ControllerConfigReference) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerConfigReference.
-func (in *ControllerConfigReference) DeepCopy() *ControllerConfigReference {
-	if in == nil {
-		return nil
-	}
-	out := new(ControllerConfigReference)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ControllerReference) DeepCopyInto(out *ControllerReference) {
 	*out = *in
@@ -504,11 +489,6 @@ func (in *PackageRevisionStatus) DeepCopy() *PackageRevisionStatus {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackageRuntimeSpec) DeepCopyInto(out *PackageRuntimeSpec) {
 	*out = *in
-	if in.ControllerConfigReference != nil {
-		in, out := &in.ControllerConfigReference, &out.ControllerConfigReference
-		*out = new(ControllerConfigReference)
-		**out = **in
-	}
 	if in.RuntimeConfigReference != nil {
 		in, out := &in.RuntimeConfigReference, &out.RuntimeConfigReference
 		*out = new(RuntimeConfigReference)
diff --git a/apis/pkg/v1beta1/zz_generated.deepcopy.go b/apis/pkg/v1beta1/zz_generated.deepcopy.go
index b4b5b5cdc..32d1cb034 100644
--- a/apis/pkg/v1beta1/zz_generated.deepcopy.go
+++ b/apis/pkg/v1beta1/zz_generated.deepcopy.go
@@ -42,21 +42,6 @@ func (in *Attestation) DeepCopy() *Attestation {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ControllerConfigReference) DeepCopyInto(out *ControllerConfigReference) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerConfigReference.
-func (in *ControllerConfigReference) DeepCopy() *ControllerConfigReference {
-	if in == nil {
-		return nil
-	}
-	out := new(ControllerConfigReference)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ControllerReference) DeepCopyInto(out *ControllerReference) {
 	*out = *in
@@ -941,11 +926,6 @@ func (in *PackageRevisionStatus) DeepCopy() *PackageRevisionStatus {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackageRuntimeSpec) DeepCopyInto(out *PackageRuntimeSpec) {
 	*out = *in
-	if in.ControllerConfigReference != nil {
-		in, out := &in.ControllerConfigReference, &out.ControllerConfigReference
-		*out = new(ControllerConfigReference)
-		**out = **in
-	}
 	if in.RuntimeConfigReference != nil {
 		in, out := &in.RuntimeConfigReference, &out.RuntimeConfigReference
 		*out = new(RuntimeConfigReference)
diff --git a/apis/pkg/v1beta1/zz_generated.package_runtime_types.go b/apis/pkg/v1beta1/zz_generated.package_runtime_types.go
index 6fc44451c..3e28b7d19 100644
--- a/apis/pkg/v1beta1/zz_generated.package_runtime_types.go
+++ b/apis/pkg/v1beta1/zz_generated.package_runtime_types.go
@@ -22,11 +22,6 @@ package v1beta1
 // Only used by packages that uses a runtime, i.e. by providers and functions
 // but not for configurations.
 type PackageRuntimeSpec struct {
-	// ControllerConfigRef references a ControllerConfig resource that will be
-	// used to configure the packaged controller Deployment.
-	// Deprecated: Use RuntimeConfigReference instead.
-	// +optional
-	ControllerConfigReference *ControllerConfigReference `json:"controllerConfigRef,omitempty"`
 	// RuntimeConfigRef references a RuntimeConfig resource that will be used
 	// to configure the package runtime.
 	// +optional
@@ -50,13 +45,6 @@ type PackageRevisionRuntimeSpec struct {
 	TLSClientSecretName *string `json:"tlsClientSecretName,omitempty"`
 }
 
-// A ControllerConfigReference to a ControllerConfig resource that will be used
-// to configure the packaged controller Deployment.
-type ControllerConfigReference struct {
-	// Name of the ControllerConfig.
-	Name string `json:"name"`
-}
-
 // A RuntimeConfigReference to a runtime config resource that will be used
 // to configure the package runtime.
 type RuntimeConfigReference struct {
diff --git a/apis/protection/v1beta1/usage_types.go b/apis/protection/v1beta1/usage_types.go
index 186151750..dfeb488e8 100644
--- a/apis/protection/v1beta1/usage_types.go
+++ b/apis/protection/v1beta1/usage_types.go
@@ -119,6 +119,7 @@ type NamespacedResource struct {
 
 // UsageSpec defines the desired state of Usage.
 // +kubebuilder:validation:XValidation:rule="has(self.by) || has(self.reason)",message="either \"spec.by\" or \"spec.reason\" must be specified."
+// +kubebuilder:validation:XValidation:rule="has(self.by) || (!has(self.of.resourceRef) || !has(self.of.resourceRef.__namespace__)) && (!has(self.of.resourceSelector) || !has(self.of.resourceSelector.__namespace__))",message="cross-namespace \"spec.of\" is not allowed without \"spec.by\" resource."
 type UsageSpec struct {
 	// Of is the resource that is "being used".
 	// +kubebuilder:validation:XValidation:rule="has(self.resourceRef) || has(self.resourceSelector)",message="either a resource reference or a resource selector should be set."
diff --git a/cluster/charts/crossplane/values.yaml b/cluster/charts/crossplane/values.yaml
index 719dfd1fa..aa0f5c101 100755
--- a/cluster/charts/crossplane/values.yaml
+++ b/cluster/charts/crossplane/values.yaml
@@ -205,13 +205,3 @@ extraVolumeMountsCrossplane: {}
 
 # -- To add arbitrary Kubernetes Objects during a Helm Install
 extraObjects: []
-  # - apiVersion: pkg.crossplane.io/v1alpha1
-  #   kind: ControllerConfig
-  #   metadata:
-  #     name: aws-config
-  #     annotations:
-  #       eks.amazonaws.com/role-arn: arn:aws:iam::123456789101:role/example
-  #       helm.sh/hook: post-install
-  #   spec:
-  #     podSecurityContext:
-  #       fsGroup: 2000
diff --git a/cluster/crds/apiextensions.crossplane.io_compositeresourcedefinitions.yaml b/cluster/crds/apiextensions.crossplane.io_compositeresourcedefinitions.yaml
index 54a6dfbb7..8d567b8f4 100644
--- a/cluster/crds/apiextensions.crossplane.io_compositeresourcedefinitions.yaml
+++ b/cluster/crds/apiextensions.crossplane.io_compositeresourcedefinitions.yaml
@@ -122,9 +122,11 @@ spec:
                   rule: '!has(self.singular) || self.singular == self.singular.lowerAscii()'
               connectionSecretKeys:
                 description: |-
-                  ConnectionSecretKeys is the list of keys that will be exposed to the end
-                  user of the defined kind.
-                  If the list is empty, all keys will be published.
+                  ConnectionSecretKeys is the list of connection secret keys the
+                  defined XR can publish. If the list is empty, all keys will be
+                  published. If the list isn't empty, any connection secret keys that
+                  don't appear in the list will be filtered out. Only LegacyCluster XRs
+                  support connection secrets.
                 items:
                   type: string
                 type: array
@@ -505,6 +507,8 @@ spec:
             x-kubernetes-validations:
             - message: Only LegacyCluster composite resources can offer claims
               rule: self.scope == 'LegacyCluster' || !has(self.claimNames)
+            - message: Only LegacyCluster composite resources support connection secrets
+              rule: self.scope == 'LegacyCluster' || !has(self.connectionSecretKeys)
           status:
             description: CompositeResourceDefinitionStatus shows the observed state
               of the definition.
@@ -701,9 +705,14 @@ spec:
                 type: object
               connectionSecretKeys:
                 description: |-
-                  ConnectionSecretKeys is the list of keys that will be exposed to the end
-                  user of the defined kind.
-                  If the list is empty, all keys will be published.
+                  ConnectionSecretKeys is the list of connection secret keys the
+                  defined XR can publish. If the list is empty, all keys will be
+                  published. If the list isn't empty, any connection secret keys that
+                  don't appear in the list will be filtered out. Only LegacyCluster XRs
+                  support connection secrets.
+
+                  Deprecated: XR connection secrets aren't supported in
+                  apiextensions.crossplane.io/v2. Compose a secret instead.
                 items:
                   type: string
                 type: array
@@ -1078,6 +1087,8 @@ spec:
             x-kubernetes-validations:
             - message: Claims aren't supported in apiextensions.crossplane.io/v2
               rule: '!has(self.claimNames)'
+            - message: XR connection secrets aren't supported in apiextensions.crossplane.io/v2
+              rule: '!has(self.connectionSecretKeys)'
           status:
             description: CompositeResourceDefinitionStatus shows the observed state
               of the definition.
diff --git a/cluster/crds/pkg.crossplane.io_functionrevisions.yaml b/cluster/crds/pkg.crossplane.io_functionrevisions.yaml
index 3bc4c096f..a826dcd92 100644
--- a/cluster/crds/pkg.crossplane.io_functionrevisions.yaml
+++ b/cluster/crds/pkg.crossplane.io_functionrevisions.yaml
@@ -84,18 +84,6 @@ spec:
                   and services.
                   More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
                 type: object
-              controllerConfigRef:
-                description: |-
-                  ControllerConfigRef references a ControllerConfig resource that will be
-                  used to configure the packaged controller Deployment.
-                  Deprecated: Use RuntimeConfigReference instead.
-                properties:
-                  name:
-                    description: Name of the ControllerConfig.
-                    type: string
-                required:
-                - name
-                type: object
               desiredState:
                 description: DesiredState of the PackageRevision. Can be either Active
                   or Inactive.
@@ -382,18 +370,6 @@ spec:
                   and services.
                   More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
                 type: object
-              controllerConfigRef:
-                description: |-
-                  ControllerConfigRef references a ControllerConfig resource that will be
-                  used to configure the packaged controller Deployment.
-                  Deprecated: Use RuntimeConfigReference instead.
-                properties:
-                  name:
-                    description: Name of the ControllerConfig.
-                    type: string
-                required:
-                - name
-                type: object
               desiredState:
                 description: DesiredState of the PackageRevision. Can be either Active
                   or Inactive.
diff --git a/cluster/crds/pkg.crossplane.io_functions.yaml b/cluster/crds/pkg.crossplane.io_functions.yaml
index 67b74eedf..d331fefe0 100644
--- a/cluster/crds/pkg.crossplane.io_functions.yaml
+++ b/cluster/crds/pkg.crossplane.io_functions.yaml
@@ -69,18 +69,6 @@ spec:
                   and services.
                   More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
                 type: object
-              controllerConfigRef:
-                description: |-
-                  ControllerConfigRef references a ControllerConfig resource that will be
-                  used to configure the packaged controller Deployment.
-                  Deprecated: Use RuntimeConfigReference instead.
-                properties:
-                  name:
-                    description: Name of the ControllerConfig.
-                    type: string
-                required:
-                - name
-                type: object
               ignoreCrossplaneConstraints:
                 default: false
                 description: |-
@@ -314,18 +302,6 @@ spec:
                   and services.
                   More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
                 type: object
-              controllerConfigRef:
-                description: |-
-                  ControllerConfigRef references a ControllerConfig resource that will be
-                  used to configure the packaged controller Deployment.
-                  Deprecated: Use RuntimeConfigReference instead.
-                properties:
-                  name:
-                    description: Name of the ControllerConfig.
-                    type: string
-                required:
-                - name
-                type: object
               ignoreCrossplaneConstraints:
                 default: false
                 description: |-
diff --git a/cluster/crds/pkg.crossplane.io_providerrevisions.yaml b/cluster/crds/pkg.crossplane.io_providerrevisions.yaml
index 687d12b23..c1f5c967b 100644
--- a/cluster/crds/pkg.crossplane.io_providerrevisions.yaml
+++ b/cluster/crds/pkg.crossplane.io_providerrevisions.yaml
@@ -84,18 +84,6 @@ spec:
                   and services.
                   More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
                 type: object
-              controllerConfigRef:
-                description: |-
-                  ControllerConfigRef references a ControllerConfig resource that will be
-                  used to configure the packaged controller Deployment.
-                  Deprecated: Use RuntimeConfigReference instead.
-                properties:
-                  name:
-                    description: Name of the ControllerConfig.
-                    type: string
-                required:
-                - name
-                type: object
               desiredState:
                 description: DesiredState of the PackageRevision. Can be either Active
                   or Inactive.
diff --git a/cluster/crds/pkg.crossplane.io_providers.yaml b/cluster/crds/pkg.crossplane.io_providers.yaml
index 69abaefd8..7416d0e2f 100644
--- a/cluster/crds/pkg.crossplane.io_providers.yaml
+++ b/cluster/crds/pkg.crossplane.io_providers.yaml
@@ -71,18 +71,6 @@ spec:
                   and services.
                   More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
                 type: object
-              controllerConfigRef:
-                description: |-
-                  ControllerConfigRef references a ControllerConfig resource that will be
-                  used to configure the packaged controller Deployment.
-                  Deprecated: Use RuntimeConfigReference instead.
-                properties:
-                  name:
-                    description: Name of the ControllerConfig.
-                    type: string
-                required:
-                - name
-                type: object
               ignoreCrossplaneConstraints:
                 default: false
                 description: |-
diff --git a/cluster/crds/protection.crossplane.io_usages.yaml b/cluster/crds/protection.crossplane.io_usages.yaml
index 625959734..09db2b8b4 100644
--- a/cluster/crds/protection.crossplane.io_usages.yaml
+++ b/cluster/crds/protection.crossplane.io_usages.yaml
@@ -164,6 +164,10 @@ spec:
             x-kubernetes-validations:
             - message: either "spec.by" or "spec.reason" must be specified.
               rule: has(self.by) || has(self.reason)
+            - message: cross-namespace "spec.of" is not allowed without "spec.by"
+                resource.
+              rule: has(self.by) || (!has(self.of.resourceRef) || !has(self.of.resourceRef.__namespace__))
+                && (!has(self.of.resourceSelector) || !has(self.of.resourceSelector.__namespace__))
           status:
             description: UsageStatus defines the observed state of Usage.
             properties:
diff --git a/cmd/crank/render/cmd.go b/cmd/crank/render/cmd.go
index 1a8f710a4..27817cb49 100644
--- a/cmd/crank/render/cmd.go
+++ b/cmd/crank/render/cmd.go
@@ -33,6 +33,7 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/logging"
 
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
+	"github.com/crossplane/crossplane/internal/xcrd"
 	"github.com/crossplane/crossplane/internal/xresource/unstructured/composed"
 )
 
@@ -54,6 +55,7 @@ type Cmd struct {
 	FunctionCredentials    string            `help:"A YAML file or directory of YAML files specifying credentials to use for Functions to render the XR."                                      placeholder:"PATH" predictor:"yaml_file_or_directory" type:"path"`
 
 	Timeout time.Duration `default:"1m" help:"How long to run before timing out."`
+	XRD     string        `help:"A YAML file specifying the CompositeResourceDefinition (XRD) that defines the XR's schema and properties."                                                  optional:""        placeholder:"PATH" type:"existingfile"`
 
 	fs afero.Fs
 }
@@ -174,7 +176,19 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger) error { //nolint:gocognit
 	if err != nil {
 		return errors.Wrapf(err, "cannot load functions from %q", c.Functions)
 	}
-
+	if c.XRD != "" {
+		xrd, err := LoadXRD(c.fs, c.XRD)
+		if err != nil {
+			return errors.Wrapf(err, "cannot load XRD from %q", c.XRD)
+		}
+		crd, err := xcrd.ForCompositeResource(xrd)
+		if err != nil {
+			return errors.Wrapf(err, "cannot derive composite CRD from XRD %q", xrd.GetName())
+		}
+		if err := DefaultValues(xr.UnstructuredContent(), xr.GetAPIVersion(), *crd); err != nil {
+			return errors.Wrapf(err, "cannot default values for XR %q", xr.GetName())
+		}
+	}
 	fcreds := []corev1.Secret{}
 	if c.FunctionCredentials != "" {
 		fcreds, err = LoadCredentials(c.fs, c.FunctionCredentials)
diff --git a/cmd/crank/render/load.go b/cmd/crank/render/load.go
index 793e85b4d..a361ef2d0 100644
--- a/cmd/crank/render/load.go
+++ b/cmd/crank/render/load.go
@@ -67,6 +67,16 @@ func LoadComposition(fs afero.Fs, file string) (*apiextensionsv1.Composition, er
 	}
 }
 
+// LoadXRD from a YAML manifest.
+func LoadXRD(fs afero.Fs, file string) (*apiextensionsv1.CompositeResourceDefinition, error) {
+	y, err := afero.ReadFile(fs, file)
+	if err != nil {
+		return nil, errors.Wrap(err, "cannot read XRD file")
+	}
+	xrd := &apiextensionsv1.CompositeResourceDefinition{}
+	return xrd, errors.Wrap(yaml.Unmarshal(y, xrd), "cannot unmarshal XRD YAML")
+}
+
 // TODO(negz): Support optionally loading functions and observed resources from
 // a directory of manifests instead of a single stream.
 
diff --git a/cmd/crank/render/xrd.go b/cmd/crank/render/xrd.go
new file mode 100644
index 000000000..961ec422e
--- /dev/null
+++ b/cmd/crank/render/xrd.go
@@ -0,0 +1,34 @@
+package render
+
+import (
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
+	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	schema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
+	structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
+)
+
+// DefaultValues sets default values on the XR based on the CRD schema.
+func DefaultValues(xr map[string]any, apiVersion string, crd extv1.CustomResourceDefinition) error {
+	var k apiextensions.JSONSchemaProps
+	var version *extv1.CustomResourceDefinitionVersion
+	for _, vr := range crd.Spec.Versions {
+		checkAPIVersion := crd.Spec.Group + "/" + vr.Name
+		if checkAPIVersion == apiVersion {
+			version = &vr
+			break
+		}
+	}
+	if version == nil {
+		return errors.Errorf("the specified API version '%s' does not exist in the XRD", apiVersion)
+	}
+	if err := extv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(version.Schema.OpenAPIV3Schema, &k, nil); err != nil {
+		return err
+	}
+	crdWithDefaults, err := schema.NewStructural(&k)
+	if err != nil {
+		return err
+	}
+	structuraldefaulting.Default(xr, crdWithDefaults)
+	return nil
+}
diff --git a/internal/controller/apiextensions/composite/reconciler.go b/internal/controller/apiextensions/composite/reconciler.go
index 3fa6b21f6..95f3d0a32 100644
--- a/internal/controller/apiextensions/composite/reconciler.go
+++ b/internal/controller/apiextensions/composite/reconciler.go
@@ -417,10 +417,13 @@ func NewReconciler(cached client.Client, of schema.GroupVersionKind, opts ...Rec
 			CompositionSelector: NewAPILabelSelectorResolver(cached),
 			Configurator:        NewConfiguratorChain(NewAPINamingConfigurator(cached), NewAPIConfigurator(cached)),
 
-			// TODO(negz): In practice this is a filtered publisher that will
-			// never filter any keys. Is there an unfiltered variant we could
-			// use by default instead?
-			ConnectionPublisher: NewAPIFilteredSecretPublisher(cached, []string{}),
+			// Modern v2 XRs don't support connection details, but
+			// legacy v1 XRs do. Publishing is disabled by default.
+			// The definition (XRD) reconciler configures a real
+			// secret publisher for legacy XRs.
+			ConnectionPublisher: ConnectionPublisherFn(func(_ context.Context, _ xresource.ConnectionSecretOwner, _ managed.ConnectionDetails) (bool, error) {
+				return false, nil
+			}),
 		},
 
 		// We use a nop Composer by default. The real composed is passed in by
diff --git a/internal/controller/apiextensions/definition/reconciler.go b/internal/controller/apiextensions/definition/reconciler.go
index 5ed241c2d..56334ad5e 100644
--- a/internal/controller/apiextensions/definition/reconciler.go
+++ b/internal/controller/apiextensions/definition/reconciler.go
@@ -492,7 +492,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
 
 	ro := []composite.ReconcilerOption{
 		composite.WithCompositeSchema(schema),
-		composite.WithConnectionPublishers(composite.NewAPIFilteredSecretPublisher(r.engine.GetCached(), d.GetConnectionSecretKeys())),
 		composite.WithCompositionSelector(composite.NewCompositionSelectorChain(
 			composite.NewEnforcedCompositionSelector(*d, r.record),
 			composite.NewAPIDefaultCompositionSelector(r.engine.GetCached(), *meta.ReferenceTo(d, v1.CompositeResourceDefinitionGroupVersionKind), r.record),
@@ -505,6 +504,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
 		composite.WithFeatures(r.options.Features),
 	}
 
+	if schema == ucomposite.SchemaLegacy {
+		ro = append(ro,
+			composite.WithConnectionPublishers(composite.NewAPIFilteredSecretPublisher(r.engine.GetCached(), d.GetConnectionSecretKeys())),
+		)
+	}
+
 	// If realtime compositions are enabled we pass the ControllerEngine to the
 	// XR reconciler so that it can start watches for composed resources.
 	if r.options.Features.Enabled(features.EnableBetaRealtimeCompositions) {
diff --git a/internal/controller/apiextensions/definition/watch.go b/internal/controller/apiextensions/definition/watch.go
index d515d359c..559b35489 100644
--- a/internal/controller/apiextensions/definition/watch.go
+++ b/internal/controller/apiextensions/definition/watch.go
@@ -1,6 +1,8 @@
 package definition
 
 import (
+	"slices"
+
 	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 
@@ -17,11 +19,6 @@ func IsCompositeResourceCRD() resource.PredicateFn {
 		if !ok {
 			return false
 		}
-		for _, c := range crd.Spec.Names.Categories {
-			if c == xcrd.CategoryComposite {
-				return true
-			}
-		}
-		return false
+		return slices.Contains(crd.Spec.Names.Categories, xcrd.CategoryComposite)
 	}
 }
diff --git a/internal/controller/apiextensions/offered/watch.go b/internal/controller/apiextensions/offered/watch.go
index 42189fda4..0d38de810 100644
--- a/internal/controller/apiextensions/offered/watch.go
+++ b/internal/controller/apiextensions/offered/watch.go
@@ -18,6 +18,7 @@ package offered
 
 import (
 	"context"
+	"slices"
 
 	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -52,12 +53,7 @@ func IsClaimCRD() resource.PredicateFn {
 		if !ok {
 			return false
 		}
-		for _, c := range d.Spec.Names.Categories {
-			if c == xcrd.CategoryClaim {
-				return true
-			}
-		}
-		return false
+		return slices.Contains(d.Spec.Names.Categories, xcrd.CategoryClaim)
 	}
 }
 
diff --git a/internal/xcrd/schemas.go b/internal/xcrd/schemas.go
index 3e1222be9..ab75b20a3 100644
--- a/internal/xcrd/schemas.go
+++ b/internal/xcrd/schemas.go
@@ -147,18 +147,10 @@ func CompositeResourceSpecProps(s v1.CompositeResourceScope, defaultPol *xpv1.Up
 			// Controllers should replace the entire resourceRefs array.
 			XListType: ptr.To("atomic"),
 		},
-		"writeConnectionSecretToRef": {
-			Type:     "object",
-			Required: []string{"name", "namespace"},
-			Properties: map[string]extv1.JSONSchemaProps{
-				"name":      {Type: "string"},
-				"namespace": {Type: "string"},
-			},
-		},
 	}
 
-	// Namespaced XRs don't get to reference secrets or composed resources in
-	// other namespaces.
+	// Namespaced XRs don't get to reference composed resources in other
+	// namespaces.
 	if s == v1.CompositeResourceScopeNamespaced {
 		props["resourceRefs"] = extv1.JSONSchemaProps{
 			Type: "array",
@@ -176,18 +168,10 @@ func CompositeResourceSpecProps(s v1.CompositeResourceScope, defaultPol *xpv1.Up
 			// Controllers should replace the entire resourceRefs array.
 			XListType: ptr.To("atomic"),
 		}
-
-		props["writeConnectionSecretToRef"] = extv1.JSONSchemaProps{
-			Type:     "object",
-			Required: []string{"name"},
-			Properties: map[string]extv1.JSONSchemaProps{
-				"name": {Type: "string"},
-			},
-		}
 	}
 
 	// Legacy XRs have their Crossplane machinery fields directly under spec.
-	// They also support referencing a claim.
+	// They also support referencing a claim, and writing a secret.
 	if s == v1.CompositeResourceScopeLegacyCluster {
 		props["claimRef"] = extv1.JSONSchemaProps{
 			Type:     "object",
@@ -200,6 +184,15 @@ func CompositeResourceSpecProps(s v1.CompositeResourceScope, defaultPol *xpv1.Up
 			},
 		}
 
+		props["writeConnectionSecretToRef"] = extv1.JSONSchemaProps{
+			Type:     "object",
+			Required: []string{"name", "namespace"},
+			Properties: map[string]extv1.JSONSchemaProps{
+				"name":      {Type: "string"},
+				"namespace": {Type: "string"},
+			},
+		}
+
 		return props
 	}
 
diff --git a/internal/xresource/unstructured/composite/composite.go b/internal/xresource/unstructured/composite/composite.go
index e47a95d34..352854382 100644
--- a/internal/xresource/unstructured/composite/composite.go
+++ b/internal/xresource/unstructured/composite/composite.go
@@ -293,13 +293,13 @@ func (c *Unstructured) GetReference() *reference.Composite {
 
 // GetWriteConnectionSecretToReference of this composite resource.
 func (c *Unstructured) GetWriteConnectionSecretToReference() *xpv1.SecretReference {
-	path := "spec.crossplane.writeConnectionSecretToRef"
-	if c.Schema == SchemaLegacy {
-		path = "spec.writeConnectionSecretToRef"
+	// Only legacy XRs support connection secrets.
+	if c.Schema != SchemaLegacy {
+		return nil
 	}
 
 	out := &xpv1.SecretReference{}
-	if err := fieldpath.Pave(c.Object).GetValueInto(path, out); err != nil {
+	if err := fieldpath.Pave(c.Object).GetValueInto("spec.writeConnectionSecretToRef", out); err != nil {
 		return nil
 	}
 	return out
@@ -307,12 +307,12 @@ func (c *Unstructured) GetWriteConnectionSecretToReference() *xpv1.SecretReferen
 
 // SetWriteConnectionSecretToReference of this composite resource.
 func (c *Unstructured) SetWriteConnectionSecretToReference(ref *xpv1.SecretReference) {
-	path := "spec.crossplane.writeConnectionSecretToRef"
-	if c.Schema == SchemaLegacy {
-		path = "spec.writeConnectionSecretToRef"
+	// Only legacy XRs support connection secrets.
+	if c.Schema != SchemaLegacy {
+		return
 	}
 
-	_ = fieldpath.Pave(c.Object).SetValue(path, ref)
+	_ = fieldpath.Pave(c.Object).SetValue("spec.writeConnectionSecretToRef", ref)
 }
 
 // GetCondition of this composite resource.



Test patch:
diff --git a/cmd/crank/beta/trace/internal/resource/xrm/client_test.go b/cmd/crank/beta/trace/internal/resource/xrm/client_test.go
index 497e07601..4b628cd5e 100644
--- a/cmd/crank/beta/trace/internal/resource/xrm/client_test.go
+++ b/cmd/crank/beta/trace/internal/resource/xrm/client_test.go
@@ -65,12 +65,6 @@ func withXRRefs(refs ...v1.ObjectReference) xrOpt {
 	}
 }
 
-func withXRSecretRef(ref *xpv1.SecretReference) xrOpt {
-	return func(c *composite.Unstructured) {
-		c.SetWriteConnectionSecretToReference(ref)
-	}
-}
-
 func buildXR(name string, opts ...xrOpt) *unstructured.Unstructured {
 	c := composite.New()
 	c.SetName(name)
@@ -219,68 +213,6 @@ func TestGetResourceChildrenRefs(t *testing.T) {
 				},
 			},
 		},
-		"XRWithChildrenAndSecret": {
-			reason: "Should return a list of children refs for an XR.",
-			args: args{
-				witSecrets: true,
-				resource: &resource2.Resource{
-					Unstructured: *buildXR("root-xr", withXRSecretRef(&xpv1.SecretReference{
-						Name:      "secret-1",
-						Namespace: "ns-1",
-					}), withXRRefs(v1.ObjectReference{
-						APIVersion: "example.com/v1",
-						Kind:       "MR",
-						Name:       "mr-1",
-					},
-						v1.ObjectReference{
-							APIVersion: "example2.com/v1",
-							Kind:       "MR",
-							Name:       "mr-2",
-						},
-						v1.ObjectReference{
-							APIVersion: "example2.com/v1",
-							Kind:       "XR",
-							Name:       "xr-1",
-						},
-						v1.ObjectReference{
-							APIVersion: "example2.com/v1",
-							Kind:       "XRC",
-							Name:       "xrc-1",
-						},
-					)),
-				},
-			},
-			want: want{
-				refs: []v1.ObjectReference{
-					{
-						APIVersion: "v1",
-						Kind:       "Secret",
-						Namespace:  "ns-1",
-						Name:       "secret-1",
-					},
-					{
-						APIVersion: "example.com/v1",
-						Kind:       "MR",
-						Name:       "mr-1",
-					},
-					{
-						APIVersion: "example2.com/v1",
-						Kind:       "MR",
-						Name:       "mr-2",
-					},
-					{
-						APIVersion: "example2.com/v1",
-						Kind:       "XR",
-						Name:       "xr-1",
-					},
-					{
-						APIVersion: "example2.com/v1",
-						Kind:       "XRC",
-						Name:       "xrc-1",
-					},
-				},
-			},
-		},
 	}
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
diff --git a/cmd/crank/render/load_test.go b/cmd/crank/render/load_test.go
index 959cc5ea7..285f89898 100644
--- a/cmd/crank/render/load_test.go
+++ b/cmd/crank/render/load_test.go
@@ -32,6 +32,8 @@ import (
 
 	apiextensionsv1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 	pkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
+	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
 	"github.com/crossplane/crossplane/internal/xresource/unstructured/composed"
 	"github.com/crossplane/crossplane/internal/xresource/unstructured/composite"
 )
@@ -91,6 +93,71 @@ func TestLoadCompositeResource(t *testing.T) {
 	}
 }
 
+func TestLoadXRD(t *testing.T) {
+	fs := afero.FromIOFS{FS: testdatafs}
+	type want struct {
+		xrd *apiextensionsv1.CompositeResourceDefinition
+		err error
+	}
+	cases := map[string]struct {
+		file string
+		want want
+	}{
+		"Success": {
+			file: "testdata/xrd.yaml",
+			want: want{
+				xrd: &apiextensionsv1.CompositeResourceDefinition{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       apiextensionsv1.CompositeResourceDefinitionKind,
+						APIVersion: apiextensionsv1.SchemeGroupVersion.String(),
+					},
+					ObjectMeta: metav1.ObjectMeta{Name: "xnopresources.nop.example.org"},
+					Spec: apiextensionsv1.CompositeResourceDefinitionSpec{
+						Group: "nop.example.org",
+						Names: v1.CustomResourceDefinitionNames{
+							Kind:       "XNopResource",
+							Plural:     "xnopresources",
+							Singular:   "xnopresource",
+							ShortNames: []string{"xnr"},
+						},
+						Versions: []apiextensionsv1.CompositeResourceDefinitionVersion{
+							{
+								Name:   "v1",
+								Served: true,
+								Schema: &apiextensionsv1.CompositeResourceValidation{
+									OpenAPIV3Schema: runtime.RawExtension{
+										Raw: []byte(`{"description":"A test resource","properties":{"spec":{"properties":{"coolField":{"type":"string"}},"type":"object"}},"type":"object"}`),
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"NoSuchFile": {
+			file: "testdata/nonexist.yaml",
+			want: want{
+				err: cmpopts.AnyError,
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			xrd, err := LoadXRD(fs, tc.file)
+
+			if diff := cmp.Diff(tc.want.xrd, xrd, test.EquateConditions()); diff != "" {
+				t.Errorf("LoadXRD(..), -want, +got:\n%s", diff)
+			}
+
+			if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
+				t.Errorf("LoadXRD(..), -want, +got:\n%s", diff)
+			}
+		})
+	}
+}
+
 func TestLoadComposition(t *testing.T) {
 	fs := afero.FromIOFS{FS: testdatafs}
 
diff --git a/cmd/crank/render/testdata/xrd.yaml b/cmd/crank/render/testdata/xrd.yaml
new file mode 100644
index 000000000..66cef8284
--- /dev/null
+++ b/cmd/crank/render/testdata/xrd.yaml
@@ -0,0 +1,26 @@
+apiVersion: apiextensions.crossplane.io/v1
+kind: CompositeResourceDefinition
+metadata:
+  name: xnopresources.nop.example.org
+spec:
+  group: nop.example.org
+  names:
+    kind: XNopResource
+    plural: xnopresources
+    singular: xnopresource
+    shortNames:
+      - xnr
+  versions:
+    - name: v1
+      served: true
+      storage: true
+      schema:
+        openAPIV3Schema:
+          type: object
+          description: "A test resource"
+          properties:
+            spec:
+              type: object
+              properties:
+                coolField:
+                  type: string
\ No newline at end of file
diff --git a/cmd/crank/render/xrd_test.go b/cmd/crank/render/xrd_test.go
new file mode 100644
index 000000000..e9ca9e744
--- /dev/null
+++ b/cmd/crank/render/xrd_test.go
@@ -0,0 +1,250 @@
+package render
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+)
+
+func TestDefaultValues(t *testing.T) {
+	type args struct {
+		xr         map[string]any
+		crd        extv1.CustomResourceDefinition
+		apiVersion string
+	}
+
+	cases := map[string]struct {
+		name    string
+		args    args
+		want    map[string]any
+		wantErr bool
+	}{
+		"SetDefaultValues": {
+			name: "Should set default values according to schema",
+			args: args{
+				apiVersion: "example.com/v1",
+				xr: map[string]any{
+					"spec": map[string]any{},
+				},
+				crd: extv1.CustomResourceDefinition{
+					Spec: extv1.CustomResourceDefinitionSpec{
+						Group: "example.com",
+						Versions: []extv1.CustomResourceDefinitionVersion{
+							{
+								Name: "v1",
+								Schema: &extv1.CustomResourceValidation{
+									OpenAPIV3Schema: &extv1.JSONSchemaProps{
+										Type: "object",
+										Properties: map[string]extv1.JSONSchemaProps{
+											"spec": {
+												Type: "object",
+												Properties: map[string]extv1.JSONSchemaProps{
+													"cooldown": {
+														Type:    "integer",
+														Default: &extv1.JSON{Raw: []byte(`5`)},
+													},
+													"enabled": {
+														Type:    "boolean",
+														Default: &extv1.JSON{Raw: []byte(`true`)},
+													},
+													"status": {
+														Type: "string",
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: map[string]any{
+				"spec": map[string]any{
+					"cooldown": int64(5),
+					"enabled":  true,
+				},
+			},
+			wantErr: false,
+		},
+		"DontOverwriteExistingValues": {
+			name: "Should not overwrite existing values with defaults",
+			args: args{
+				apiVersion: "example.com/v1",
+				xr: map[string]any{
+					"spec": map[string]any{
+						"cooldown": int64(10),
+						"enabled":  false,
+					},
+				},
+				crd: extv1.CustomResourceDefinition{
+					Spec: extv1.CustomResourceDefinitionSpec{
+						Group: "example.com",
+						Versions: []extv1.CustomResourceDefinitionVersion{
+							{
+								Name: "v1",
+								Schema: &extv1.CustomResourceValidation{
+									OpenAPIV3Schema: &extv1.JSONSchemaProps{
+										Type: "object",
+										Properties: map[string]extv1.JSONSchemaProps{
+											"spec": {
+												Type: "object",
+												Properties: map[string]extv1.JSONSchemaProps{
+													"cooldown": {
+														Type:    "integer",
+														Default: &extv1.JSON{Raw: []byte(`5`)},
+													},
+													"enabled": {
+														Type:    "boolean",
+														Default: &extv1.JSON{Raw: []byte(`true`)},
+													},
+													"status": {
+														Type: "string",
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: map[string]any{
+				"spec": map[string]any{
+					"cooldown": int64(10),
+					"enabled":  false,
+				},
+			},
+			wantErr: false,
+		},
+		"MultipleVersions": {
+			name: "Should only apply defaults from requested version",
+			args: args{
+				apiVersion: "example.com/v2",
+				xr: map[string]any{
+					"spec": map[string]any{},
+				},
+				crd: extv1.CustomResourceDefinition{
+					Spec: extv1.CustomResourceDefinitionSpec{
+						Group: "example.com",
+						Versions: []extv1.CustomResourceDefinitionVersion{
+							{
+								Name: "v1",
+								Schema: &extv1.CustomResourceValidation{
+									OpenAPIV3Schema: &extv1.JSONSchemaProps{
+										Type: "object",
+										Properties: map[string]extv1.JSONSchemaProps{
+											"spec": {
+												Type: "object",
+												Properties: map[string]extv1.JSONSchemaProps{
+													"cooldown": {
+														Type:    "integer",
+														Default: &extv1.JSON{Raw: []byte(`5`)},
+													},
+													"enabled": {
+														Type:    "boolean",
+														Default: &extv1.JSON{Raw: []byte(`true`)},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+							{
+								Name: "v2",
+								Schema: &extv1.CustomResourceValidation{
+									OpenAPIV3Schema: &extv1.JSONSchemaProps{
+										Type: "object",
+										Properties: map[string]extv1.JSONSchemaProps{
+											"spec": {
+												Type: "object",
+												Properties: map[string]extv1.JSONSchemaProps{
+													"cooldown": {
+														Type:    "integer",
+														Default: &extv1.JSON{Raw: []byte(`15`)},
+													},
+													"enabled": {
+														Type:    "boolean",
+														Default: &extv1.JSON{Raw: []byte(`false`)},
+													},
+													"newField": {
+														Type:    "string",
+														Default: &extv1.JSON{Raw: []byte(`"default"`)},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: map[string]any{
+				"spec": map[string]any{
+					"cooldown": int64(15),
+					"enabled":  false,
+					"newField": "default",
+				},
+			},
+			wantErr: false,
+		},
+		"IncorrectAPIVersion": {
+			name: "Should return error for incorrect API version",
+			args: args{
+				apiVersion: "wrong-group/v1",
+				xr: map[string]any{
+					"spec": map[string]any{},
+				},
+				crd: extv1.CustomResourceDefinition{
+					Spec: extv1.CustomResourceDefinitionSpec{
+						Group: "example.com",
+						Versions: []extv1.CustomResourceDefinitionVersion{
+							{
+								Name: "v1",
+								Schema: &extv1.CustomResourceValidation{
+									OpenAPIV3Schema: &extv1.JSONSchemaProps{
+										Type: "object",
+										Properties: map[string]extv1.JSONSchemaProps{
+											"spec": {
+												Type: "object",
+												Properties: map[string]extv1.JSONSchemaProps{
+													"cooldown": {
+														Type:    "integer",
+														Default: &extv1.JSON{Raw: []byte(`5`)},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: map[string]any{
+				"spec": map[string]any{},
+			},
+			wantErr: true,
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			err := DefaultValues(tc.args.xr, tc.args.apiVersion, tc.args.crd)
+			if (err != nil) != tc.wantErr {
+				t.Errorf("DefaultValues() error = %v, wantErr %v", err, tc.wantErr)
+			}
+			if diff := cmp.Diff(tc.want, tc.args.xr); diff != "" {
+				t.Errorf("DefaultValues() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/internal/xcrd/crd_test.go b/internal/xcrd/crd_test.go
index 5945ab568..71bec7b0d 100644
--- a/internal/xcrd/crd_test.go
+++ b/internal/xcrd/crd_test.go
@@ -387,13 +387,6 @@ func TestForCompositeResource(t *testing.T) {
 															},
 															XListType: ptr.To("atomic"),
 														},
-														"writeConnectionSecretToRef": {
-															Type:     "object",
-															Required: []string{"name"},
-															Properties: map[string]extv1.JSONSchemaProps{
-																"name": {Type: "string"},
-															},
-														},
 													},
 												},
 											},
diff --git a/internal/xresource/unstructured/composite/composite_test.go b/internal/xresource/unstructured/composite/composite_test.go
index ed709f56a..91ed911b9 100644
--- a/internal/xresource/unstructured/composite/composite_test.go
+++ b/internal/xresource/unstructured/composite/composite_test.go
@@ -398,7 +398,7 @@ func TestWriteConnectionSecretToReference(t *testing.T) {
 		want *xpv1.SecretReference
 	}{
 		"NewRef": {
-			u:    New(),
+			u:    New(WithSchema(SchemaLegacy)),
 			set:  ref,
 			want: ref,
 		},
diff --git a/test/e2e/apiextensions_compositions_legacy_test.go b/test/e2e/apiextensions_compositions_legacy_test.go
index f0c54bf9e..0537e6973 100644
--- a/test/e2e/apiextensions_compositions_legacy_test.go
+++ b/test/e2e/apiextensions_compositions_legacy_test.go
@@ -20,6 +20,8 @@ import (
 	"testing"
 	"time"
 
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/e2e-framework/pkg/features"
 	"sigs.k8s.io/e2e-framework/third_party/helm"
 
@@ -53,6 +55,9 @@ func TestLegacyComposition(t *testing.T) {
 			Assess("ClaimHasPatchedField",
 				funcs.ResourcesHaveFieldValueWithin(5*time.Minute, manifests, "claim.yaml", "status.coolerField", "I'M COOLER!"),
 			).
+			Assess("ConnectionSecretCreated",
+				funcs.ResourceHasFieldValueWithin(30*time.Second, &v1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "basic-secret"}}, "data[super]", "c2VjcmV0Cg=="),
+			).
 			WithTeardown("DeleteClaim", funcs.AllOf(
 				funcs.DeleteResources(manifests, "claim.yaml"),
 				funcs.ResourcesDeletedWithin(2*time.Minute, manifests, "claim.yaml"),
diff --git a/test/e2e/apiextensions_compositions_test.go b/test/e2e/apiextensions_compositions_test.go
index dc3b8867d..bb5aea82b 100644
--- a/test/e2e/apiextensions_compositions_test.go
+++ b/test/e2e/apiextensions_compositions_test.go
@@ -21,7 +21,6 @@ import (
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/e2e-framework/pkg/features"
 	"sigs.k8s.io/e2e-framework/third_party/helm"
 
@@ -118,9 +117,6 @@ func TestBasicCompositionNamespaced(t *testing.T) {
 			Assess("XRHasStatusField",
 				funcs.ResourcesHaveFieldValueWithin(5*time.Minute, manifests, "xr.yaml", "status.coolerField", "I'M COOLER!"),
 			).
-			Assess("ConnectionSecretCreated",
-				funcs.ResourceHasFieldValueWithin(30*time.Second, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "basic-secret-namespaced"}}, "data[super]", "c2VjcmV0Cg=="),
-			).
 			WithTeardown("DeleteXR", funcs.AllOf(
 				funcs.DeleteResources(manifests, "xr.yaml"),
 				funcs.ResourcesDeletedWithin(2*time.Minute, manifests, "xr.yaml"),
@@ -151,9 +147,6 @@ func TestBasicCompositionCluster(t *testing.T) {
 			Assess("XRHasStatusField",
 				funcs.ResourcesHaveFieldValueWithin(5*time.Minute, manifests, "xr.yaml", "status.coolerField", "I'M COOLER!"),
 			).
-			Assess("ConnectionSecretCreated",
-				funcs.ResourceHasFieldValueWithin(30*time.Second, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "basic-secret-cluster"}}, "data[super]", "c2VjcmV0Cg=="),
-			).
 			WithTeardown("DeleteXR", funcs.AllOf(
 				funcs.DeleteResources(manifests, "xr.yaml"),
 				funcs.ResourcesDeletedWithin(2*time.Minute, manifests, "xr.yaml"),
diff --git a/test/e2e/manifests/apiextensions/composition/basic-cluster/setup/composition.yaml b/test/e2e/manifests/apiextensions/composition/basic-cluster/setup/composition.yaml
index 66c782482..864da5da1 100644
--- a/test/e2e/manifests/apiextensions/composition/basic-cluster/setup/composition.yaml
+++ b/test/e2e/manifests/apiextensions/composition/basic-cluster/setup/composition.yaml
@@ -22,8 +22,6 @@ spec:
             resource:
               status:
                 coolerField: "I'M COOLER!"
-            connectionDetails:
-              super: c2VjcmV0Cg==  # The string 'secret'
           resources:
             configmap:
               resource:
diff --git a/test/e2e/manifests/apiextensions/composition/basic-cluster/setup/definition.yaml b/test/e2e/manifests/apiextensions/composition/basic-cluster/setup/definition.yaml
index 9d9f1918f..75ddbc4ad 100644
--- a/test/e2e/manifests/apiextensions/composition/basic-cluster/setup/definition.yaml
+++ b/test/e2e/manifests/apiextensions/composition/basic-cluster/setup/definition.yaml
@@ -8,8 +8,6 @@ spec:
   names:
     kind: ClusterTest
     plural: clustertests
-  connectionSecretKeys:
-  - super
   versions:
   - name: v1alpha1
     served: true
diff --git a/test/e2e/manifests/apiextensions/composition/basic-cluster/xr.yaml b/test/e2e/manifests/apiextensions/composition/basic-cluster/xr.yaml
index f0f597aa0..1967f47c5 100644
--- a/test/e2e/manifests/apiextensions/composition/basic-cluster/xr.yaml
+++ b/test/e2e/manifests/apiextensions/composition/basic-cluster/xr.yaml
@@ -7,6 +7,3 @@ spec:
   crossplane:
     compositionRef:
       name: basic-composition-cluster
-    writeConnectionSecretToRef:
-      name: basic-secret-cluster
-      namespace: default
diff --git a/test/e2e/manifests/apiextensions/composition/basic-namespaced/setup/composition.yaml b/test/e2e/manifests/apiextensions/composition/basic-namespaced/setup/composition.yaml
index 2f82f4fa7..14629bda1 100644
--- a/test/e2e/manifests/apiextensions/composition/basic-namespaced/setup/composition.yaml
+++ b/test/e2e/manifests/apiextensions/composition/basic-namespaced/setup/composition.yaml
@@ -22,8 +22,6 @@ spec:
             resource:
               status:
                 coolerField: "I'M COOLER!"
-            connectionDetails:
-              super: c2VjcmV0Cg==  # The string 'secret'
           resources:
             configmap:
               resource:
diff --git a/test/e2e/manifests/apiextensions/composition/basic-namespaced/setup/definition.yaml b/test/e2e/manifests/apiextensions/composition/basic-namespaced/setup/definition.yaml
index ec59f8896..aba3e0e81 100644
--- a/test/e2e/manifests/apiextensions/composition/basic-namespaced/setup/definition.yaml
+++ b/test/e2e/manifests/apiextensions/composition/basic-namespaced/setup/definition.yaml
@@ -8,8 +8,6 @@ spec:
   names:
     kind: Test
     plural: tests
-  connectionSecretKeys:
-  - super
   versions:
   - name: v1alpha1
     served: true
diff --git a/test/e2e/manifests/apiextensions/composition/basic-namespaced/xr.yaml b/test/e2e/manifests/apiextensions/composition/basic-namespaced/xr.yaml
index 82dfa114b..1f7c779b8 100644
--- a/test/e2e/manifests/apiextensions/composition/basic-namespaced/xr.yaml
+++ b/test/e2e/manifests/apiextensions/composition/basic-namespaced/xr.yaml
@@ -8,5 +8,3 @@ spec:
   crossplane:
     compositionRef:
       name: basic-composition-namespaced
-    writeConnectionSecretToRef:
-      name: basic-secret-namespaced
diff --git a/test/e2e/manifests/apiextensions/composition/legacy/claim.yaml b/test/e2e/manifests/apiextensions/composition/legacy/claim.yaml
index 563592abf..7b29d5c59 100644
--- a/test/e2e/manifests/apiextensions/composition/legacy/claim.yaml
+++ b/test/e2e/manifests/apiextensions/composition/legacy/claim.yaml
@@ -10,3 +10,5 @@ spec:
   # This is necessary to ensure the claim's MRs are actually gone before we
   # delete the Provider - https://github.com/crossplane/crossplane/issues/4251
   compositeDeletePolicy: Foreground
+  writeConnectionSecretToRef:
+    name: basic-secret
diff --git a/test/e2e/manifests/apiextensions/composition/legacy/setup/composition.yaml b/test/e2e/manifests/apiextensions/composition/legacy/setup/composition.yaml
index 8c1463135..74eef01be 100644
--- a/test/e2e/manifests/apiextensions/composition/legacy/setup/composition.yaml
+++ b/test/e2e/manifests/apiextensions/composition/legacy/setup/composition.yaml
@@ -22,6 +22,8 @@ spec:
             resource:
               status:
                 coolerField: "I'M COOLER!"
+            connectionDetails:
+              super: c2VjcmV0Cg==  # The string 'secret'
           resources:
             # We compose a nested XR, which in turn composes an MR. This tests
             # regression of https://github.com/crossplane/crossplane/issues/4715.
@@ -45,3 +47,4 @@ spec:
   - step: detect-readiness
     functionRef:
       name: function-auto-ready-ext-functions
+  writeConnectionSecretsToNamespace: crossplane-system
diff --git a/test/e2e/manifests/apiextensions/composition/legacy/setup/definition.yaml b/test/e2e/manifests/apiextensions/composition/legacy/setup/definition.yaml
index afe55de66..e09f3133c 100644
--- a/test/e2e/manifests/apiextensions/composition/legacy/setup/definition.yaml
+++ b/test/e2e/manifests/apiextensions/composition/legacy/setup/definition.yaml
@@ -11,7 +11,7 @@ spec:
     kind: NopResource
     plural: nopresources
   connectionSecretKeys:
-  - test
+  - super
   versions:
   - name: v1alpha1
     served: true
@@ -31,4 +31,4 @@ spec:
           type: object
           properties:
             coolerField:
-              type: string
\ No newline at end of file
+              type: string
diff --git a/test/e2e/manifests/protection/usage/standalone-namespaced/ref-without-by-cross-namespace-invalid/usage.yaml b/test/e2e/manifests/protection/usage/standalone-namespaced/ref-without-by-cross-namespace-invalid/usage.yaml
new file mode 100644
index 000000000..0588f2072
--- /dev/null
+++ b/test/e2e/manifests/protection/usage/standalone-namespaced/ref-without-by-cross-namespace-invalid/usage.yaml
@@ -0,0 +1,14 @@
+apiVersion: protection.crossplane.io/v1beta1
+kind: Usage
+metadata:
+  namespace: default
+  name: using-uses-used-across-namespaces-invalid
+spec:
+  replayDeletion: true
+  of:
+    apiVersion: v1
+    kind: ConfigMap
+    resourceRef:
+      name: using-resource
+      namespace: crossplane-system
+  reason: "This resource is protected across namespaces!"
diff --git a/test/e2e/manifests/protection/usage/standalone-namespaced/selector-without-by-cross-namespace-invalid/usage.yaml b/test/e2e/manifests/protection/usage/standalone-namespaced/selector-without-by-cross-namespace-invalid/usage.yaml
new file mode 100644
index 000000000..2401d6e49
--- /dev/null
+++ b/test/e2e/manifests/protection/usage/standalone-namespaced/selector-without-by-cross-namespace-invalid/usage.yaml
@@ -0,0 +1,15 @@
+apiVersion: protection.crossplane.io/v1beta1
+kind: Usage
+metadata:
+  namespace: default
+  name: using-uses-used-across-namespaces-invalid
+spec:
+  replayDeletion: true
+  of:
+    apiVersion: v1
+    kind: ConfigMap
+    resourceSelector:
+        matchLabels:
+            foo: bar
+        namespace: crossplane-system
+  reason: "This resource is protected across namespaces!"
diff --git a/test/e2e/protection_usage_test.go b/test/e2e/protection_usage_test.go
index 0ea709233..3e1070178 100644
--- a/test/e2e/protection_usage_test.go
+++ b/test/e2e/protection_usage_test.go
@@ -84,6 +84,20 @@ func TestUsageStandaloneNamespaced(t *testing.T) {
 				funcs.ResourcesDeletedWithin(30*time.Second, manifests, "with-reason/used.yaml"),
 			),
 		},
+		{
+			// Usages across namespaces should be blocked if the by resource is not specified, using resourceRef.
+			Name: "CrossNamespaceUsageBlockedWithoutByResourceRef",
+			Assessment: funcs.AllOf(
+				funcs.ResourcesFailToApply(FieldManager, manifests, "ref-without-by-cross-namespace-invalid/*.yaml"),
+			),
+		},
+		{
+			// Usages across namespaces should be blocked if the by resource is not specified, using resourceSelector.
+			Name: "CrossNamespaceUsageBlockedWithoutByResourceSelector",
+			Assessment: funcs.AllOf(
+				funcs.ResourcesFailToApply(FieldManager, manifests, "selector-without-by-cross-namespace-invalid/*.yaml"),
+			),
+		},
 	}
 
 	environment.Test(t,