Skip to content

Commit

Permalink
Merge pull request #169 from linode/firewall-configmap
Browse files Browse the repository at this point in the history
Adds a new feature - ccm managed firewalls. Firewall rules can by specified by the user using an annotation - service.beta.kubernetes.io/linode-loadbalancer-firewall-acl
  • Loading branch information
tchinmai7 authored Jan 30, 2024
2 parents 00ff396 + 8476f12 commit 49549b4
Show file tree
Hide file tree
Showing 10 changed files with 1,100 additions and 84 deletions.
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

0 comments on commit 49549b4

Please sign in to comment.