diff --git a/controls/C-0274-unauthenticatedservice.json b/controls/C-0274-unauthenticatedservice.json new file mode 100644 index 00000000..17d4e11b --- /dev/null +++ b/controls/C-0274-unauthenticatedservice.json @@ -0,0 +1,35 @@ +{ + "name": "Verify Authenticated Service", + "controlID": "C-0274", + "description": "Verifies if the service is authenticated", + "long_description": "Verifies that in order to access the service, the user must be authenticated.", + "remediation": "Configure the service to require authentication.", + "manual_test": "", + "attributes": { + "controlTypeTags": [ + "security" + ], + "attackTracks": [ + { + "attackTrack": "workload-external-track", + "categories": [ + "Data Collection" + ] + } + ] + }, + "rulesNames": [ + "unauthenticated-service" + ], + "baseScore": 7, + "impact_statement": "", + "default_value": "", + "category": { + "name": "Network" + }, + "scanningScope": { + "matches": [ + "cluster" + ] + } +} \ No newline at end of file diff --git a/frameworks/security.json b/frameworks/security.json index 0b1651f6..2bdf8449 100644 --- a/frameworks/security.json +++ b/frameworks/security.json @@ -223,6 +223,12 @@ "patch": { "name": "Exposure to internet via Gateway API" } + }, + { + "controlID": "C-0274", + "patch": { + "name": "Verify Authenticated Service" + } } ] } \ No newline at end of file diff --git a/rules/unauthenticated-service/raw.rego b/rules/unauthenticated-service/raw.rego new file mode 100644 index 00000000..950b484e --- /dev/null +++ b/rules/unauthenticated-service/raw.rego @@ -0,0 +1,51 @@ +package armo_builtins + +import future.keywords.contains +import future.keywords.if + +deny contains msga if { + service := input[_] + service.kind == "Service" + + wl := input[_] + spec_template_spec_patterns := {"Deployment", "ReplicaSet", "DaemonSet", "StatefulSet", "Pod", "Job", "CronJob"} + spec_template_spec_patterns[wl.kind] + wl_connected_to_service(wl, service) + + service_scan_result := input[_] + service_scan_result.kind == "ServiceScanResult" + service_name := service.metadata.name + has_unauthenticated_service(service_name, service.metadata.namespace, service_scan_result) + + # Path to the service object + path := "spec" + + msga := { + "alertMessage": sprintf("Unauthenticated service %v exposes %v", [service_name, wl.metadata.name]), + "alertScore": 7, + "fixPaths": [], + "reviewPaths": [path], + "failedPaths": [], + "packagename": "armo_builtins", + "alertObject": {"k8sApiObjects": [service]}, + "relatedObjects": [ + {"object": wl}, + {"object": service}, + {"object": service_scan_result}, + ], + } +} + +has_unauthenticated_service(service_name, namespace, service_scan_result) if { + service_scan_result.metadata.name == service_name + service_scan_result.metadata.namespace == namespace + service_scan_result.spec.ports[_].authenticated == false +} + +wl_connected_to_service(wl, svc) if { + count({x | svc.spec.selector[x] == wl.metadata.labels[x]}) == count(svc.spec.selector) +} + +wl_connected_to_service(wl, svc) if { + wl.spec.selector.matchLabels == svc.spec.selector +} diff --git a/rules/unauthenticated-service/rule.metadata.json b/rules/unauthenticated-service/rule.metadata.json new file mode 100644 index 00000000..3ce6e83d --- /dev/null +++ b/rules/unauthenticated-service/rule.metadata.json @@ -0,0 +1,61 @@ +{ + "name": "unauthenticated-service", + "ruleLanguage": "Rego", + "match": [ + { + "apiGroups": [ + "" + ], + "apiVersions": [ + "v1" + ], + "resources": [ + "Pod", + "Service" + ] + }, + { + "apiGroups": [ + "apps" + ], + "apiVersions": [ + "v1" + ], + "resources": [ + "Deployment", + "ReplicaSet", + "DaemonSet", + "StatefulSet" + ] + }, + { + "apiGroups": [ + "batch" + ], + "apiVersions": [ + "*" + ], + "resources": [ + "Job", + "CronJob" + ] + }, + { + "apiGroups": [ + "kubescape.io" + ], + "apiVersions": [ + "v1" + ], + "resources": [ + "servicesscanresults" + ] + } + ], + "dynamicMatch": [ + ], + "ruleDependencies": [], + "description": "Verifies that the service is authenticated", + "remediation": "Add authentication to the service", + "ruleQuery": "armo_builtins" +} \ No newline at end of file diff --git a/rules/unauthenticated-service/test/fail_service/expected.json b/rules/unauthenticated-service/test/fail_service/expected.json new file mode 100644 index 00000000..bd3170cf --- /dev/null +++ b/rules/unauthenticated-service/test/fail_service/expected.json @@ -0,0 +1,118 @@ +[ + { + "alertMessage": "Unauthenticated service operator exposes operator", + "alertObject": { + "k8sApiObjects": [ + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "operator" + } + } + ] + }, + "alertScore": 7, + "deletePaths": null, + "failedPaths": [], + "fixPaths": [], + "packagename": "armo_builtins", + "relatedObjects": [ + { + "deletePaths": null, + "failedPaths": null, + "fixPaths": null, + "object": { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "labels": { + "app": "operator" + }, + "name": "operator", + "namespace": "kubescape" + }, + "spec": { + "containers": [ + { + "image": "your-operator-image:latest", + "name": "operator-container", + "ports": [ + { + "containerPort": 8080 + } + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "1Gi" + } + } + } + ] + } + }, + "reviewPaths": null + }, + { + "deletePaths": null, + "failedPaths": null, + "fixPaths": null, + "object": { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "operator", + "namespace": "kubescape" + }, + "spec": { + "ports": [ + { + "port": 4002, + "protocol": "TCP", + "targetPort": 8080 + } + ], + "selector": { + "app": "operator" + } + } + }, + "reviewPaths": null + }, + { + "deletePaths": null, + "failedPaths": null, + "fixPaths": null, + "object": { + "apiVersion": "kubescape.io/v1", + "kind": "ServiceScanResult", + "metadata": { + "creationTimestamp": "2024-07-03T04:40:17Z", + "generation": 4, + "name": "operator", + "namespace": "kubescape", + "resourceVersion": "2772", + "uid": "24dc622d-ee78-40c2-8654-2a5604715f95" + }, + "spec": { + "clusterIP": "10.103.207.220", + "ports": [ + { + "applicationLayer": "", + "authenticated": false, + "port": 4002, + "presentationLayer": "http", + "protocol": "TCP", + "sessionLayer": "tcp" + } + ] + } + }, + "reviewPaths": null + } + ], + "reviewPaths": ["spec"], + "ruleStatus": "" + } +] diff --git a/rules/unauthenticated-service/test/fail_service/input/operator.yaml b/rules/unauthenticated-service/test/fail_service/input/operator.yaml new file mode 100644 index 00000000..2905008e --- /dev/null +++ b/rules/unauthenticated-service/test/fail_service/input/operator.yaml @@ -0,0 +1,18 @@ +apiVersion: kubescape.io/v1 +kind: ServiceScanResult +metadata: + creationTimestamp: "2024-07-03T04:40:17Z" + generation: 4 + name: operator + namespace: kubescape + resourceVersion: "2772" + uid: 24dc622d-ee78-40c2-8654-2a5604715f95 +spec: + clusterIP: 10.103.207.220 + ports: + - applicationLayer: "" + authenticated: false + port: 4002 + presentationLayer: http + protocol: TCP + sessionLayer: tcp diff --git a/rules/unauthenticated-service/test/fail_service/input/operator2.yaml b/rules/unauthenticated-service/test/fail_service/input/operator2.yaml new file mode 100644 index 00000000..96a49561 --- /dev/null +++ b/rules/unauthenticated-service/test/fail_service/input/operator2.yaml @@ -0,0 +1,18 @@ +apiVersion: kubescape.io/v1 +kind: ServiceScanResult +metadata: + creationTimestamp: "2024-07-03T04:40:17Z" + generation: 4 + name: operator2 + namespace: kubescape + resourceVersion: "2772" + uid: 24dc622d-ee78-40c2-8654-2a5604715f95 +spec: + clusterIP: 10.103.207.220 + ports: + - applicationLayer: "" + authenticated: true + port: 4002 + presentationLayer: http + protocol: TCP + sessionLayer: tcp diff --git a/rules/unauthenticated-service/test/fail_service/input/pod.yaml b/rules/unauthenticated-service/test/fail_service/input/pod.yaml new file mode 100644 index 00000000..f91f8973 --- /dev/null +++ b/rules/unauthenticated-service/test/fail_service/input/pod.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: operator + namespace: kubescape + labels: + app: operator +spec: + containers: + - name: operator-container + image: your-operator-image:latest + ports: + - containerPort: 8080 + resources: + limits: + cpu: "1" + memory: "1Gi" \ No newline at end of file diff --git a/rules/unauthenticated-service/test/fail_service/input/pod2.yaml b/rules/unauthenticated-service/test/fail_service/input/pod2.yaml new file mode 100644 index 00000000..160f5645 --- /dev/null +++ b/rules/unauthenticated-service/test/fail_service/input/pod2.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: operator2 + namespace: kubescape + labels: + app: operator2 +spec: + containers: + - name: operator-container + image: your-operator-image:latest + ports: + - containerPort: 8080 + resources: + limits: + cpu: "1" + memory: "1Gi" \ No newline at end of file diff --git a/rules/unauthenticated-service/test/fail_service/input/service.yaml b/rules/unauthenticated-service/test/fail_service/input/service.yaml new file mode 100644 index 00000000..9d2ae271 --- /dev/null +++ b/rules/unauthenticated-service/test/fail_service/input/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: operator + namespace: kubescape +spec: + selector: + app: operator + ports: + - protocol: TCP + port: 4002 + targetPort: 8080 \ No newline at end of file diff --git a/rules/unauthenticated-service/test/fail_service/input/service2.yaml b/rules/unauthenticated-service/test/fail_service/input/service2.yaml new file mode 100644 index 00000000..bcec6292 --- /dev/null +++ b/rules/unauthenticated-service/test/fail_service/input/service2.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: operator2 + namespace: kubescape +spec: + selector: + app: operator2 + ports: + - protocol: TCP + port: 4002 + targetPort: 8080 \ No newline at end of file diff --git a/rules/unauthenticated-service/test/pass/expected.json b/rules/unauthenticated-service/test/pass/expected.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/rules/unauthenticated-service/test/pass/expected.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/rules/unauthenticated-service/test/pass/input/deploy.yaml b/rules/unauthenticated-service/test/pass/input/deploy.yaml new file mode 100644 index 00000000..608beba2 --- /dev/null +++ b/rules/unauthenticated-service/test/pass/input/deploy.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + deployment.kubernetes.io/revision: "1" + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"operator-deployment","namespace":"new-namespace"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"operator"}},"template":{"metadata":{"labels":{"app":"operator"}},"spec":{"containers":[{"args":["-c","nc -lnvp 8080"[],"command":["/bin/sh"],"image":"alpine:3.18.2","name":"operator-container","ports":[{"containerPort":8080}],"volumeMounts":[{"mountPath":"/etc/config","name":"config-volume"}]}],"volumes":[{"configMap":{"name":"operator-configmap"},"name":"config-volume"}]}}}} + creationTimestamp: "2024-07-15T11:38:56Z" + generation: 1 + name: operator-deployment + namespace: new-namespace + resourceVersion: "1118651" + uid: d613b9a8-7ed8-4e0c-b80d-b14023b8d346 +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: operator + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + creationTimestamp: null + labels: + app: operator + spec: + containers: + - args: + - -c + - nc -lnvp 8080 + command: + - /bin/sh + image: alpine:3.18.2 + imagePullPolicy: IfNotPresent + name: operator-container + ports: + - containerPort: 8080 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /etc/config + name: config-volume + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 420 + name: operator-configmap + name: config-volume +status: + availableReplicas: 1 + conditions: + - lastTransitionTime: "2024-07-15T11:39:01Z" + lastUpdateTime: "2024-07-15T11:39:01Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: "True" + type: Available + - lastTransitionTime: "2024-07-15T11:38:56Z" + lastUpdateTime: "2024-07-15T11:39:01Z" + message: ReplicaSet "operator-deployment-748b6d7d54" has successfully progressed. + reason: NewReplicaSetAvailable + status: "True" + type: Progressing + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 \ No newline at end of file diff --git a/rules/unauthenticated-service/test/pass/input/service.yaml b/rules/unauthenticated-service/test/pass/input/service.yaml new file mode 100644 index 00000000..de9e1b93 --- /dev/null +++ b/rules/unauthenticated-service/test/pass/input/service.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"operator","namespace":"new-namespace"},"spec":{"ports":[{"port":4002,"protocol":"TCP","targetPort":8080}],"selector":{"app":"operator"},"type":"NodePort"}} + creationTimestamp: "2024-07-15T11:38:56Z" + name: operator + namespace: new-namespace + resourceVersion: "1118630" + uid: 9cb0d9b9-c4d7-4b48-b456-71229bdc7216 +spec: + clusterIP: 10.105.77.60 + clusterIPs: + - 10.105.77.60 + externalTrafficPolicy: Cluster + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - nodePort: 31624 + port: 4002 + protocol: TCP + targetPort: 8080 + selector: + app: operator + sessionAffinity: None + type: NodePort +status: + loadBalancer: {} \ No newline at end of file diff --git a/rules/unauthenticated-service/test/pass/input/service_result.yaml b/rules/unauthenticated-service/test/pass/input/service_result.yaml new file mode 100644 index 00000000..b46f4179 --- /dev/null +++ b/rules/unauthenticated-service/test/pass/input/service_result.yaml @@ -0,0 +1,21 @@ +apiVersion: kubescape.io/v1 +kind: ServiceScanResult +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"kubescape.io/v1","kind":"ServiceScanResult","metadata":{"annotations":{},"name":"operator","namespace":"new-namespace"},"spec":{"clusterIP":"10.103.207.220","ports":[{"applicationLayer":"","authenticated":false,"port":4002,"presentationLayer":"http","protocol":"TCP","sessionLayer":"tcp"}]}} + creationTimestamp: "2024-07-15T11:39:46Z" + generation: 1 + name: operator + namespace: new-namespace + resourceVersion: "1118691" + uid: cd049412-c329-48ce-82b8-dfa56d6e85fd +spec: + clusterIP: 10.103.207.220 + ports: + - applicationLayer: "" + authenticated: true + port: 4002 + presentationLayer: http + protocol: TCP + sessionLayer: tcp \ No newline at end of file