Skip to content

Commit

Permalink
Merge pull request #639 from kubescape/implement-c-266-for-istio-ingress
Browse files Browse the repository at this point in the history
Implementing the exposure detection in case of Istio Ingress Gateway
  • Loading branch information
matthyx authored Nov 21, 2024
2 parents 152bc44 + 864bde7 commit 03d793d
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 5 deletions.
8 changes: 4 additions & 4 deletions controls/C-0266-exposuretointernet-gateway.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "Exposure to internet via Gateway API",
"name": "Exposure to internet via Gateway API or Istio Ingress",
"attributes": {
"controlTypeTags": [
"security"
Expand Down Expand Up @@ -31,10 +31,10 @@
}
]
},
"description": "This control detect workloads that are exposed on Internet through a Gateway API (HTTPRoute,TCPRoute, UDPRoute). It fails in case it find workloads connected with these resources.",
"description": "This control detect workloads that are exposed on Internet through a Gateway API (HTTPRoute,TCPRoute, UDPRoute) or Istio Gateway. It fails in case it find workloads connected with these resources.",
"remediation": "The user can evaluate its exposed resources and apply relevant changes wherever needed.",
"rulesNames": ["exposure-to-internet-via-gateway-api"],
"test": "Checks if workloads are exposed through the use of Gateway API (HTTPRoute,TCPRoute, UDPRoute).",
"rulesNames": ["exposure-to-internet-via-gateway-api","exposure-to-internet-via-istio-ingress"],
"test": "Checks if workloads are exposed through the use of Gateway API (HTTPRoute,TCPRoute, UDPRoute) or Istio Gateway.",
"controlID": "C-0266",
"baseScore": 7.0,
"scanningScope": {
Expand Down
2 changes: 1 addition & 1 deletion rules/.regal/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ rules:
level: ignore
rule-length:
level: error
max-rule-length: 50
max-rule-length: 100
todo-comment:
level: ignore
use-assignment-operator:
Expand Down
144 changes: 144 additions & 0 deletions rules/exposure-to-internet-via-istio-ingress/raw.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package armo_builtins
import future.keywords.in


deny[msga] {
virtualservice := input[_]
virtualservice.kind == "VirtualService"

# Check if the VirtualService is connected to a Gateway
gateway := input[_]
gateway.kind == "Gateway"

is_same_namespace(gateway, virtualservice)
virtualservice.spec.gateways[_] == gateway.metadata.name

# Find the connected Istio Ingress Gateway that should be a LoadBalancer if it is exposed to the internet
istioingressgateway := input[_]
istioingressgateway.kind == "Service"
istioingressgateway.metadata.namespace == "istio-system"
gateway.spec.selector[_] == istioingressgateway.metadata.labels[_]


# Check if the Istio Ingress Gateway is exposed to the internet
is_exposed_service(istioingressgateway)

# Check if the VirtualService is connected to an workload
# First, find the service that the VirtualService is connected to
connected_service := input[_]
connected_service.kind == "Service"
fqsn := get_fqsn(get_namespace(virtualservice), virtualservice.spec.http[_].route[_].destination.host)
target_ns := split(fqsn,".")[1]
target_name := split(fqsn,".")[0]
# Check if the service is in the same namespace as the VirtualService
get_namespace(connected_service) == target_ns
# Check if the service is the target of the VirtualService
connected_service.metadata.name == target_name

# Check if the service is connected to a workload
wl := input[_]
is_same_namespace(connected_service, wl)
spec_template_spec_patterns := {"Deployment", "ReplicaSet", "DaemonSet", "StatefulSet", "Pod", "Job", "CronJob"}
spec_template_spec_patterns[wl.kind]
wl_connected_to_service(wl, connected_service)

result := svc_connected_to_virtualservice(connected_service, virtualservice)

msga := {
"alertMessage": sprintf("workload '%v' is exposed through virtualservice '%v'", [wl.metadata.name, virtualservice.metadata.name]),
"packagename": "armo_builtins",
"failedPaths": [],
"fixPaths": [],
"alertScore": 7,
"alertObject": {
"k8sApiObjects": [wl]
},
"relatedObjects": [
{
"object": virtualservice,
"reviewPaths": result,
"failedPaths": result,
},
{
"object": connected_service,
}
]
}
}

# ====================================================================================

get_namespace(obj) = namespace {
obj.metadata
obj.metadata.namespace
namespace := obj.metadata.namespace
}

get_namespace(obj) = namespace {
not obj.metadata.namespace
namespace := "default"
}

is_same_namespace(obj1, obj2) {
obj1.metadata.namespace == obj2.metadata.namespace
}

is_same_namespace(obj1, obj2) {
not obj1.metadata.namespace
obj2.metadata.namespace == "default"
}

is_same_namespace(obj1, obj2) {
not obj2.metadata.namespace
obj1.metadata.namespace == "default"
}

is_same_namespace(obj1, obj2) {
not obj1.metadata.namespace
not obj2.metadata.namespace
}

is_exposed_service(svc) {
svc.spec.type == "NodePort"
}

is_exposed_service(svc) {
svc.spec.type == "LoadBalancer"
}

wl_connected_to_service(wl, svc) {
count({x | svc.spec.selector[x] == wl.metadata.labels[x]}) == count(svc.spec.selector)
}

wl_connected_to_service(wl, svc) {
wl.spec.selector.matchLabels == svc.spec.selector
}

wl_connected_to_service(wl, svc) {
count({x | svc.spec.selector[x] == wl.spec.template.metadata.labels[x]}) == count(svc.spec.selector)
}

svc_connected_to_virtualservice(svc, virtualservice) = result {
host := virtualservice.spec.http[i].route[j].destination.host
svc.metadata.name == host
result := [sprintf("spec.http[%d].routes[%d].destination.host", [i,j])]
}

get_fqsn(ns, dest_host) = fqsn {
# verify that this name is without the namespace
count(split(".", dest_host)) == 1
fqsn := sprintf("%v.%v.svc.cluster.local", [dest_host, ns])
}

get_fqsn(ns, dest_host) = fqsn {
count(split(".", dest_host)) == 2
fqsn := sprintf("%v.svc.cluster.local", [dest_host])
}

get_fqsn(ns, dest_host) = fqsn {
count(split(".", dest_host)) == 4
fqsn := dest_host
}



62 changes: 62 additions & 0 deletions rules/exposure-to-internet-via-istio-ingress/rule.metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "exposure-to-internet-via-istio-ingress",
"attributes": {
"useFromKubescapeVersion": "v3.0.9"
},
"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": [
"networking.istio.io"
],
"apiVersions": [
"v1"
],
"resources": [
"VirtualService",
"Gateways"
]
}
],
"description": "fails if the running workload is bound to a Service that is exposed to the Internet through Istio Gateway.",
"remediation": "",
"ruleQuery": "armo_builtins"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
[
{
"alertMessage": "workload 'nginx' is exposed through virtualservice 'nginx'",
"failedPaths": [],
"reviewPaths": null,
"deletePaths": null,
"fixPaths": [],
"ruleStatus": "",
"packagename": "armo_builtins",
"alertScore": 7,
"alertObject": {
"k8sApiObjects": [
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"labels": {
"app": "nginx"
},
"name": "nginx"
}
}
]
},
"relatedObjects": [
{
"object": {
"apiVersion": "networking.istio.io/v1alpha3",
"kind": "VirtualService",
"metadata": {
"name": "nginx"
},
"spec": {
"gateways": [
"nginx-gateway"
],
"hosts": [
"*"
],
"http": [
{
"route": [
{
"destination": {
"host": "nginx",
"port": {
"number": 80
}
}
}
]
}
]
}
},
"failedPaths": [
"spec.http[0].routes[0].destination.host"
],
"reviewPaths": [
"spec.http[0].routes[0].destination.host"
],
"deletePaths": null,
"fixPaths": null
},
{
"object": {
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "nginx"
},
"spec": {
"ports": [
{
"port": 80,
"protocol": "TCP",
"targetPort": 80
}
],
"selector": {
"app": "nginx"
}
}
},
"failedPaths": null,
"reviewPaths": null,
"deletePaths": null,
"fixPaths": null
}
]
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
creationTimestamp: "2024-11-19T19:33:37Z"
generation: 1
labels:
app: nginx
name: nginx
namespace: default
resourceVersion: "826"
uid: 84c22298-82c1-4ca1-bc23-aeb210beffd7
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: nginx
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: nginx
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status:
availableReplicas: 1
conditions:
- lastTransitionTime: "2024-11-19T19:33:45Z"
lastUpdateTime: "2024-11-19T19:33:45Z"
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
- lastTransitionTime: "2024-11-19T19:33:37Z"
lastUpdateTime: "2024-11-19T19:33:45Z"
message: ReplicaSet "nginx-7854ff8877" has successfully progressed.
reason: NewReplicaSetAvailable
status: "True"
type: Progressing
observedGeneration: 1
readyReplicas: 1
replicas: 1
updatedReplicas: 1
Loading

0 comments on commit 03d793d

Please sign in to comment.