Skip to content

Commit 03d793d

Browse files
authored
Merge pull request #639 from kubescape/implement-c-266-for-istio-ingress
Implementing the exposure detection in case of Istio Ingress Gateway
2 parents 152bc44 + 864bde7 commit 03d793d

File tree

10 files changed

+461
-5
lines changed

10 files changed

+461
-5
lines changed

controls/C-0266-exposuretointernet-gateway.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "Exposure to internet via Gateway API",
2+
"name": "Exposure to internet via Gateway API or Istio Ingress",
33
"attributes": {
44
"controlTypeTags": [
55
"security"
@@ -31,10 +31,10 @@
3131
}
3232
]
3333
},
34-
"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.",
34+
"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.",
3535
"remediation": "The user can evaluate its exposed resources and apply relevant changes wherever needed.",
36-
"rulesNames": ["exposure-to-internet-via-gateway-api"],
37-
"test": "Checks if workloads are exposed through the use of Gateway API (HTTPRoute,TCPRoute, UDPRoute).",
36+
"rulesNames": ["exposure-to-internet-via-gateway-api","exposure-to-internet-via-istio-ingress"],
37+
"test": "Checks if workloads are exposed through the use of Gateway API (HTTPRoute,TCPRoute, UDPRoute) or Istio Gateway.",
3838
"controlID": "C-0266",
3939
"baseScore": 7.0,
4040
"scanningScope": {

rules/.regal/config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ rules:
2626
level: ignore
2727
rule-length:
2828
level: error
29-
max-rule-length: 50
29+
max-rule-length: 100
3030
todo-comment:
3131
level: ignore
3232
use-assignment-operator:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package armo_builtins
2+
import future.keywords.in
3+
4+
5+
deny[msga] {
6+
virtualservice := input[_]
7+
virtualservice.kind == "VirtualService"
8+
9+
# Check if the VirtualService is connected to a Gateway
10+
gateway := input[_]
11+
gateway.kind == "Gateway"
12+
13+
is_same_namespace(gateway, virtualservice)
14+
virtualservice.spec.gateways[_] == gateway.metadata.name
15+
16+
# Find the connected Istio Ingress Gateway that should be a LoadBalancer if it is exposed to the internet
17+
istioingressgateway := input[_]
18+
istioingressgateway.kind == "Service"
19+
istioingressgateway.metadata.namespace == "istio-system"
20+
gateway.spec.selector[_] == istioingressgateway.metadata.labels[_]
21+
22+
23+
# Check if the Istio Ingress Gateway is exposed to the internet
24+
is_exposed_service(istioingressgateway)
25+
26+
# Check if the VirtualService is connected to an workload
27+
# First, find the service that the VirtualService is connected to
28+
connected_service := input[_]
29+
connected_service.kind == "Service"
30+
fqsn := get_fqsn(get_namespace(virtualservice), virtualservice.spec.http[_].route[_].destination.host)
31+
target_ns := split(fqsn,".")[1]
32+
target_name := split(fqsn,".")[0]
33+
# Check if the service is in the same namespace as the VirtualService
34+
get_namespace(connected_service) == target_ns
35+
# Check if the service is the target of the VirtualService
36+
connected_service.metadata.name == target_name
37+
38+
# Check if the service is connected to a workload
39+
wl := input[_]
40+
is_same_namespace(connected_service, wl)
41+
spec_template_spec_patterns := {"Deployment", "ReplicaSet", "DaemonSet", "StatefulSet", "Pod", "Job", "CronJob"}
42+
spec_template_spec_patterns[wl.kind]
43+
wl_connected_to_service(wl, connected_service)
44+
45+
result := svc_connected_to_virtualservice(connected_service, virtualservice)
46+
47+
msga := {
48+
"alertMessage": sprintf("workload '%v' is exposed through virtualservice '%v'", [wl.metadata.name, virtualservice.metadata.name]),
49+
"packagename": "armo_builtins",
50+
"failedPaths": [],
51+
"fixPaths": [],
52+
"alertScore": 7,
53+
"alertObject": {
54+
"k8sApiObjects": [wl]
55+
},
56+
"relatedObjects": [
57+
{
58+
"object": virtualservice,
59+
"reviewPaths": result,
60+
"failedPaths": result,
61+
},
62+
{
63+
"object": connected_service,
64+
}
65+
]
66+
}
67+
}
68+
69+
# ====================================================================================
70+
71+
get_namespace(obj) = namespace {
72+
obj.metadata
73+
obj.metadata.namespace
74+
namespace := obj.metadata.namespace
75+
}
76+
77+
get_namespace(obj) = namespace {
78+
not obj.metadata.namespace
79+
namespace := "default"
80+
}
81+
82+
is_same_namespace(obj1, obj2) {
83+
obj1.metadata.namespace == obj2.metadata.namespace
84+
}
85+
86+
is_same_namespace(obj1, obj2) {
87+
not obj1.metadata.namespace
88+
obj2.metadata.namespace == "default"
89+
}
90+
91+
is_same_namespace(obj1, obj2) {
92+
not obj2.metadata.namespace
93+
obj1.metadata.namespace == "default"
94+
}
95+
96+
is_same_namespace(obj1, obj2) {
97+
not obj1.metadata.namespace
98+
not obj2.metadata.namespace
99+
}
100+
101+
is_exposed_service(svc) {
102+
svc.spec.type == "NodePort"
103+
}
104+
105+
is_exposed_service(svc) {
106+
svc.spec.type == "LoadBalancer"
107+
}
108+
109+
wl_connected_to_service(wl, svc) {
110+
count({x | svc.spec.selector[x] == wl.metadata.labels[x]}) == count(svc.spec.selector)
111+
}
112+
113+
wl_connected_to_service(wl, svc) {
114+
wl.spec.selector.matchLabels == svc.spec.selector
115+
}
116+
117+
wl_connected_to_service(wl, svc) {
118+
count({x | svc.spec.selector[x] == wl.spec.template.metadata.labels[x]}) == count(svc.spec.selector)
119+
}
120+
121+
svc_connected_to_virtualservice(svc, virtualservice) = result {
122+
host := virtualservice.spec.http[i].route[j].destination.host
123+
svc.metadata.name == host
124+
result := [sprintf("spec.http[%d].routes[%d].destination.host", [i,j])]
125+
}
126+
127+
get_fqsn(ns, dest_host) = fqsn {
128+
# verify that this name is without the namespace
129+
count(split(".", dest_host)) == 1
130+
fqsn := sprintf("%v.%v.svc.cluster.local", [dest_host, ns])
131+
}
132+
133+
get_fqsn(ns, dest_host) = fqsn {
134+
count(split(".", dest_host)) == 2
135+
fqsn := sprintf("%v.svc.cluster.local", [dest_host])
136+
}
137+
138+
get_fqsn(ns, dest_host) = fqsn {
139+
count(split(".", dest_host)) == 4
140+
fqsn := dest_host
141+
}
142+
143+
144+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"name": "exposure-to-internet-via-istio-ingress",
3+
"attributes": {
4+
"useFromKubescapeVersion": "v3.0.9"
5+
},
6+
"ruleLanguage": "Rego",
7+
"match": [
8+
{
9+
"apiGroups": [
10+
""
11+
],
12+
"apiVersions": [
13+
"v1"
14+
],
15+
"resources": [
16+
"Pod",
17+
"Service"
18+
]
19+
},
20+
{
21+
"apiGroups": [
22+
"apps"
23+
],
24+
"apiVersions": [
25+
"v1"
26+
],
27+
"resources": [
28+
"Deployment",
29+
"ReplicaSet",
30+
"DaemonSet",
31+
"StatefulSet"
32+
]
33+
},
34+
{
35+
"apiGroups": [
36+
"batch"
37+
],
38+
"apiVersions": [
39+
"*"
40+
],
41+
"resources": [
42+
"Job",
43+
"CronJob"
44+
]
45+
},
46+
{
47+
"apiGroups": [
48+
"networking.istio.io"
49+
],
50+
"apiVersions": [
51+
"v1"
52+
],
53+
"resources": [
54+
"VirtualService",
55+
"Gateways"
56+
]
57+
}
58+
],
59+
"description": "fails if the running workload is bound to a Service that is exposed to the Internet through Istio Gateway.",
60+
"remediation": "",
61+
"ruleQuery": "armo_builtins"
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
[
2+
{
3+
"alertMessage": "workload 'nginx' is exposed through virtualservice 'nginx'",
4+
"failedPaths": [],
5+
"reviewPaths": null,
6+
"deletePaths": null,
7+
"fixPaths": [],
8+
"ruleStatus": "",
9+
"packagename": "armo_builtins",
10+
"alertScore": 7,
11+
"alertObject": {
12+
"k8sApiObjects": [
13+
{
14+
"apiVersion": "apps/v1",
15+
"kind": "Deployment",
16+
"metadata": {
17+
"labels": {
18+
"app": "nginx"
19+
},
20+
"name": "nginx"
21+
}
22+
}
23+
]
24+
},
25+
"relatedObjects": [
26+
{
27+
"object": {
28+
"apiVersion": "networking.istio.io/v1alpha3",
29+
"kind": "VirtualService",
30+
"metadata": {
31+
"name": "nginx"
32+
},
33+
"spec": {
34+
"gateways": [
35+
"nginx-gateway"
36+
],
37+
"hosts": [
38+
"*"
39+
],
40+
"http": [
41+
{
42+
"route": [
43+
{
44+
"destination": {
45+
"host": "nginx",
46+
"port": {
47+
"number": 80
48+
}
49+
}
50+
}
51+
]
52+
}
53+
]
54+
}
55+
},
56+
"failedPaths": [
57+
"spec.http[0].routes[0].destination.host"
58+
],
59+
"reviewPaths": [
60+
"spec.http[0].routes[0].destination.host"
61+
],
62+
"deletePaths": null,
63+
"fixPaths": null
64+
},
65+
{
66+
"object": {
67+
"apiVersion": "v1",
68+
"kind": "Service",
69+
"metadata": {
70+
"name": "nginx"
71+
},
72+
"spec": {
73+
"ports": [
74+
{
75+
"port": 80,
76+
"protocol": "TCP",
77+
"targetPort": 80
78+
}
79+
],
80+
"selector": {
81+
"app": "nginx"
82+
}
83+
}
84+
},
85+
"failedPaths": null,
86+
"reviewPaths": null,
87+
"deletePaths": null,
88+
"fixPaths": null
89+
}
90+
]
91+
}
92+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
annotations:
5+
deployment.kubernetes.io/revision: "1"
6+
creationTimestamp: "2024-11-19T19:33:37Z"
7+
generation: 1
8+
labels:
9+
app: nginx
10+
name: nginx
11+
namespace: default
12+
resourceVersion: "826"
13+
uid: 84c22298-82c1-4ca1-bc23-aeb210beffd7
14+
spec:
15+
progressDeadlineSeconds: 600
16+
replicas: 1
17+
revisionHistoryLimit: 10
18+
selector:
19+
matchLabels:
20+
app: nginx
21+
strategy:
22+
rollingUpdate:
23+
maxSurge: 25%
24+
maxUnavailable: 25%
25+
type: RollingUpdate
26+
template:
27+
metadata:
28+
labels:
29+
app: nginx
30+
spec:
31+
containers:
32+
- image: nginx
33+
imagePullPolicy: Always
34+
name: nginx
35+
resources: {}
36+
terminationMessagePath: /dev/termination-log
37+
terminationMessagePolicy: File
38+
dnsPolicy: ClusterFirst
39+
restartPolicy: Always
40+
schedulerName: default-scheduler
41+
securityContext: {}
42+
terminationGracePeriodSeconds: 30
43+
status:
44+
availableReplicas: 1
45+
conditions:
46+
- lastTransitionTime: "2024-11-19T19:33:45Z"
47+
lastUpdateTime: "2024-11-19T19:33:45Z"
48+
message: Deployment has minimum availability.
49+
reason: MinimumReplicasAvailable
50+
status: "True"
51+
type: Available
52+
- lastTransitionTime: "2024-11-19T19:33:37Z"
53+
lastUpdateTime: "2024-11-19T19:33:45Z"
54+
message: ReplicaSet "nginx-7854ff8877" has successfully progressed.
55+
reason: NewReplicaSetAvailable
56+
status: "True"
57+
type: Progressing
58+
observedGeneration: 1
59+
readyReplicas: 1
60+
replicas: 1
61+
updatedReplicas: 1

0 commit comments

Comments
 (0)