Example #1
0
    def allowed_connections(self, from_peer, to_peer, is_ingress):
        """
        Evaluate the set of connections this policy allows/denies/passes between two peers
        :param Peer.Peer from_peer: The source peer
        :param Peer.Peer to_peer:  The target peer
        :param bool is_ingress: whether we evaluate ingress rules only or egress rules only
        :return: A PolicyConnections object containing sets of allowed/denied/pass connections
        :rtype: PolicyConnections
        """

        # TODO: currently not handling egress, istio authorization policies have no egress rules
        if not is_ingress:
            return PolicyConnections(False, ConnectionSet(True))

        captured = to_peer in self.selected_peers
        if not captured:
            return PolicyConnections(False)

        allowed_conns = ConnectionSet()
        denied_conns = ConnectionSet()

        collected_conns = allowed_conns if self.action == IstioNetworkPolicy.ActionType.Allow else denied_conns
        for rule in self.ingress_rules:
            if from_peer in rule.peer_set:
                collected_conns |= rule.connections

        return PolicyConnections(True, allowed_conns, denied_conns)
    def parse_ingress_egress_rule(self, rule, peer_array_key):
        """
        Parse a single ingres/egress rule, producing a K8sPolicyRule
        :param dict rule: The rule to parse
        :param str peer_array_key: The key which defined the peer set ('from' for ingress, 'to' for egress)
        :return: A K8sPolicyRule with the proper PeerSet and ConnectionSet
        :rtype: K8sPolicyRule
        """
        self.check_fields_validity(rule, 'ingress/egress rule', {
            peer_array_key: [0, list],
            'ports': [0, list]
        })
        peer_array = rule.get(peer_array_key, [])
        if peer_array:
            res_pods = Peer.PeerSet()
            for peer in peer_array:
                res_pods |= self.parse_peer(peer)
        else:
            res_pods = self.peer_container.get_all_peers_group(True)

        ports_array = rule.get('ports', [])
        if ports_array:
            res_ports = ConnectionSet()
            for port in ports_array:
                res_ports |= self.parse_port(port)
        else:
            res_ports = ConnectionSet(True)

        if not res_pods:
            self.warning('Rule selects no pods', rule)

        return K8sPolicyRule(res_pods, res_ports)
Example #3
0
    def allowed_connections(self, from_peer, to_peer):
        """
        This is the core of the whole application - computes the set of allowed connections from one peer to another.
        In our connectivity model, this function computes the labels for the edges in our directed graph.
        :param Peer.Peer from_peer: The source peer
        :param Peer.Peer to_peer: The target peer
        :return: a 4-tuple with:
          - allowed_conns: all allowed connections (captured/non-captured)
          - captured_flag: flag to indicate if any of the policies captured one of the peers (src/dst)
          - allowed_captured_conns: allowed captured connections (can be used only if the captured flag is True)
          - denied_conns: connections denied by the policies (captured)
        :rtype: ConnectionSet, bool, ConnectionSet, ConnectionSet
        """
        if isinstance(to_peer, Peer.IpBlock):
            ingress_conns = PolicyConnections(
                captured=False, all_allowed_conns=ConnectionSet(True))
        else:
            ingress_conns = self._allowed_xgress_conns(from_peer, to_peer,
                                                       True)

        if isinstance(from_peer, Peer.IpBlock):
            egress_conns = PolicyConnections(
                captured=False, all_allowed_conns=ConnectionSet(True))
        else:
            egress_conns = self._allowed_xgress_conns(from_peer, to_peer,
                                                      False)

        captured_flag = ingress_conns.captured or egress_conns.captured
        denied_conns = ingress_conns.denied_conns | egress_conns.denied_conns
        allowed_conns = ingress_conns.all_allowed_conns & egress_conns.all_allowed_conns
        # captured connections are where at least one if ingress / egress is captured
        allowed_captured_conns = (ingress_conns.allowed_conns & egress_conns.all_allowed_conns) | \
            (egress_conns.allowed_conns & ingress_conns.all_allowed_conns)

        return allowed_conns, captured_flag, allowed_captured_conns, denied_conns
    def allowed_connections(self, from_peer, to_peer, is_ingress):
        """
        Evaluate the set of connections this ingress resource allows between two peers
        :param Peer.Peer from_peer: The source peer
        :param Peer.Peer to_peer:  The target peer
        :param bool is_ingress: For compatibility with other policies.
         Will return the set of allowed connections only for is_ingress being False.
        :return: A PolicyConnections object containing sets of allowed connections
        :rtype: PolicyConnections
        """

        captured = from_peer in self.selected_peers
        if not captured:
            return PolicyConnections(False)
        if is_ingress:
            return PolicyConnections(False)

        allowed_conns = ConnectionSet()
        denied_conns = ConnectionSet()
        conns = allowed_conns if self.action == IngressPolicy.ActionType.Allow else denied_conns
        for rule in self.egress_rules:
            if to_peer in rule.peer_set:
                assert not rule.connections.has_named_ports()
                conns |= rule.connections

        return PolicyConnections(True, allowed_conns, denied_conns)
Example #5
0
    def parse_ingress_rule(self, rule):
        """
        Parse a single ingress rule, producing a IstioPolicyRule.
        :param dict rule: The dict with the rule fields
        :return: A IstioPolicyRule with the proper PeerSet and ConnectionSet
        :rtype: IstioPolicyRule
        """
        if rule is None:
            self.syntax_error('Authorization policy rule cannot be null. ')

        allowed_elements = {
            'from': [0, list],
            'to': [0, list],
            'when': [0, list]
        }
        self.check_fields_validity(rule, 'authorization policy rule',
                                   allowed_elements)
        for key_elem in allowed_elements:
            self.validate_existing_key_is_not_null(rule, key_elem)

        # collect source peers into res_peers
        from_array = self.get_key_array_and_validate_not_empty(rule, 'from')
        if from_array is not None:
            res_peers = PeerSet()
            for source_dict in from_array:
                res_peers |= self.parse_source(source_dict)
        else:  # no 'from' in the rule => all source peers allowed
            res_peers = self.peer_container.get_all_peers_group(True)

        to_array = self.get_key_array_and_validate_not_empty(rule, 'to')
        # currently parsing only ports
        # TODO: extend operations parsing to include other attributes
        if to_array is not None:
            connections = ConnectionSet()
            for operation_dict in to_array:
                connections |= self.parse_operation(operation_dict)
        else:  # no 'to' in the rule => all connections allowed
            connections = ConnectionSet(True)

        # condition possible result value: source-ip (from) , source-namespace (from) [Peerset], destination.port (to) [ConnectionSet]
        # should update either res_pods or connections according to the condition
        condition_array = rule.get(
            'when')  # this array can be empty (unlike 'to' and 'from')
        # the combined condition ("AND" of all conditions) should be applied
        if condition_array is not None:
            for condition in condition_array:
                condition_res = self.parse_condition(condition)
                if isinstance(condition_res, PeerSet):
                    res_peers &= condition_res
                elif isinstance(condition_res, ConnectionSet):
                    connections &= condition_res

        if not res_peers:
            self.warning('Rule selects no pods', rule)

        return IstioPolicyRule(res_peers, connections)
Example #6
0
class PolicyConnections:
    """
    A class to contain the effect of applying policies to a pair of peers
    """
    captured: bool  # Whether policy(ies) selectors captured relevant peers (can have empty allowed-conns with captured==True)
    allowed_conns: ConnectionSet = ConnectionSet(
    )  # Connections allowed (and captured) by the policy(ies)
    denied_conns: ConnectionSet = ConnectionSet(
    )  # Connections denied by the policy(ies)
    pass_conns: ConnectionSet = ConnectionSet(
    )  # Connections specified as PASS by the policy(ies)
    all_allowed_conns: ConnectionSet = ConnectionSet(
    )  # all (captured+ non-captured) Connections allowed by the policy(ies)
    def verify_named_ports(self, rule, rule_pods, rule_conns):
        """
        Check the validity of named ports in a given rule: whether a relevant pod refers to the named port and whether
        the protocol defined in the policy matches the protocol defined by the Pod. Issue warnings as required.
        :param dict rule: The unparsed rule (for reference in warnings)
        :param Peer.PeerSet rule_pods: The set of Pods in which the named ports should be defined
        :param ConnectionSet rule_conns: The rule-specified connections, possibly containing named ports
        :return: None
        """
        if not rule_conns.has_named_ports():
            return
        named_ports = rule_conns.get_named_ports()
        for protocol, rule_ports in named_ports:
            for port in rule_ports:
                port_used = False
                for pod in rule_pods:
                    pod_named_port = pod.named_ports.get(port)
                    if pod_named_port:
                        port_used = True
                        if ConnectionSet.protocol_name_to_number(
                                pod_named_port[1]) != protocol:
                            self.warning(
                                f'Protocol mismatch for named port {port} (vs. Pod {pod.full_name()})',
                                rule['ports'])

                if not port_used:
                    self.warning(
                        f'Named port {port} is not defined in any selected pod',
                        rule['ports'])
