Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing the exposure detection in case of Istio Ingress Gateway #639

Merged
merged 4 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading