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

CCM Managed firewalls #169

Merged
merged 19 commits into from
Jan 30, 2024
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ Annotation (Suffix) | Values | Default | Description
`nodebalancer-id` | string | | The ID of the NodeBalancer to front the service. When not specified, a new NodeBalancer will be created. This can be configured on service creation or patching
`hostname-only-ingress` | [bool](#annotation-bool-values) | `false` | When `true`, the LoadBalancerStatus for the service will only contain the Hostname. This is useful for bypassing kube-proxy's rerouting of in-cluster requests originally intended for the external LoadBalancer to the service's constituent pod IPs.
`tags` | string | | A comma seperated list of tags to be applied to the createad NodeBalancer instance
`firewall-id` | string | | The Firewall ID that's applied to the NodeBalancer instance.
`firewall-id` | string | | An existing Cloud Firewall ID to be attached to the NodeBalancer instance. See [Firewalls](#firewalls).
`firewall-acl` | string | | The Firewall rules to be applied to the NodeBalancer. Adding this annotation creates a new CCM managed Linode CloudFirewall instance. See [Firewalls](#firewalls).

#### Deprecated Annotations
These annotations are deprecated, and will be removed in a future release.
Expand All @@ -77,6 +78,56 @@ Key | Values | Default | Description
`proxy-protocol` | `none`, `v1`, `v2` | `none` | Specifies whether to use a version of Proxy Protocol on the underlying NodeBalancer. Overwrites `default-proxy-protocol`.
`tls-secret-name` | string | | Specifies a secret to use for TLS. The secret type should be `kubernetes.io/tls`.

#### Firewalls
Firewall rules can be applied to the CCM Managed NodeBalancers in two distinct ways.

##### CCM Managed Firewall
To use this feature, ensure that the linode api token used with the ccm has the `add_firewalls` grant.

The CCM accepts firewall ACLs in json form. The ACL can either be an `allowList` or a `denyList`. Supplying both is not supported. Supplying neither is not supported. The `allowList` sets up a CloudFirewall that `ACCEPT`s traffic only from the specified IPs/CIDRs and `DROP`s everything else. The `denyList` sets up a CloudFirewall that `DROP`s traffic only from the specified IPs/CIDRs and `ACCEPT`s everything else. Ports are automatically inferred from the service configuration.

See [Firewall rules](https://www.linode.com/docs/api/networking/#firewall-create__request-body-schema) for more details on how to specify the IPs/CIDRs

Example usage of an ACL to allow traffic from a specific set of addresses

```yaml
kind: Service
apiVersion: v1
metadata:
name: https-lb
annotations:
service.beta.kubernetes.io/linode-loadbalancer-firewall-acl: |
{
"allowList": {
"ipv4": ["192.166.0.0/16", "172.23.41.0/24"],
"ipv6": ["2001:DB8::/128"]
},
}
spec:
type: LoadBalancer
selector:
app: nginx-https-example
ports:
- name: http
protocol: TCP
port: 80
targetPort: http
- name: https
protocol: TCP
port: 443
targetPort: https
```


##### User Managed Firewall
Users can create CloudFirewall instances, supply their own rules and attach them to the NodeBalancer. To do so, set the
`service.beta.kubernetes.io/linode-loadbalancer-firewall-id` annotation to the ID of the cloud firewall. The CCM does not manage the lifecycle of the CloudFirewall Instance in this case. Users are responsible for ensuring the policies are correct.

**Note**<br/>
If the user supplies a firewall-id, and later switches to using an ACL, the CCM will take over the CloudFirewall Instance. To avoid this, delete the service, and re-create it so the original CloudFirewall is left undisturbed.



### Nodes
Kubernetes Nodes can be configured with the following annotations.

Expand Down
1 change: 1 addition & 0 deletions cloud/linode/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
annLinodeHostnameOnlyIngress = "service.beta.kubernetes.io/linode-loadbalancer-hostname-only-ingress"
annLinodeLoadBalancerTags = "service.beta.kubernetes.io/linode-loadbalancer-tags"
annLinodeCloudFirewallID = "service.beta.kubernetes.io/linode-loadbalancer-firewall-id"
annLinodeCloudFirewallACL = "service.beta.kubernetes.io/linode-loadbalancer-firewall-acl"

annLinodeNodePrivateIP = "node.k8s.linode.com/private-ip"
annLinodeHostUUID = "node.k8s.linode.com/host-uuid"
Expand Down
2 changes: 2 additions & 0 deletions cloud/linode/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type Client interface {
CreateFirewallDevice(ctx context.Context, firewallID int, opts linodego.FirewallDeviceCreateOptions) (*linodego.FirewallDevice, error)
CreateFirewall(ctx context.Context, opts linodego.FirewallCreateOptions) (*linodego.Firewall, error)
DeleteFirewall(ctx context.Context, fwid int) error
GetFirewall(context.Context, int) (*linodego.Firewall, error)
UpdateFirewallRules(context.Context, int, linodego.FirewallRuleSet) (*linodego.FirewallRuleSet, error)
}

// linodego.Client implements Client
Expand Down
47 changes: 42 additions & 5 deletions cloud/linode/fake_linode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ type fakeAPI struct {
nb map[string]*linodego.NodeBalancer
nbc map[string]*linodego.NodeBalancerConfig
nbn map[string]*linodego.NodeBalancerNode
fw map[int]*linodego.Firewall
fwd map[int]map[int]*linodego.FirewallDevice
fw map[int]*linodego.Firewall // map of firewallID -> firewall
fwd map[int]map[int]*linodego.FirewallDevice // map of firewallID -> firewallDeviceID:FirewallDevice

requests map[fakeRequest]struct{}
}
Expand Down Expand Up @@ -186,12 +186,15 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
},
Data: data,
}
rr, _ := json.Marshal(resp)
rr, err := json.Marshal(resp)
if err != nil {
f.t.Fatal(err)
}
_, _ = w.Write(rr)
return
}

rx, _ = regexp.Compile("/nodebalancers/[0-9]+/firewalls")
rx = regexp.MustCompile("/nodebalancers/[0-9]+/firewalls")
if rx.MatchString(urlPath) {
id := strings.Split(urlPath, "/")[2]
devID, err := strconv.Atoi(id)
Expand Down Expand Up @@ -726,13 +729,47 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rr, _ := json.Marshal(resp)
_, _ = w.Write(rr)

} else if strings.Contains(urlPath, "firewalls") {
// path is networking/firewalls/%d/rules
parts := strings.Split(urlPath[1:], "/")
fwrs := new(linodego.FirewallRuleSet)
if err := json.NewDecoder(r.Body).Decode(fwrs); err != nil {
f.t.Fatal(err)
}

fwID, err := strconv.Atoi(parts[2])
if err != nil {
f.t.Fatal(err)
}

if firewall, found := f.fw[fwID]; found {
firewall.Rules.Inbound = fwrs.Inbound
firewall.Rules.InboundPolicy = fwrs.InboundPolicy
// outbound rules do not apply, ignoring.
f.fw[fwID] = firewall
resp, err := json.Marshal(firewall)
if err != nil {
f.t.Fatal(err)
}
_, _ = w.Write(resp)
return
}

w.WriteHeader(404)
resp := linodego.APIError{
Errors: []linodego.APIErrorReason{
{Reason: "Not Found"},
},
}
rr, _ := json.Marshal(resp)
_, _ = w.Write(rr)
}
}
}

func createFirewallDevice(fwId int, f *fakeAPI, fdco linodego.FirewallDeviceCreateOptions) linodego.FirewallDevice {
fwd := linodego.FirewallDevice{
ID: rand.Intn(9999),
ID: fdco.ID,
Entity: linodego.FirewallDeviceEntity{
ID: fdco.ID,
Type: fdco.Type,
Expand Down
Loading
Loading