Example #8
0
    def allowed_connections(self, from_peer, to_peer, is_ingress):
        """
        Evaluate the set of connections this policy allows between two peers
        (either the allowed ingress into to_peer or the allowed egress from from_peer).
        :param Peer.Peer from_peer: The source peer
        :param Peer.Peer to_peer:  The target peer
        :param bool is_ingress: whether we evaluate ingress rules only or egress rules only
        :return: A PolicyConnections object containing sets of allowed connections
        :rtype: PolicyConnections
        """
        captured = is_ingress and self.affects_ingress and to_peer in self.selected_peers or \
            not is_ingress and self.affects_egress and from_peer in self.selected_peers
        if not captured:
            return PolicyConnections(False)

        allowed_conns = ConnectionSet()
        rules = self.ingress_rules if is_ingress else self.egress_rules
        other_peer = from_peer if is_ingress else to_peer
        for rule in rules:
            if other_peer in rule.peer_set:
                rule_conns = rule.port_set.copy()  # we need a copy because convert_named_ports is destructive
                rule_conns.convert_named_ports(to_peer.get_named_ports())
                allowed_conns |= rule_conns

        return PolicyConnections(True, allowed_conns)
Example #9
0
 def _get_connection_set_from_properties(dest_ports,
                                         method_set=MethodSet(True),
                                         paths_dfa=None,
                                         hosts_dfa=None):
     """
     get ConnectionSet with TCP allowed connections, corresponding to input properties cube
     :param PortSet dest_ports: ports set for dset_ports dimension
     :param MethodSet method_set: methods set for methods dimension
     :param MinDFA paths_dfa: MinDFA obj for paths dimension
     :param MinDFA hosts_dfa: MinDFA obj for hosts dimension
     :return: ConnectionSet with TCP allowed connections , corresponding to input properties cube
     """
     tcp_properties = TcpLikeProperties(source_ports=PortSet(True),
                                        dest_ports=dest_ports,
                                        methods=method_set,
                                        paths=paths_dfa,
                                        hosts=hosts_dfa)
     res = ConnectionSet()
     res.add_connections('TCP', tcp_properties)
     return res
