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

exposure analysis with pod selectors #343

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
35 changes: 35 additions & 0 deletions pkg/netpol/connlist/connlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -834,4 +834,39 @@ var goodPathTests = []struct {
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_conn_entire_cluster_with_empty_selectors",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_conn_to_all_pods_in_a_new_ns",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_conn_with_new_pod_selector_and_ns_selector",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_conn_with_only_pod_selector",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_conn_with_pod_selector_in_any_ns",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
{
testDirName: "onlineboutique_workloads",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
{
testDirName: "k8s_ingress_test_new",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
}
36 changes: 27 additions & 9 deletions pkg/netpol/connlist/conns_formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,38 @@ func formExposureItemAsSingleConnFiled(peerStr string, exposureItem XgressExposu
if exposureItem.IsExposedToEntireCluster() {
return formSingleExposureConn(peerStr, entireCluster, exposureItem.PotentialConnectivity(), isIngress)
}
if len(exposureItem.NamespaceLabels()) > 0 {
return formSingleExposureConn(peerStr, peerStrWithNsLabels(exposureItem.NamespaceLabels()),
exposureItem.PotentialConnectivity(), isIngress)
}
// @todo handle podLabels
return singleConnFields{}
repPeerStr := getRepresentativeNamespaceString(exposureItem.NamespaceLabels()) + "/" +
getRepresentativePodString(exposureItem.PodLabels())
return formSingleExposureConn(peerStr, repPeerStr, exposureItem.PotentialConnectivity(), isIngress)
}

// convertLabelsMapToString returns a string representation of the given labels map
func convertLabelsMapToString(labelsMap map[string]string) string {
return labels.SelectorFromSet(labels.Set(labelsMap)).String()
}

// peerStrWithNsLabels returns a string representation of a potential peer with namespace labels
func peerStrWithNsLabels(nsLabels map[string]string) string {
return "namespace with " + convertLabelsMapToString(nsLabels)
const (
mapOpen = "{"
mapClose = "}"
)

// getRepresentativeNamespaceString returns a string representation of a potential peer with namespace labels
func getRepresentativeNamespaceString(nsLabels map[string]string) string {
nsName, ok := nsLabels[common.K8sNsNameLabelKey]
if len(nsLabels) == 1 && ok {
return nsName
}
if len(nsLabels) > 0 {
return "namespace with " + mapOpen + convertLabelsMapToString(nsLabels) + mapClose
}
return allNamespacesLbl
}

// getRepresentativePodString returns a string representation of potential peer with pod labels
// or all pods string for empty pod labels map (which indicates all pods)
func getRepresentativePodString(podLabels map[string]string) string {
if len(podLabels) == 0 {
return allPeersLbl
}
return "pod with " + mapOpen + convertLabelsMapToString(podLabels) + mapClose
}
34 changes: 19 additions & 15 deletions pkg/netpol/connlist/conns_formatter_dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
entireClusterShape = " shape=diamond"
peerLineClosing = "]"
allPeersLbl = "all pods"
allNamespacesLbl = "all namespaces"
)

var edgeLineFormat = fmt.Sprintf("\t%%q -> %%q [label=%%q color=\"gold2\" fontcolor=\"darkgreen\"]")
Expand Down Expand Up @@ -116,10 +117,10 @@ func addExposureOutputData(exposureConns []ExposedPeer, peersVisited map[string]
dotformatting.AddPeerToNsGroup(ep.ExposedPeer().Namespace(), exposedPeerLine, nsPeers)
}
ingressExpEdges := getXgressExposureEdges(ep.ExposedPeer().String(), ep.IngressExposure(), ep.IsProtectedByIngressNetpols(),
true, representativeVisited, nsRepPeers)
true, representativeVisited, nsPeers, nsRepPeers)
exposureEdges = append(exposureEdges, ingressExpEdges...)
egressExpEdges := getXgressExposureEdges(ep.ExposedPeer().String(), ep.EgressExposure(), ep.IsProtectedByEgressNetpols(),
false, representativeVisited, nsRepPeers)
false, representativeVisited, nsPeers, nsRepPeers)
exposureEdges = append(exposureEdges, egressExpEdges...)
}
// if the entire-cluster marked as visited add its line too (this ensures the entire-cluster is added only once to the graph)
Expand All @@ -131,7 +132,7 @@ func addExposureOutputData(exposureConns []ExposedPeer, peersVisited map[string]

// getXgressExposureEdges returns the edges' lines of the exposure data in the given direction ingress/egress
func getXgressExposureEdges(exposedPeerStr string, xgressExpData []XgressExposureData, isProtected, isIngress bool,
representativeVisited map[string]bool, nsRepPeers map[string][]string) (xgressEdges []string) {
representativeVisited map[string]bool, nsPeers, nsRepPeers map[string][]string) (xgressEdges []string) {
if !isProtected { // a connection to entire cluster is enabled, (connection to all ips is already in the graph)
representativeVisited[entireCluster] = true
xgressEdges = append(xgressEdges, getExposureEdgeLine(exposedPeerStr, entireCluster, isIngress, common.MakeConnectionSet(true)))
Expand All @@ -143,17 +144,21 @@ func getXgressExposureEdges(exposedPeerStr string, xgressExpData []XgressExposur
data.PotentialConnectivity().(*common.ConnectionSet)))
continue // if a data contains exposure to entire cluster it does not specify labels
}
// @todo consider data.PodLabels
if len(data.NamespaceLabels()) > 0 {
nsRepLabel := convertLabelsMapToString(data.NamespaceLabels())
repPeersStr := allPeersLbl + "_in_" + nsRepLabel // used for getting a unique node name for the peer in the graph
if !representativeVisited[repPeersStr] {
representativeVisited[repPeersStr] = true
dotformatting.AddPeerToNsGroup(peerStrWithNsLabels(data.NamespaceLabels()), getRepPeerLine(repPeersStr), nsRepPeers)
nsRepLabel := getRepresentativeNamespaceString(data.NamespaceLabels())
repPeerLabel := getRepresentativePodString(data.PodLabels())
repPeersStr := repPeerLabel + "_in_" + nsRepLabel // to get a unique string name of the peer node
if !representativeVisited[repPeersStr] {
representativeVisited[repPeersStr] = true
peerLine := getRepPeerLine(repPeersStr, repPeerLabel)
// ns label maybe a name of an existing namespace, so check where to add the peer
if _, ok := nsPeers[nsRepLabel]; ok { // in real ns
dotformatting.AddPeerToNsGroup(getRepresentativeNamespaceString(data.NamespaceLabels()), peerLine, nsPeers)
} else { // in a representative ns
dotformatting.AddPeerToNsGroup(getRepresentativeNamespaceString(data.NamespaceLabels()), peerLine, nsRepPeers)
}
xgressEdges = append(xgressEdges, getExposureEdgeLine(exposedPeerStr, repPeersStr, isIngress,
data.PotentialConnectivity().(*common.ConnectionSet)))
}
xgressEdges = append(xgressEdges, getExposureEdgeLine(exposedPeerStr, repPeersStr, isIngress,
data.PotentialConnectivity().(*common.ConnectionSet)))
}
}
return xgressEdges
Expand All @@ -174,7 +179,6 @@ func getExposureEdgeLine(realPeerStr, repPeerStr string, isIngress bool, conn *c
}

// getRepPeerLine formats a representative peer line for dot graph
func getRepPeerLine(peerStr string) string {
// todo : support cases of peer is representative is with pod selector labels
return fmt.Sprintf(peerLineFormatPrefix+peerLineClosing, peerStr, allPeersLbl, representativeObjColor, representativeObjColor)
func getRepPeerLine(peerStr, peerLabel string) string {
return fmt.Sprintf(peerLineFormatPrefix+peerLineClosing, peerStr, peerLabel, representativeObjColor, representativeObjColor)
}
80 changes: 73 additions & 7 deletions pkg/netpol/connlist/exposure_analysis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ func newTCPConnWithPorts(ports []int) *common.ConnectionSet {
return conn
}

func newExpDataWithLabelAndTCPConn(key, val string, ports []int) *xgressExposure {
func newExpDataWithLabelAndTCPConn(nsLabels, podLabels map[string]string, ports []int) *xgressExposure {
conn := common.MakeConnectionSet(true)
if len(ports) > 0 {
conn = newTCPConnWithPorts(ports)
}
return &xgressExposure{
exposedToEntireCluster: false,
namespaceLabels: map[string]string{key: val},
podLabels: map[string]string{},
namespaceLabels: nsLabels,
podLabels: podLabels,
potentialConn: conn,
}
}
Expand Down Expand Up @@ -144,7 +144,7 @@ func TestExposureBehavior(t *testing.T) {
lenIngressExposedConns: 2,
ingressExp: []*xgressExposure{
peerExposedToEntireClusterOnTCP8050,
newExpDataWithLabelAndTCPConn("foo.com/managed-state", "managed", []int{8050, 8090}),
newExpDataWithLabelAndTCPConn(map[string]string{"foo.com/managed-state": "managed"}, nil, []int{8050, 8090}),
},
lenEgressExposedConns: 0,
},
Expand Down Expand Up @@ -179,9 +179,9 @@ func TestExposureBehavior(t *testing.T) {
isEgressProtected: false,
lenIngressExposedConns: 3,
ingressExp: []*xgressExposure{
newExpDataWithLabelAndTCPConn("foo.com/managed-state", "managed", []int{8050}),
newExpDataWithLabelAndTCPConn("release", "stable", []int{}),
newExpDataWithLabelAndTCPConn("effect", "NoSchedule", []int{8050}),
newExpDataWithLabelAndTCPConn(map[string]string{"foo.com/managed-state": "managed"}, nil, []int{8050}),
newExpDataWithLabelAndTCPConn(map[string]string{"release": "stable"}, nil, []int{}),
newExpDataWithLabelAndTCPConn(map[string]string{"effect": "NoSchedule"}, nil, []int{8050}),
},
lenEgressExposedConns: 0,
},
Expand Down Expand Up @@ -228,6 +228,72 @@ func TestExposureBehavior(t *testing.T) {
},
},
},
{
testName: "test_conn_entire_cluster_with_empty_selectors", // only workload-a in manifests
expectedNumRepresentativePeers: 0,
expectedLenOfExposedPeerList: 1,
// workload 1 is exposed to entire cluster on ingress and egress
wl1ExpDataInfo: expectedPeerResultInfo{
isIngressProtected: true,
isEgressProtected: true,
lenIngressExposedConns: 1,
lenEgressExposedConns: 1,
ingressExp: []*xgressExposure{
peerExposedToEntireClusterOnTCP8050,
},
egressExp: []*xgressExposure{
peerExposedToEntireCluster,
},
},
},
{
testName: "test_conn_to_all_pods_in_a_new_ns", // only workload-a in manifests
expectedNumRepresentativePeers: 1,
expectedLenOfExposedPeerList: 1,
// workload-a is exposed to entire cluster on egress, to a rep. peer on ingress
wl1ExpDataInfo: expectedPeerResultInfo{
isIngressProtected: true,
isEgressProtected: true,
lenIngressExposedConns: 1,
lenEgressExposedConns: 1,
ingressExp: []*xgressExposure{
newExpDataWithLabelAndTCPConn(map[string]string{common.K8sNsNameLabelKey: "backend"},
map[string]string{}, []int{8050}),
},
egressExp: []*xgressExposure{
peerExposedToEntireCluster,
},
},
},
{
testName: "test_conn_with_new_pod_selector_and_ns_selector", // only workload-a in manifests
expectedNumRepresentativePeers: 1,
expectedLenOfExposedPeerList: 1,
wl1ExpDataInfo: expectedPeerResultInfo{
isIngressProtected: true,
isEgressProtected: false,
lenIngressExposedConns: 1,
lenEgressExposedConns: 0,
ingressExp: []*xgressExposure{
newExpDataWithLabelAndTCPConn(map[string]string{"effect": "NoSchedule"}, map[string]string{"role": "monitoring"}, []int{8050}),
},
},
},
{
testName: "test_conn_with_only_pod_selector", // only workload-a in manifests
expectedNumRepresentativePeers: 1,
expectedLenOfExposedPeerList: 1,
wl1ExpDataInfo: expectedPeerResultInfo{
isIngressProtected: true,
isEgressProtected: false,
lenIngressExposedConns: 1,
lenEgressExposedConns: 0,
ingressExp: []*xgressExposure{
newExpDataWithLabelAndTCPConn(map[string]string{common.K8sNsNameLabelKey: "hello-world"}, map[string]string{"role": "monitoring"},
[]int{8050}),
},
},
},
}
for _, tt := range cases {
tt := tt
Expand Down
4 changes: 2 additions & 2 deletions pkg/netpol/connlist/exposure_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,15 @@ func (ex *exposureMaps) addConnToExposureMap(pe *eval.PolicyEngine, allowedConne
// the peer is protected, check if peer is in the relevant map; if not initialize a new entry
ex.addNewEntry(peer, true, isIngress)

nsLabels, err := pe.GetPeerNsLabels(representativePeer)
podLabels, nsLabels, err := pe.GetPeerLabels(representativePeer)
if err != nil {
return err
}
// store connection data
expData := &xgressExposure{
exposedToEntireCluster: false,
namespaceLabels: nsLabels,
podLabels: map[string]string{}, // will be empty since in this branch rules with namespaceSelectors only supported
podLabels: podLabels,
potentialConn: allowedConnSet,
}
ex.appendPeerXgressExposureData(peer, expData, isIngress)
Expand Down
22 changes: 15 additions & 7 deletions pkg/netpol/eval/exposure.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,24 @@ func generateNewNamespaceName(policyName string, index int) string {
return repNsNamePrefix + policyName + fmt.Sprint(index)
}

func generateNewPodName(index int) string {
return k8s.RepresentativePodName + "-" + fmt.Sprint(index)
}

// generateRepresentativePeers : generates and adds to policy engine representative peers where each peer
// has namespace and pod labels inferred from single policy rule labels in the given list of selectors;
// for example, if a rule within policy has namespaceSelector "foo: managed", then a representative pod in such a
// namespace with those labels will be added, representing all potential pods in such a namespace.
// generated representative peers are unique; i.e. if different rules (e.g in different policies or different directions) has same labels :
// one representative peer is generated to represent both
func (pe *PolicyEngine) generateRepresentativePeers(selectorsLabels []k8s.SingleRuleLabels, policyName string) error {
func (pe *PolicyEngine) generateRepresentativePeers(selectorsLabels []k8s.SingleRuleLabels, policyName, policyNs string) (err error) {
for i := range selectorsLabels {
// @todo : when supporting the PodSelector: differentiate also pod names
_, err := pe.AddPodByNameAndNamespace(k8s.RepresentativePodName, generateNewNamespaceName(policyName, i), &selectorsLabels[i])
// if ns labels of the rule selector was nil, then the namespace of the pod is same as the policy's namespace
if selectorsLabels[i].PolicyNsFlag {
_, err = pe.AddPodByNameAndNamespace(generateNewPodName(i), policyNs, &selectorsLabels[i])
} else {
_, err = pe.AddPodByNameAndNamespace(generateNewPodName(i), generateNewNamespaceName(policyName, i), &selectorsLabels[i])
}
if err != nil {
return err
}
Expand Down Expand Up @@ -130,12 +138,12 @@ func (pe *PolicyEngine) IsRepresentativePeer(peer Peer) bool {
return ok
}

// GetPeerNsLabels returns namespace labels defining the given representative peer
// GetPeerLabels returns the labels defining the given representative peer and its namespace
// relevant only for RepresentativePeer
func (pe *PolicyEngine) GetPeerNsLabels(p Peer) (map[string]string, error) {
func (pe *PolicyEngine) GetPeerLabels(p Peer) (podLabels, nsLabels map[string]string, err error) {
peer, ok := p.(*k8s.RepresentativePeer)
if !ok { // should not get here
return nil, errors.New(netpolerrors.NotRepresentativePeerErrStr(p.String()))
return nil, nil, errors.New(netpolerrors.NotRepresentativePeerErrStr(p.String()))
}
return peer.PotentialNamespaceLabels, nil
return peer.Pod.Labels, peer.PotentialNamespaceLabels, nil
}
9 changes: 4 additions & 5 deletions pkg/netpol/eval/internal/k8s/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ package k8s

import (
corev1 "k8s.io/api/core/v1"

"github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common"
)

// Namespace encapsulates k8s namespace fields that are relevant for evaluating network policies
Expand All @@ -23,9 +25,6 @@ type Namespace struct {
Labels map[string]string
}

// The Kubernetes API server sets this label on all namespaces
const K8sNsNameLabelKey = "kubernetes.io/metadata.name"

// @todo need a Namespace collection type along with convenience methods?
// if so, also consider concurrent access (or declare not goroutine safe?)

Expand All @@ -42,8 +41,8 @@ func NamespaceFromCoreObject(ns *corev1.Namespace) (*Namespace, error) {
// @todo/tbd : should also add the name label as "name:<val>" or assume policy rules
// selecting a namespace with name labels always use "kubernetes.io/metadata.name"
// if missing, the label set by k8s API server must be added to the namespace labels
if _, ok := n.Labels[K8sNsNameLabelKey]; !ok {
n.Labels[K8sNsNameLabelKey] = ns.Name
if _, ok := n.Labels[common.K8sNsNameLabelKey]; !ok {
n.Labels[common.K8sNsNameLabelKey] = ns.Name
}

return n, nil
Expand Down
Loading