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

Policy simplification #670

Merged
merged 9 commits into from
May 15, 2017
Merged

Policy simplification #670

merged 9 commits into from
May 15, 2017

Conversation

tgraf
Copy link
Member

@tgraf tgraf commented May 8, 2017

Fixes: #674

@tgraf tgraf added the wip label May 8, 2017
// accumulated additively
//
// The rule is split into an ingress section which contains all rules
// applicable at ingress, and an egress section applicable at egress.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applicable -> enforced

// accumulated additively
//
// The rule is split into an ingress section which contains all rules
// applicable at ingress, and an egress section applicable at egress.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also specify that for a flow between two endpoints to be allowed by Cilium, it must be allowed by both the egress section of the source endpoint and by the ingress section of the destination endpoint.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is only true to some extent. Endpoint to endpoint policy is only specified at Ingress. We only do egress specification for things where we do not control the receiving endpoint.

I've added a note to clarify this.

// rule. Rules cannot be identified by comment.
//
// +optional
Comment string `json:"comment,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment -> Description (?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I inherited comment from iptables but I like Description better as well, fixed.

// If omitted, no rules will be applied at ingress via this rule.
//
// +optional
Ingress *IngressRule `json:"ingress,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a slice, not a single rule.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All members of IngressRule are sliced which saves us to enforce oneOf at IngressRule level. What benefit do you see in one over the over? I'm fine with both actually.

Copy link
Contributor

@rlenglet rlenglet May 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the semantics is different.
I would like to be able to specify:

  • allow from 10/8 to port 80
    and
  • allow from 11/8 to port 443

That's very different from:

  • allow from "either 10/8 or 11/8" to "either port 80 or port 443"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in another comment as well: The datapath does support the coupling of L3 and L4 rules at this point and we have to support the decoupled version anyway for k8s policy.

I'm fine changing this as I said but we can't support the coupling right now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually you could. It would just cost you the calculation of a cartesian product of the k8s L3 rules with the K8s L4 rules in the same k8s policy, to obtain those "coupled" rule versions. It may be costly only in case of complicated k8s policies, e.g. with contain many criteria. I don't think that will be common, nor very costly.

// If omitted, no rules will be applied at egress via this rule
//
// +optional
Egress *EgressRule `json:"egress,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a slice, not a single rule.

// protocol which the endpoint subject to the rule is allowed to
// connect to.
//
// If omitted and no other IngressRule applies a ToPorts rule on the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why mentioning "IngressRule" here? Did you mean "EgressRule"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant EgressRule but I believe the paragraph is not required at all for understanding.

// protocol which the endpoint subject to the rule is allowed to
// connect to.
//
// If omitted and no other IngressRule applies a ToPorts rule on the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this confusing. This looks like a conjunction of rules.
I would prefer if all the rules form a disjunction, i.e. if a rule allows some flows, no addition or removal of any other rule can alter that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure which paragraph this comment refers to.

foo string
}

// PortProtocol specifices an L4 port with an optional protocol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

specifices -> specifies
protocol -> transport protocol

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


// PortProtocol specifices an L4 port with an optional protocol
//
// TODO: allow port range?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's usually useful. Actually, a list combining individual ports and ranges seems common, e.g.:
["80", "443", "2000-2100"]
The only negative aspect about it is that it requires the data to be passed as strings, but that's a small price to pay.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I will change it to a string for now so we can easily add ranges later on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about:

type PortProtocol struct {
      Port PortRange
}

type PortRange stuct{
      min uint16
      max uint16
}

func NewPort(p uint16) *PortRange{
     return &PortRange{min: p, max: p}
}

func NewPortRange(min, max uint16) *PortRange{
     return &PortRange{min: min, max: max}
}

func (p *PortRange) Contains(p uint16) bool {
   return p.min >= p && p <= p.max
}

At least we don't deal with strings

Port uint16 `json:"port"`

// Protocol is the L4 protocol. If omitted, any protocol matches
// Accepted values: "tcp", "udp", ""/"any"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case of ICMP, how can one match on the code / type?
If that's not supported, specify it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note added, we do not support matching on ICMP.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about port 0 with proto any?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such a rule would never match right now. I've added the case to the Validate() function.

Copy link
Contributor

@ashwinp ashwinp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial review comments.

type Rule struct {
// EndpointSelector selects all endpoints which should be subject to
// this rule. Cannot be empty.
EndpointSelector EndpointSelector `json:"selector"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider renaming this to AppliedEndpoints.