Example #10
0
    def allowed_connections(self, from_peer, to_peer, is_ingress):
        """
        Evaluate the set of connections this policy allows/denies/passes between two peers
        :param Peer.Peer from_peer: The source peer
        :param Peer.Peer to_peer:  The target peer
        :param bool is_ingress: whether we evaluate ingress rules only or egress rules only
        :return: A PolicyConnections object containing sets of allowed/denied/pass connections
        :rtype: PolicyConnections
        """
        captured = is_ingress and self.affects_ingress and to_peer in self.selected_peers or \
            not is_ingress and self.affects_egress and from_peer in self.selected_peers
        if not captured:
            return PolicyConnections(False)

        allowed_conns = ConnectionSet()
        denied_conns = ConnectionSet()
        pass_conns = ConnectionSet()
        rules = self.ingress_rules if is_ingress else self.egress_rules
        for rule in rules:
            if from_peer in rule.src_peers and to_peer in rule.dst_peers:
                rule_conns = rule.connections.copy(
                )  # we need a copy because convert_named_ports is destructive
                rule_conns.convert_named_ports(to_peer.get_named_ports())

                if rule.action == CalicoPolicyRule.ActionType.Allow:
                    rule_conns -= denied_conns
                    rule_conns -= pass_conns
                    allowed_conns |= rule_conns
                elif rule.action == CalicoPolicyRule.ActionType.Deny:
                    rule_conns -= allowed_conns
                    rule_conns -= pass_conns
                    denied_conns |= rule_conns
                elif rule.action == CalicoPolicyRule.ActionType.Pass:
                    rule_conns -= allowed_conns
                    rule_conns -= denied_conns
                    pass_conns |= rule_conns
                else:
                    pass  # Nothing to do for Log action - does not affect connectivity

        return PolicyConnections(True, allowed_conns, denied_conns, pass_conns)
 def _make_deny_rules(self, allowed_conns):
     """
     Make deny rules from the given connections
     :param TcpLikeProperties allowed_conns: the given allowed connections
     :return: the list of deny IngressPolicyRules
     """
     all_peers_and_ip_blocks = self.peer_container.peer_set.copy()
     all_peers_and_ip_blocks.add(
         IpBlock.get_all_ips_block())  # add IpBlock of all IPs
     all_conns = self._make_tcp_like_properties(PortSet(True),
                                                all_peers_and_ip_blocks)
     denied_conns = all_conns - allowed_conns
     res = self._make_rules_from_conns(denied_conns)
     # Add deny rule for all protocols but TCP , relevant for all peers and ip blocks
     non_tcp_conns = ConnectionSet.get_non_tcp_connections()
     res.append(IngressPolicyRule(all_peers_and_ip_blocks, non_tcp_conns))
     return res
Example #12
0
    def _parse_protocol(self, protocol, rule):
        """
        Parse the protocol/notProtocol field in a rule
        :param protocol: The protocol (a string or an int)
        :param dict rule: The parsed rule object (for context)
        :return: The protocol number
        :rtype: int
        """
        if not protocol:
            return None
        if isinstance(protocol, int):
            if protocol < 1 or protocol > 255:
                self.syntax_error(
                    'protocol must be a string or an integer in the range 1-255',
                    rule)
            return protocol

        if protocol not in ['TCP', 'UDP', 'ICMP', 'ICMPv6', 'SCTP', 'UDPLite']:
            self.syntax_error('invalid protocol name: ' + protocol, rule)
        return ConnectionSet.protocol_name_to_number(protocol)
    def __init__(self, cluster_info, allowed_labels, output_config):
        """
        create an object of MinimizeCsFwRules
        :param cluster_info:  an object of type ClusterInfo, with relevant cluster topology info
        :param allowed_labels: a set of label keys (set[str]) that appear in one of the policy yaml files.
                          using this set to determine which label can be used for grouping pods in fw-rules computation
        :param output_config: an OutputConfiguration object

        """

        self.cluster_info = cluster_info
        self.allowed_labels = allowed_labels
        self.output_config = output_config

        self.peer_pairs = set()
        self.connections = ConnectionSet()
        self.peer_pairs_in_containing_connections = set()
        self.ns_pairs = set()
        self.peer_pairs_with_partial_ns_expr = set()
        self.peer_pairs_without_ns_expr = set()
        self.covered_peer_pairs_union = set()
        self.results_info_per_option = dict()
        self.minimized_fw_rules = [
        ]  # holds the computation result of minimized fw-rules
Example #14
0
    def _allowed_xgress_conns(self, from_peer, to_peer, is_ingress):
        """
        get allowed and denied ingress/egress connections between from_peer and to_peer,
        considering all config's policies (and defaults)
        :param from_peer: the source peer
        :param to_peer: the dest peer
        :param is_ingress: flag to indicate if should return ingress connections or egress connections
        :return: PolicyConnections object with:
          - captured: flag to indicate if any of the policies captured one of the peers (src/dst)
          - allowed_conns: allowed captured connections (can be used only if the captured flag is True)
          - denied_conns: connections denied by the policies (captured)
          - pass_conns: irrelevant , always empty
          - all_allowed_conns: all allowed connections (captured/non-captured)
        :rtype: PolicyConnections
        """
        allowed_conns = ConnectionSet()
        denied_conns = ConnectionSet()
        pass_conns = ConnectionSet()

        policy_captured = False
        has_allow_policies_for_target = False
        for policy in self.sorted_policies:
            policy_conns = policy.allowed_connections(from_peer, to_peer,
                                                      is_ingress)
            assert isinstance(policy_conns, PolicyConnections)
            if policy_conns.captured:
                policy_captured = True
                if isinstance(
                        policy, IstioNetworkPolicy
                ) and policy.action == IstioNetworkPolicy.ActionType.Allow:
                    has_allow_policies_for_target = True
                policy_conns.denied_conns -= allowed_conns
                policy_conns.denied_conns -= pass_conns
                denied_conns |= policy_conns.denied_conns
                policy_conns.allowed_conns -= denied_conns
                policy_conns.allowed_conns -= pass_conns
                allowed_conns |= policy_conns.allowed_conns
                policy_conns.pass_conns -= denied_conns
                policy_conns.pass_conns -= allowed_conns
                pass_conns |= policy_conns.pass_conns

        if self.type == NetworkConfig.ConfigType.Istio:
            # for istio initialize non-captured conns with non-TCP connections
            allowed_non_captured_conns = ConnectionSet.get_non_tcp_connections(
            )
            if not is_ingress:
                allowed_non_captured_conns = ConnectionSet(
                    True)  # egress currently always allowed and not captured
            elif not has_allow_policies_for_target:
                # add connections allowed by default that are not captured
                allowed_non_captured_conns |= (ConnectionSet(True) -
                                               denied_conns)

            return PolicyConnections(has_allow_policies_for_target,
                                     allowed_conns,
                                     denied_conns,
                                     all_allowed_conns=allowed_conns
                                     | allowed_non_captured_conns)

        allowed_non_captured_conns = ConnectionSet()
        if not policy_captured:
            if self.type in [
                    NetworkConfig.ConfigType.K8s,
                    NetworkConfig.ConfigType.Unknown
            ]:
                allowed_non_captured_conns = ConnectionSet(
                    True
                )  # default Allow-all ingress in k8s or in case of no policy
            else:
                if self.profiles:
                    allowed_non_captured_conns = self._get_profile_conns(
                        from_peer, to_peer, is_ingress).allowed_conns
        elif pass_conns:
            allowed_conns |= pass_conns & self._get_profile_conns(
                from_peer, to_peer, is_ingress).allowed_conns
        return PolicyConnections(policy_captured,
                                 allowed_conns,
                                 denied_conns,
                                 all_allowed_conns=allowed_conns
                                 | allowed_non_captured_conns)
    def _parse_xgress_rule(self, rule, is_ingress, policy_selected_eps,
                           is_profile):
        """
        Parse a single ingres/egress rule, producing a CalicoPolicyRule
        :param dict rule: The rule element to parse
        :param bool is_ingress: Whether this is an ingress rule
        :param PeerSet policy_selected_eps: The endpoints the policy captured
        :param bool is_profile: Whether the parsed policy is a Profile object
        :return: A CalicoPolicyRule with the proper PeerSets, ConnectionSets and Action
        :rtype: CalicoPolicyRule
        """
        allowed_keys = {
            'action': 1,
            'protocol': 0,
            'notProtocol': 0,
            'icmp': 0,
            'notICMP': 0,
            'ipVersion': 0,
            'source': 0,
            'destination': 0,
            'http': 2
        }
        self.check_fields_validity(rule, 'ingress/egress rule', allowed_keys)

        action = CalicoPolicyRule.action_str_to_action_type(rule['action'])
        if action is None:
            self.syntax_error('Invalid rule action: ' + rule['action'], rule)
        if is_profile and action == CalicoPolicyRule.ActionType.Pass:
            self.warning('Pass actions in Profile rules will be ignored', rule)

        protocol = self._parse_protocol(rule.get('protocol'), rule)
        protocol_supports_ports = ConnectionSet.protocol_supports_ports(
            protocol)
        not_protocol = self._parse_protocol(rule.get('notProtocol'), rule)
        src_entity_rule = rule.get('source')
        if src_entity_rule:
            src_res_pods, src_res_ports = self._parse_entity_rule(
                src_entity_rule, protocol_supports_ports)
        else:
            src_res_pods = self.peer_container.get_all_peers_group(True)
            src_res_ports = PortSet(True)

        dst_entity_rule = rule.get('destination')
        if dst_entity_rule:
            dst_res_pods, dst_res_ports = self._parse_entity_rule(
                dst_entity_rule, protocol_supports_ports)
        else:
            dst_res_pods = self.peer_container.get_all_peers_group(True)
            dst_res_ports = PortSet(True)

        if is_ingress:  # FIXME: We do not handle well the case where dst_res_pods or src_res_pods contain ipBlocks
            dst_res_pods &= policy_selected_eps
        else:
            src_res_pods &= policy_selected_eps

        connections = ConnectionSet()
        if protocol is not None:
            if not_protocol is not None:
                if protocol == not_protocol:
                    self.warning(
                        'Protocol and notProtocol are conflicting, no traffic will be matched',
                        rule)
                else:
                    self.warning('notProtocol field has no effect', rule)
            else:
                if protocol_supports_ports:
                    connections.add_connections(
                        protocol,
                        TcpLikeProperties(src_res_ports, dst_res_ports))
                elif ConnectionSet.protocol_is_icmp(protocol):
                    connections.add_connections(
                        protocol,
                        self._parse_icmp(rule.get('icmp'),
                                         rule.get('notICMP')))
                else:
                    connections.add_connections(protocol, True)
        elif not_protocol is not None:
            connections.add_all_connections()
            connections.remove_protocol(not_protocol)
        else:
            connections.allow_all = True

        self._verify_named_ports(rule, dst_res_pods, connections)

        if not src_res_pods and policy_selected_eps and (is_ingress
                                                         or not is_profile):
            self.warning('Rule selects no source endpoints', rule)
        if not dst_res_pods and policy_selected_eps and (not is_ingress
                                                         or not is_profile):
            self.warning('Rule selects no destination endpoints', rule)

        return CalicoPolicyRule(src_res_pods, dst_res_pods, connections,
                                action)
    def parse_port(self, port):
        """
        Parse an element of the "ports" phrase of a policy rule
        :param dict port: The element to parse
        :return: A ConnectionSet representing the allowed connections by this element (protocols X port numbers)
        :rtype: ConnectionSet
        """
        self.check_fields_validity(port, 'NetworkPolicyPort', {
            'port': 0,
            'protocol': [0, str],
            'endPort': [0, int]
        }, {'protocol': ['TCP', 'UDP', 'SCTP']})
        port_id = port.get('port')
        protocol = port.get('protocol')
        end_port_num = port.get('endPort')
        if not protocol:
            protocol = 'TCP'

        res = ConnectionSet()
        dest_port_set = PortSet(port_id is None)
        if port_id is not None and end_port_num is not None:
            if isinstance(port_id, str):
                self.syntax_error(
                    'endPort cannot be defined if the port field is defined '
                    'as a named (string) port', port)
            if not isinstance(port_id, int):
                self.syntax_error(
                    'type of port is not numerical in NetworkPolicyPort', port)
            self.validate_value_in_domain(port_id, 'dst_ports', port,
                                          'Port number')
            self.validate_value_in_domain(end_port_num, 'dst_ports', port,
                                          'endPort number')
            if port_id > end_port_num:
                self.syntax_error('endPort must be equal or greater than port',
                                  port)
            dest_port_set.add_port_range(port_id, end_port_num)
        elif port_id is not None:
            if not isinstance(port_id, str) and not isinstance(port_id, int):
                self.syntax_error(
                    'type of port is not numerical or named (string) in NetworkPolicyPort',
                    port)
            if isinstance(port_id, int):
                self.validate_value_in_domain(port_id, 'dst_ports', port,
                                              'Port number')
            if isinstance(port_id, str):
                if len(port_id) > 15:
                    self.syntax_error(
                        'port name  must be no more than 15 characters', port)
                if re.fullmatch(r"[a-z\d]([-a-z\d]*[a-z\d])?",
                                port_id) is None:
                    self.syntax_error(
                        'port name should contain only lowercase alphanumeric characters or "-", '
                        'and start and end with alphanumeric characters', port)

            dest_port_set.add_port(port_id)
        elif end_port_num:
            self.syntax_error(
                'endPort cannot be defined if the port field is not defined ',
                port)

        res.add_connections(
            protocol, TcpLikeProperties(
                PortSet(True),
                dest_port_set))  # K8s doesn't reason about src ports
        return res