Repository: nginx/nginx-gateway-fabric

Issue:
Enable Connection to NGINX One Console
**As an** NGINX Plus user of NGF
**I want** my NGF fleet management telemetry to be sent to the NGINX One Console or NIM
**So that** I can include my NGF installations in my fleet management view in the NGINX One Console
**And so that** I can later see Observability metrics as soon as possible with minimal setup.

### Acceptance
- When the user configures their [data plane key](https://docs.nginx.com/nginx-one/connect-instances/create-manage-data-plane-keys/) within NGF, fleet management telemetry will be sent to the URL specified by the user (N1C or NIM) or if none is specified, N1C by default.
- NGF-specific labels are appended to the telemetry data (via Agent) before it is sent.
  - product-type
  - product-version
  - cluster-id
  - control-name
  - control-id
  - control-namespace
  - ~~data-name (pending)~~
  - ~~data-id (pending)~~
  - ~~data-namespace (pending)~~


Edit: decided to not include data-plane metadata in this story until N1C implements support on their side. 

Discussion:

Solution patch:
diff --git a/charts/nginx-gateway-fabric/README.md b/charts/nginx-gateway-fabric/README.md
index 3bcafee8..d740c649 100644
--- a/charts/nginx-gateway-fabric/README.md
+++ b/charts/nginx-gateway-fabric/README.md
@@ -264,7 +264,7 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri
 | `certGenerator.ttlSecondsAfterFinished` | How long to wait after the cert generator job has finished before it is removed by the job controller. | int | `30` |
 | `clusterDomain` | The DNS cluster domain of your Kubernetes cluster. | string | `"cluster.local"` |
 | `gateways` | A list of Gateway objects. View https://gateway-api.sigs.k8s.io/reference/spec/#gateway for full Gateway reference. | list | `[]` |
-| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"config":{},"container":{"hostPorts":[],"lifecycle":{},"readinessProbe":{},"resources":{},"volumeMounts":[]},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","resolver":"","secretName":"nplus-license","skipVerify":false}}` |
+| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"config":{},"container":{"hostPorts":[],"lifecycle":{},"readinessProbe":{},"resources":{},"volumeMounts":[]},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","nginxOneConsole":{"dataplaneKeySecretName":"","endpointHost":"agent.connect.nginx.com","endpointPort":443,"skipVerify":false},"plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","resolver":"","secretName":"nplus-license","skipVerify":false}}` |
 | `nginx.config` | The configuration for the data plane that is contained in the NginxProxy resource. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{}` |
 | `nginx.container` | The container configuration for the NGINX container. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{"hostPorts":[],"lifecycle":{},"readinessProbe":{},"resources":{},"volumeMounts":[]}` |
 | `nginx.container.hostPorts` | A list of HostPorts to expose on the host. This configuration allows containers to bind to a specific port on the host node, enabling external network traffic to reach the container directly through the host's IP address and port. Use this option when you need to expose container ports on the host for direct access, such as for debugging, legacy integrations, or when NodePort/LoadBalancer services are not suitable. Note: Using hostPort may have security and scheduling implications, as it ties pods to specific nodes and ports. | list | `[]` |
@@ -276,6 +276,11 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri
 | `nginx.imagePullSecret` | The name of the secret containing docker registry credentials. Secret must exist in the same namespace as the helm release. The control plane will copy this secret into any namespace where NGINX is deployed. | string | `""` |
 | `nginx.imagePullSecrets` | A list of secret names containing docker registry credentials. Secrets must exist in the same namespace as the helm release. The control plane will copy these secrets into any namespace where NGINX is deployed. | list | `[]` |
 | `nginx.kind` | The kind of NGINX deployment. | string | `"deployment"` |
+| `nginx.nginxOneConsole` | Configuration for NGINX One Console. | object | `{"dataplaneKeySecretName":"","endpointHost":"agent.connect.nginx.com","endpointPort":443,"skipVerify":false}` |
+| `nginx.nginxOneConsole.dataplaneKeySecretName` | Name of the secret which holds the dataplane key that is required to authenticate with the NGINX One Console. Secret must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `""` |
+| `nginx.nginxOneConsole.endpointHost` | The Endpoint host that the NGINX One Console telemetry metrics will be sent to. | string | `"agent.connect.nginx.com"` |
+| `nginx.nginxOneConsole.endpointPort` | The endpoint port that the NGINX One Console telemetry metrics will be sent to. | int | `443` |
+| `nginx.nginxOneConsole.skipVerify` | Skip TLS verification for NGINX One Console connections. | bool | `false` |
 | `nginx.plus` | Is NGINX Plus image being used. | bool | `false` |
 | `nginx.pod` | The pod configuration for the NGINX data plane pod. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{}` |
 | `nginx.replicas` | The number of replicas of the NGINX Deployment. | int | `1` |
diff --git a/charts/nginx-gateway-fabric/templates/deployment.yaml b/charts/nginx-gateway-fabric/templates/deployment.yaml
index 6b34a7e9..9be1b13f 100644
--- a/charts/nginx-gateway-fabric/templates/deployment.yaml
+++ b/charts/nginx-gateway-fabric/templates/deployment.yaml
@@ -103,6 +103,18 @@ spec:
         {{- if .Capabilities.APIVersions.Has "security.openshift.io/v1/SecurityContextConstraints" }}
         - --nginx-scc={{ include "nginx-gateway.scc-name" . }}-nginx
         {{- end}}
+        {{- if .Values.nginx.nginxOneConsole.dataplaneKeySecretName }}
+        - --nginx-one-dataplane-key-secret={{ .Values.nginx.nginxOneConsole.dataplaneKeySecretName }}
+          {{- if .Values.nginx.nginxOneConsole.endpointHost }}
+        - --nginx-one-telemetry-endpoint-host={{ .Values.nginx.nginxOneConsole.endpointHost }}
+          {{- end }}
+          {{- if .Values.nginx.nginxOneConsole.endpointPort }}
+        - --nginx-one-telemetry-endpoint-port={{ .Values.nginx.nginxOneConsole.endpointPort }}
+          {{- end }}
+          {{- if .Values.nginx.nginxOneConsole.skipVerify }}
+        - --nginx-one-tls-skip-verify
+          {{- end }}
+        {{- end }}
         env:
         - name: POD_NAMESPACE
           valueFrom:
diff --git a/charts/nginx-gateway-fabric/values.schema.json b/charts/nginx-gateway-fabric/values.schema.json
index 88edebfd..16611858 100644
--- a/charts/nginx-gateway-fabric/values.schema.json
+++ b/charts/nginx-gateway-fabric/values.schema.json
@@ -445,6 +445,44 @@
           "required": [],
           "title": "kind"
         },
+        "nginxOneConsole": {
+          "description": "Configuration for NGINX One Console.",
+          "properties": {
+            "dataplaneKeySecretName": {
+              "default": "",
+              "description": "Name of the secret which holds the dataplane key that is required to authenticate with the NGINX One Console.\nSecret must exist in the same namespace that the NGINX Gateway Fabric control plane is running in\n(default namespace: nginx-gateway).",
+              "required": [],
+              "title": "dataplaneKeySecretName",
+              "type": "string"
+            },
+            "endpointHost": {
+              "default": "agent.connect.nginx.com",
+              "description": "The Endpoint host that the NGINX One Console telemetry metrics will be sent to.",
+              "required": [],
+              "title": "endpointHost",
+              "type": "string"
+            },
+            "endpointPort": {
+              "default": 443,
+              "description": "The endpoint port that the NGINX One Console telemetry metrics will be sent to.",
+              "maximum": 65535,
+              "minimum": 1,
+              "required": [],
+              "title": "endpointPort",
+              "type": "integer"
+            },
+            "skipVerify": {
+              "default": false,
+              "description": "Skip TLS verification for NGINX One Console connections.",
+              "required": [],
+              "title": "skipVerify",
+              "type": "boolean"
+            }
+          },
+          "required": [],
+          "title": "nginxOneConsole",
+          "type": "object"
+        },
         "plus": {
           "default": false,
           "description": "Is NGINX Plus image being used.",
diff --git a/charts/nginx-gateway-fabric/values.yaml b/charts/nginx-gateway-fabric/values.yaml
index 361a5076..a3c0bdcc 100644
--- a/charts/nginx-gateway-fabric/values.yaml
+++ b/charts/nginx-gateway-fabric/values.yaml
@@ -212,6 +212,27 @@ nginx:
   # -- Is NGINX Plus image being used.
   plus: false
 
+  # -- Configuration for NGINX One Console.
+  nginxOneConsole:
+    # -- Name of the secret which holds the dataplane key that is required to authenticate with the NGINX One Console.
+    # Secret must exist in the same namespace that the NGINX Gateway Fabric control plane is running in
+    # (default namespace: nginx-gateway).
+    dataplaneKeySecretName: ""
+
+    # -- The Endpoint host that the NGINX One Console telemetry metrics will be sent to.
+    endpointHost: "agent.connect.nginx.com"
+
+    # @schema
+    # type: integer
+    # minimum: 1
+    # maximum: 65535
+    # @schema
+    # -- The endpoint port that the NGINX One Console telemetry metrics will be sent to.
+    endpointPort: 443
+
+    # -- Skip TLS verification for NGINX One Console connections.
+    skipVerify: false
+
   # -- The name of the secret containing docker registry credentials.
   # Secret must exist in the same namespace as the helm release. The control
   # plane will copy this secret into any namespace where NGINX is deployed.
diff --git a/cmd/gateway/commands.go b/cmd/gateway/commands.go
index 37a30006..9f83cbcf 100644
--- a/cmd/gateway/commands.go
+++ b/cmd/gateway/commands.go
@@ -6,7 +6,6 @@ import (
 	"os"
 	"runtime/debug"
 	"strconv"
-	"strings"
 	"time"
 
 	"github.com/spf13/cobra"
@@ -38,8 +37,9 @@ const (
 		`The controller name must be of the form: DOMAIN/PATH. The controller's domain is '%s'`
 	plusFlag = "nginx-plus"
 
-	serverTLSSecret = "server-tls"
-	agentTLSSecret  = "agent-tls"
+	serverTLSSecret               = "server-tls"
+	agentTLSSecret                = "agent-tls"
+	nginxOneTelemetryEndpointHost = "agent.connect.nginx.com"
 )
 
 func createRootCommand() *cobra.Command {
@@ -58,27 +58,31 @@ func createRootCommand() *cobra.Command {
 func createControllerCommand() *cobra.Command {
 	// flag names
 	const (
-		configFlag                     = "config"
-		serviceFlag                    = "service"
-		agentTLSSecretFlag             = "agent-tls-secret"
-		metricsDisableFlag             = "metrics-disable"
-		metricsSecureFlag              = "metrics-secure-serving"
-		metricsPortFlag                = "metrics-port"
-		healthDisableFlag              = "health-disable"
-		healthPortFlag                 = "health-port"
-		leaderElectionDisableFlag      = "leader-election-disable"
-		leaderElectionLockNameFlag     = "leader-election-lock-name"
-		productTelemetryDisableFlag    = "product-telemetry-disable"
-		gwAPIExperimentalFlag          = "gateway-api-experimental-features"
-		nginxDockerSecretFlag          = "nginx-docker-secret" //nolint:gosec // not credentials
-		usageReportSecretFlag          = "usage-report-secret"
-		usageReportEndpointFlag        = "usage-report-endpoint"
-		usageReportResolverFlag        = "usage-report-resolver"
-		usageReportSkipVerifyFlag      = "usage-report-skip-verify"
-		usageReportClientSSLSecretFlag = "usage-report-client-ssl-secret" //nolint:gosec // not credentials
-		usageReportCASecretFlag        = "usage-report-ca-secret"         //nolint:gosec // not credentials
-		snippetsFiltersFlag            = "snippets-filters"
-		nginxSCCFlag                   = "nginx-scc"
+		configFlag                        = "config"
+		serviceFlag                       = "service"
+		agentTLSSecretFlag                = "agent-tls-secret"
+		nginxOneDataplaneKeySecretFlag    = "nginx-one-dataplane-key-secret" //nolint:gosec // not credentials
+		nginxOneTelemetryEndpointHostFlag = "nginx-one-telemetry-endpoint-host"
+		nginxOneTelemetryEndpointPortFlag = "nginx-one-telemetry-endpoint-port"
+		nginxOneTLSSkipVerifyFlag         = "nginx-one-tls-skip-verify"
+		metricsDisableFlag                = "metrics-disable"
+		metricsSecureFlag                 = "metrics-secure-serving"
+		metricsPortFlag                   = "metrics-port"
+		healthDisableFlag                 = "health-disable"
+		healthPortFlag                    = "health-port"
+		leaderElectionDisableFlag         = "leader-election-disable"
+		leaderElectionLockNameFlag        = "leader-election-lock-name"
+		productTelemetryDisableFlag       = "product-telemetry-disable"
+		gwAPIExperimentalFlag             = "gateway-api-experimental-features"
+		nginxDockerSecretFlag             = "nginx-docker-secret" //nolint:gosec // not credentials
+		usageReportSecretFlag             = "usage-report-secret"
+		usageReportEndpointFlag           = "usage-report-endpoint"
+		usageReportResolverFlag           = "usage-report-resolver"
+		usageReportSkipVerifyFlag         = "usage-report-skip-verify"
+		usageReportClientSSLSecretFlag    = "usage-report-client-ssl-secret" //nolint:gosec // not credentials
+		usageReportCASecretFlag           = "usage-report-ca-secret"         //nolint:gosec // not credentials
+		snippetsFiltersFlag               = "snippets-filters"
+		nginxSCCFlag                      = "nginx-scc"
 	)
 
 	// flag values
@@ -101,7 +105,19 @@ func createControllerCommand() *cobra.Command {
 			validator: validateResourceName,
 			value:     agentTLSSecret,
 		}
-		nginxSCCName = stringValidatingValue{
+		nginxOneConsoleDataplaneKeySecretName = stringValidatingValue{
+			validator: validateResourceName,
+		}
+		nginxOneConsoleTelemetryEndpointHost = stringValidatingValue{
+			validator: validateResourceName,
+			value:     nginxOneTelemetryEndpointHost,
+		}
+		nginxOneConsoleTelemetryEndpointPort = intValidatingValue{
+			validator: validateAnyPort,
+			value:     443,
+		}
+		nginxOneConsoleTLSSkipVerify bool
+		nginxSCCName                 = stringValidatingValue{
 			validator: validateResourceName,
 		}
 		disableMetrics    bool
@@ -257,6 +273,12 @@ func createControllerCommand() *cobra.Command {
 				NginxDockerSecretNames: nginxDockerSecrets.values,
 				AgentTLSSecretName:     agentTLSSecretName.value,
 				NGINXSCCName:           nginxSCCName.value,
+				NginxOneConsoleTelemetryConfig: config.NginxOneConsoleTelemetryConfig{
+					DataplaneKeySecretName: nginxOneConsoleDataplaneKeySecretName.value,
+					EndpointHost:           nginxOneConsoleTelemetryEndpointHost.value,
+					EndpointPort:           nginxOneConsoleTelemetryEndpointPort.value,
+					EndpointTLSSkipVerify:  nginxOneConsoleTLSSkipVerify,
+				},
 			}
 
 			if err := controller.StartManager(conf); err != nil {
@@ -304,6 +326,32 @@ func createControllerCommand() *cobra.Command {
 			`NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway).`,
 	)
 
+	cmd.Flags().Var(
+		&nginxOneConsoleDataplaneKeySecretName,
+		nginxOneDataplaneKeySecretFlag,
+		`The name of the Secret containing the NGINX One Console's dataplane key. Must exist in the same namespace that `+
+			`the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway).`,
+	)
+
+	cmd.Flags().Var(
+		&nginxOneConsoleTelemetryEndpointHost,
+		nginxOneTelemetryEndpointHostFlag,
+		`The host of the NGINX One Console's telemetry endpoint.`,
+	)
+
+	cmd.Flags().Var(
+		&nginxOneConsoleTelemetryEndpointPort,
+		nginxOneTelemetryEndpointPortFlag,
+		`The port of the NGINX One Console's telemetry endpoint.`,
+	)
+
+	cmd.Flags().BoolVar(
+		&nginxOneConsoleTLSSkipVerify,
+		nginxOneTLSSkipVerifyFlag,
+		false,
+		"Disable client verification of the NGINX One Console's telemetry endpoint server certificate.",
+	)
+
 	cmd.Flags().BoolVar(
 		&disableMetrics,
 		metricsDisableFlag,
@@ -741,19 +789,13 @@ func createGatewayPodConfig(version, svcName string) (config.GatewayPodConfig, e
 		return config.GatewayPodConfig{}, err
 	}
 
-	// use image tag version if set, otherwise fall back to binary version
-	ngfVersion := version
-	if imageParts := strings.Split(image, ":"); len(imageParts) == 2 {
-		ngfVersion = imageParts[1]
-	}
-
 	c := config.GatewayPodConfig{
 		ServiceName:  svcName,
 		Namespace:    ns,
 		Name:         name,
 		UID:          podUID,
 		InstanceName: instance,
-		Version:      ngfVersion,
+		Version:      version,
 		Image:        image,
 	}
 
diff --git a/cmd/gateway/validation.go b/cmd/gateway/validation.go
index a953c522..26b16d8b 100644
--- a/cmd/gateway/validation.go
+++ b/cmd/gateway/validation.go
@@ -157,6 +157,15 @@ func validatePort(port int) error {
 	return nil
 }
 
+// validateAnyPort makes sure a given port is inside the valid range for all ports.
+// This includes protected ports (1-1023) and unprivileged ports (1024-65535).
+func validateAnyPort(port int) error {
+	if port < 1 || port > 65535 {
+		return fmt.Errorf("port outside of valid port range [1 - 65535]: %v", port)
+	}
+	return nil
+}
+
 // ensureNoPortCollisions checks if the same port has been defined multiple times.
 func ensureNoPortCollisions(ports ...int) error {
 	seen := make(map[int]struct{})
diff --git a/internal/controller/config/config.go b/internal/controller/config/config.go
index 25630eb8..ff6744b1 100644
--- a/internal/controller/config/config.go
+++ b/internal/controller/config/config.go
@@ -12,36 +12,38 @@ const DefaultNginxMetricsPort = int32(9113)
 type Config struct {
 	// AtomicLevel is an atomically changeable, dynamic logging level.
 	AtomicLevel zap.AtomicLevel
-	// UsageReportConfig specifies the NGINX Plus usage reporting configuration.
-	UsageReportConfig UsageReportConfig
-	// ImageSource is the source of the NGINX Gateway image.
-	ImageSource string
-	// Flags contains the NGF command-line flag names and values.
-	Flags Flags
 	// GatewayPodConfig contains information about this Pod.
 	GatewayPodConfig GatewayPodConfig
 	// Logger is the Zap Logger used by all components.
 	Logger logr.Logger
-	// GatewayCtlrName is the name of this controller.
-	GatewayCtlrName string
+	// NGINXSCCName is the name of the SecurityContextConstraints for the NGINX Pods. Only applicable in OpenShift.
+	NGINXSCCName string
 	// ConfigName is the name of the NginxGateway resource for this controller.
 	ConfigName string
-	// GatewayClassName is the name of the GatewayClass resource that the Gateway will use.
-	GatewayClassName string
 	// AgentTLSSecretName is the name of the TLS Secret used by NGINX Agent to communicate with the control plane.
 	AgentTLSSecretName string
-	// NGINXSCCName is the name of the SecurityContextConstraints for the NGINX Pods. Only applicable in OpenShift.
-	NGINXSCCName string
-	// NginxDockerSecretNames are the names of any Docker registry Secrets for the NGINX container.
-	NginxDockerSecretNames []string
+	// GatewayClassName is the name of the GatewayClass resource that the Gateway will use.
+	GatewayClassName string
+	// ImageSource is the source of the NGINX Gateway image.
+	ImageSource string
+	// GatewayCtlrName is the name of this controller.
+	GatewayCtlrName string
+	// UsageReportConfig specifies the NGINX Plus usage reporting configuration.
+	UsageReportConfig UsageReportConfig
+	// Flags contains the NGF command-line flag names and values.
+	Flags Flags
 	// LeaderElection contains the configuration for leader election.
 	LeaderElection LeaderElectionConfig
+	// NginxDockerSecretNames are the names of any Docker registry Secrets for the NGINX container.
+	NginxDockerSecretNames []string
+	// NginxOneConsoleTelemetryConfig contains the configuration for NGINX One Console telemetry.
+	NginxOneConsoleTelemetryConfig NginxOneConsoleTelemetryConfig
 	// ProductTelemetryConfig contains the configuration for collecting product telemetry.
 	ProductTelemetryConfig ProductTelemetryConfig
-	// MetricsConfig specifies the metrics config.
-	MetricsConfig MetricsConfig
 	// HealthConfig specifies the health probe config.
 	HealthConfig HealthConfig
+	// MetricsConfig specifies the metrics config.
+	MetricsConfig MetricsConfig
 	// Plus indicates whether NGINX Plus is being used.
 	Plus bool
 	// ExperimentalFeatures indicates if experimental features are enabled.
@@ -134,3 +136,15 @@ type Flags struct {
 	// Each Value will be either true or false for boolean flags and default or user-defined for non-boolean flags.
 	Values []string
 }
+
+// NginxOneConsoleTelemetryConfig contains the configuration for NGINX One Console telemetry.
+type NginxOneConsoleTelemetryConfig struct {
+	// DataplaneKeySecretName is the name of the Secret containing the dataplane key.
+	DataplaneKeySecretName string
+	// EndpointHost is the host of the telemetry endpoint.
+	EndpointHost string
+	// EndpointPort is the port of the telemetry endpoint.
+	EndpointPort int
+	// EndpointTLSSkipVerify specifies whether to skip TLS verification for the telemetry endpoint.
+	EndpointTLSSkipVerify bool
+}
diff --git a/internal/controller/manager.go b/internal/controller/manager.go
index bb95a366..94c929d4 100644
--- a/internal/controller/manager.go
+++ b/internal/controller/manager.go
@@ -204,17 +204,18 @@ func StartManager(cfg config.Config) error {
 		ctx,
 		mgr,
 		provisioner.Config{
-			DeploymentStore:        nginxUpdater.NginxDeployments,
-			StatusQueue:            statusQueue,
-			Logger:                 cfg.Logger.WithName("provisioner"),
-			EventRecorder:          recorder,
-			GatewayPodConfig:       &cfg.GatewayPodConfig,
-			GCName:                 cfg.GatewayClassName,
-			AgentTLSSecretName:     cfg.AgentTLSSecretName,
-			NGINXSCCName:           cfg.NGINXSCCName,
-			Plus:                   cfg.Plus,
-			NginxDockerSecretNames: cfg.NginxDockerSecretNames,
-			PlusUsageConfig:        &cfg.UsageReportConfig,
+			DeploymentStore:                nginxUpdater.NginxDeployments,
+			StatusQueue:                    statusQueue,
+			Logger:                         cfg.Logger.WithName("provisioner"),
+			EventRecorder:                  recorder,
+			GatewayPodConfig:               &cfg.GatewayPodConfig,
+			GCName:                         cfg.GatewayClassName,
+			AgentTLSSecretName:             cfg.AgentTLSSecretName,
+			NGINXSCCName:                   cfg.NGINXSCCName,
+			Plus:                           cfg.Plus,
+			NginxDockerSecretNames:         cfg.NginxDockerSecretNames,
+			PlusUsageConfig:                &cfg.UsageReportConfig,
+			NginxOneConsoleTelemetryConfig: cfg.NginxOneConsoleTelemetryConfig,
 		},
 	)
 	if err != nil {
diff --git a/internal/controller/provisioner/eventloop.go b/internal/controller/provisioner/eventloop.go
index 7e076e21..98325115 100644
--- a/internal/controller/provisioner/eventloop.go
+++ b/internal/controller/provisioner/eventloop.go
@@ -31,15 +31,20 @@ func newEventLoop(
 	ngfNamespace string,
 	dockerSecrets []string,
 	agentTLSSecret string,
+	dataplaneKeySecret string,
 	usageConfig *config.UsageReportConfig,
 	isOpenshift bool,
 ) (*events.EventLoop, error) {
 	nginxResourceLabelPredicate := predicate.NginxLabelPredicate(selector)
 
-	secretsToWatch := make([]string, 0, len(dockerSecrets)+4)
+	secretsToWatch := make([]string, 0, len(dockerSecrets)+5)
 	secretsToWatch = append(secretsToWatch, agentTLSSecret)
 	secretsToWatch = append(secretsToWatch, dockerSecrets...)
 
+	if dataplaneKeySecret != "" {
+		secretsToWatch = append(secretsToWatch, dataplaneKeySecret)
+	}
+
 	if usageConfig != nil {
 		if usageConfig.SecretName != "" {
 			secretsToWatch = append(secretsToWatch, usageConfig.SecretName)
diff --git a/internal/controller/provisioner/handler.go b/internal/controller/provisioner/handler.go
index 03795457..e3327cab 100644
--- a/internal/controller/provisioner/handler.go
+++ b/internal/controller/provisioner/handler.go
@@ -256,6 +256,8 @@ func (h *eventHandler) provisionResourceForAllGateways(
 
 // deprovisionSecretsForAllGateways cleans up any secrets that a user deleted that were duplicated
 // for all Gateways. For example, NGINX Plus secrets.
+//
+//nolint:gocyclo // will refactor at some point
 func (h *eventHandler) deprovisionSecretsForAllGateways(ctx context.Context, secret string) error {
 	var allErrs []error
 
@@ -283,6 +285,10 @@ func (h *eventHandler) deprovisionSecretsForAllGateways(ctx context.Context, sec
 			if err := h.provisioner.deleteObject(ctx, &corev1.Secret{ObjectMeta: resources.PlusClientSSLSecret}); err != nil {
 				allErrs = append(allErrs, err)
 			}
+		case strings.HasSuffix(resources.DataplaneKeySecret.Name, secret):
+			if err := h.provisioner.deleteObject(ctx, &corev1.Secret{ObjectMeta: resources.DataplaneKeySecret}); err != nil {
+				allErrs = append(allErrs, err)
+			}
 		default:
 			for _, dockerSecret := range resources.DockerSecrets {
 				if strings.HasSuffix(dockerSecret.Name, secret) {
diff --git a/internal/controller/provisioner/objects.go b/internal/controller/provisioner/objects.go
index 47cd2212..4f9f71e2 100644
--- a/internal/controller/provisioner/objects.go
+++ b/internal/controller/provisioner/objects.go
@@ -71,6 +71,14 @@ func (p *NginxProvisioner) buildNginxResourceObjects(
 		}
 	}
 
+	var dataplaneKeySecretName string
+	if p.cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName != "" {
+		dataplaneKeySecretName = controller.CreateNginxResourceName(
+			resourceName,
+			p.cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName,
+		)
+	}
+
 	// map key is the new name, value is the original name
 	dockerSecretNames := make(map[string]string)
 	for _, name := range p.cfg.NginxDockerSecretNames {
@@ -112,6 +120,7 @@ func (p *NginxProvisioner) buildNginxResourceObjects(
 		jwtSecretName,
 		caSecretName,
 		clientSSLSecretName,
+		dataplaneKeySecretName,
 	)
 	if err != nil {
 		errs = append(errs, err)
@@ -158,6 +167,7 @@ func (p *NginxProvisioner) buildNginxResourceObjects(
 		jwtSecretName,
 		caSecretName,
 		clientSSLSecretName,
+		dataplaneKeySecretName,
 	)
 	if err != nil {
 		errs = append(errs, err)
@@ -190,6 +200,7 @@ func (p *NginxProvisioner) buildNginxSecrets(
 	jwtSecretName string,
 	caSecretName string,
 	clientSSLSecretName string,
+	dataplaneKeySecretName string,
 ) ([]client.Object, error) {
 	var secrets []client.Object
 	var errs []error
@@ -290,6 +301,24 @@ func (p *NginxProvisioner) buildNginxSecrets(
 		}
 	}
 
+	if dataplaneKeySecretName != "" {
+		newSecret, err := p.getAndUpdateSecret(
+			p.cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName,
+			metav1.ObjectMeta{
+				Name:        dataplaneKeySecretName,
+				Namespace:   objectMeta.Namespace,
+				Labels:      objectMeta.Labels,
+				Annotations: objectMeta.Annotations,
+			},
+			corev1.SecretTypeOpaque,
+		)
+		if err != nil {
+			errs = append(errs, err)
+		} else {
+			secrets = append(secrets, newSecret)
+		}
+	}
+
 	return secrets, errors.Join(errs...)
 }
 
@@ -379,12 +408,20 @@ func (p *NginxProvisioner) buildNginxConfigMaps(
 		"Namespace":     p.cfg.GatewayPodConfig.Namespace,
 		"EnableMetrics": enableMetrics,
 		"MetricsPort":   metricsPort,
+		"AgentLabels":   p.cfg.AgentLabels,
 	}
 
 	if logging != nil && logging.AgentLevel != nil {
 		agentFields["LogLevel"] = *logging.AgentLevel
 	}
 
+	if p.cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName != "" {
+		agentFields["NginxOneReporting"] = true
+		agentFields["EndpointHost"] = p.cfg.NginxOneConsoleTelemetryConfig.EndpointHost
+		agentFields["EndpointPort"] = strconv.Itoa(p.cfg.NginxOneConsoleTelemetryConfig.EndpointPort)
+		agentFields["EndpointTLSSkipVerify"] = p.cfg.NginxOneConsoleTelemetryConfig.EndpointTLSSkipVerify
+	}
+
 	agentCM := &corev1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        ngxAgentConfigMapName,
@@ -540,6 +577,7 @@ func (p *NginxProvisioner) buildNginxDeployment(
 	jwtSecretName string,
 	caSecretName string,
 	clientSSLSecretName string,
+	dataplaneKeySecretName string,
 ) (client.Object, error) {
 	podTemplateSpec := p.buildNginxPodTemplateSpec(
 		objectMeta,
@@ -552,6 +590,7 @@ func (p *NginxProvisioner) buildNginxDeployment(
 		jwtSecretName,
 		caSecretName,
 		clientSSLSecretName,
+		dataplaneKeySecretName,
 	)
 
 	if nProxyCfg != nil && nProxyCfg.Kubernetes != nil && nProxyCfg.Kubernetes.DaemonSet != nil {
@@ -671,6 +710,7 @@ func (p *NginxProvisioner) buildNginxPodTemplateSpec(
 	jwtSecretName string,
 	caSecretName string,
 	clientSSLSecretName string,
+	dataplaneKeySecretName string,
 ) corev1.PodTemplateSpec {
 	containerPorts := make([]corev1.ContainerPort, 0, len(ports))
 	for port := range ports {
@@ -983,6 +1023,22 @@ func (p *NginxProvisioner) buildNginxPodTemplateSpec(
 		spec.Spec.Containers[0].VolumeMounts = volumeMounts
 	}
 
+	if p.cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName != "" {
+		volumeMounts := spec.Spec.Containers[0].VolumeMounts
+
+		volumeMounts = append(volumeMounts, corev1.VolumeMount{
+			Name:      "agent-dataplane-key",
+			MountPath: "/etc/nginx-agent/secrets/dataplane.key",
+			SubPath:   "dataplane.key",
+		})
+		spec.Spec.Volumes = append(spec.Spec.Volumes, corev1.Volume{
+			Name:         "agent-dataplane-key",
+			VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: dataplaneKeySecretName}},
+		})
+
+		spec.Spec.Containers[0].VolumeMounts = volumeMounts
+	}
+
 	return spec
 }
 
@@ -1134,6 +1190,21 @@ func (p *NginxProvisioner) buildNginxResourceObjectsForDeletion(deploymentNSName
 		objects = append(objects, jwtSecret)
 	}
 
+	var dataplaneKeySecretName string
+	if p.cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName != "" {
+		dataplaneKeySecretName = controller.CreateNginxResourceName(
+			deploymentNSName.Name,
+			p.cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName,
+		)
+		dataplaneKeySecret := &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      dataplaneKeySecretName,
+				Namespace: deploymentNSName.Namespace,
+			},
+		}
+		objects = append(objects, dataplaneKeySecret)
+	}
+
 	return objects
 }
 
diff --git a/internal/controller/provisioner/provisioner.go b/internal/controller/provisioner/provisioner.go
index 7cfb91f3..6ebc5dc1 100644
--- a/internal/controller/provisioner/provisioner.go
+++ b/internal/controller/provisioner/provisioner.go
@@ -28,6 +28,7 @@ import (
 	"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/provisioner/openshift"
 	"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/state/graph"
 	"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/status"
+	"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/telemetry"
 	"github.com/nginx/nginx-gateway-fabric/v2/internal/framework/controller"
 	"github.com/nginx/nginx-gateway-fabric/v2/internal/framework/events"
 )
@@ -43,19 +44,19 @@ type Provisioner interface {
 
 // Config is the configuration for the Provisioner.
 type Config struct {
-	GCName             string
-	AgentTLSSecretName string
-	NGINXSCCName       string
-
-	DeploymentStore        agent.DeploymentStorer
-	StatusQueue            *status.Queue
-	GatewayPodConfig       *config.GatewayPodConfig
-	PlusUsageConfig        *config.UsageReportConfig
-	EventRecorder          record.EventRecorder
-	Logger                 logr.Logger
-	NginxDockerSecretNames []string
-
-	Plus bool
+	DeploymentStore                agent.DeploymentStorer
+	EventRecorder                  record.EventRecorder
+	PlusUsageConfig                *config.UsageReportConfig
+	StatusQueue                    *status.Queue
+	GatewayPodConfig               *config.GatewayPodConfig
+	AgentLabels                    map[string]string
+	Logger                         logr.Logger
+	NGINXSCCName                   string
+	GCName                         string
+	AgentTLSSecretName             string
+	NginxDockerSecretNames         []string
+	NginxOneConsoleTelemetryConfig config.NginxOneConsoleTelemetryConfig
+	Plus                           bool
 }
 
 // NginxProvisioner handles provisioning nginx kubernetes resources.
@@ -75,6 +76,23 @@ type NginxProvisioner struct {
 
 var apiChecker openshift.APIChecker = &openshift.APICheckerImpl{}
 
+var labelCollectorFactory func(mgr manager.Manager, cfg Config) AgentLabelCollector = defaultLabelCollectorFactory
+
+func defaultLabelCollectorFactory(mgr manager.Manager, cfg Config) AgentLabelCollector {
+	return telemetry.NewLabelCollector(telemetry.LabelCollectorConfig{
+		K8sClientReader: mgr.GetAPIReader(),
+		Version:         cfg.GatewayPodConfig.Version,
+		PodNSName: types.NamespacedName{
+			Namespace: cfg.GatewayPodConfig.Namespace,
+			Name:      cfg.GatewayPodConfig.Name,
+		},
+	})
+}
+
+type AgentLabelCollector interface {
+	Collect(ctx context.Context) (map[string]string, error)
+}
+
 // NewNginxProvisioner returns a new instance of a Provisioner that will deploy nginx resources.
 func NewNginxProvisioner(
 	ctx context.Context,
@@ -88,12 +106,18 @@ func NewNginxProvisioner(
 		clientSSLSecretName = cfg.PlusUsageConfig.ClientSSLSecretName
 	}
 
+	var dataplaneKeySecretName string
+	if cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName != "" {
+		dataplaneKeySecretName = cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName
+	}
+
 	store := newStore(
 		cfg.NginxDockerSecretNames,
 		cfg.AgentTLSSecretName,
 		jwtSecretName,
 		caSecretName,
 		clientSSLSecretName,
+		dataplaneKeySecretName,
 	)
 
 	selector := metav1.LabelSelector{
@@ -111,6 +135,13 @@ func NewNginxProvisioner(
 		cfg.Logger.Error(err, "could not determine if running in openshift, will not create Role/RoleBinding")
 	}
 
+	agentLabelCollector := labelCollectorFactory(mgr, cfg)
+	agentLabels, err := agentLabelCollector.Collect(ctx)
+	if err != nil {
+		cfg.Logger.Error(err, "failed to collect agent labels")
+	}
+	cfg.AgentLabels = agentLabels
+
 	provisioner := &NginxProvisioner{
 		k8sClient:                  mgr.GetClient(),
 		store:                      store,
@@ -134,6 +165,7 @@ func NewNginxProvisioner(
 		cfg.GatewayPodConfig.Namespace,
 		cfg.NginxDockerSecretNames,
 		cfg.AgentTLSSecretName,
+		dataplaneKeySecretName,
 		cfg.PlusUsageConfig,
 		isOpenshift,
 	)
@@ -417,6 +449,10 @@ func (p *NginxProvisioner) isUserSecret(name string) bool {
 		return true
 	}
 
+	if p.cfg.NginxOneConsoleTelemetryConfig.DataplaneKeySecretName == name {
+		return true
+	}
+
 	if p.cfg.PlusUsageConfig != nil {
 		return name == p.cfg.PlusUsageConfig.SecretName ||
 			name == p.cfg.PlusUsageConfig.CASecretName ||
diff --git a/internal/controller/provisioner/store.go b/internal/controller/provisioner/store.go
index 2eb0d154..19d0a194 100644
--- a/internal/controller/provisioner/store.go
+++ b/internal/controller/provisioner/store.go
@@ -31,6 +31,7 @@ type NginxResources struct {
 	PlusJWTSecret       metav1.ObjectMeta
 	PlusClientSSLSecret metav1.ObjectMeta
 	PlusCASecret        metav1.ObjectMeta
+	DataplaneKeySecret  metav1.ObjectMeta
 	DockerSecrets       []metav1.ObjectMeta
 }
 
@@ -50,6 +51,9 @@ type store struct {
 	caSecretName        string
 	clientSSLSecretName string
 
+	// NGINX One Dataplane key secret
+	dataplaneKeySecretName string
+
 	lock sync.RWMutex
 }
 
@@ -58,7 +62,8 @@ func newStore(
 	agentTLSSecretName,
 	jwtSecretName,
 	caSecretName,
-	clientSSLSecretName string,
+	clientSSLSecretName,
+	dataplaneKeySecretName string,
 ) *store {
 	dockerSecretNamesMap := make(map[string]struct{})
 	for _, name := range dockerSecretNames {
@@ -66,13 +71,14 @@ func newStore(
 	}
 
 	return &store{
-		gateways:            make(map[types.NamespacedName]*gatewayv1.Gateway),
-		nginxResources:      make(map[types.NamespacedName]*NginxResources),
-		dockerSecretNames:   dockerSecretNamesMap,
-		agentTLSSecretName:  agentTLSSecretName,
-		jwtSecretName:       jwtSecretName,
-		caSecretName:        caSecretName,
-		clientSSLSecretName: clientSSLSecretName,
+		gateways:               make(map[types.NamespacedName]*gatewayv1.Gateway),
+		nginxResources:         make(map[types.NamespacedName]*NginxResources),
+		dockerSecretNames:      dockerSecretNamesMap,
+		agentTLSSecretName:     agentTLSSecretName,
+		jwtSecretName:          jwtSecretName,
+		caSecretName:           caSecretName,
+		clientSSLSecretName:    clientSSLSecretName,
+		dataplaneKeySecretName: dataplaneKeySecretName,
 	}
 }
 
@@ -226,6 +232,10 @@ func (s *store) registerSecretInGatewayConfig(obj *corev1.Secret, gatewayNSName
 			s.nginxResources[gatewayNSName] = &NginxResources{
 				PlusClientSSLSecret: obj.ObjectMeta,
 			}
+		case hasSuffix(obj.GetName(), s.dataplaneKeySecretName):
+			s.nginxResources[gatewayNSName] = &NginxResources{
+				DataplaneKeySecret: obj.ObjectMeta,
+			}
 		}
 
 		for secret := range s.dockerSecretNames {
@@ -246,6 +256,8 @@ func (s *store) registerSecretInGatewayConfig(obj *corev1.Secret, gatewayNSName
 			cfg.PlusCASecret = obj.ObjectMeta
 		case hasSuffix(obj.GetName(), s.clientSSLSecretName):
 			cfg.PlusClientSSLSecret = obj.ObjectMeta
+		case hasSuffix(obj.GetName(), s.dataplaneKeySecretName):
+			cfg.DataplaneKeySecret = obj.ObjectMeta
 		}
 
 		for secret := range s.dockerSecretNames {
@@ -357,6 +369,10 @@ func secretResourceMatches(resources *NginxResources, nsName types.NamespacedNam
 		return true
 	}
 
+	if resourceMatches(resources.DataplaneKeySecret, nsName) {
+		return true
+	}
+
 	return resourceMatches(resources.PlusCASecret, nsName)
 }
 
@@ -437,6 +453,9 @@ func getResourceVersionForSecret(resources *NginxResources, secret *corev1.Secre
 	if resources.PlusCASecret.GetName() == secret.GetName() {
 		return resources.PlusCASecret.GetResourceVersion()
 	}
+	if resources.DataplaneKeySecret.GetName() == secret.GetName() {
+		return resources.DataplaneKeySecret.GetResourceVersion()
+	}
 
 	return ""
 }
diff --git a/internal/controller/provisioner/templates.go b/internal/controller/provisioner/templates.go
index 9e557ffe..c6eb8283 100644
--- a/internal/controller/provisioner/templates.go
+++ b/internal/controller/provisioner/templates.go
@@ -12,7 +12,7 @@ const mainTemplateText = `
 error_log stderr {{ .ErrorLevel }};
 
 events {
-  worker_connections {{ .WorkerConnections }};
+    worker_connections {{ .WorkerConnections }};
 }`
 
 const mgmtTemplateText = `mgmt {
@@ -61,21 +61,32 @@ features:
 log:
     level: {{ .LogLevel }}
 {{- end }}
+labels:
+    {{- range $key, $value := .AgentLabels }}
+    {{ $key }}: {{ $value }}
+    {{- end }}
+
+{{- if .NginxOneReporting }}
+auxiliary_command:
+    server:
+        host: {{ .EndpointHost }}
+        port: {{ .EndpointPort }}
+        type: grpc
+    auth:
+        tokenpath: /etc/nginx-agent/secrets/dataplane.key
+    tls:
+        skip_verify: {{ .EndpointTLSSkipVerify }}
+{{- end }}
 {{- if .EnableMetrics }}
 collector:
-    receivers:
-        container_metrics:
-            collection_interval: 1m0s
-        host_metrics:
-            collection_interval: 1m0s
-            initial_delay: 1s
-            scrapers:
-                network: {}
-    processors:
-        batch: {}
     exporters:
         prometheus:
             server:
                 host: "0.0.0.0"
                 port: {{ .MetricsPort }}
+    pipelines:
+        metrics:
+            "ngf":
+                receivers: ["host_metrics", "nginx_metrics"]
+                exporters: ["prometheus"]
 {{- end }}`
diff --git a/internal/controller/telemetry/agent_labels.go b/internal/controller/telemetry/agent_labels.go
new file mode 100644
index 00000000..d6a8d8b7
--- /dev/null
+++ b/internal/controller/telemetry/agent_labels.go
@@ -0,0 +1,62 @@
+package telemetry
+
+import (
+	"context"
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// LabelCollectorConfig holds configuration parameters for LabelCollector.
+type LabelCollectorConfig struct {
+	// K8sClientReader is a Kubernetes API client Reader.
+	K8sClientReader client.Reader
+	// Version is the NGF version.
+	Version string
+	// PodNSName is the NamespacedName of the NGF Pod.
+	PodNSName types.NamespacedName
+}
+
+// LabelCollector is an implementation of AgentLabelCollector.
+type LabelCollector struct {
+	cfg LabelCollectorConfig
+}
+
+// NewLabelCollector creates a new LabelCollector.
+func NewLabelCollector(
+	cfg LabelCollectorConfig,
+) *LabelCollector {
+	return &LabelCollector{
+		cfg: cfg,
+	}
+}
+
+// Collect gathers metadata labels needed for reporting to Agent v3.
+func (l *LabelCollector) Collect(ctx context.Context) (map[string]string, error) {
+	agentLabels := make(map[string]string)
+
+	clusterID, err := collectClusterID(ctx, l.cfg.K8sClientReader)
+	if err != nil {
+		return nil, fmt.Errorf("failed to collect cluster information: %w", err)
+	}
+
+	replicaSet, err := getPodReplicaSet(ctx, l.cfg.K8sClientReader, l.cfg.PodNSName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get replica set for pod %v: %w", l.cfg.PodNSName, err)
+	}
+
+	deploymentID, err := getDeploymentID(replicaSet)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get NGF deploymentID: %w", err)
+	}
+
+	agentLabels["product-type"] = "ngf"
+	agentLabels["product-version"] = l.cfg.Version
+	agentLabels["cluster-id"] = clusterID
+	agentLabels["control-name"] = l.cfg.PodNSName.Name
+	agentLabels["control-namespace"] = l.cfg.PodNSName.Namespace
+	agentLabels["control-id"] = deploymentID
+
+	return agentLabels, nil
+}



Test patch:
diff --git a/cmd/gateway/commands_test.go b/cmd/gateway/commands_test.go
index ba65d2fa..0ecd4111 100644
--- a/cmd/gateway/commands_test.go
+++ b/cmd/gateway/commands_test.go
@@ -156,6 +156,10 @@ func TestControllerCmdFlagValidation(t *testing.T) {
 				"--usage-report-client-ssl-secret=client-secret",
 				"--snippets-filters",
 				"--nginx-scc=nginx-sscc-name",
+				"--nginx-one-dataplane-key-secret=dataplane-key-secret",
+				"--nginx-one-telemetry-endpoint-host=telemetry-endpoint-host",
+				"--nginx-one-telemetry-endpoint-port=443",
+				"--nginx-one-tls-skip-verify",
 			},
 			wantErr: false,
 		},
@@ -426,6 +430,66 @@ func TestControllerCmdFlagValidation(t *testing.T) {
 			wantErr:           true,
 			expectedErrPrefix: `invalid argument "!@#$" for "--nginx-scc" flag: invalid format: `,
 		},
+		{
+			name: "nginx-one-dataplane-key-secret is set to empty string",
+			args: []string{
+				"--nginx-one-dataplane-key-secret=",
+			},
+			wantErr:           true,
+			expectedErrPrefix: `invalid argument "" for "--nginx-one-dataplane-key-secret" flag: must be set`,
+		},
+		{
+			name: "nginx-one-dataplane-key-secret is invalid",
+			args: []string{
+				"--nginx-one-dataplane-key-secret=!@#$",
+			},
+			wantErr:           true,
+			expectedErrPrefix: `invalid argument "!@#$" for "--nginx-one-dataplane-key-secret" flag: invalid format: `,
+		},
+		{
+			name: "nginx-one-telemetry-endpoint-host is set to empty string",
+			args: []string{
+				"--nginx-one-telemetry-endpoint-host=",
+			},
+			wantErr:           true,
+			expectedErrPrefix: `invalid argument "" for "--nginx-one-telemetry-endpoint-host" flag: must be set`,
+		},
+		{
+			name: "nginx-one-telemetry-endpoint-host is invalid",
+			args: []string{
+				"--nginx-one-telemetry-endpoint-host=!@#$",
+			},
+			wantErr: true,
+			expectedErrPrefix: `invalid argument "!@#$" for "--nginx-one-telemetry-endpoint-host" ` +
+				`flag: invalid format: `,
+		},
+		{
+			name: "nginx-one-telemetry-endpoint-port is invalid type",
+			args: []string{
+				"--nginx-one-telemetry-endpoint-port=invalid", // not an int
+			},
+			wantErr: true,
+			expectedErrPrefix: `invalid argument "invalid" for "--nginx-one-telemetry-endpoint-port" ` +
+				`flag: failed to parse int value: strconv.ParseInt: parsing "invalid": invalid syntax`,
+		},
+		{
+			name: "nginx-one-telemetry-endpoint-port is outside of range",
+			args: []string{
+				"--nginx-one-telemetry-endpoint-port=65536", // outside of range
+			},
+			wantErr: true,
+			expectedErrPrefix: `invalid argument "65536" for "--nginx-one-telemetry-endpoint-port" flag:` +
+				` port outside of valid port range [1 - 65535]: 65536`,
+		},
+		{
+			name: "nginx-one-tls-skip-verify is not a bool",
+			expectedErrPrefix: `invalid argument "not-a-bool" for "--nginx-one-tls-skip-verify" flag:` +
+				` strconv.ParseBool: parsing "not-a-bool": invalid syntax`,
+			args: []string{
+				"--nginx-one-tls-skip-verify=not-a-bool",
+			},
+			wantErr: true,
+		},
 	}
 
 	// common flags validation is tested separately
@@ -753,21 +817,13 @@ func TestCreateGatewayPodConfig(t *testing.T) {
 		Name:         "my-pod",
 		UID:          "1234",
 		InstanceName: "my-pod-xyz",
-		Version:      "tag",
+		Version:      "0.0.0",
 		Image:        "my-pod-image:tag",
 	}
 	cfg, err := createGatewayPodConfig(version, "svc")
 	g.Expect(err).To(Not(HaveOccurred()))
 	g.Expect(cfg).To(Equal(expCfg))
 
-	// unset image tag and use provided version
-	g.Expect(os.Setenv("IMAGE_NAME", "my-pod-image")).To(Succeed())
-	expCfg.Version = version
-	expCfg.Image = "my-pod-image"
-	cfg, err = createGatewayPodConfig(version, "svc")
-	g.Expect(err).To(Not(HaveOccurred()))
-	g.Expect(cfg).To(Equal(expCfg))
-
 	// unset image name
 	g.Expect(os.Unsetenv("IMAGE_NAME")).To(Succeed())
 	cfg, err = createGatewayPodConfig(version, "svc")
diff --git a/cmd/gateway/validation_test.go b/cmd/gateway/validation_test.go
index 665bd915..ab5c94e6 100644
--- a/cmd/gateway/validation_test.go
+++ b/cmd/gateway/validation_test.go
@@ -414,6 +414,45 @@ func TestValidatePort(t *testing.T) {
 	}
 }
 
+func TestProtocolPort(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name   string
+		port   int
+		expErr bool
+	}{
+		{
+			name:   "port under minimum allowed value",
+			port:   0,
+			expErr: true,
+		},
+		{
+			name:   "port over maximum allowed value",
+			port:   65536,
+			expErr: true,
+		},
+		{
+			name:   "valid port",
+			port:   443,
+			expErr: false,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			g := NewWithT(t)
+
+			err := validateAnyPort(tc.port)
+			if !tc.expErr {
+				g.Expect(err).ToNot(HaveOccurred())
+			} else {
+				g.Expect(err).To(HaveOccurred())
+			}
+		})
+	}
+}
+
 func TestEnsureNoPortCollisions(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
diff --git a/internal/controller/provisioner/handler_test.go b/internal/controller/provisioner/handler_test.go
index 45fd0cc4..4c705cd0 100644
--- a/internal/controller/provisioner/handler_test.go
+++ b/internal/controller/provisioner/handler_test.go
@@ -23,7 +23,7 @@ func TestHandleEventBatch_Upsert(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
 
-	store := newStore([]string{dockerTestSecretName}, "", jwtTestSecretName, "", "")
+	store := newStore([]string{dockerTestSecretName}, "", jwtTestSecretName, "", "", "")
 	provisioner, fakeClient, _ := defaultNginxProvisioner()
 	provisioner.cfg.StatusQueue = status.NewQueue()
 
@@ -213,6 +213,7 @@ func TestHandleEventBatch_Delete(t *testing.T) {
 		jwtTestSecretName,
 		caTestSecretName,
 		clientTestSecretName,
+		nginxOneDataplaneKeySecretName,
 	)
 	provisioner, fakeClient, _ := defaultNginxProvisioner()
 	provisioner.cfg.StatusQueue = status.NewQueue()
@@ -298,6 +299,14 @@ func TestHandleEventBatch_Delete(t *testing.T) {
 	}
 	g.Expect(fakeClient.Create(ctx, userDockerSecret)).To(Succeed())
 
+	userDataplaneKeySecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      nginxOneDataplaneKeySecretName,
+			Namespace: ngfNamespace,
+		},
+	}
+	g.Expect(fakeClient.Create(ctx, userDataplaneKeySecret)).To(Succeed())
+
 	upsertEvent := &events.UpsertEvent{Resource: gateway}
 	batch := events.EventBatch{upsertEvent}
 	handler.HandleEventBatch(ctx, logger, batch)
@@ -341,6 +350,7 @@ func TestHandleEventBatch_Delete(t *testing.T) {
 	verifySecret(caTestSecretName, userCASecret)
 	verifySecret(clientTestSecretName, userClientSSLSecret)
 	verifySecret(dockerTestSecretName, userDockerSecret)
+	verifySecret(nginxOneDataplaneKeySecretName, userDataplaneKeySecret)
 
 	// delete Gateway when provisioner is not leader
 	provisioner.leader = false
diff --git a/internal/controller/provisioner/objects_test.go b/internal/controller/provisioner/objects_test.go
index 247e20ba..6b1b86fe 100644
--- a/internal/controller/provisioner/objects_test.go
+++ b/internal/controller/provisioner/objects_test.go
@@ -785,6 +785,86 @@ func TestBuildNginxResourceObjects_OpenShift(t *testing.T) {
 	g.Expect(roleBinding.GetLabels()).To(Equal(expLabels))
 }
 
+func TestBuildNginxResourceObjects_DataplaneKeySecret(t *testing.T) {
+	t.Parallel()
+	g := NewWithT(t)
+
+	agentTLSSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      agentTLSTestSecretName,
+			Namespace: ngfNamespace,
+		},
+		Data: map[string][]byte{"tls.crt": []byte("tls")},
+	}
+	dataplaneKeySecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "dataplane-key-secret",
+			Namespace: ngfNamespace,
+		},
+		Data: map[string][]byte{"dataplane.key": []byte("keydata")},
+	}
+	fakeClient := fake.NewFakeClient(agentTLSSecret, dataplaneKeySecret)
+
+	dataplaneKeySecretName := "dataplane-key-secret" //nolint:gosec // not credentials
+
+	provisioner := &NginxProvisioner{
+		cfg: Config{
+			GatewayPodConfig: &config.GatewayPodConfig{
+				Namespace: ngfNamespace,
+			},
+			AgentTLSSecretName: agentTLSTestSecretName,
+			NginxOneConsoleTelemetryConfig: config.NginxOneConsoleTelemetryConfig{
+				DataplaneKeySecretName: dataplaneKeySecretName,
+				EndpointHost:           "my.endpoint.com",
+				EndpointPort:           443,
+				EndpointTLSSkipVerify:  false,
+			},
+		},
+		k8sClient: fakeClient,
+		baseLabelSelector: metav1.LabelSelector{
+			MatchLabels: map[string]string{
+				"app": "nginx",
+			},
+		},
+	}
+
+	gateway := &gatewayv1.Gateway{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "gw",
+			Namespace: "default",
+		},
+	}
+
+	resourceName := "gw-nginx"
+	objects, err := provisioner.buildNginxResourceObjects(resourceName, gateway, &graph.EffectiveNginxProxy{})
+	g.Expect(err).ToNot(HaveOccurred())
+	g.Expect(objects).To(HaveLen(7)) // 2 secrets, 2 configmaps, serviceaccount, service, deployment
+
+	// Find the dataplane key secret
+	var found bool
+	for _, obj := range objects {
+		if s, ok := obj.(*corev1.Secret); ok {
+			if s.GetName() == controller.CreateNginxResourceName(resourceName, dataplaneKeySecretName) {
+				found = true
+				g.Expect(s.Data).To(HaveKey("dataplane.key"))
+				g.Expect(s.Data["dataplane.key"]).To(Equal([]byte("keydata")))
+			}
+		}
+	}
+	g.Expect(found).To(BeTrue())
+
+	// Check deployment mounts the secret
+	dep, ok := objects[6].(*appsv1.Deployment)
+	g.Expect(ok).To(BeTrue())
+	g.Expect(dep).ToNot(BeNil())
+	container := dep.Spec.Template.Spec.Containers[0]
+	g.Expect(container.VolumeMounts).To(ContainElement(corev1.VolumeMount{
+		Name:      "agent-dataplane-key",
+		MountPath: "/etc/nginx-agent/secrets/dataplane.key",
+		SubPath:   "dataplane.key",
+	}))
+}
+
 func TestGetAndUpdateSecret_NotFound(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
@@ -989,6 +1069,50 @@ func TestBuildNginxResourceObjectsForDeletion_OpenShift(t *testing.T) {
 	validateMeta(roleBinding, deploymentNSName.Name)
 }
 
+func TestBuildNginxResourceObjectsForDeletion_DataplaneKeySecret(t *testing.T) {
+	t.Parallel()
+	g := NewWithT(t)
+
+	dataplaneKeySecretName := "dataplane-key-secret" //nolint:gosec // not credentials
+
+	provisioner := &NginxProvisioner{
+		cfg: Config{
+			NginxOneConsoleTelemetryConfig: config.NginxOneConsoleTelemetryConfig{
+				DataplaneKeySecretName: dataplaneKeySecretName,
+			},
+			AgentTLSSecretName: agentTLSTestSecretName,
+		},
+	}
+
+	deploymentNSName := types.NamespacedName{
+		Name:      "gw-nginx",
+		Namespace: "default",
+	}
+
+	objects := provisioner.buildNginxResourceObjectsForDeletion(deploymentNSName)
+
+	// Should include the dataplane key secret in the objects list
+	// Default: deployment, daemonset, service, serviceaccount, 2 configmaps, agentTLSSecret, dataplaneKeySecret
+	g.Expect(objects).To(HaveLen(8))
+
+	validateMeta := func(obj client.Object, name string) {
+		g.Expect(obj.GetName()).To(Equal(name))
+		g.Expect(obj.GetNamespace()).To(Equal(deploymentNSName.Namespace))
+	}
+
+	// Validate the dataplane key secret is present
+	found := false
+	for _, obj := range objects {
+		if s, ok := obj.(*corev1.Secret); ok {
+			if s.GetName() == controller.CreateNginxResourceName(deploymentNSName.Name, dataplaneKeySecretName) {
+				validateMeta(s, controller.CreateNginxResourceName(deploymentNSName.Name, dataplaneKeySecretName))
+				found = true
+			}
+		}
+	}
+	g.Expect(found).To(BeTrue())
+}
+
 func TestSetIPFamily(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
@@ -1070,6 +1194,46 @@ func TestBuildNginxConfigMaps_WorkerConnections(t *testing.T) {
 	g.Expect(bootstrapCM.Data["main.conf"]).To(ContainSubstring("worker_connections 2048;"))
 }
 
+func TestBuildNginxConfigMaps_AgentFields(t *testing.T) {
+	t.Parallel()
+	g := NewWithT(t)
+
+	provisioner := &NginxProvisioner{
+		cfg: Config{
+			GatewayPodConfig: &config.GatewayPodConfig{
+				Namespace:   "default",
+				ServiceName: "test-service",
+			},
+			AgentLabels: map[string]string{
+				"key1": "val1",
+				"key2": "val2",
+			},
+			NginxOneConsoleTelemetryConfig: config.NginxOneConsoleTelemetryConfig{
+				DataplaneKeySecretName: "dataplane-key-secret",
+				EndpointHost:           "console.example.com",
+				EndpointPort:           443,
+				EndpointTLSSkipVerify:  false,
+			},
+		},
+	}
+	objectMeta := metav1.ObjectMeta{Name: "test", Namespace: "default"}
+
+	nProxyCfgEmpty := &graph.EffectiveNginxProxy{}
+
+	configMaps := provisioner.buildNginxConfigMaps(objectMeta, nProxyCfgEmpty, "test-bootstrap", "test-agent", true, true)
+	g.Expect(configMaps).To(HaveLen(2))
+
+	agentCM, ok := configMaps[1].(*corev1.ConfigMap)
+	g.Expect(ok).To(BeTrue())
+	data := agentCM.Data["nginx-agent.conf"]
+
+	g.Expect(data).To(ContainSubstring("key1: val1"))
+	g.Expect(data).To(ContainSubstring("key2: val2"))
+	g.Expect(data).To(ContainSubstring("host: console.example.com"))
+	g.Expect(data).To(ContainSubstring("port: 443"))
+	g.Expect(data).To(ContainSubstring("skip_verify: false"))
+}
+
 func TestBuildReadinessProbe(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/controller/provisioner/provisioner_test.go b/internal/controller/provisioner/provisioner_test.go
index 07de8ff5..1e1842b0 100644
--- a/internal/controller/provisioner/provisioner_test.go
+++ b/internal/controller/provisioner/provisioner_test.go
@@ -25,16 +25,18 @@ import (
 	"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/provisioner/openshift/openshiftfakes"
 	"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/state/graph"
 	"github.com/nginx/nginx-gateway-fabric/v2/internal/framework/controller"
+	"github.com/nginx/nginx-gateway-fabric/v2/internal/framework/controller/controllerfakes"
 	"github.com/nginx/nginx-gateway-fabric/v2/internal/framework/helpers"
 )
 
 const (
-	agentTLSTestSecretName = "agent-tls-secret"
-	jwtTestSecretName      = "jwt-secret"
-	caTestSecretName       = "ca-secret"
-	clientTestSecretName   = "client-secret"
-	dockerTestSecretName   = "docker-secret"
-	ngfNamespace           = "nginx-gateway"
+	agentTLSTestSecretName         = "agent-tls-secret"
+	jwtTestSecretName              = "jwt-secret"
+	caTestSecretName               = "ca-secret"
+	clientTestSecretName           = "client-secret"
+	dockerTestSecretName           = "docker-secret"
+	ngfNamespace                   = "nginx-gateway"
+	nginxOneDataplaneKeySecretName = "dataplane-key"
 )
 
 func createScheme() *runtime.Scheme {
@@ -164,6 +166,7 @@ func defaultNginxProvisioner(
 			jwtTestSecretName,
 			caTestSecretName,
 			clientTestSecretName,
+			nginxOneDataplaneKeySecretName,
 		),
 		k8sClient: fakeClient,
 		cfg: Config{
@@ -183,11 +186,31 @@ func defaultNginxProvisioner(
 			},
 			NginxDockerSecretNames: []string{dockerTestSecretName},
 			AgentTLSSecretName:     agentTLSTestSecretName,
+			NginxOneConsoleTelemetryConfig: config.NginxOneConsoleTelemetryConfig{
+				DataplaneKeySecretName: "dataplane-key",
+				EndpointHost:           "agent.connect.nginx.com",
+				EndpointPort:           443,
+				EndpointTLSSkipVerify:  false,
+			},
+			AgentLabels: map[string]string{
+				"product-type":      "ngf",
+				"product-version":   "ngf-version",
+				"cluster-id":        "my-cluster-id",
+				"control-name":      "my-control-plane-name",
+				"control-id":        "my-control-plane-id",
+				"control-namespace": "my-control-plane-namespace",
+			},
 		},
 		leader: true,
 	}, fakeClient, deploymentStore
 }
 
+type fakeLabelCollector struct{}
+
+func (f *fakeLabelCollector) Collect(_ context.Context) (map[string]string, error) {
+	return map[string]string{"product-type": "fake"}, nil
+}
+
 func TestNewNginxProvisioner(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
@@ -201,9 +224,16 @@ func TestNewNginxProvisioner(t *testing.T) {
 			InstanceName: "test-instance",
 		},
 		Logger: logr.Discard(),
+		NginxOneConsoleTelemetryConfig: config.NginxOneConsoleTelemetryConfig{
+			DataplaneKeySecretName: "dataplane-key",
+		},
 	}
 
 	apiChecker = &openshiftfakes.FakeAPIChecker{}
+	labelCollectorFactory = func(_ manager.Manager, _ Config) AgentLabelCollector {
+		return &fakeLabelCollector{}
+	}
+
 	provisioner, eventLoop, err := NewNginxProvisioner(context.TODO(), mgr, cfg)
 	g.Expect(err).ToNot(HaveOccurred())
 	g.Expect(provisioner).NotTo(BeNil())
@@ -216,6 +246,8 @@ func TestNewNginxProvisioner(t *testing.T) {
 		},
 	}
 	g.Expect(provisioner.baseLabelSelector).To(Equal(labelSelector))
+
+	g.Expect(provisioner.store.dataplaneKeySecretName).To(Equal("dataplane-key"))
 }
 
 func TestEnable(t *testing.T) {
@@ -532,3 +564,21 @@ func TestProvisionerRestartsDaemonSet(t *testing.T) {
 	g.Expect(fakeClient.Get(context.TODO(), key, ds)).To(Succeed())
 	g.Expect(ds.Spec.Template.GetAnnotations()).To(HaveKey(controller.RestartedAnnotation))
 }
+
+func TestDefaultLabelCollectorFactory(t *testing.T) {
+	t.Parallel()
+	g := NewWithT(t)
+
+	mgr := &controllerfakes.FakeManager{}
+
+	cfg := Config{
+		GatewayPodConfig: &config.GatewayPodConfig{
+			Namespace: "pod-namespace",
+			Name:      "pod-name",
+			Version:   "my-version",
+		},
+	}
+
+	collector := defaultLabelCollectorFactory(mgr, cfg)
+	g.Expect(collector).NotTo(BeNil())
+}
diff --git a/internal/controller/provisioner/store_test.go b/internal/controller/provisioner/store_test.go
index 06b1a33c..b9ccb68b 100644
--- a/internal/controller/provisioner/store_test.go
+++ b/internal/controller/provisioner/store_test.go
@@ -23,7 +23,14 @@ func TestNewStore(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
 
-	store := newStore([]string{"docker-secret"}, "agent-tls-secret", "jwt-secret", "ca-secret", "client-ssl-secret")
+	store := newStore(
+		[]string{"docker-secret"},
+		"agent-tls-secret",
+		"jwt-secret",
+		"ca-secret",
+		"client-ssl-secret",
+		"dataplane-key",
+	)
 
 	g.Expect(store).NotTo(BeNil())
 	g.Expect(store.dockerSecretNames).To(HaveKey("docker-secret"))
@@ -31,13 +38,14 @@ func TestNewStore(t *testing.T) {
 	g.Expect(store.jwtSecretName).To(Equal("jwt-secret"))
 	g.Expect(store.caSecretName).To(Equal("ca-secret"))
 	g.Expect(store.clientSSLSecretName).To(Equal("client-ssl-secret"))
+	g.Expect(store.dataplaneKeySecretName).To(Equal("dataplane-key"))
 }
 
 func TestUpdateGateway(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
 
-	store := newStore(nil, "", "", "", "")
+	store := newStore(nil, "", "", "", "", "")
 	gateway := &gatewayv1.Gateway{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "test-gateway",
@@ -56,7 +64,7 @@ func TestDeleteGateway(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
 
-	store := newStore(nil, "", "", "", "")
+	store := newStore(nil, "", "", "", "", "")
 	nsName := types.NamespacedName{Name: "test-gateway", Namespace: "default"}
 	store.gateways[nsName] = &gatewayv1.Gateway{}
 
@@ -70,7 +78,7 @@ func TestGetGateways(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
 
-	store := newStore(nil, "", "", "", "")
+	store := newStore(nil, "", "", "", "", "")
 	gateway1 := &gatewayv1.Gateway{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "test-gateway-1",
@@ -101,7 +109,14 @@ func TestRegisterResourceInGatewayConfig(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
 
-	store := newStore([]string{"docker-secret"}, "agent-tls-secret", "jwt-secret", "ca-secret", "client-ssl-secret")
+	store := newStore(
+		[]string{"docker-secret"},
+		"agent-tls-secret",
+		"jwt-secret",
+		"ca-secret",
+		"client-ssl-secret",
+		"dataplane-key",
+	)
 	nsName := types.NamespacedName{Name: "test-gateway", Namespace: "default"}
 
 	registerAndGetResources := func(obj any) *NginxResources {
@@ -312,6 +327,22 @@ func TestRegisterResourceInGatewayConfig(t *testing.T) {
 	// Docker Secret again, already exists
 	resources = registerAndGetResources(dockerSecret)
 	g.Expect(resources.DockerSecrets).To(ContainElement(dockerSecretMeta))
+
+	// clear out resources before next test
+	store.deleteResourcesForGateway(nsName)
+
+	// Dataplane Key Secret
+	dataplaneKeySecretMeta := metav1.ObjectMeta{
+		Name:      controller.CreateNginxResourceName(defaultMeta.Name, store.dataplaneKeySecretName),
+		Namespace: defaultMeta.Namespace,
+	}
+	dataplaneKeySecret := &corev1.Secret{ObjectMeta: dataplaneKeySecretMeta}
+	resources = registerAndGetResources(dataplaneKeySecret)
+	g.Expect(resources.DataplaneKeySecret).To(Equal(dataplaneKeySecretMeta))
+
+	// Dataplane Key Secret again, already exists
+	resources = registerAndGetResources(dataplaneKeySecret)
+	g.Expect(resources.DataplaneKeySecret).To(Equal(dataplaneKeySecretMeta))
 }
 
 func TestGatewayChanged(t *testing.T) {
@@ -415,7 +446,7 @@ func TestDeleteResourcesForGateway(t *testing.T) {
 	t.Parallel()
 	g := NewWithT(t)
 
-	store := newStore(nil, "", "", "", "")
+	store := newStore(nil, "", "", "", "", "")
 	nsName := types.NamespacedName{Name: "test-gateway", Namespace: "default"}
 	store.nginxResources[nsName] = &NginxResources{}
 
@@ -427,7 +458,7 @@ func TestDeleteResourcesForGateway(t *testing.T) {
 func TestGatewayExistsForResource(t *testing.T) {
 	t.Parallel()
 
-	store := newStore(nil, "", "", "", "")
+	store := newStore(nil, "", "", "", "", "")
 	gateway := &graph.Gateway{}
 	store.nginxResources[types.NamespacedName{Name: "test-gateway", Namespace: "default"}] = &NginxResources{
 		Gateway: gateway,
@@ -485,6 +516,10 @@ func TestGatewayExistsForResource(t *testing.T) {
 				Namespace: "default",
 			},
 		},
+		DataplaneKeySecret: metav1.ObjectMeta{
+			Name:      "test-dataplane-key-secret",
+			Namespace: "default",
+		},
 	}
 
 	tests := []struct {
@@ -622,6 +657,16 @@ func TestGatewayExistsForResource(t *testing.T) {
 			},
 			expected: gateway,
 		},
+		{
+			name: "Dataplane Key Secret exists",
+			object: &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-dataplane-key-secret",
+					Namespace: "default",
+				},
+			},
+			expected: gateway,
+		},
 		{
 			name: "Resource does not exist",
 			object: &corev1.Service{
@@ -648,7 +693,7 @@ func TestGatewayExistsForResource(t *testing.T) {
 func TestGetResourceVersionForObject(t *testing.T) {
 	t.Parallel()
 
-	store := newStore(nil, "", "", "", "")
+	store := newStore(nil, "", "", "", "", "")
 	nsName := types.NamespacedName{Name: "test-gateway", Namespace: "default"}
 	store.nginxResources[nsName] = &NginxResources{
 		Deployment: metav1.ObjectMeta{
@@ -718,6 +763,11 @@ func TestGetResourceVersionForObject(t *testing.T) {
 				ResourceVersion: "13",
 			},
 		},
+		DataplaneKeySecret: metav1.ObjectMeta{
+			Name:            "test-dataplane-key-secret",
+			Namespace:       "default",
+			ResourceVersion: "14",
+		},
 	}
 
 	tests := []struct {
@@ -855,6 +905,16 @@ func TestGetResourceVersionForObject(t *testing.T) {
 			},
 			expectedResult: "13",
 		},
+		{
+			name: "Dataplane Key Secret resource version",
+			object: &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-dataplane-key-secret",
+					Namespace: "default",
+				},
+			},
+			expectedResult: "14",
+		},
 		{
 			name: "Non-existent resource",
 			object: &corev1.Service{
diff --git a/internal/controller/telemetry/agent_labels_test.go b/internal/controller/telemetry/agent_labels_test.go
new file mode 100644
index 00000000..de61c210
--- /dev/null
+++ b/internal/controller/telemetry/agent_labels_test.go
@@ -0,0 +1,226 @@
+package telemetry_test
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	. "github.com/onsi/gomega"
+	appsv1 "k8s.io/api/apps/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/telemetry"
+	"github.com/nginx/nginx-gateway-fabric/v2/internal/framework/kubernetes/kubernetesfakes"
+)
+
+func TestCollect_Success(t *testing.T) {
+	t.Parallel()
+	g := NewWithT(t)
+
+	ngfPod := &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "ngf-pod",
+			Namespace: "nginx-gateway",
+			OwnerReferences: []metav1.OwnerReference{
+				{
+					Kind: "ReplicaSet",
+					Name: "replicaset1",
+				},
+			},
+		},
+	}
+
+	replicas := int32(1)
+	ngfReplicaSet := &appsv1.ReplicaSet{
+		Spec: appsv1.ReplicaSetSpec{
+			Replicas: &replicas,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "replica",
+			OwnerReferences: []metav1.OwnerReference{
+				{
+					Kind: "Deployment",
+					Name: "Deployment1",
+					UID:  "test-uid-replicaSet",
+				},
+			},
+		},
+	}
+
+	kubeNamespace := &v1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: metav1.NamespaceSystem,
+			UID:  "test-uid",
+		},
+	}
+
+	k8sClientReader := &kubernetesfakes.FakeReader{}
+
+	cfg := telemetry.LabelCollectorConfig{
+		K8sClientReader: k8sClientReader,
+		Version:         "my-version",
+		PodNSName: types.NamespacedName{
+			Name:      "ngf-pod",
+			Namespace: "nginx-gateway",
+		},
+	}
+
+	baseGetCalls := createGetCallsFunc(ngfPod, ngfReplicaSet, kubeNamespace)
+	k8sClientReader.GetCalls(baseGetCalls)
+
+	c := telemetry.NewLabelCollector(cfg)
+	labels, err := c.Collect(context.TODO())
+	g.Expect(err).ToNot(HaveOccurred())
+
+	g.Expect(labels).To(Equal(map[string]string{
+		"product-type":      "ngf",
+		"product-version":   "my-version",
+		"cluster-id":        "test-uid",
+		"control-name":      "ngf-pod",
+		"control-namespace": "nginx-gateway",
+		"control-id":        "test-uid-replicaSet",
+	}))
+}
+
+func TestCollect_Errors(t *testing.T) {
+	t.Parallel()
+	g := NewWithT(t)
+
+	ngfPod := &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "ngf-pod",
+			Namespace: "nginx-gateway",
+			OwnerReferences: []metav1.OwnerReference{
+				{
+					Kind: "ReplicaSet",
+					Name: "replicaset1",
+				},
+			},
+		},
+	}
+
+	replicas := int32(1)
+	ngfReplicaSet := &appsv1.ReplicaSet{
+		Spec: appsv1.ReplicaSetSpec{
+			Replicas: &replicas,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "replica",
+			OwnerReferences: []metav1.OwnerReference{
+				{
+					Kind: "Deployment",
+					Name: "Deployment1",
+					UID:  "test-uid-replicaSet",
+				},
+			},
+		},
+	}
+
+	kubeNamespace := &v1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: metav1.NamespaceSystem,
+			UID:  "test-uid",
+		},
+	}
+
+	baseGetCalls := createGetCallsFunc(ngfPod, ngfReplicaSet, kubeNamespace)
+
+	mergeGetCallsWithBase := func(f getCallsFunc) getCallsFunc {
+		return func(
+			ctx context.Context,
+			nsName types.NamespacedName,
+			object client.Object,
+			option ...client.GetOption,
+		) error {
+			err := baseGetCalls(ctx, nsName, object, option...)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			return f(ctx, nsName, object, option...)
+		}
+	}
+
+	tests := []struct {
+		name           string
+		getCallsFunc   getCallsFunc
+		wantErrContain string
+	}{
+		{
+			name: "collectClusterID error",
+			getCallsFunc: mergeGetCallsWithBase(func(
+				_ context.Context,
+				_ types.NamespacedName,
+				object client.Object,
+				_ ...client.GetOption,
+			) error {
+				if _, ok := object.(*v1.Namespace); ok {
+					return errors.New("clusterID fail")
+				}
+				return nil
+			}),
+			wantErrContain: "failed to collect cluster information",
+		},
+		{
+			name: "getPodReplicaSet error",
+			getCallsFunc: mergeGetCallsWithBase(func(
+				_ context.Context,
+				_ types.NamespacedName,
+				object client.Object,
+				_ ...client.GetOption,
+			) error {
+				if _, ok := object.(*appsv1.ReplicaSet); ok {
+					return errors.New("replicaSet fail")
+				}
+				return nil
+			}),
+			wantErrContain: "failed to get replica set for pod",
+		},
+		{
+			name: "getDeploymentID error",
+			getCallsFunc: mergeGetCallsWithBase(createGetCallsFunc(&appsv1.ReplicaSet{
+				Spec: appsv1.ReplicaSetSpec{
+					Replicas: &replicas,
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					OwnerReferences: []metav1.OwnerReference{
+						{
+							Name: "replica",
+							Kind: "Deployment",
+						},
+					},
+				},
+			})),
+			wantErrContain: "failed to get NGF deploymentID",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			g := NewWithT(t)
+
+			getCalls := tt.getCallsFunc
+
+			k8sClientReader := &kubernetesfakes.FakeReader{}
+			k8sClientReader.GetCalls(getCalls)
+
+			cfg := telemetry.LabelCollectorConfig{
+				K8sClientReader: k8sClientReader,
+				Version:         "my-version",
+				PodNSName: types.NamespacedName{
+					Name:      "ngf-pod",
+					Namespace: "nginx-gateway",
+				},
+			}
+
+			c := telemetry.NewLabelCollector(cfg)
+
+			labels, err := c.Collect(context.TODO())
+			g.Expect(err).To(HaveOccurred())
+			g.Expect(err.Error()).To(ContainSubstring(tt.wantErrContain))
+			g.Expect(labels).To(BeNil())
+		})
+	}
+}
diff --git a/internal/controller/telemetry/collector_test.go b/internal/controller/telemetry/collector_test.go
index 850b5756..ea900450 100644
--- a/internal/controller/telemetry/collector_test.go
+++ b/internal/controller/telemetry/collector_test.go
@@ -59,9 +59,7 @@ type getCallsFunc = func(
 ) error
 
 func createGetCallsFunc(objects ...client.Object) getCallsFunc {
-	return func(_ context.Context, _ types.NamespacedName, object client.Object, option ...client.GetOption) error {
-		Expect(option).To(BeEmpty())
-
+	return func(_ context.Context, _ types.NamespacedName, object client.Object, _ ...client.GetOption) error {
 		for _, obj := range objects {
 			if reflect.TypeOf(obj) == reflect.TypeOf(object) {
 				reflect.ValueOf(object).Elem().Set(reflect.ValueOf(obj).Elem())