// Mandatory field. Identifies the endpoints where this rule should be applied.
AppliedEndpoints EndpointSelector json:"selector"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like selector because it is in line with the k8s API which uses the word selector for everything to select objects to apply rules onto.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Thanks.

// If omitted, no rules will be applied at ingress via this rule.
//
// +optional
Ingress *IngressRule `json:"ingress,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field is a pointer to IngressRule, and can hold only 1 ingress rule. The document needs to be fixed, or we need to have a collection of IngressRules here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment fixed

//
// Either ingres, egress, or both can be provided. If both ingress and egress
// are omitted, the rule has no effect.
type Rule struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider renaming this to Policy. A Policy can contain multiple rules. Each rule can be of type Egress or Ingress.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what it is. The policy is not part of the API. Cilium owns a policy repository which consists of a list of rules which can be modified via the API by adding/removing/changing rules.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Thanks.

// If omitted, no rules will be applied at egress via this rule
//
// +optional
Egress *EgressRule `json:"egress,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simplify this by having a collection of Rules (a new type that only contains match predicates)? A Rule can either be of type Egress or Ingress. The current type Rule can be renamed to Policy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the comment from @rlenglet. I'm trying to avoid oneOf as it can't be described and enforced with swagger.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks.

// endpoints which are whitelisted in fromCIDR.
//
// +optional
FromEndpoints []EndpointSelector `json:"fromEndpoints,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider renaming this to SourceEndpoints

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source has some implications as it could be confused for source addresses. "From" is clearer in that sense, "rule selects b and allows ingress from a"

// EndpointSelector which are allowed to communicate with the endpoint
// subject to the rule.
//
// If omitted and no other IngressRule applies a FromEndpoints rule on
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this paragraph. We're referring to fromCIDR, which is defined later, and the comment is redundant as the params are tagged as optional.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, only obfuscates, removed both occurrences.

// +optional
FromEndpoints []EndpointSelector `json:"fromEndpoints,omitempty"`

// FromRequires is a selector which must match in combination with at
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not understand the purpose of FromRequires. The example about group=A talks about labels, but isn't this field of type endpointselector?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reworded the paragraph to make it clearer

// ports.
//
// +optional
ToPorts []PortRule `json:"toPorts,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider renaming to destinationPorts

// endpoints which are whitelisted in FromEndpoints.
//
// +optional
FromCIDR []CIDR `json:"toCIDR,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider renaming to SourceCIDRs

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SourceCIDRs would definitely be mistaken for source address CIDR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2nd thought, this is exactly what we want so I'm making this change.

// TODO: allow port range?
type PortProtocol struct {
// Port is an L4 port number
Port uint16 `json:"port"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this:
Ports []uint16 ?
That way, one can specify TCP: [80, 8080], as an example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See @rlenglet comment to move to strong to support ranges, I like that approach. We could eventually support all of "10", "10, 11", "10-15"

