CVE-2023-1065: Improper Authentication (CWE-287)
value: This vulnerability in the Snyk Kubernetes Monitor can result in irrelevant data being posted to a Snyk Organization, which could in turn obfuscate other, relevant, security issues. It does not expose the user of the integration to any direct security risk and no user data can be leaked. To exploit the vulnerability the attacker does not need to be authenticated to Snyk but does need to know the target's Integration ID (which may or may not be the same as the Organization ID, although this is an unpredictable UUID in either case).
CWE description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
CVSS v3 base score: 5.3
Published: 2023-02-28T19:15Z
Repository: https://github.com/snyk/kubernetes-monitor
Language: TypeScript
Files changed: .circleci/config.yml, .circleci/config/jobs/operator_upgrade_tests.yml, README.md, config.default.json, scripts/circleci-jobs/deploy_to_dev.sh, scripts/local-testing.sh, snyk-monitor-deployment.yaml, snyk-operator-certified/README.md, snyk-operator-certified/bundle/manifests/snyk-monitor.clusterserviceversion.yaml, snyk-operator-certified/config/manifests/bases/snyk-monitor.clusterserviceversion.yaml, snyk-operator-certified/helm-charts/snyk-monitor/templates/deployment.yaml, snyk-operator/certified-operator/README.md, snyk-operator/deploy/olm-catalog/snyk-operator/0.0.0/snyk-operator.v0.0.0.clusterserviceversion.yaml, src/common/config.ts, src/common/types.ts, src/transmitter/index.ts, test/common/config.spec.ts, test/fixtures/operator/custom-resource-k8s.yaml, test/fixtures/operator/custom-resource.yaml, test/setup/deployers/helm-with-proxy.ts, test/setup/deployers/helm.ts, test/setup/deployers/yaml.ts, test/setup/index.ts, test/unit/data-scraper/scrape-data.spec.ts
Commit date: 2023-02-28 14:56:30 +0000
Commit message: feat: call authenticated snyk API endpoints with token

BREAKING CHANGE: use a service account API token to perform
authenticated requests to snyk API.

Diff: diff --git a/.circleci/config.yml b/.circleci/config.yml
index dfc2fb9..d10fa4f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -517,11 +517,12 @@ jobs:
                       kubectl create ns snyk-monitor
                     fi
 
                     set -e
                     INTEGRATION_ID=$(uuidgen)
-                    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=integrationId=${INTEGRATION_ID} --from-literal=dockercfg.json={}
+                    SERVICE_ACCOUNT_API_TOKEN=$(uuidgen)
+                    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=integrationId=${INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SERVICE_ACCOUNT_API_TOKEN} --from-literal=dockercfg.json={}
                 name: Configure snyk-monitor namespace
             - run:
                 command: |
                     source $BASH_ENV
                     set -xe
diff --git a/.circleci/config/jobs/operator_upgrade_tests.yml b/.circleci/config/jobs/operator_upgrade_tests.yml
index f48ac89..fdb3a25 100644
--- a/.circleci/config/jobs/operator_upgrade_tests.yml
+++ b/.circleci/config/jobs/operator_upgrade_tests.yml
@@ -118,11 +118,12 @@ steps:
           kubectl create ns snyk-monitor
         fi
 
         set -e
         INTEGRATION_ID=$(uuidgen)
-        kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=integrationId=${INTEGRATION_ID} --from-literal=dockercfg.json={}
+        SERVICE_ACCOUNT_API_TOKEN=$(uuidgen)
+        kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=integrationId=${INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SERVICE_ACCOUNT_API_TOKEN} --from-literal=dockercfg.json={}
 
   - run:
       name: Install Operator
       command: |
         source $BASH_ENV
diff --git a/README.md b/README.md
index 0cc7c4d..893b479 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,11 @@
 A containerized application that is deployed with Helm. Monitors the security of a Kubernetes cluster by analyzing container images.
 
 ## Prerequisites ##
 
 * 50 GiB of storage in the form of [emptyDir](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) or a [PersistentVolumeClaim](https://kubernetes.io/docs/concepts/storage/persistent-volumes/).
-* External internet access from the Kubernetes cluster to `kubernetes-upstream.snyk.io`.
+* External internet access from the Kubernetes cluster to `api.snyk.io`.
 * 1 CPU, 2 GiB RAM
 * 1 Kubernetes worker node of type `linux/amd64` - supported and tested only on the AMD64 CPU architecture
 
 Supported Kubernetes distributions:
 
diff --git a/config.default.json b/config.default.json
index c8f6ae0..072d406 100644
--- a/config.default.json
+++ b/config.default.json
@@ -17,9 +17,10 @@
     "MAX_AGE_MS": 60000
   },
   "REQUEST_QUEUE_LENGTH": 2,
   "QUEUE_LENGTH_LOG_FREQUENCY_MINUTES": 1,
   "INTEGRATION_ID": "",
-  "DEFAULT_KUBERNETES_UPSTREAM_URL": "https://kubernetes-upstream.snyk.io",
+  "SERVICE_ACCOUNT_API_TOKEN": "",
+  "DEFAULT_KUBERNETES_UPSTREAM_URL": "https://api.snyk.io/v2/kubernetes-upstream",
   "MAX_RETRY_BACKOFF_DURATION_SECONDS": 300,
   "USE_KEEPALIVE": true
 }
diff --git a/scripts/circleci-jobs/deploy_to_dev.sh b/scripts/circleci-jobs/deploy_to_dev.sh
index 71de46c..28487f1 100755
--- a/scripts/circleci-jobs/deploy_to_dev.sh
+++ b/scripts/circleci-jobs/deploy_to_dev.sh
@@ -18,11 +18,11 @@ git clone https://$GH_TOKEN@github.com/snyk/$KUBERNETES_MONITOR_DEPLOYER_REPO.gi
 cp -r snyk-monitor/* $KUBERNETES_MONITOR_DEPLOYER_REPO/helm
 
 # Create helm values for different envs
 cat >$KUBERNETES_MONITOR_DEPLOYER_REPO/helm/values/multi-tenant-gcp-pre-production.yaml <<EOF
 clusterName: "Development cluster"
-integrationApi: https://kubernetes-upstream.dev.snyk.io
+integrationApi: "https://api.dev.snyk.io/v2/kubernetes-upstream"
 log_level: "DEBUG"
 skip_k8s_jobs: true
 
 policyOrgs:
   - $POLICY_ORG_PRE_PROD
diff --git a/scripts/local-testing.sh b/scripts/local-testing.sh
index 3d17713..2b6c547 100755
--- a/scripts/local-testing.sh
+++ b/scripts/local-testing.sh
@@ -13,12 +13,17 @@ IP=$(ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk 'NR==1{print $2}')
 if [ "$INTEGRATION_ID" == "" ]; then
     echo "Enter your integration ID: "
     read INTEGRATION_ID
 fi
 
+if [ "$SERVICE_ACCOUNT_API_TOKEN" == "" ]; then
+    echo "Enter your service account API token: "
+    read SERVICE_ACCOUNT_API_TOKEN
+fi
+
 kind delete cluster
 kind create cluster
 kubectl create namespace snyk-monitor
-kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${INTEGRATION_ID}
+kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SERVICE_ACCOUNT_API_TOKEN}
 helm upgrade --install snyk-monitor ./snyk-monitor --namespace snyk-monitor --set clusterName="kind localhost" --set image.tag=${LATEST_TAG} --set integrationApi=http://${IP}:9000
 
 printf "\r\n\r\nYou can now check if the pod is running using:\r\n\tkubectl get pod -n snyk-monitor\r\n"
diff --git a/snyk-monitor-deployment.yaml b/snyk-monitor-deployment.yaml
index aa93f47..2e2ee98 100644
--- a/snyk-monitor-deployment.yaml
+++ b/snyk-monitor-deployment.yaml
@@ -41,10 +41,15 @@ spec:
           - name: SNYK_INTEGRATION_ID
             valueFrom:
               secretKeyRef:
                 name: snyk-monitor
                 key: integrationId
+          - name: SNYK_SERVICE_ACCOUNT_API_TOKEN
+            valueFrom:
+              secretKeyRef:
+                name: snyk-monitor
+                key: serviceAccountApiToken
           - name: SNYK_WATCH_NAMESPACE
             valueFrom:
               configMapKeyRef:
                 name: snyk-monitor
                 key: namespace
diff --git a/snyk-operator-certified/README.md b/snyk-operator-certified/README.md
index e42192b..91156b7 100644
--- a/snyk-operator-certified/README.md
+++ b/snyk-operator-certified/README.md
@@ -39,11 +39,11 @@ apiVersion: charts.snyk.io/v1alpha1 # this has changed, used to be "charts.helm.
 kind: SnykMonitor
 metadata:
   name: snyk-monitor
   namespace: snyk-monitor
 spec:
-  integrationApi: https://kubernetes-upstream.dev.snyk.io
+  integrationApi: https://api.dev.snyk.io/v2/kubernetes-upstream
   temporaryStorageSize: 20Gi
   pvc:
     enabled: true
 ---
 apiVersion: operators.coreos.com/v1
diff --git a/snyk-operator-certified/bundle/manifests/snyk-monitor.clusterserviceversion.yaml b/snyk-operator-certified/bundle/manifests/snyk-monitor.clusterserviceversion.yaml
index 4b2df0c..0105520 100644
--- a/snyk-operator-certified/bundle/manifests/snyk-monitor.clusterserviceversion.yaml
+++ b/snyk-operator-certified/bundle/manifests/snyk-monitor.clusterserviceversion.yaml
@@ -145,15 +145,15 @@ spec:
 
     Snyk integrates with Kubernetes, enabling you to import and test your running workloads and identify vulnerabilities in their associated images and configurations that might make those workloads less secure. Once imported, Snyk continues to monitor those workloads, identifying additional security issues as new images are deployed and the workload configuration changes.
 
     ## Prerequisites
 
-    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId and Docker config file:
+    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId, serviceAccountApiToken and Docker config file:
 
     ```
     kubectl create namespace snyk-monitor
-    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID}
+    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SNYK_SERVICE_ACCOUNT_API_TOKEN}
     ```
 
     Refer to the [Snyk documentation](https://support.snyk.io/hc/en-us/articles/360003916138-Kubernetes-integration-overview) for more details.
   displayName: Snyk Operator
   icon:
diff --git a/snyk-operator-certified/config/manifests/bases/snyk-monitor.clusterserviceversion.yaml b/snyk-operator-certified/config/manifests/bases/snyk-monitor.clusterserviceversion.yaml
index 1ba5db2..044c9ff 100644
--- a/snyk-operator-certified/config/manifests/bases/snyk-monitor.clusterserviceversion.yaml
+++ b/snyk-operator-certified/config/manifests/bases/snyk-monitor.clusterserviceversion.yaml
@@ -127,15 +127,15 @@ spec:
 
     Snyk integrates with Kubernetes, enabling you to import and test your running workloads and identify vulnerabilities in their associated images and configurations that might make those workloads less secure. Once imported, Snyk continues to monitor those workloads, identifying additional security issues as new images are deployed and the workload configuration changes.
 
     ## Prerequisites
 
-    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId and Docker config file:
+    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId, serviceAccountApiToken and Docker config file:
 
     ```
     kubectl create namespace snyk-monitor
-    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID}
+    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SNYK_SERVICE_ACCOUNT_API_TOKEN}
     ```
 
     Refer to the [Snyk documentation](https://support.snyk.io/hc/en-us/articles/360003916138-Kubernetes-integration-overview) for more details.
   displayName: Snyk Operator
   icon:
diff --git a/snyk-operator-certified/helm-charts/snyk-monitor/templates/deployment.yaml b/snyk-operator-certified/helm-charts/snyk-monitor/templates/deployment.yaml
index 28c711c..2d3cb72 100644
--- a/snyk-operator-certified/helm-charts/snyk-monitor/templates/deployment.yaml
+++ b/snyk-operator-certified/helm-charts/snyk-monitor/templates/deployment.yaml
@@ -93,10 +93,15 @@ spec:
           - name: SNYK_INTEGRATION_ID
             valueFrom:
               secretKeyRef:
                 name: {{ .Values.monitorSecrets }}
                 key: integrationId
+          - name: SNYK_SERVICE_ACCOUNT_API_TOKEN
+            valueFrom:
+              secretKeyRef:
+                name: snyk-monitor
+                key: serviceAccountApiToken
           - name: SNYK_WATCH_NAMESPACE
             value: {{ include "snyk-monitor.scope" . }}
           - name: SNYK_DEPLOYMENT_NAMESPACE
             valueFrom:
               fieldRef:
diff --git a/snyk-operator/certified-operator/README.md b/snyk-operator/certified-operator/README.md
index 2575570..d5a73fb 100644
--- a/snyk-operator/certified-operator/README.md
+++ b/snyk-operator/certified-operator/README.md
@@ -38,11 +38,11 @@ apiVersion: charts.snyk.io/v1alpha1 # this has changed, used to be "charts.helm.
 kind: SnykMonitor
 metadata:
   name: snyk-monitor
   namespace: snyk-monitor
 spec:
-  integrationApi: https://kubernetes-upstream.dev.snyk.io
+  integrationApi: https://api.dev.snyk.io/v2/kubernetes-upstream
   temporaryStorageSize: 20Gi
   pvc:
     enabled: true
 ---
 apiVersion: operators.coreos.com/v1
diff --git a/snyk-operator/deploy/olm-catalog/snyk-operator/0.0.0/snyk-operator.v0.0.0.clusterserviceversion.yaml b/snyk-operator/deploy/olm-catalog/snyk-operator/0.0.0/snyk-operator.v0.0.0.clusterserviceversion.yaml
index 49952a5..4b6b88b 100644
--- a/snyk-operator/deploy/olm-catalog/snyk-operator/0.0.0/snyk-operator.v0.0.0.clusterserviceversion.yaml
+++ b/snyk-operator/deploy/olm-catalog/snyk-operator/0.0.0/snyk-operator.v0.0.0.clusterserviceversion.yaml
@@ -185,15 +185,15 @@ spec:
 
     Snyk integrates with Kubernetes, enabling you to import and test your running workloads and identify vulnerabilities in their associated images and configurations that might make those workloads less secure. Once imported, Snyk continues to monitor those workloads, identifying additional security issues as new images are deployed and the workload configuration changes.
 
     ## Prerequisites
 
-    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId and Docker config file:
+    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId, serviceAccountApiToken and Docker config file:
 
     ```
     kubectl create namespace snyk-monitor
-    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID}
+    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SNYK_SERVICE_ACCOUNT_API_TOKEN}
     ```
 
     Refer to the [Snyk documentation](https://support.snyk.io/hc/en-us/articles/360003916138-Kubernetes-integration-overview) for more details.
   displayName: Snyk Operator
   install:
diff --git a/src/common/config.ts b/src/common/config.ts
index 7c79fcd..3da8e50 100644
--- a/src/common/config.ts
+++ b/src/common/config.ts
@@ -37,10 +37,11 @@ function getClusterName(): string {
 
 // NOTE: The agent identifier is replaced with a stable identifier once snyk-monitor starts up
 config.AGENT_ID = randomUUID();
 
 config.INTEGRATION_ID = config.INTEGRATION_ID.trim();
+config.SERVICE_ACCOUNT_API_TOKEN = config.SERVICE_ACCOUNT_API_TOKEN.trim();
 config.CLUSTER_NAME = getClusterName();
 config.IMAGE_STORAGE_ROOT = '/var/tmp';
 config.POLICIES_STORAGE_ROOT = '/tmp/policies';
 config.EXCLUDED_NAMESPACES = loadExcludedNamespaces();
 config.WORKERS_COUNT = Number(config.WORKERS_COUNT) || 10;
diff --git a/src/common/types.ts b/src/common/types.ts
index 268415b..9d6b8dd 100644
--- a/src/common/types.ts
+++ b/src/common/types.ts
@@ -17,10 +17,11 @@ export interface Config {
   };
   WORKERS_COUNT: number;
   REQUEST_QUEUE_LENGTH: number;
   QUEUE_LENGTH_LOG_FREQUENCY_MINUTES: number;
   INTEGRATION_ID: string;
+  SERVICE_ACCOUNT_API_TOKEN: string;
   DEFAULT_KUBERNETES_UPSTREAM_URL: string;
   MAX_RETRY_BACKOFF_DURATION_SECONDS: number;
 
   // ----------------------------------------
   // Properties injected by Helm (via environment variables) or manually set in code:
diff --git a/src/transmitter/index.ts b/src/transmitter/index.ts
index e56d0c9..30aeca3 100644
--- a/src/transmitter/index.ts
+++ b/src/transmitter/index.ts
@@ -19,10 +19,11 @@ import {
   IRuntimeDataPayload,
 } from './types';
 import { getProxyAgent } from './proxy';
 
 import type { queueAsPromised } from 'fastq';
+import path from 'path';
 
 interface KubernetesUpstreamRequest {
   method: NeedleHttpVerbs;
   url: string;
   payload:
@@ -30,15 +31,18 @@ interface KubernetesUpstreamRequest {
     | ScanResultsPayload
     | IWorkloadMetadataPayload
     | IDeleteWorkloadPayload
     | IClusterMetadataPayload
     | IRuntimeDataPayload;
+  options: NeedleOptions;
 }
 
 const upstreamUrl =
   config.INTEGRATION_API || config.DEFAULT_KUBERNETES_UPSTREAM_URL;
 
+const upstreamRequestVersion = '2023-02-10';
+
 let httpAgent = new HttpAgent({
   keepAlive: config.USE_KEEPALIVE,
 });
 
 let httpsAgent = new HttpsAgent({
@@ -53,26 +57,46 @@ function getAgent(u: string): HttpAgent {
 // Async queue wraps around the call to retryRequest in order to limit
 // the number of requests in flight to kubernetes upstream at any one time.
 const reqQueue: queueAsPromised<unknown> = fastq.promise(async function (
   req: KubernetesUpstreamRequest,
 ) {
-  return await retryRequest(req.method, req.url, req.payload);
+  return await retryRequest(req.method, req.url, req.payload, req.options);
 },
 config.REQUEST_QUEUE_LENGTH);
 
+const upstreamRequestOptions = {
+  headers: {
+    Authorization: `token ${config.SERVICE_ACCOUNT_API_TOKEN}`,
+  },
+};
+
+function constructUpstreamRequestUrl(
+  requestPath: string,
+  queryParams?: Record<string, string>,
+): string {
+  const requestUrl = new URL(upstreamUrl);
+  requestUrl.pathname = path.join(requestUrl.pathname, requestPath);
+  requestUrl.searchParams.set('version', upstreamRequestVersion);
+  for (const key in queryParams) {
+    requestUrl.searchParams.set(key, queryParams[key]);
+  }
+  return requestUrl.toString();
+}
+
 export async function sendDepGraph(
   ...payloads: IDependencyGraphPayload[]
 ): Promise<void> {
   for (const payload of payloads) {
     // Intentionally removing dependencyGraph as it would be too big to log
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     const { dependencyGraph, ...payloadWithoutDepGraph } = payload;
     try {
       const request: KubernetesUpstreamRequest = {
         method: 'post',
-        url: `${upstreamUrl}/api/v1/dependency-graph`,
+        url: constructUpstreamRequestUrl('/api/v1/dependency-graph'),
         payload,
+        options: upstreamRequestOptions,
       };
 
       const { response, attempt } = await reqQueue.push(request);
       if (!isSuccessStatusCode(response.statusCode)) {
         throw new Error(`${response.statusCode} ${response.statusMessage}`);
@@ -98,12 +122,13 @@ export async function sendScanResults(
     // Intentionally removing scan results as they would be too big to log
     const payloadWithoutScanResults = { ...payload, scanResults: undefined };
     try {
       const request: KubernetesUpstreamRequest = {
         method: 'post',
-        url: `${upstreamUrl}/api/v1/scan-results`,
+        url: constructUpstreamRequestUrl('/api/v1/scan-results'),
         payload,
+        options: upstreamRequestOptions,
       };
 
       const { response, attempt } = await reqQueue.push(request);
       if (!isSuccessStatusCode(response.statusCode)) {
         throw new Error(`${response.statusCode} ${response.statusMessage}`);
@@ -134,12 +159,13 @@ export async function sendWorkloadMetadata(
       'attempting to send workload metadata upstream',
     );
 
     const request: KubernetesUpstreamRequest = {
       method: 'post',
-      url: `${upstreamUrl}/api/v1/workload`,
+      url: constructUpstreamRequestUrl('/api/v1/workload'),
       payload,
+      options: upstreamRequestOptions,
     };
 
     const { response, attempt } = await reqQueue.push(request);
     if (!isSuccessStatusCode(response.statusCode)) {
       throw new Error(`${response.statusCode} ${response.statusMessage}`);
@@ -170,12 +196,13 @@ export async function sendWorkloadEventsPolicy(
       'attempting to send workload auto-import policy',
     );
 
     const { response, attempt } = await retryRequest(
       'post',
-      `${upstreamUrl}/api/v1/policy`,
+      constructUpstreamRequestUrl('/api/v1/policy'),
       payload,
+      upstreamRequestOptions,
     );
     if (!isSuccessStatusCode(response.statusCode)) {
       throw new Error(`${response.statusCode} ${response.statusMessage}`);
     }
 
@@ -205,15 +232,23 @@ export async function deleteWorkload(
   payload: IDeleteWorkloadPayload,
 ): Promise<void> {
   try {
     const { workloadLocator, agentId } = payload;
     const { userLocator, cluster, namespace, type, name } = workloadLocator;
-    const query = `userLocator=${userLocator}&cluster=${cluster}&namespace=${namespace}&type=${type}&name=${name}&agentId=${agentId}`;
+    const queryParams: Record<string, string> = {
+      userLocator,
+      cluster,
+      namespace,
+      type,
+      name,
+      agentId,
+    };
     const request: KubernetesUpstreamRequest = {
       method: 'delete',
-      url: `${upstreamUrl}/api/v1/workload?${query}`,
+      url: constructUpstreamRequestUrl('api/v1/workload', queryParams),
       payload,
+      options: upstreamRequestOptions,
     };
 
     const { response, attempt } = await reqQueue.push(request);
     // TODO: Remove this check, the upstream no longer returns 404 in such cases
     if (response.statusCode === 404) {
@@ -346,12 +381,13 @@ export async function sendClusterMetadata(): Promise<void> {
       'attempting to send cluster metadata',
     );
 
     const request: KubernetesUpstreamRequest = {
       method: 'post',
-      url: `${upstreamUrl}/api/v1/cluster`,
+      url: constructUpstreamRequestUrl('/api/v1/cluster'),
       payload,
+      options: upstreamRequestOptions,
     };
 
     const { response, attempt } = await reqQueue.push(request);
     if (!isSuccessStatusCode(response.statusCode)) {
       throw new Error(`${response.statusCode} ${response.statusMessage}`);
@@ -392,12 +428,13 @@ export async function sendRuntimeData(
   try {
     logger.info(logContext, 'attempting to send runtime data');
 
     const request: KubernetesUpstreamRequest = {
       method: 'post',
-      url: `${upstreamUrl}/api/v1/runtime-results`,
+      url: constructUpstreamRequestUrl('/api/v1/runtime-results'),
       payload,
+      options: upstreamRequestOptions,
     };
 
     const { response, attempt } = await reqQueue.push(request);
 
     if (!isSuccessStatusCode(response.statusCode)) {
diff --git a/test/common/config.spec.ts b/test/common/config.spec.ts
index ba35b3c..584d8d2 100644
--- a/test/common/config.spec.ts
+++ b/test/common/config.spec.ts
@@ -1,10 +1,12 @@
 describe('extractNamespaceName()', () => {
+  const apiToken = '46766a0a-ed0b-4e91-84c8-ea1c827f2a73';
   beforeEach(() => {
     jest.resetModules();
     process.env.SNYK_SYSDIG_ENDPOINT = 'https://api/v1/images/';
     process.env.SNYK_SYSDIG_TOKEN = '1432gtrhtrw32raf';
+    process.env.SNYK_SERVICE_ACCOUNT_API_TOKEN = apiToken;
   });
 
   afterEach(() => {
     jest.restoreAllMocks();
     delete process.env.SNYK_CLUSTER_NAME;
@@ -66,19 +68,20 @@ describe('extractNamespaceName()', () => {
 
   it('loads the expected configuration values', () => {
     const { config } = require('../../src/common/config');
     expect(config.AGENT_ID).toEqual(expect.any(String));
     expect(config.INTEGRATION_ID).toEqual(expect.any(String));
+    expect(config.SERVICE_ACCOUNT_API_TOKEN).toEqual(apiToken);
     expect(config.CLUSTER_NAME).toEqual('Default cluster');
     expect(config.IMAGE_STORAGE_ROOT).toEqual('/var/tmp');
     expect(config.EXCLUDED_NAMESPACES).toBeNull();
     expect(config.HTTPS_PROXY).toBeUndefined();
     expect(config.HTTP_PROXY).toBeUndefined();
     expect(config.NO_PROXY).toBeUndefined();
     expect(config.USE_KEEPALIVE).toEqual(true);
     expect(config.SKIP_K8S_JOBS).toEqual(false);
-    expect(config.WORKERS_COUNT).toEqual(10);
+    expect(config.WORKERS_COUNT).toEqual(5);
     expect(config.SKOPEO_COMPRESSION_LEVEL).toEqual(6);
     expect(config.SYSDIG_ENDPOINT).toEqual('https://api/v1/images/');
     expect(config.SYSDIG_TOKEN).toEqual('1432gtrhtrw32raf');
   });
 
diff --git a/test/fixtures/operator/custom-resource-k8s.yaml b/test/fixtures/operator/custom-resource-k8s.yaml
index b2649f9..1f97056 100644
--- a/test/fixtures/operator/custom-resource-k8s.yaml
+++ b/test/fixtures/operator/custom-resource-k8s.yaml
@@ -2,11 +2,11 @@ apiVersion: charts.helm.k8s.io/v1alpha1
 kind: SnykMonitor
 metadata:
   name: snyk-monitor
   namespace: marketplace
 spec:
-  integrationApi: https://kubernetes-upstream.dev.snyk.io
+  integrationApi: https://api.dev.snyk.io/v2/kubernetes-upstream
   temporaryStorageSize: 20Gi
   clusterName: ""
   pvc:
     enabled: true
     create: true
diff --git a/test/fixtures/operator/custom-resource.yaml b/test/fixtures/operator/custom-resource.yaml
index f1a5e02..7096c91 100644
--- a/test/fixtures/operator/custom-resource.yaml
+++ b/test/fixtures/operator/custom-resource.yaml
@@ -2,11 +2,11 @@ apiVersion: charts.helm.k8s.io/v1alpha1
 kind: SnykMonitor
 metadata:
   name: snyk-monitor
   namespace: snyk-monitor
 spec:
-  integrationApi: https://kubernetes-upstream.dev.snyk.io
+  integrationApi: https://api.dev.snyk.io/v2/kubernetes-upstream
   temporaryStorageSize: 20Gi
   clusterName: ""
   pvc:
     enabled: true
     create: true
diff --git a/test/setup/deployers/helm-with-proxy.ts b/test/setup/deployers/helm-with-proxy.ts
index e6e87bd..2508dc8 100644
--- a/test/setup/deployers/helm-with-proxy.ts
+++ b/test/setup/deployers/helm-with-proxy.ts
@@ -35,11 +35,11 @@ async function deployKubernetesMonitor(
   await exec(
     `${helmPath} upgrade --install snyk-monitor ${helmChartPath} --namespace snyk-monitor ` +
       `--set image.repository=${imageName} ` +
       `--set image.tag=${imageTag} ` +
       `--set image.pullPolicy=${imagePullPolicy} ` +
-      '--set integrationApi=https://kubernetes-upstream.dev.snyk.io ' +
+      '--set integrationApi=https://api.dev.snyk.io/v2/kubernetes-upstream ' +
       `--set clusterName=${deployOptions.clusterName} ` +
       '--set https_proxy=http://forwarding-proxy:8080',
   );
   console.log(
     `Deployed ${imageOptions.nameAndTag} with pull policy ${imageOptions.pullPolicy}`,
diff --git a/test/setup/deployers/helm.ts b/test/setup/deployers/helm.ts
index 32d04e7..6b65065 100644
--- a/test/setup/deployers/helm.ts
+++ b/test/setup/deployers/helm.ts
@@ -28,11 +28,11 @@ async function deployKubernetesMonitor(
   await exec(
     `${helmPath} upgrade --install snyk-monitor ${helmChartPath} --namespace snyk-monitor ` +
       `--set image.repository=${imageName} ` +
       `--set image.tag=${imageTag} ` +
       `--set image.pullPolicy=${imagePullPolicy} ` +
-      '--set integrationApi=https://kubernetes-upstream.dev.snyk.io ' +
+      '--set integrationApi=https://api.dev.snyk.io/v2/kubernetes-upstream ' +
       `--set clusterName=${deployOptions.clusterName} ` +
       '--set nodeSelector."kubernetes\\.io/os"=linux ' +
       '--set pvc.enabled=true ' +
       '--set pvc.create=true ' +
       '--set log_level="INFO" ' +
diff --git a/test/setup/deployers/yaml.ts b/test/setup/deployers/yaml.ts
index e0547cd..9b23be7 100644
--- a/test/setup/deployers/yaml.ts
+++ b/test/setup/deployers/yaml.ts
@@ -45,11 +45,11 @@ function createTestYamlDeployment(
 
   // Inject the baseUrl of kubernetes-upstream that snyk-monitor container use to send metadata
   const envVar = container.env.find(
     (env) => env.name === 'SNYK_INTEGRATION_API',
   );
-  envVar.value = 'https://kubernetes-upstream.dev.snyk.io';
+  envVar.value = 'https://api.dev.snyk.io/v2/kubernetes-upstream';
   delete envVar.valueFrom;
 
   if (clusterName) {
     const clusterNameEnvVar = container.env.find(
       (env) => env.name === 'SNYK_CLUSTER_NAME',
diff --git a/test/setup/index.ts b/test/setup/index.ts
index adccb30..97e5a76 100644
--- a/test/setup/index.ts
+++ b/test/setup/index.ts
@@ -22,10 +22,18 @@ function getClusterName(): string {
   const clusterName = `cluster_${randomUUID()}`;
   console.log(`Generated new Cluster Name ${clusterName}`);
   return clusterName;
 }
 
+function getServiceAccountApiToken(): string {
+  const serviceAccountApiToken = randomUUID();
+  console.log(
+    `Generated new service account API token ${serviceAccountApiToken}`,
+  );
+  return serviceAccountApiToken;
+}
+
 function getEnvVariableOrDefault(
   envVarName: string,
   defaultValue: string,
 ): string {
   const value = process.env[envVarName];
@@ -82,10 +90,11 @@ async function createEnvironment(): Promise<void> {
   await sleep(5000);
 }
 
 async function predeploy(
   integrationId: string,
+  serviceAccountApiToken: string,
   namespace: string,
 ): Promise<void> {
   try {
     const secretName = 'snyk-monitor';
     console.log(`Creating namespace ${namespace} and secret ${secretName}`);
@@ -97,10 +106,11 @@ async function predeploy(
     }
     const gcrDockercfg = process.env['PRIVATE_REGISTRIES_DOCKERCFG'] || '{}';
     await kubectl.createSecret(secretName, namespace, {
       'dockercfg.json': gcrDockercfg,
       integrationId,
+      serviceAccountApiToken,
     });
     await createRegistriesConfigMap(namespace);
     console.log(`Namespace ${namespace} and secret ${secretName} created`);
   } catch (error) {
     console.log(
@@ -191,11 +201,12 @@ export async function deployMonitor(): Promise<{
     await createEnvironment();
     await createSecretForGcrIoAccess();
     await createSecretForDockerHubAccess();
 
     const integrationId = getIntegrationId();
-    await predeploy(integrationId, namespace);
+    const serviceAccountApiToken = getServiceAccountApiToken();
+    await predeploy(integrationId, serviceAccountApiToken, namespace);
 
     // TODO: hack, rewrite this
     const imagePullPolicy =
       testPlatform === 'kind' || testPlatform === 'kindolm'
         ? 'Never'
diff --git a/test/unit/data-scraper/scrape-data.spec.ts b/test/unit/data-scraper/scrape-data.spec.ts
index aa3f15f..3d37d1f 100644
--- a/test/unit/data-scraper/scrape-data.spec.ts
+++ b/test/unit/data-scraper/scrape-data.spec.ts
@@ -57,12 +57,12 @@ describe('dataScraper()', () => {
     nock('https://sysdig', { reqheaders: { authorization: expectedHeader } })
       .get('/v1/runtimeimages?limit=10&cursor=xxx')
       .times(1)
       .reply(200, bodyNoToken);
 
-    nock('https://kubernetes-upstream.snyk.io')
-      .post('/api/v1/runtime-results')
+    nock('https://api.snyk.io')
+      .post('/v2/kubernetes-upstream/api/v1/runtime-results?version=2023-02-10')
       .times(1)
       .reply(200, (uri, requestBody: transmitterTypes.IRuntimeDataPayload) => {
         expect(requestBody).toEqual<transmitterTypes.IRuntimeDataPayload>({
           identity: {
             type: 'sysdig',
@@ -78,11 +78,11 @@ describe('dataScraper()', () => {
               data: bodyWithToken.data,
             },
           ],
         });
       })
-      .post('/api/v1/runtime-results')
+      .post('/v2/kubernetes-upstream/api/v1/runtime-results?version=2023-02-10')
       .times(1)
       .reply(200, (uri, requestBody: transmitterTypes.IRuntimeDataPayload) => {
         expect(requestBody).toEqual<transmitterTypes.IRuntimeDataPayload>({
           identity: {
             type: 'sysdig',
@@ -100,7 +100,14 @@ describe('dataScraper()', () => {
           ],
         });
       });
 
     await scrapeData();
+
+    try {
+      expect(nock.isDone()).toBeTruthy();
+    } catch (err) {
+      console.error(`nock pending mocks: ${nock.pendingMocks()}`);
+      throw err;
+    }
   });
 });

Vulnerable code:                     kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=integrationId=${INTEGRATION_ID} --from-literal=dockercfg.json={}
        kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=integrationId=${INTEGRATION_ID} --from-literal=dockercfg.json={}
* External internet access from the Kubernetes cluster to `kubernetes-upstream.snyk.io`.
  "DEFAULT_KUBERNETES_UPSTREAM_URL": "https://kubernetes-upstream.snyk.io",
integrationApi: https://kubernetes-upstream.dev.snyk.io
kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${INTEGRATION_ID}
  integrationApi: https://kubernetes-upstream.dev.snyk.io
    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId and Docker config file:
    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID}
    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId and Docker config file:
    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID}
  integrationApi: https://kubernetes-upstream.dev.snyk.io
    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId and Docker config file:
    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID}
  return await retryRequest(req.method, req.url, req.payload);
        url: `${upstreamUrl}/api/v1/dependency-graph`,
        url: `${upstreamUrl}/api/v1/scan-results`,
      url: `${upstreamUrl}/api/v1/workload`,
      `${upstreamUrl}/api/v1/policy`,
    const query = `userLocator=${userLocator}&cluster=${cluster}&namespace=${namespace}&type=${type}&name=${name}&agentId=${agentId}`;
      url: `${upstreamUrl}/api/v1/workload?${query}`,
      url: `${upstreamUrl}/api/v1/cluster`,
      url: `${upstreamUrl}/api/v1/runtime-results`,
    expect(config.WORKERS_COUNT).toEqual(10);
  integrationApi: https://kubernetes-upstream.dev.snyk.io
  integrationApi: https://kubernetes-upstream.dev.snyk.io
      '--set integrationApi=https://kubernetes-upstream.dev.snyk.io ' +
      '--set integrationApi=https://kubernetes-upstream.dev.snyk.io ' +
  envVar.value = 'https://kubernetes-upstream.dev.snyk.io';
    await predeploy(integrationId, namespace);
    nock('https://kubernetes-upstream.snyk.io')
      .post('/api/v1/runtime-results')
      .post('/api/v1/runtime-results')
Fixed code:                     SERVICE_ACCOUNT_API_TOKEN=$(uuidgen)
                    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=integrationId=${INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SERVICE_ACCOUNT_API_TOKEN} --from-literal=dockercfg.json={}
        SERVICE_ACCOUNT_API_TOKEN=$(uuidgen)
        kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=integrationId=${INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SERVICE_ACCOUNT_API_TOKEN} --from-literal=dockercfg.json={}
* External internet access from the Kubernetes cluster to `api.snyk.io`.
  "SERVICE_ACCOUNT_API_TOKEN": "",
  "DEFAULT_KUBERNETES_UPSTREAM_URL": "https://api.snyk.io/v2/kubernetes-upstream",
integrationApi: "https://api.dev.snyk.io/v2/kubernetes-upstream"
if [ "$SERVICE_ACCOUNT_API_TOKEN" == "" ]; then
    echo "Enter your service account API token: "
    read SERVICE_ACCOUNT_API_TOKEN
fi

kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SERVICE_ACCOUNT_API_TOKEN}
          - name: SNYK_SERVICE_ACCOUNT_API_TOKEN
            valueFrom:
              secretKeyRef:
                name: snyk-monitor
                key: serviceAccountApiToken
  integrationApi: https://api.dev.snyk.io/v2/kubernetes-upstream
    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId, serviceAccountApiToken and Docker config file:
    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SNYK_SERVICE_ACCOUNT_API_TOKEN}
    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId, serviceAccountApiToken and Docker config file:
    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SNYK_SERVICE_ACCOUNT_API_TOKEN}
          - name: SNYK_SERVICE_ACCOUNT_API_TOKEN
            valueFrom:
              secretKeyRef:
                name: snyk-monitor
                key: serviceAccountApiToken
  integrationApi: https://api.dev.snyk.io/v2/kubernetes-upstream
    To start using the Operator and the Snyk controller that it manages, you will need to create a secret containing your Snyk integrationId, serviceAccountApiToken and Docker config file:
    kubectl create secret generic snyk-monitor -n snyk-monitor --from-literal=dockercfg.json={} --from-literal=integrationId=${SNYK_INTEGRATION_ID} --from-literal=serviceAccountApiToken=${SNYK_SERVICE_ACCOUNT_API_TOKEN}
config.SERVICE_ACCOUNT_API_TOKEN = config.SERVICE_ACCOUNT_API_TOKEN.trim();
  SERVICE_ACCOUNT_API_TOKEN: string;
import path from 'path';
  options: NeedleOptions;
const upstreamRequestVersion = '2023-02-10';

  return await retryRequest(req.method, req.url, req.payload, req.options);
const upstreamRequestOptions = {
  headers: {
    Authorization: `token ${config.SERVICE_ACCOUNT_API_TOKEN}`,
  },
};

function constructUpstreamRequestUrl(
  requestPath: string,
  queryParams?: Record<string, string>,
): string {
  const requestUrl = new URL(upstreamUrl);
  requestUrl.pathname = path.join(requestUrl.pathname, requestPath);
  requestUrl.searchParams.set('version', upstreamRequestVersion);
  for (const key in queryParams) {
    requestUrl.searchParams.set(key, queryParams[key]);
  }
  return requestUrl.toString();
}

        url: constructUpstreamRequestUrl('/api/v1/dependency-graph'),
        options: upstreamRequestOptions,
        url: constructUpstreamRequestUrl('/api/v1/scan-results'),
        options: upstreamRequestOptions,
      url: constructUpstreamRequestUrl('/api/v1/workload'),
      options: upstreamRequestOptions,
      constructUpstreamRequestUrl('/api/v1/policy'),
      upstreamRequestOptions,
    const queryParams: Record<string, string> = {
      userLocator,
      cluster,
      namespace,
      type,
      name,
      agentId,
    };
      url: constructUpstreamRequestUrl('api/v1/workload', queryParams),
      options: upstreamRequestOptions,
      url: constructUpstreamRequestUrl('/api/v1/cluster'),
      options: upstreamRequestOptions,
      url: constructUpstreamRequestUrl('/api/v1/runtime-results'),
      options: upstreamRequestOptions,
  const apiToken = '46766a0a-ed0b-4e91-84c8-ea1c827f2a73';
    process.env.SNYK_SERVICE_ACCOUNT_API_TOKEN = apiToken;
    expect(config.SERVICE_ACCOUNT_API_TOKEN).toEqual(apiToken);
    expect(config.WORKERS_COUNT).toEqual(5);
  integrationApi: https://api.dev.snyk.io/v2/kubernetes-upstream
  integrationApi: https://api.dev.snyk.io/v2/kubernetes-upstream
      '--set integrationApi=https://api.dev.snyk.io/v2/kubernetes-upstream ' +
      '--set integrationApi=https://api.dev.snyk.io/v2/kubernetes-upstream ' +
  envVar.value = 'https://api.dev.snyk.io/v2/kubernetes-upstream';
function getServiceAccountApiToken(): string {
  const serviceAccountApiToken = randomUUID();
  console.log(
    `Generated new service account API token ${serviceAccountApiToken}`,
  );
  return serviceAccountApiToken;
}

  serviceAccountApiToken: string,
      serviceAccountApiToken,
    const serviceAccountApiToken = getServiceAccountApiToken();
    await predeploy(integrationId, serviceAccountApiToken, namespace);
    nock('https://api.snyk.io')
      .post('/v2/kubernetes-upstream/api/v1/runtime-results?version=2023-02-10')
      .post('/v2/kubernetes-upstream/api/v1/runtime-results?version=2023-02-10')

    try {
      expect(nock.isDone()).toBeTruthy();
    } catch (err) {
      console.error(`nock pending mocks: ${nock.pendingMocks()}`);
      throw err;
    }