Skip to content

Commit

Permalink
feat: allow scanning only current revision of deployment (#870)
Browse files Browse the repository at this point in the history
Adds the OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS
environment variable to allow scanning only the current revision of
a Deployment. If a ReplicaSet is owned by a Deployment the code will check if
it's the current revision and scan it for vulnerabilities. Inactive ReplicaSets
will be ignored.

To check the owner of a ReplicaSet additional permissions to get, list, and
watch Deployments is required.

Resolves: #858
Resolves: #668

Signed-off-by: Edvin Norling <edvin.norling@xenit.se>
Co-authored-by: Zach Stone <z.stone91@gmail.com>
Co-authored-by: Daniel Pacak <pacak.daniel@gmail.com>
  • Loading branch information
3 people committed Jan 7, 2022
1 parent 5ab3973 commit 53b6ca8
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 43 deletions.
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ These guidelines will help you get started with the Starboard project.
## Build Binaries

| Binary | Image | Description |
| ------------------------ | ---------------------------------------------- | ------------------------------------------------------------ |
| ------------------------ | ---------------------------------------------- | ------------------------------------------------------------- |
| `starboard` | `docker.io/aquasec/starboard:dev` | Starboard command-line interface |
| `starboard-operator` | `docker.io/aquasec/starboard-operator:dev` | Starboard Operator |
| `starboard-scanner-aqua` | `docker.io/aquasec/starboard-scanner-aqua:dev` | Starboard plugin to integrate with Aqua vulnerability scanner |
Expand Down Expand Up @@ -237,6 +237,7 @@ basic development workflow. For other install modes see [Operator Multitenancy w
OPERATOR_LOG_DEV_MODE=true \
OPERATOR_CIS_KUBERNETES_BENCHMARK_ENABLED=true \
OPERATOR_VULNERABILITY_SCANNER_ENABLED=true \
OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS=false \
OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED=true \
OPERATOR_BATCH_DELETE_LIMIT=3 \
OPERATOR_BATCH_DELETE_DELAY="30s" \
Expand Down
2 changes: 2 additions & 0 deletions deploy/helm/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ spec:
value: {{ .Values.operator.vulnerabilityScannerEnabled | quote }}
- name: OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED
value: {{ .Values.operator.configAuditScannerEnabled | quote }}
- name: OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS
value: {{ .Values.operator.vulnerabilityScannerScanOnlyCurrentRevisions | quote }}
{{- if gt (int .Values.operator.replicas) 1 }}
- name: OPERATOR_LEADER_ELECTION_ENABLED
value: "true"
Expand Down
1 change: 1 addition & 0 deletions deploy/helm/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ rules:
- replicasets
- statefulsets
- daemonsets
- deployments
verbs:
- get
- list
Expand Down
2 changes: 2 additions & 0 deletions deploy/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ operator:
kubernetesBenchmarkEnabled: true
# batchDeleteLimit the maximum number of config audit reports deleted by the operator when the plugin's config has changed.
batchDeleteLimit: 10
# vulnerabilityScannerScanOnlyCurrentRevisions the flag to only create vulnerability scans on the current revision of a deployment.
vulnerabilityScannerScanOnlyCurrentRevisions: false
# batchDeleteDelay the duration to wait before deleting another batch of config audit reports.
batchDeleteDelay: 10s
image:
Expand Down
1 change: 1 addition & 0 deletions deploy/static/02-starboard-operator.rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ rules:
- replicasets
- statefulsets
- daemonsets
- deployments
verbs:
- get
- list
Expand Down
2 changes: 2 additions & 0 deletions deploy/static/04-starboard-operator.deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ spec:
value: "true"
- name: OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED
value: "true"
- name: OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS
value: "false"
ports:
- name: metrics
containerPort: 8080
Expand Down
47 changes: 24 additions & 23 deletions docs/operator/configuration.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
Configuration of the operator's Pod is done via environment variables at startup.

| NAME | DEFAULT | DESCRIPTION |
| ------------------------------------------- | ---------------------- | ----------- |
| `OPERATOR_NAMESPACE` | N/A | See [Install modes](#install-modes) |
| `OPERATOR_TARGET_NAMESPACES` | N/A | See [Install modes](#install-modes) |
| `OPERATOR_SERVICE_ACCOUNT` | `starboard-operator` | The name of the service account assigned to the operator's pod |
| `OPERATOR_LOG_DEV_MODE` | `false` | The flag to use (or not use) development mode (more human-readable output, extra stack traces and logging information, etc). |
| `OPERATOR_SCAN_JOB_TIMEOUT` | `5m` | The length of time to wait before giving up on a scan job |
| `OPERATOR_CONCURRENT_SCAN_JOBS_LIMIT` | `10` | The maximum number of scan jobs create by the operator |
| `OPERATOR_SCAN_JOB_RETRY_AFTER` | `30s` | The duration to wait before retrying a failed scan job |
| `OPERATOR_BATCH_DELETE_LIMIT` | `10` | The maximum number of config audit reports deleted by the operator when the plugin's config has changed. |
| `OPERATOR_BATCH_DELETE_DELAY` | `10s` | The duration to wait before deleting another batch of config audit reports. |
| `OPERATOR_METRICS_BIND_ADDRESS` | `:8080` | The TCP address to bind to for serving [Prometheus][prometheus] metrics. It can be set to `0` to disable the metrics serving. |
| `OPERATOR_HEALTH_PROBE_BIND_ADDRESS` | `:9090` | The TCP address to bind to for serving health probes, i.e. `/healthz/` and `/readyz/` endpoints. |
| `OPERATOR_CIS_KUBERNETES_BENCHMARK_ENABLED` | `true` | The flag to enable CIS Kubernetes Benchmark scanner |
| `OPERATOR_VULNERABILITY_SCANNER_ENABLED` | `true` | The flag to enable vulnerability scanner |
| `OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED` | `true` | The flag to enable configuration audit scanner |
| `OPERATOR_LEADER_ELECTION_ENABLED` | `false` | The flag to enable operator replica leader election |
| `OPERATOR_LEADER_ELECTION_ID` | `starboard-lock` | The name of the resource lock for leader election |
| NAME | DEFAULT | DESCRIPTION |
| ------------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `OPERATOR_NAMESPACE` | N/A | See [Install modes](#install-modes) |
| `OPERATOR_TARGET_NAMESPACES` | N/A | See [Install modes](#install-modes) |
| `OPERATOR_SERVICE_ACCOUNT` | `starboard-operator` | The name of the service account assigned to the operator's pod |
| `OPERATOR_LOG_DEV_MODE` | `false` | The flag to use (or not use) development mode (more human-readable output, extra stack traces and logging information, etc). |
| `OPERATOR_SCAN_JOB_TIMEOUT` | `5m` | The length of time to wait before giving up on a scan job |
| `OPERATOR_CONCURRENT_SCAN_JOBS_LIMIT` | `10` | The maximum number of scan jobs create by the operator |
| `OPERATOR_SCAN_JOB_RETRY_AFTER` | `30s` | The duration to wait before retrying a failed scan job |
| `OPERATOR_BATCH_DELETE_LIMIT` | `10` | The maximum number of config audit reports deleted by the operator when the plugin's config has changed. |
| `OPERATOR_BATCH_DELETE_DELAY` | `10s` | The duration to wait before deleting another batch of config audit reports. |
| `OPERATOR_METRICS_BIND_ADDRESS` | `:8080` | The TCP address to bind to for serving [Prometheus][prometheus] metrics. It can be set to `0` to disable the metrics serving. |
| `OPERATOR_HEALTH_PROBE_BIND_ADDRESS` | `:9090` | The TCP address to bind to for serving health probes, i.e. `/healthz/` and `/readyz/` endpoints. |
| `OPERATOR_CIS_KUBERNETES_BENCHMARK_ENABLED` | `true` | The flag to enable CIS Kubernetes Benchmark scanner |
| `OPERATOR_VULNERABILITY_SCANNER_ENABLED` | `true` | The flag to enable vulnerability scanner |
| `OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED` | `true` | The flag to enable configuration audit scanner |
| `OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS` | `false` | The flag to enable vulnerability scanner to only scan the current revision of a deployment |
| `OPERATOR_LEADER_ELECTION_ENABLED` | `false` | The flag to enable operator replica leader election |
| `OPERATOR_LEADER_ELECTION_ID` | `starboard-lock` | The name of the resource lock for leader election |

## Install Modes

The values of the `OPERATOR_NAMESPACE` and `OPERATOR_TARGET_NAMESPACES` determine
the install mode, which in turn determines the multitenancy support of the operator.

| MODE | OPERATOR_NAMESPACE | OPERATOR_TARGET_NAMESPACES | DESCRIPTION |
| --------------- | ------------------ | -------------------------- | ----------- |
| OwnNamespace | `operators` | `operators` | The operator can be configured to watch events in the namespace it is deployed in. |
| MODE | OPERATOR_NAMESPACE | OPERATOR_TARGET_NAMESPACES | DESCRIPTION |
| --------------- | ------------------ | -------------------------- | -------------------------------------------------------------------------------------------------------------- |
| OwnNamespace | `operators` | `operators` | The operator can be configured to watch events in the namespace it is deployed in. |
| SingleNamespace | `operators` | `foo` | The operator can be configured to watch for events in a single namespace that the operator is not deployed in. |
| MultiNamespace | `operators` | `foo,bar,baz` | The operator can be configured to watch for events in more than one namespace. |
| AllNamespaces | `operators` | (blank string) | The operator can be configured to watch for events in all namespaces. |
| MultiNamespace | `operators` | `foo,bar,baz` | The operator can be configured to watch for events in more than one namespace. |
| AllNamespaces | `operators` | (blank string) | The operator can be configured to watch for events in all namespaces. |

[prometheus]: https://github.com/prometheus
3 changes: 3 additions & 0 deletions docs/operator/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ collection. For example, when the previous ReplicaSet named `nginx-6d4cf56db6` i
`replicaset-nginx-6d4cf56db6-nginx` as well as the ConfigAuditReport named `replicaset-nginx-6d4cf56db6` are
automatically garbage collected.

If you only want the latest replicaset in your deployment to be scanned for vulnerabilities you can define `OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS=true`
in your operator deployment. This can be useful if you only want to know about vulnerability that is currently a potential issue.

!!! tip
You can get and describe `vulnerabilityreports` and `configauditreports` as built-in Kubernetes objects:
```
Expand Down
25 changes: 22 additions & 3 deletions pkg/kube/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ const (
KindRole Kind = "Role"
KindRoleBinding Kind = "RoleBinding"

KindClusterRole Kind = "ClusterRole"
KindClusterRoleBindings Kind = "ClusterRoleBinding"
KindCustomResourceDefinition Kind = "CustomResourceDefinition"
KindClusterRole Kind = "ClusterRole"
KindClusterRoleBindings Kind = "ClusterRoleBinding"
KindCustomResourceDefinition Kind = "CustomResourceDefinition"
deploymentAnnotation string = "deployment.kubernetes.io/revision"
)

// IsBuiltInWorkload returns true if the specified v1.OwnerReference
Expand Down Expand Up @@ -488,3 +489,21 @@ func (o *ObjectResolver) getReplicaSetByPod(ctx context.Context, object Object)
}
return controller.Name, nil
}

func (o *ObjectResolver) IsActiveReplicaSet(ctx context.Context, workloadObj client.Object, controller *metav1.OwnerReference) (bool, error) {
if controller != nil && controller.Kind == string(KindDeployment) {
deploymentObject := &appsv1.Deployment{}

err := o.Client.Get(ctx, client.ObjectKey{
Namespace: workloadObj.GetNamespace(),
Name: controller.Name,
}, deploymentObject)
if err != nil {
return false, err
}
deploymentRevisionAnnotation := deploymentObject.GetAnnotations()
replicasetRevisionAnnotation := workloadObj.GetAnnotations()
return replicasetRevisionAnnotation[deploymentAnnotation] == deploymentRevisionAnnotation[deploymentAnnotation], nil
}
return true, nil
}
181 changes: 181 additions & 0 deletions pkg/kube/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -957,3 +957,184 @@ func TestObjectResolver_ReportOwner(t *testing.T) {
})
}
}

func TestObjectResolver_IsActiveReplicaSet(t *testing.T) {
nginxDeploy := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "Deployment",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: corev1.NamespaceDefault,
Name: "nginx",
Labels: map[string]string{
"app": "nginx",
},
Annotations: map[string]string{
"deployment.kubernetes.io/revision": "1",
},
UID: "734c1370-2281-4946-9b5f-940b33f3e4b8",
},
Spec: appsv1.DeploymentSpec{
Replicas: pointer.Int32Ptr(1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "nginx",
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Namespace: corev1.NamespaceDefault,
Name: "nginx",
Labels: map[string]string{
"app": "nginx",
},
Annotations: map[string]string{
"deployment.kubernetes.io/revision": "1",
},
},
},
},
}
nginxReplicaSet := &appsv1.ReplicaSet{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: corev1.NamespaceDefault,
Name: "nginx-6d4cf56db6",
Labels: map[string]string{
"app": "nginx",
"pod-template-hash": "6d4cf56db6",
},
Annotations: map[string]string{
"deployment.kubernetes.io/desired-replicas": "1",
"deployment.kubernetes.io/max-replicas": "4",
"deployment.kubernetes.io/revision": "1",
},
UID: "ecfff877-784c-4f05-8b70-abe441ca1976",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "nginx",
UID: "734c1370-2281-4946-9b5f-940b33f3e4b8",
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
Spec: appsv1.ReplicaSetSpec{
Replicas: pointer.Int32(1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "nginx",
"pod-template-hash": "6d4cf56db6",
},
},
},
}
notActiveNginxReplicaSet := &appsv1.ReplicaSet{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: corev1.NamespaceDefault,
Name: "nginx-f88799b98",
Labels: map[string]string{
"app": "nginx",
"pod-template-hash": "f88799b98",
},
Annotations: map[string]string{
"deployment.kubernetes.io/desired-replicas": "1",
"deployment.kubernetes.io/max-replicas": "4",
"deployment.kubernetes.io/revision": "2",
},
UID: "6fd87db4-d557-4b84-92b7-653c3f4e5c7d",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "nginx",
UID: "734c1370-2281-4946-9b5f-940b33f3e4b8",
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
},
},
},
Spec: appsv1.ReplicaSetSpec{
Replicas: pointer.Int32(1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "nginx",
"pod-template-hash": "f88799b98",
},
},
},
}
standAloneNginxReplicaSet := &appsv1.ReplicaSet{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: corev1.NamespaceDefault,
Name: "nginx-d54df7dc7",
Labels: map[string]string{
"app": "nginx",
"pod-template-hash": "d54df7dc7",
},
Annotations: map[string]string{
"deployment.kubernetes.io/desired-replicas": "1",
"deployment.kubernetes.io/max-replicas": "4",
},
UID: "0eed5ccf-4518-4ae7-933e-cafded6cf356",
},
Spec: appsv1.ReplicaSetSpec{
Replicas: pointer.Int32(1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "nginx",
"pod-template-hash": "d54df7dc7",
},
},
},
}
testClient := fake.NewClientBuilder().WithScheme(starboard.NewScheme()).WithObjects(
nginxDeploy,
nginxReplicaSet,
notActiveNginxReplicaSet,
).Build()
testCases := []struct {
name string
resource *appsv1.ReplicaSet
result bool
}{
{
name: "activeReplicaset",
resource: nginxReplicaSet,
result: true,
},
{
name: "noneActiveReplicaset",
resource: notActiveNginxReplicaSet,
result: false,
},
{
name: "standAloneReplicaset",
resource: standAloneNginxReplicaSet,
result: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
or := kube.ObjectResolver{Client: testClient}
controller := metav1.GetControllerOf(tc.resource)
isActive, err := or.IsActiveReplicaSet(context.TODO(), tc.resource, controller)
require.NoError(t, err)
assert.Equal(t, isActive, tc.result)
})
}
}
12 changes: 12 additions & 0 deletions pkg/operator/controller/vulnerabilityreport.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ func (r *VulnerabilityReportReconciler) reconcileWorkload(workloadKind kube.Kind
}
}

if r.Config.VulnerabilityScannerScanOnlyCurrentRevisions && workloadKind == kube.KindReplicaSet {
controller := metav1.GetControllerOf(workloadObj)
activeReplicaSet, err := r.IsActiveReplicaSet(ctx, workloadObj, controller)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed checking current revision: %w", err)
}
if !activeReplicaSet {
log.V(1).Info("Ignoring inactive ReplicaSet", "controllerKind", controller.Kind, "controllerName", controller.Name)
return ctrl.Result{}, nil
}
}

// Skip processing if it's a Job controlled by CronJob.
if workloadKind == kube.KindJob {
controller := metav1.GetControllerOf(workloadObj)
Expand Down

0 comments on commit 53b6ca8

Please sign in to comment.