Skip to content

Commit 31023e2

Browse files
authored
matching namespaced services + restrict to used ports (#56)
* New example Signed-off-by: Ziv Nevo <[email protected]> * renamed function Signed-off-by: Ziv Nevo <[email protected]> * reduce indexing and check ConfigMapKeyRef value Signed-off-by: Ziv Nevo <[email protected]> * match namespaced service patterns also restrict netpol ports to only used ports Signed-off-by: Ziv Nevo <[email protected]> * Ensure same namespace for resource and its service Signed-off-by: Ziv Nevo <[email protected]> * port names needed for multi-port service Signed-off-by: Ziv Nevo <[email protected]> * NamespaceSelectors in synthesized netpols Signed-off-by: Ziv Nevo <[email protected]>
1 parent 50f003d commit 31023e2

15 files changed

+625
-88
lines changed

pkg/analyzer/scan.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func ScanK8sConfigmapObject(kind string, objDataBuf []byte) (common.CfgMap, erro
8080
fullName := obj.ObjectMeta.Namespace + "/" + obj.ObjectMeta.Name
8181
data := map[string]string{}
8282
for k, v := range obj.Data {
83-
isPotentialAddress := identifyAddressValue(v)
83+
isPotentialAddress := IsNetworkAddressValue(v)
8484
if isPotentialAddress {
8585
data[k] = v
8686
}
@@ -129,7 +129,7 @@ func parseDeployResource(podSpec *v1.PodTemplateSpec, obj metaV1.Object, resourc
129129
}
130130
for _, e := range container.Env {
131131
if e.Value != "" {
132-
isPotentialAddress := identifyAddressValue(e.Value)
132+
isPotentialAddress := IsNetworkAddressValue(e.Value)
133133
if isPotentialAddress {
134134
resourceCtx.Resource.Envs = append(resourceCtx.Resource.Envs, e.Value)
135135
}
@@ -149,8 +149,8 @@ func parseDeployResource(podSpec *v1.PodTemplateSpec, obj metaV1.Object, resourc
149149
}
150150
}
151151

152-
// identifyAddressValue checks if a given string is a potential network address
153-
func identifyAddressValue(value string) bool {
152+
// IsNetworkAddressValue checks if a given string is a potential network address
153+
func IsNetworkAddressValue(value string) bool {
154154
_, err := url.Parse(value)
155155
if err != nil {
156156
return false

pkg/common/types.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ type Resource struct {
4343
Envs []string
4444
ConfigMapRefs []string `json:"-"`
4545
ConfigMapKeyRefs []CfgMapKeyRef `json:"-"`
46-
UsedPorts []int
46+
UsedPorts []SvcNetworkAttr
4747
} `json:"resource,omitempty"`
4848
}
4949

pkg/controller/connections.go

+59-20
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func discoverConnections(resources []common.Resource, links []common.Service) ([
1717
connections := []common.Connections{}
1818
for destResIdx := range resources {
1919
destRes := &resources[destResIdx]
20-
deploymentServices := findServices(destRes.Resource.Selectors, links)
20+
deploymentServices := findServices(destRes, links)
2121
for svcIdx := range deploymentServices {
2222
svc := &deploymentServices[svcIdx]
2323
srcRes := findSource(resources, svc)
@@ -35,9 +35,10 @@ func discoverConnections(resources []common.Resource, links []common.Service) ([
3535
}
3636

3737
// areSelectorsContained returns true if selectors2 is contained in selectors1
38-
func areSelectorsContained(selectors1, selectors2 []string) bool {
38+
func areSelectorsContained(selectors1 map[string]string, selectors2 []string) bool {
3939
elementMap := make(map[string]string)
40-
for _, s := range selectors1 {
40+
for k, v := range selectors1 {
41+
s := fmt.Sprintf("%s:%s", k, v)
4142
elementMap[s] = ""
4243
}
4344
for _, val := range selectors2 {
@@ -49,14 +50,16 @@ func areSelectorsContained(selectors1, selectors2 []string) bool {
4950
return true
5051
}
5152

52-
// findServices returns a list of services that may be in front of a given deployment (represented by its selectors)
53-
func findServices(selectors []string, links []common.Service) []common.Service {
53+
// findServices returns a list of services that may be in front of a given workload resource
54+
func findServices(resource *common.Resource, links []common.Service) []common.Service {
5455
var matchedSvc []common.Service
55-
//TODO: refer to namespaces - the matching services and input deployment should be in the same namespace
5656
for linkIdx := range links {
5757
link := &links[linkIdx]
58+
if link.Resource.Namespace != resource.Resource.Namespace {
59+
continue
60+
}
5861
// all service selector values should be contained in the input selectors of the deployment
59-
res := areSelectorsContained(selectors, link.Resource.Selectors)
62+
res := areSelectorsContained(resource.Resource.Labels, link.Resource.Selectors)
6063
if res {
6164
matchedSvc = append(matchedSvc, *link)
6265
}
@@ -72,22 +75,58 @@ func findServices(selectors []string, links []common.Service) []common.Service {
7275
func findSource(resources []common.Resource, service *common.Service) []*common.Resource {
7376
tRes := []*common.Resource{}
7477
for resIdx := range resources {
75-
res := &resources[resIdx]
76-
for _, envVal := range res.Resource.Envs {
77-
envVal = strings.TrimPrefix(envVal, "http://")
78-
if service.Resource.Name == envVal { // A match without port name
79-
tRes = append(tRes, res)
80-
}
81-
for _, p := range service.Resource.Network {
82-
serviceWithPort := fmt.Sprintf("%s:%d", service.Resource.Name, p.Port)
83-
if serviceWithPort == envVal {
84-
foundSrc := *res
85-
// specify the used ports for target by the found src
86-
foundSrc.Resource.UsedPorts = []int{p.Port}
87-
tRes = append(tRes, &foundSrc)
78+
resource := &resources[resIdx]
79+
serviceAddresses := getPossibleServiceAddresses(service, resource)
80+
foundSrc := *resource // We copy the resource so we can specify the ports used by the source found
81+
matched := false
82+
for _, envVal := range resource.Resource.Envs {
83+
match, port := envValueMatchesService(envVal, service, serviceAddresses)
84+
if match {
85+
matched = true
86+
if port.Port > 0 {
87+
foundSrc.Resource.UsedPorts = append(foundSrc.Resource.UsedPorts, port)
8888
}
8989
}
9090
}
91+
if matched {
92+
tRes = append(tRes, &foundSrc)
93+
}
9194
}
9295
return tRes
9396
}
97+
98+
func getPossibleServiceAddresses(service *common.Service, resource *common.Resource) []string {
99+
svcAddresses := []string{}
100+
if service.Resource.Namespace != "" {
101+
serviceDotNamespace := fmt.Sprintf("%s.%s", service.Resource.Name, service.Resource.Namespace)
102+
svcAddresses = append(svcAddresses, serviceDotNamespace, serviceDotNamespace+".svc.cluster.local")
103+
}
104+
if service.Resource.Namespace == resource.Resource.Namespace { // both service and resource live in the same namespace
105+
svcAddresses = append(svcAddresses, service.Resource.Name)
106+
}
107+
108+
return svcAddresses
109+
}
110+
111+
func envValueMatchesService(envVal string, service *common.Service, serviceAddresses []string) (bool, common.SvcNetworkAttr) {
112+
envVal = strings.TrimPrefix(envVal, "http://")
113+
envVal = strings.TrimPrefix(envVal, "https://")
114+
115+
// first look for matches without specified port
116+
for _, svcAddress := range serviceAddresses {
117+
if svcAddress == envVal {
118+
return true, common.SvcNetworkAttr{} // this means no specified port
119+
}
120+
}
121+
122+
// Now look for matches that have port specified
123+
for _, p := range service.Resource.Network {
124+
for _, svcAddress := range serviceAddresses {
125+
serviceWithPort := fmt.Sprintf("%s:%d", svcAddress, p.Port)
126+
if envVal == serviceWithPort {
127+
return true, p
128+
}
129+
}
130+
}
131+
return false, common.SvcNetworkAttr{}
132+
}

pkg/controller/controller_test.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ func TestNetpolsJsonOutput(t *testing.T) {
8282
tests["wordpress"] = TestDetails{dirPath: filepath.Join(testsDir, "k8s_wordpress_example"),
8383
outFile: filepath.Join(testsDir, "k8s_wordpress_example", "output.json"),
8484
expectedOutput: filepath.Join(testsDir, "k8s_wordpress_example", "expected_netpol_output.json")}
85+
tests["guestbook"] = TestDetails{dirPath: filepath.Join(testsDir, "k8s_guestbook"),
86+
outFile: filepath.Join(testsDir, "k8s_guestbook", "output.json"),
87+
expectedOutput: filepath.Join(testsDir, "k8s_guestbook", "expected_netpol_output.json")}
8588

8689
for testName, testDetails := range tests {
8790
args := getTestArgs(testDetails.dirPath, testDetails.outFile, true)
@@ -170,15 +173,16 @@ func compareFiles(expectedFile, actualFile string) (bool, error) {
170173
return false, errors.New("error reading lines from file")
171174
}
172175
if len(expectedLines) != len(actualLines) {
173-
fmt.Printf("Files line count is different: expected: %d, actual: %d", len(expectedLines), len(actualLines))
176+
fmt.Printf("Files line count is different: expected(%s): %d, actual(%s): %d",
177+
expectedFile, len(expectedLines), actualFile, len(actualLines))
174178
return false, nil
175179
}
176180

177181
for i := 0; i < len(expectedLines); i++ {
178182
lineExpected := expectedLines[i]
179183
lineActual := actualLines[i]
180184
if lineExpected != lineActual && !strings.Contains(lineExpected, "\"filepath\"") {
181-
fmt.Printf("Gap in line %d: expected: %s, actual: %s", i, lineExpected, lineActual)
185+
fmt.Printf("Gap in line %d: expected(%s): %s, actual(%s): %s", i, expectedFile, lineExpected, actualFile, lineActual)
182186
return false, nil
183187
}
184188
}

pkg/controller/engine.go

+12-13
Original file line numberDiff line numberDiff line change
@@ -102,28 +102,27 @@ func parseResources(objs []parsedK8sObjects, args common.InArgs) ([]common.Resou
102102
}
103103
}
104104
for idx := range resources {
105-
resources[idx].CommitID = *args.CommitID
106-
resources[idx].GitBranch = *args.GitBranch
107-
resources[idx].GitURL = *args.GitURL
105+
res := &resources[idx]
106+
res.CommitID = *args.CommitID
107+
res.GitBranch = *args.GitBranch
108+
res.GitURL = *args.GitURL
108109

109110
// handle config maps data to be associated into relevant deployments resource objects
110-
for _, cfgMapRef := range resources[idx].Resource.ConfigMapRefs {
111-
configmapFullName := resources[idx].Resource.Namespace + "/" + cfgMapRef
111+
for _, cfgMapRef := range res.Resource.ConfigMapRefs {
112+
configmapFullName := res.Resource.Namespace + "/" + cfgMapRef
112113
if cfgMap, ok := configmaps[configmapFullName]; ok {
113-
// add to d Envs the values from data
114-
// TODO: keep only data values with addresses of known services names
115-
// pattern for relevant data value: http://[svc name]:[port] or [svc-name]:[port] or [svc-name] or http://[svc name] (implied port 80)
116-
// The port is optional when it is the default port for a given protocol (e.g., HTTP=80).
117114
for _, v := range cfgMap.Data {
118-
resources[idx].Resource.Envs = append(resources[idx].Resource.Envs, v)
115+
res.Resource.Envs = append(res.Resource.Envs, v)
119116
}
120117
}
121118
}
122-
for _, cfgMapKeyRef := range resources[idx].Resource.ConfigMapKeyRefs {
123-
configmapFullName := resources[idx].Resource.Namespace + "/" + cfgMapKeyRef.Name
119+
for _, cfgMapKeyRef := range res.Resource.ConfigMapKeyRefs {
120+
configmapFullName := res.Resource.Namespace + "/" + cfgMapKeyRef.Name
124121
if cfgMap, ok := configmaps[configmapFullName]; ok {
125122
if val, ok := cfgMap.Data[cfgMapKeyRef.Key]; ok {
126-
resources[idx].Resource.Envs = append(resources[idx].Resource.Envs, val)
123+
if analyzer.IsNetworkAddressValue(val) {
124+
res.Resource.Envs = append(res.Resource.Envs, val)
125+
}
127126
}
128127
}
129128
}

pkg/controller/synth_netpols.go

+20-16
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package controller
33
import (
44
"reflect"
55
"sort"
6-
"strings"
76

87
core "k8s.io/api/core/v1"
98
network "k8s.io/api/networking/v1"
@@ -55,17 +54,20 @@ func determineConnectivityPerDeployment(connections []common.Connections) []*Dep
5554
conn := &connections[idx]
5655
srcDeploy := findOrAddDeploymentConn(conn.Source, deploysConnectivity)
5756
dstDeploy := findOrAddDeploymentConn(conn.Target, deploysConnectivity)
58-
targetPorts := toNetpolPorts(conn.Link.Resource.Network) // TODO: filter by src ports
57+
targetPorts := toNetpolPorts(conn.Link.Resource.Network)
58+
if conn.Source != nil && len(conn.Source.Resource.UsedPorts) > 0 {
59+
targetPorts = toNetpolPorts(conn.Source.Resource.UsedPorts)
60+
}
5961

60-
egressNetpolPeer := []network.NetworkPolicyPeer{{PodSelector: getDeployConnSelector(dstDeploy)}}
6162
if srcDeploy != nil {
62-
srcDeploy.addEgressRule(egressNetpolPeer, targetPorts)
63+
netpolPeer := getNetpolPeer(srcDeploy, dstDeploy)
64+
srcDeploy.addEgressRule([]network.NetworkPolicyPeer{netpolPeer}, targetPorts)
6365
}
6466

6567
if conn.Link.Resource.Type == "LoadBalancer" || conn.Link.Resource.Type == "NodePort" {
6668
dstDeploy.addIngressRule([]network.NetworkPolicyPeer{}, targetPorts) // in these cases we want to allow traffic from all sources
6769
} else if conn.Source != nil {
68-
netpolPeer := network.NetworkPolicyPeer{PodSelector: getDeployConnSelector(srcDeploy)}
70+
netpolPeer := getNetpolPeer(dstDeploy, srcDeploy)
6971
dstDeploy.addIngressRule([]network.NetworkPolicyPeer{netpolPeer}, targetPorts) // allow traffic only from this specific source
7072
}
7173
}
@@ -94,18 +96,20 @@ func findOrAddDeploymentConn(resource *common.Resource, deployConns map[string]*
9496
return &deploy
9597
}
9698

97-
func getDeployConnSelector(deployConn *DeploymentConnectivity) *metaV1.LabelSelector {
98-
selectorsMap := map[string]string{}
99-
for _, selector := range deployConn.Resource.Resource.Selectors {
100-
colonPos := strings.Index(selector, ":")
101-
if colonPos == -1 {
102-
continue
103-
}
104-
key := selector[:colonPos]
105-
value := selector[colonPos+1:]
106-
selectorsMap[key] = value
99+
func getNetpolPeer(netpolDeploy, otherDeploy *DeploymentConnectivity) network.NetworkPolicyPeer {
100+
netpolPeer := network.NetworkPolicyPeer{PodSelector: getDeployConnSelector(otherDeploy)}
101+
if netpolDeploy.Resource.Resource.Namespace != otherDeploy.Resource.Resource.Namespace {
102+
if otherDeploy.Resource.Resource.Namespace != "" {
103+
netpolPeer.NamespaceSelector = &metaV1.LabelSelector{
104+
MatchLabels: map[string]string{"kubernetes.io/metadata.name": otherDeploy.Resource.Resource.Namespace},
105+
}
106+
} // if otherDeploy has no namespace specified, we assume it is in the same namespace as the netpolDeploy
107107
}
108-
return &metaV1.LabelSelector{MatchLabels: selectorsMap}
108+
return netpolPeer
109+
}
110+
111+
func getDeployConnSelector(deployConn *DeploymentConnectivity) *metaV1.LabelSelector {
112+
return &metaV1.LabelSelector{MatchLabels: deployConn.Resource.Resource.Labels}
109113
}
110114

111115
func toNetpolPorts(ports []common.SvcNetworkAttr) []network.NetworkPolicyPort {

0 commit comments

Comments
 (0)