Skip to content

Commit

Permalink
Merge branch 'main' into key-secret-tmpl
Browse files Browse the repository at this point in the history
  • Loading branch information
bcm820 committed Aug 23, 2024
2 parents 106b498 + 8cfd121 commit 2baef85
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 30 deletions.
2 changes: 2 additions & 0 deletions api/v1alpha2/linodefirewall_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type LinodeFirewallSpec struct {
// +optional
InboundRules []FirewallRule `json:"inboundRules,omitempty"`

// InboundPolicy determines if traffic by default should be ACCEPTed or DROPped. Defaults to ACCEPT if not defined.
// +kubebuilder:validation:Enum=ACCEPT;DROP
// +kubebuilder:default=ACCEPT
// +optional
Expand All @@ -49,6 +50,7 @@ type LinodeFirewallSpec struct {
// +optional
OutboundRules []FirewallRule `json:"outboundRules,omitempty"`

// OutboundPolicy determines if traffic by default should be ACCEPTed or DROPped. Defaults to ACCEPT if not defined.
// +kubebuilder:validation:Enum=ACCEPT;DROP
// +kubebuilder:default=ACCEPT
// +optional
Expand Down
1 change: 0 additions & 1 deletion api/v1alpha2/linodemachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ type LinodeMachineSpec struct {
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
Tags []string `json:"tags,omitempty"`
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +kubebuilder:deprecatedversion:warning="Firewalls should be referenced via FirewallRef"
FirewallID int `json:"firewallID,omitempty"`
// OSDisk is configuration for the root disk that includes the OS,
// if not specified this defaults to whatever space is not taken up by the DataDisks
Expand Down
45 changes: 36 additions & 9 deletions cloud/services/domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func EnsureAkamaiDNSEntries(ctx context.Context, cscope *scope.ClusterScope, ope

// if operation is delete and we got the record, delete it
if operation == "delete" {
return deleteAkamaiEntry(ctx, akaDNSClient, recordBody, dnsEntry, rootDomain)
return deleteAkamaiEntry(ctx, cscope, recordBody, dnsEntry)
}
// if operation is create and we got the record, update it
// Check if the target already exists in the target list
Expand Down Expand Up @@ -136,7 +136,20 @@ func createAkamaiEntry(ctx context.Context, client clients.AkamClient, dnsEntry
)
}

func deleteAkamaiEntry(ctx context.Context, client clients.AkamClient, recordBody *dns.RecordBody, dnsEntry DNSOptions, rootDomain string) error {
func deleteAkamaiEntry(ctx context.Context, cscope *scope.ClusterScope, recordBody *dns.RecordBody, dnsEntry DNSOptions) error {
linodeCluster := cscope.LinodeCluster
linodeClusterNetworkSpec := linodeCluster.Spec.Network
rootDomain := linodeClusterNetworkSpec.DNSRootDomain
// If record is A/AAAA type, verify ownership
if dnsEntry.DNSRecordType != linodego.RecordTypeTXT {
isOwner, err := IsAkamaiDomainRecordOwner(ctx, cscope)
if err != nil {
return err
}
if !isOwner {
return fmt.Errorf("the domain record is not owned by this entity. wont delete")
}
}
switch {
case len(recordBody.Target) > 1:
recordBody.Target = removeElement(
Expand All @@ -145,9 +158,9 @@ func deleteAkamaiEntry(ctx context.Context, client clients.AkamClient, recordBod
// So we need to match that
strings.Replace(dnsEntry.Target, "::", ":0:0:", 8), //nolint:mnd // 8 for 8 octest
)
return client.UpdateRecord(ctx, recordBody, rootDomain)
return cscope.AkamaiDomainsClient.UpdateRecord(ctx, recordBody, rootDomain)
default:
return client.DeleteRecord(ctx, recordBody, rootDomain)
return cscope.AkamaiDomainsClient.DeleteRecord(ctx, recordBody, rootDomain)
}
}

Expand Down Expand Up @@ -190,8 +203,8 @@ func (d *DNSEntries) getDNSEntriesToEnsure(cscope *scope.ClusterScope) ([]DNSOpt
if len(d.options) == 0 {
continue
}
d.options = append(d.options, DNSOptions{subDomain, eachMachine.Name, linodego.RecordTypeTXT, dnsTTLSec})
}
d.options = append(d.options, DNSOptions{subDomain, cscope.LinodeCluster.Name, linodego.RecordTypeTXT, dnsTTLSec})

return d.options, nil
}
Expand Down Expand Up @@ -263,7 +276,7 @@ func DeleteDomainRecord(ctx context.Context, cscope *scope.ClusterScope, domainI

// If record is A/AAAA type, verify ownership
if dnsEntry.DNSRecordType != linodego.RecordTypeTXT {
isOwner, err := IsDomainRecordOwner(ctx, cscope, dnsEntry.Hostname, domainID)
isOwner, err := IsLinodeDomainRecordOwner(ctx, cscope, dnsEntry.Hostname, domainID)
if err != nil {
return err
}
Expand All @@ -276,9 +289,9 @@ func DeleteDomainRecord(ctx context.Context, cscope *scope.ClusterScope, domainI
return cscope.LinodeDomainsClient.DeleteDomainRecord(ctx, domainID, domainRecords[0].ID)
}

func IsDomainRecordOwner(ctx context.Context, cscope *scope.ClusterScope, hostname string, domainID int) (bool, error) {
func IsLinodeDomainRecordOwner(ctx context.Context, cscope *scope.ClusterScope, hostname string, domainID int) (bool, error) {
// Check if domain record exists
filter, err := json.Marshal(map[string]interface{}{"name": hostname, "target": cscope.LinodeCluster.Name, "type": linodego.RecordTypeTXT})
filter, err := json.Marshal(map[string]interface{}{"name": hostname, "type": linodego.RecordTypeTXT})
if err != nil {
return false, err
}
Expand All @@ -290,7 +303,21 @@ func IsDomainRecordOwner(ctx context.Context, cscope *scope.ClusterScope, hostna

// If record exists, update it
if len(domainRecords) == 0 {
return false, fmt.Errorf("no txt record %s found with value %s for machine %s", hostname, cscope.LinodeCluster.Name, cscope.LinodeCluster.Name)
return false, fmt.Errorf("no txt record %s found", hostname)
}

return true, nil
}

func IsAkamaiDomainRecordOwner(ctx context.Context, cscope *scope.ClusterScope) (bool, error) {
linodeCluster := cscope.LinodeCluster
linodeClusterNetworkSpec := linodeCluster.Spec.Network
rootDomain := linodeClusterNetworkSpec.DNSRootDomain
akaDNSClient := cscope.AkamaiDomainsClient
fqdn := getSubDomain(cscope) + "." + rootDomain
recordBody, err := akaDNSClient.GetRecord(ctx, rootDomain, fqdn, string(linodego.RecordTypeTXT))
if err != nil || recordBody == nil {
return false, fmt.Errorf("no txt record %s found", fqdn)
}

return true, nil
Expand Down
26 changes: 25 additions & 1 deletion cloud/services/domains_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,14 @@ func TestAddIPToDNS(t *testing.T) {
Domain: "lkedevs.net",
},
}, nil).AnyTimes()
mockClient.EXPECT().ListDomainRecords(gomock.Any(), gomock.Any(), gomock.Any()).Return([]linodego.DomainRecord{
{
ID: 1234,
Type: "A",
Name: "test-cluster",
TTLSec: 30,
},
}, nil).AnyTimes()
},
expectedError: nil,
expectK8sClient: func(mockK8sClient *mock.MockK8sClient) {
Expand Down Expand Up @@ -1003,7 +1011,23 @@ func TestDeleteIPFromDNS(t *testing.T) {
},
},
},
expects: func(mockClient *mock.MockLinodeClient) {},
expects: func(mockClient *mock.MockLinodeClient) {
mockClient.EXPECT().ListDomains(gomock.Any(), gomock.Any()).Return([]linodego.Domain{
{
ID: 1,
Domain: "lkedevs.net",
},
}, nil).AnyTimes()
mockClient.EXPECT().ListDomainRecords(gomock.Any(), gomock.Any(), gomock.Any()).Return([]linodego.DomainRecord{
{
ID: 1234,
Type: "A",
Name: "test-cluster",
TTLSec: 30,
},
}, nil).AnyTimes()
mockClient.EXPECT().DeleteDomainRecord(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
},
expectedError: nil,
expectK8sClient: func(mockK8sClient *mock.MockK8sClient) {
mockK8sClient.EXPECT().Scheme().Return(nil).AnyTimes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ spec:
type: integer
inboundPolicy:
default: ACCEPT
description: InboundPolicy determines if traffic by default should
be ACCEPTed or DROPped. Defaults to ACCEPT if not defined.
enum:
- ACCEPT
- DROP
Expand Down Expand Up @@ -114,6 +116,8 @@ spec:
type: array
outboundPolicy:
default: ACCEPT
description: OutboundPolicy determines if traffic by default should
be ACCEPTed or DROPped. Defaults to ACCEPT if not defined.
enum:
- ACCEPT
- DROP
Expand Down
29 changes: 29 additions & 0 deletions controller/linodecluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,21 @@ var _ = Describe("cluster-lifecycle-dns", Ordered, Label("cluster", "cluster-lif
Path(
Call("cluster with dns loadbalancing is created", func(ctx context.Context, mck Mock) {
cScope.LinodeClient = mck.LinodeClient
cScope.LinodeDomainsClient = mck.LinodeClient
mck.LinodeClient.EXPECT().ListDomains(gomock.Any(), gomock.Any()).Return([]linodego.Domain{
{
ID: 1,
Domain: "lkedevs.net",
},
}, nil).AnyTimes()
mck.LinodeClient.EXPECT().ListDomainRecords(gomock.Any(), gomock.Any(), gomock.Any()).Return([]linodego.DomainRecord{
{
ID: 1234,
Type: "A",
Name: "test-cluster",
TTLSec: 30,
},
}, nil).AnyTimes()
}),
Result("cluster created", func(ctx context.Context, mck Mock) {
reconciler.Client = k8sClient
Expand Down Expand Up @@ -529,6 +544,20 @@ var _ = Describe("dns-override-endpoint", Ordered, Label("cluster", "dns-overrid
}
Expect(k8sClient.Create(ctx, &linodeMachine)).To(Succeed())
cScope.LinodeMachines = linodeMachines
mck.LinodeClient.EXPECT().ListDomains(gomock.Any(), gomock.Any()).Return([]linodego.Domain{
{
ID: 1,
Domain: "lkedevs.net",
},
}, nil).AnyTimes()
mck.LinodeClient.EXPECT().ListDomainRecords(gomock.Any(), gomock.Any(), gomock.Any()).Return([]linodego.DomainRecord{
{
ID: 1234,
Type: "A",
Name: "test-cluster",
TTLSec: 30,
},
}, nil).AnyTimes()
}),
Result("cluster created", func(ctx context.Context, mck Mock) {
reconciler.Client = k8sClient
Expand Down
17 changes: 7 additions & 10 deletions controller/linodefirewall_controller_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,10 @@ func processACL(firewall *infrav1alpha2.LinodeFirewall) (
})
}
}
if firewall.Spec.InboundPolicy == "ACCEPT" {
// if an allow list is present, we drop everything else.
createOpts.Rules.InboundPolicy = "DROP"
} else {
// if a deny list is present, we accept everything else.
if firewall.Spec.InboundPolicy == "" {
createOpts.Rules.InboundPolicy = "ACCEPT"
} else {
createOpts.Rules.InboundPolicy = firewall.Spec.InboundPolicy
}

// process outbound rules
Expand Down Expand Up @@ -255,12 +253,11 @@ func processACL(firewall *infrav1alpha2.LinodeFirewall) (
})
}
}
if firewall.Spec.OutboundPolicy == "ACCEPT" {
// if an allow list is present, we drop everything else.
createOpts.Rules.OutboundPolicy = "DROP"
} else {
// if a deny list is present, we accept everything else.

if firewall.Spec.OutboundPolicy == "" {
createOpts.Rules.OutboundPolicy = "ACCEPT"
} else {
createOpts.Rules.OutboundPolicy = firewall.Spec.OutboundPolicy
}

// need to check if we ended up needing to make too many rules
Expand Down
60 changes: 51 additions & 9 deletions docs/src/topics/firewalling.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# Firewalling

This guide covers how Cilium can be set up to act as a [host firewall](https://docs.cilium.io/en/latest/security/host-firewall/) on CAPL clusters.
This guide covers how Cilium and Cloud Firewalls can be used for firewalling CAPL clusters.

## Default Configuration
By default, the following policies are set to audit mode(without any enforcement) on CAPL clusters
## Cilium Firewalls

Cilium provides cluster-wide firewalling via [Host Policies](https://docs.cilium.io/en/latest/security/policy/language/#hostpolicies)
which enforce access control over connectivity to and from cluster nodes.
Cilium's [host firewall](https://docs.cilium.io/en/latest/security/host-firewall/) is responsible for enforcing the security policies.

### Default Cilium Host Firewall Configuration
By default, the following Host Policies are set to audit mode (without any enforcement) on CAPL clusters:

* [Kubeadm](./flavors/default.md) cluster allow rules

Expand All @@ -30,13 +36,13 @@ For kubeadm clusters running outside of VPC, ports 2379 and 2380 are also allowe
| 6443 | API Server Traffic | World |
| * | In Cluster Communication | Intra Cluster and VPC Traffic |

## Enabling Firewall Enforcement
In order to turn the cilium network policy from audit to enforce mode use the environment variable `FW_AUDIT_ONLY=false`
### Enabling Cilium Host Policy Enforcement
In order to turn the Cilium Host Policies from audit to enforce mode, use the environment variable `FW_AUDIT_ONLY=false`
when generating the cluster. This will set the [policy-audit-mode](https://docs.cilium.io/en/latest/security/policy-creation/#creating-policies-from-verdicts)
on the cilium deployment
on the Cilium deployment.

## Adding Additional Rules
Additional rules can be added to the `default-policy`
### Adding Additional Cilium Host Policies
Additional rules can be added to the `default-policy`:
```yaml
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
Expand All @@ -57,7 +63,7 @@ spec:
- port: "22" # added for SSH Access to the nodes
- port: "${APISERVER_PORT:=6443}"
```
Alternatively, additional rules can be added by creating a new policy
Alternatively, additional rules can be added by creating a new policy:
```yaml
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
Expand All @@ -73,3 +79,39 @@ spec:
- ports:
- port: "22"
```
## Cloud Firewalls
For controlling firewalls via Linode resources, a [Cloud Firewall](https://www.linode.com/products/cloud-firewall/) can
be defined and provisioned via the `LinodeFirewall` resource in CAPL.

The created Cloud Firewall can be used on a `LinodeMachine` or a `LinodeMachineTemplate` by setting the `firewallRef` field.
Alternatively, the provisioned Cloud Firewall's ID can be used in the `firewallID` field.

```admonish note
The `firewallRef` and `firewallID` fields are currently immutable for `LinodeMachines` and `LinodeMachineTemplates`. This will
be addressed in a later release.
```

Example `LinodeFirewall`:
```yaml
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2
kind: LinodeFirewall
metadata:
name: sample-fw
spec:
enabled: true
inboundPolicy: DROP
inboundRules:
- action: ACCEPT
label: k8s-api
ports: "6443"
protocol: "TCP"
addresses:
ipv4:
- "10.0.0.0/24"
# outboundPolicy: ACCEPT
# outboundRules: []
```

Cloud Firewalls are not automatically created for any CAPL flavor at this time.

0 comments on commit 2baef85

Please sign in to comment.