@tgraf tgraf force-pushed the policy-simplification branch 8 times, most recently from 8fb64f4 to eaf8c02 Compare May 11, 2017 01:04
func ParseLabelArray(labels ...string) LabelArray {
array := make([]*Label, len(labels))
for i := range labels {
array[i] = ParseLabel(labels[i])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a label is not valid?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can a label not be valid?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, since it returns a pointer I was assuming it could be nil

EndpointSelector EndpointSelector `json:"endpointSelector"`

// Ingress is a list of IngressRule which are enforced at ingress.
// If omitted, this rule does not apply at ingress.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the omitted ones please also add the case where they are empty. "If omitted, or empty,..."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, will do.

// unique, multiple rules can have overlapping or identical labels.
//
// +optional
Labels labels.LabelArray `json:"group,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about this. What about "Metadata" instead, seems wrong?
Also, why the json variable is called Group? I mean, I would like to see Group instead of Labels in the go struct.

// ParseEndpointSelector parses a list of labels in the format of
// strings and returns an EndpointSelector
func ParseEndpointSelector(list ...string) EndpointSelector {
array := make([]*labels.Label, len(list))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[]*labels.Label -> labels.LabelArray

// is entering the endpoint selected by the endpointSelector.
//
// - All members of this structure are optional.
// - All rule types are considered independently
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it this means all slices in this structure will be "ORed" or "ANDed"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ORed. Rephrased.

switch r.canReach(ctx) {
// The rule contained a constraint which was not met, this
// connection is not allowed
case api.Denied:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this, if the policy has multiple rules, and one of the rules can't be reachable why does that means it is automatically denied?

Shouldn't you first check for all the rules that have its ingress matching the ctx.To and then check if one of them allows receiving from ctx.From?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api.Denied is only returned if it is denied for sure. This only happens for Require rules. If the requirement for a require rule is not met, the potential consumer is never allowed.

}

// AddList inserts a rule into the policy repository
func (p *Repository) AddList(rules []*api.Rule) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a pointer to the rule itself?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've masked it behind a new api.Rules type now so we can easily change this. I don't see a problem with a pointer.

defer p.Mutex.Unlock()

// Validate entire rule list first and only append array if
// all rules are valid
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


// GetJSON returns all rules of the policy repository as string in JSON
// representation
func (p *Repository) GetJSON() string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not implement the MarshalJSON itself? because it returns byte and error

Why not ToString() or String() or even GoString()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we return JSON :-) It's not meant to be human readable.

}

func mergeL4Port(r api.PortRule, p api.PortProtocol, proto string, resMap L4PolicyMap) int {
fmt := fmt.Sprintf("%s:%d", proto, p.Port)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not %s/%d instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either is fine, it doesn't matter. Never exposed. Will change this if you look this better.

@tgraf
Copy link
Member Author

tgraf commented May 11, 2017

What about port 0 with proto any?

@tgraf tgraf closed this May 11, 2017
@tgraf tgraf reopened this May 11, 2017
@ashwinp
Copy link
Contributor

ashwinp commented May 12, 2017

pkg/policy/api/rule.go, line 24 at r3 (raw file):

// labels contained in the endpointSelector
//
// Multiple rules are related to each with a logical OR.

related to each?


Comments from Reviewable

@tgraf tgraf force-pushed the policy-simplification branch 2 times, most recently from e1c1792 to 97013e2 Compare May 12, 2017 09:21
@tgraf tgraf added kind/bug This is a bug in the Cilium logic. kind/enhancement This would improve or streamline existing functionality. pending-review and removed wip labels May 12, 2017
@tgraf tgraf force-pushed the policy-simplification branch 4 times, most recently from 4ab023a to 95c3c8b Compare May 12, 2017 17:44
@ashwinp
Copy link
Contributor

ashwinp commented May 12, 2017

pkg/policy/api/rule.go, line 44 at r4 (raw file):

	// Egress is a list of EgressRule which are enforced at egress.
	// If omitted or empty, this rule does not apply at ingress.

typo: at egress.


Comments from Reviewable

@tgraf
Copy link
Member Author

tgraf commented May 12, 2017

typo: at egress.

Fixed, thanks

@ashwinp
Copy link
Contributor

ashwinp commented May 12, 2017

pkg/policy/api/rule.go, line 183 at r4 (raw file):

// rules which must be met.
type PortRule struct {
	// Ports is a lst of L4 port/protocol

typo: list


Comments from Reviewable

@ashwinp
Copy link
Contributor

ashwinp commented May 12, 2017

pkg/policy/api/rule.go, line 193 at r4 (raw file):

	// RedirectPort is the L4 port which, if set, all traffic matching the
	// Ports ie being redirected to. Whatever listener behind that port

/ie/is/


Comments from Reviewable

// - All members of this structure are optional. If omitted or empty, the
// member will have no effect on the rule.
// - All members of this structure are evaluated independently, i.e. L4 ports
// allowed with ToPorts do not depend on a match of the FromEndpoints in the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the comment; There is no FromEndpoints in the EgressRule.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

//
// - All members of this structure are optional. If omitted or empty, the
// member will have no effect on the rule.
// - All members of this structure are evaluated independently, i.e. L4 ports
Copy link
Contributor

@ashwinp ashwinp May 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"All members of this structure are evaluated independently". This would imply that as a user, I cannot enforce the following:

  • Allow Ingress only from container labeled A (FromEndpoint=A) to port 80.
  • Allow Ingress only from container labeled B (FromEndpoint=B) to port 8080.

B will be allowed to connect to port 80 in addition to port 8080. Similarly, A will be allowed to connect to port 8080 in addition to port 80. Right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Two reasons:

  • The BPF datapath is currently not capable of enforcing this. L3 endpoints are whitelisted via BPF map. The per endpoint L4 map is another BPF map. Referring to a 2nd BPF map from a 1st BPF map value was only just recently added so we will start supporting this with more recent kernels.
  • k8s NetworkPolicy also defines this separate notion and we need to able to map k8s NetworkPolicy to our policy.

The idea is to add a field later on which introduces L4 policies which depend on a specific L3 rule. This could be done by adding a new ToPorts inside FromEndpoints or to add a flag to ToPorts which allows to tie the ToPorts to FromEndpoints and FromCIDR within the same IngressRule.

tgraf added 9 commits May 13, 2017 15:16
Signed-off-by: Thomas Graf <thomas@cilium.io>
- Replacement of tree structure with list of rules. Within the list, all
  rules enjoy the same priority
- Separation of concerns will be addresses via a different concept
  introduced later. Users without an interest in separation of concern
  are not introduce to unnecessary complexity
- Removal of deny rules. Require rules provide a great baseline and all
  use cases so far could be addressed with require rules without
  requiring implicit denies.
- Removal of tree allows to drastically simplify label handling as
  labels are now always absolute. No need for a label to have a path.

Signed-off-by: Thomas Graf <thomas@cilium.io>
Signed-off-by: Thomas Graf <thomas@cilium.io>
- Removes the {path} parameter from the API
- Introduces search & delete by labels

Signed-off-by: Thomas Graf <thomas@cilium.io>
Signed-off-by: Thomas Graf <thomas@cilium.io>
Signed-off-by: Thomas Graf <thomas@cilium.io>
Signed-off-by: Thomas Graf <thomas@cilium.io>
Fixes sporadic testsuite failures

Signed-off-by: Thomas Graf <thomas@cilium.io>
Signed-off-by: Thomas Graf <thomas@cilium.io>
@tgraf tgraf force-pushed the policy-simplification branch 5 times, most recently from 337a293 to 9a466b0 Compare May 14, 2017 23:53
@tgraf tgraf requested a review from aanm May 15, 2017 10:59
@tgraf tgraf force-pushed the policy-simplification branch 2 times, most recently from b6b5b63 to 42c977e Compare May 15, 2017 11:46
Copy link
Member

@aanm aanm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really awesome work @tgraf !

@tgraf tgraf merged commit 9eec5e9 into master May 15, 2017
@tgraf tgraf deleted the policy-simplification branch May 15, 2017 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/bug This is a bug in the Cilium logic. kind/enhancement This would improve or streamline existing functionality.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants