Skip to content
Permalink
Browse files Browse the repository at this point in the history
feat: call authenticated snyk API endpoints with token
BREAKING CHANGE: use a service account API token to perform
authenticated requests to snyk API.
  • Loading branch information
minsiyang authored and kat1906 committed Feb 28, 2023
1 parent 130c30e commit 5b9a782
Show file tree
Hide file tree
Showing 24 changed files with 111 additions and 33 deletions.
3 changes: 2 additions & 1 deletion .circleci/config.yml
Expand Up @@ -519,7 +519,8 @@ jobs:
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: |
Expand Down
3 changes: 2 additions & 1 deletion .circleci/config/jobs/operator_upgrade_tests.yml
Expand Up @@ -120,7 +120,8 @@ steps:
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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -10,7 +10,7 @@ A containerized application that is deployed with Helm. Monitors the security of
## 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

Expand Down
3 changes: 2 additions & 1 deletion config.default.json
Expand Up @@ -19,7 +19,8 @@
"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
}
2 changes: 1 addition & 1 deletion scripts/circleci-jobs/deploy_to_dev.sh
Expand Up @@ -20,7 +20,7 @@ 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
Expand Down
7 changes: 6 additions & 1 deletion scripts/local-testing.sh
Expand Up @@ -15,10 +15,15 @@ if [ "$INTEGRATION_ID" == "" ]; then
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"
5 changes: 5 additions & 0 deletions snyk-monitor-deployment.yaml
Expand Up @@ -43,6 +43,11 @@ spec:
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:
Expand Down
2 changes: 1 addition & 1 deletion snyk-operator-certified/README.md
Expand Up @@ -41,7 +41,7 @@ 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
Expand Down
Expand Up @@ -147,11 +147,11 @@ spec:
## 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.
Expand Down
Expand Up @@ -129,11 +129,11 @@ spec:
## 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.
Expand Down
Expand Up @@ -95,6 +95,11 @@ spec:
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
Expand Down
2 changes: 1 addition & 1 deletion snyk-operator/certified-operator/README.md
Expand Up @@ -40,7 +40,7 @@ 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
Expand Down
Expand Up @@ -187,11 +187,11 @@ spec:
## 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.
Expand Down
1 change: 1 addition & 0 deletions src/common/config.ts
Expand Up @@ -39,6 +39,7 @@ function getClusterName(): string {
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';
Expand Down
1 change: 1 addition & 0 deletions src/common/types.ts
Expand Up @@ -19,6 +19,7 @@ export interface Config {
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;

Expand Down
55 changes: 46 additions & 9 deletions src/transmitter/index.ts
Expand Up @@ -21,6 +21,7 @@ import {
import { getProxyAgent } from './proxy';

import type { queueAsPromised } from 'fastq';
import path from 'path';

interface KubernetesUpstreamRequest {
method: NeedleHttpVerbs;
Expand All @@ -32,11 +33,14 @@ interface KubernetesUpstreamRequest {
| 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,
});
Expand All @@ -55,10 +59,29 @@ function getAgent(u: string): HttpAgent {
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> {
Expand All @@ -69,8 +92,9 @@ export async function sendDepGraph(
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);
Expand Down Expand Up @@ -100,8 +124,9 @@ export async function sendScanResults(
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);
Expand Down Expand Up @@ -136,8 +161,9 @@ export async function sendWorkloadMetadata(

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);
Expand Down Expand Up @@ -172,8 +198,9 @@ export async function sendWorkloadEventsPolicy(

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}`);
Expand Down Expand Up @@ -207,11 +234,19 @@ export async function deleteWorkload(
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);
Expand Down Expand Up @@ -348,8 +383,9 @@ export async function sendClusterMetadata(): Promise<void> {

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);
Expand Down Expand Up @@ -394,8 +430,9 @@ export async function sendRuntimeData(

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);
Expand Down
5 changes: 4 additions & 1 deletion test/common/config.spec.ts
@@ -1,8 +1,10 @@
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(() => {
Expand Down Expand Up @@ -68,6 +70,7 @@ describe('extractNamespaceName()', () => {
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();
Expand All @@ -76,7 +79,7 @@ describe('extractNamespaceName()', () => {
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');
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/operator/custom-resource-k8s.yaml
Expand Up @@ -4,7 +4,7 @@ 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:
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/operator/custom-resource.yaml
Expand Up @@ -4,7 +4,7 @@ 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:
Expand Down
2 changes: 1 addition & 1 deletion test/setup/deployers/helm-with-proxy.ts
Expand Up @@ -37,7 +37,7 @@ async function deployKubernetesMonitor(
`--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',
);
Expand Down
2 changes: 1 addition & 1 deletion test/setup/deployers/helm.ts
Expand Up @@ -30,7 +30,7 @@ async function deployKubernetesMonitor(
`--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 ' +
Expand Down
2 changes: 1 addition & 1 deletion test/setup/deployers/yaml.ts
Expand Up @@ -47,7 +47,7 @@ function createTestYamlDeployment(
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) {
Expand Down

0 comments on commit 5b9a782

Please sign in to comment.