Skip to content

Commit 49549b4

Browse files
authored
Merge pull request #169 from linode/firewall-configmap
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
2 parents 00ff396 + 8476f12 commit 49549b4

File tree

10 files changed

+1100
-84
lines changed

10 files changed

+1100
-84
lines changed

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ Annotation (Suffix) | Values | Default | Description
5656
`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
5757
`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.
5858
`tags` | string | | A comma seperated list of tags to be applied to the createad NodeBalancer instance
59-
`firewall-id` | string | | The Firewall ID that's applied to the NodeBalancer instance.
59+
`firewall-id` | string | | An existing Cloud Firewall ID to be attached to the NodeBalancer instance. See [Firewalls](#firewalls).
60+
`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).
6061

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

81+
#### Firewalls
82+
Firewall rules can be applied to the CCM Managed NodeBalancers in two distinct ways.
83+
84+
##### CCM Managed Firewall
85+
To use this feature, ensure that the linode api token used with the ccm has the `add_firewalls` grant.
86+
87+
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.
88+
89+
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
90+
91+
Example usage of an ACL to allow traffic from a specific set of addresses
92+
93+
```yaml
94+
kind: Service
95+
apiVersion: v1
96+
metadata:
97+
name: https-lb
98+
annotations:
99+
service.beta.kubernetes.io/linode-loadbalancer-firewall-acl: |
100+
{
101+
"allowList": {
102+
"ipv4": ["192.166.0.0/16", "172.23.41.0/24"],
103+
"ipv6": ["2001:DB8::/128"]
104+
},
105+
}
106+
spec:
107+
type: LoadBalancer
108+
selector:
109+
app: nginx-https-example
110+
ports:
111+
- name: http
112+
protocol: TCP
113+
port: 80
114+
targetPort: http
115+
- name: https
116+
protocol: TCP
117+
port: 443
118+
targetPort: https
119+
```
120+
121+
122+
##### User Managed Firewall
123+
Users can create CloudFirewall instances, supply their own rules and attach them to the NodeBalancer. To do so, set the
124+
`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.
125+
126+
**Note**<br/>
127+
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.
128+
129+
130+
80131
### Nodes
81132
Kubernetes Nodes can be configured with the following annotations.
82133

cloud/linode/annotations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
annLinodeHostnameOnlyIngress = "service.beta.kubernetes.io/linode-loadbalancer-hostname-only-ingress"
2828
annLinodeLoadBalancerTags = "service.beta.kubernetes.io/linode-loadbalancer-tags"
2929
annLinodeCloudFirewallID = "service.beta.kubernetes.io/linode-loadbalancer-firewall-id"
30+
annLinodeCloudFirewallACL = "service.beta.kubernetes.io/linode-loadbalancer-firewall-acl"
3031

3132
annLinodeNodePrivateIP = "node.k8s.linode.com/private-ip"
3233
annLinodeHostUUID = "node.k8s.linode.com/host-uuid"

cloud/linode/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ type Client interface {
3232
CreateFirewallDevice(ctx context.Context, firewallID int, opts linodego.FirewallDeviceCreateOptions) (*linodego.FirewallDevice, error)
3333
CreateFirewall(ctx context.Context, opts linodego.FirewallCreateOptions) (*linodego.Firewall, error)
3434
DeleteFirewall(ctx context.Context, fwid int) error
35+
GetFirewall(context.Context, int) (*linodego.Firewall, error)
36+
UpdateFirewallRules(context.Context, int, linodego.FirewallRuleSet) (*linodego.FirewallRuleSet, error)
3537
}
3638

3739
// linodego.Client implements Client

cloud/linode/fake_linode_test.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ type fakeAPI struct {
2424
nb map[string]*linodego.NodeBalancer
2525
nbc map[string]*linodego.NodeBalancerConfig
2626
nbn map[string]*linodego.NodeBalancerNode
27-
fw map[int]*linodego.Firewall
28-
fwd map[int]map[int]*linodego.FirewallDevice
27+
fw map[int]*linodego.Firewall // map of firewallID -> firewall
28+
fwd map[int]map[int]*linodego.FirewallDevice // map of firewallID -> firewallDeviceID:FirewallDevice
2929

3030
requests map[fakeRequest]struct{}
3131
}
@@ -186,12 +186,15 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
186186
},
187187
Data: data,
188188
}
189-
rr, _ := json.Marshal(resp)
189+
rr, err := json.Marshal(resp)
190+
if err != nil {
191+
f.t.Fatal(err)
192+
}
190193
_, _ = w.Write(rr)
191194
return
192195
}
193196

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

732+
} else if strings.Contains(urlPath, "firewalls") {
733+
// path is networking/firewalls/%d/rules
734+
parts := strings.Split(urlPath[1:], "/")
735+
fwrs := new(linodego.FirewallRuleSet)
736+
if err := json.NewDecoder(r.Body).Decode(fwrs); err != nil {
737+
f.t.Fatal(err)
738+
}
739+
740+
fwID, err := strconv.Atoi(parts[2])
741+
if err != nil {
742+
f.t.Fatal(err)
743+
}
744+
745+
if firewall, found := f.fw[fwID]; found {
746+
firewall.Rules.Inbound = fwrs.Inbound
747+
firewall.Rules.InboundPolicy = fwrs.InboundPolicy
748+
// outbound rules do not apply, ignoring.
749+
f.fw[fwID] = firewall
750+
resp, err := json.Marshal(firewall)
751+
if err != nil {
752+
f.t.Fatal(err)
753+
}
754+
_, _ = w.Write(resp)
755+
return
756+
}
757+
758+
w.WriteHeader(404)
759+
resp := linodego.APIError{
760+
Errors: []linodego.APIErrorReason{
761+
{Reason: "Not Found"},
762+
},
763+
}
764+
rr, _ := json.Marshal(resp)
765+
_, _ = w.Write(rr)
729766
}
730767
}
731768
}
732769

733770
func createFirewallDevice(fwId int, f *fakeAPI, fdco linodego.FirewallDeviceCreateOptions) linodego.FirewallDevice {
734771
fwd := linodego.FirewallDevice{
735-
ID: rand.Intn(9999),
772+
ID: fdco.ID,
736773
Entity: linodego.FirewallDeviceEntity{
737774
ID: fdco.ID,
738775
Type: fdco.Type,

0 commit comments

Comments
 (0)