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

add support for filtering public ingress traffic by IP or CIDR #1198

Merged
merged 10 commits into from
Sep 25, 2024
4 changes: 2 additions & 2 deletions charts/radix-operator/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: v2
name: radix-operator
version: 1.40.0
appVersion: 1.60.0
version: 1.41.0
appVersion: 1.61.0
kubeVersion: ">=1.24.0"
description: Radix Operator
keywords:
Expand Down
44 changes: 44 additions & 0 deletions charts/radix-operator/templates/radixapplication.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,29 @@ spec:
Enabled or disables collection of custom Prometheus metrics.
More info: https://www.radix.equinor.com/references/reference-radix-config/#monitoring
type: boolean
network:
description: Environment specific network settings.
properties:
ingress:
description: Ingress defines settings for ingress
traffic.
properties:
public:
description: Public defines settings for public
traffic.
properties:
allow:
description: |-
Allow defines a list of public IP addresses or CIDRs which are allowed to access the component.
All IP addresses are allowed if this field is empty or not set.
items:
description: IP address or CIDR.
pattern: ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$
type: string
type: array
type: object
type: object
type: object
node:
description: |-
Environment specific GPU requirements for the component.
Expand Down Expand Up @@ -1406,6 +1429,27 @@ spec:
minLength: 1
pattern: ^(([a-z0-9][-a-z0-9]*)?[a-z0-9])?$
type: string
network:
description: Network settings.
properties:
ingress:
description: Ingress defines settings for ingress traffic.
properties:
public:
description: Public defines settings for public traffic.
properties:
allow:
description: |-
Allow defines a list of public IP addresses or CIDRs which are allowed to access the component.
All IP addresses are allowed if this field is empty or not set.
items:
description: IP address or CIDR.
pattern: ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$
type: string
type: array
type: object
type: object
type: object
node:
description: |-
Defines GPU requirements for the component.
Expand Down
54 changes: 54 additions & 0 deletions json-schema/radixapplication.json
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,33 @@
"description": "Enabled or disables collection of custom Prometheus metrics.\nMore info: https://www.radix.equinor.com/references/reference-radix-config/#monitoring",
"type": "boolean"
},
"network": {
"description": "Environment specific network settings.",
"properties": {
"ingress": {
"description": "Ingress defines settings for ingress traffic.",
"properties": {
"public": {
"description": "Public defines settings for public traffic.",
"properties": {
"allow": {
"description": "Allow defines a list of public IP addresses or CIDRs which are allowed to access the component.\nAll IP addresses are allowed if this field is empty or not set.",
"items": {
"description": "IP address or CIDR.",
"pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\/([0-9]|[1-2][0-9]|3[0-2]))?$",
"type": "string"
},
"type": "array"
}
},
"type": "object"
}
},
"type": "object"
}
},
"type": "object"
},
"node": {
"description": "Environment specific GPU requirements for the component.\nMore info: https://www.radix.equinor.com/references/reference-radix-config/#node",
"properties": {
Expand Down Expand Up @@ -1394,6 +1421,33 @@
"pattern": "^(([a-z0-9][-a-z0-9]*)?[a-z0-9])?$",
"type": "string"
},
"network": {
"description": "Network settings.",
"properties": {
"ingress": {
"description": "Ingress defines settings for ingress traffic.",
"properties": {
"public": {
"description": "Public defines settings for public traffic.",
"properties": {
"allow": {
"description": "Allow defines a list of public IP addresses or CIDRs which are allowed to access the component.\nAll IP addresses are allowed if this field is empty or not set.",
"items": {
"description": "IP address or CIDR.",
"pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\/([0-9]|[1-2][0-9]|3[0-2]))?$",
"type": "string"
},
"type": "array"
}
},
"type": "object"
}
},
"type": "object"
}
},
"type": "object"
},
"node": {
"description": "Defines GPU requirements for the component.\nMore info: https://www.radix.equinor.com/references/reference-radix-config/#node",
"properties": {
Expand Down
21 changes: 21 additions & 0 deletions pkg/apis/deployment/radixcomponent.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,33 @@ func GetRadixComponentsForEnv(ctx context.Context, radixApplication *radixv1.Rad
if deployComponent.VolumeMounts, err = getRadixCommonComponentVolumeMounts(&radixComponent, environmentSpecificConfig); err != nil {
return nil, err
}
if deployComponent.Network, err = getRadixComponentNetwork(&radixComponent, environmentSpecificConfig); err != nil {
return nil, err
}
deployComponents = append(deployComponents, deployComponent)
}

return deployComponents, nil
}

func getRadixComponentNetwork(component *radixv1.RadixComponent, environmentConfig *radixv1.RadixEnvironmentConfig) (*radixv1.Network, error) {
var dst *radixv1.Network
if component.Network != nil {
dst = component.Network.DeepCopy()
}

if environmentConfig != nil && environmentConfig.Network != nil {
if dst == nil {
dst = &radixv1.Network{}
}
if err := mergo.Merge(dst, environmentConfig.Network, mergo.WithOverride, mergo.WithOverrideEmptySlice); err != nil {
return nil, err
}
}

return dst, nil
}

func getRadixCommonComponentReadOnlyFileSystem(radixComponent radixv1.RadixCommonComponent, environmentSpecificConfig radixv1.RadixCommonEnvironmentConfig) *bool {
if !commonutils.IsNil(environmentSpecificConfig) && environmentSpecificConfig.GetReadOnlyFileSystem() != nil {
return environmentSpecificConfig.GetReadOnlyFileSystem()
Expand Down
70 changes: 70 additions & 0 deletions pkg/apis/deployment/radixcomponent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,76 @@ func Test_GetRadixComponentsForEnv_Runtime_AlwaysUseFromDeployComponentImages(t
}
}

func Test_Test_GetRadixComponentsForEnv_NetworkIngressPublicAllow(t *testing.T) {
tests := map[string]struct {
commonConfig *radixv1.Network
envConfig *radixv1.Network
setEnv bool
expected *radixv1.Network
}{
"nil in common": {
setEnv: false,
expected: nil,
},
"empty in common, env not set": {
commonConfig: &radixv1.Network{},
setEnv: false,
expected: &radixv1.Network{},
},
"empty in common and nil in env": {
commonConfig: &radixv1.Network{},
setEnv: true,
expected: &radixv1.Network{},
},
"empty in common and env": {
commonConfig: &radixv1.Network{},
setEnv: true,
envConfig: &radixv1.Network{},
expected: &radixv1.Network{},
},
"allow set in common, nil in env": {
commonConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("commonip1"), radixv1.IPOrCIDR("commonip2")}}}},
setEnv: true,
envConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{}}},
expected: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("commonip1"), radixv1.IPOrCIDR("commonip2")}}}},
},
"allow set in common, empty in env": {
commonConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("commonip1"), radixv1.IPOrCIDR("commonip2")}}}},
setEnv: true,
envConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{}}}},
expected: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{}}}},
},
"allow set in common and env": {
commonConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("commonip1"), radixv1.IPOrCIDR("commonip2")}}}},
setEnv: true,
envConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("envip1"), radixv1.IPOrCIDR("envip2")}}}},
expected: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("envip1"), radixv1.IPOrCIDR("envip2")}}}},
},
"allow nil in common set in env": {
commonConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{}}},
setEnv: true,
envConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("envip1"), radixv1.IPOrCIDR("envip2")}}}},
expected: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("envip1"), radixv1.IPOrCIDR("envip2")}}}},
},
}

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
const envName = "anyenv"
component := utils.AnApplicationComponent().WithName("anycomponent").WithNetwork(test.commonConfig)
if test.setEnv {
component = component.WithEnvironmentConfigs(
utils.AnEnvironmentConfig().WithEnvironment(envName).WithNetwork(test.envConfig),
)
}
ra := utils.ARadixApplication().WithComponents(component).BuildRA()
components, err := GetRadixComponentsForEnv(context.Background(), ra, nil, envName, make(pipeline.DeployComponentImages), make(radixv1.EnvVarsMap), nil)
require.NoError(t, err)
assert.Equal(t, test.expected, components[0].Network)
})
}
}

func convertRadixDeployComponentToNameSet(deployComponents []radixv1.RadixDeployComponent) map[string]bool {
set := make(map[string]bool)
for _, deployComponent := range deployComponents {
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,6 @@ func GetAnnotationProvider(ingressConfiguration IngressConfiguration, certificat
NewIngressConfigurationAnnotationProvider(ingressConfiguration),
NewClientCertificateAnnotationProvider(certificateNamespace),
NewOAuth2AnnotationProvider(oauth2DefaultConfig),
NewIngressPublicAllowListAnnotationProvider(),
}
}
21 changes: 21 additions & 0 deletions pkg/apis/ingress/ingressannotationprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"fmt"
"strings"

"github.com/equinor/radix-common/utils/slice"
"github.com/equinor/radix-operator/pkg/apis/defaults"
radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1"
"github.com/equinor/radix-operator/pkg/apis/utils"
oauthutil "github.com/equinor/radix-operator/pkg/apis/utils/oauth"
)

type AnnotationProvider interface {
// GetAnnotations returns annotations for use on Ingress resources
GetAnnotations(component radixv1.RadixCommonDeployComponent, namespace string) (map[string]string, error)
}

Expand Down Expand Up @@ -128,3 +130,22 @@ func (provider *oauth2AnnotationProvider) GetAnnotations(component radixv1.Radix

return annotations, nil
}

// NewIngressPublicAllowListAnnotationProvider provides Ingress annotations for allowing
// only public traffic from IP addresses defined in Network.Ingress.Public.Allow field
func NewIngressPublicAllowListAnnotationProvider() AnnotationProvider {
return &ingressPublicAllowListAnnotationProvider{}
}

type ingressPublicAllowListAnnotationProvider struct{}

// GetAnnotations returns annotations for only allowing public ingress traffic
// for IPs or CIDRs defined in Network.Ingress.Public.Allow for a component
func (*ingressPublicAllowListAnnotationProvider) GetAnnotations(component radixv1.RadixCommonDeployComponent, _ string) (map[string]string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing docs comment (or in the interface)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

if network := component.GetNetwork(); network == nil || network.Ingress == nil || network.Ingress.Public == nil || network.Ingress.Public.Allow == nil || len(*network.Ingress.Public.Allow) == 0 {
return nil, nil
}

addressList := slice.Map(*component.GetNetwork().Ingress.Public.Allow, func(v radixv1.IPOrCIDR) string { return string(v) })
return map[string]string{"nginx.ingress.kubernetes.io/whitelist-source-range": strings.Join(addressList, ",")}, nil
}
50 changes: 50 additions & 0 deletions pkg/apis/ingress/ingressannotationprovider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,56 @@ func Test_ClientCertificateAnnotations(t *testing.T) {
assert.Empty(t, result, "Expected Annotations to be empty")
}

func Test_PublicIngressAllowAnnotationProvider(t *testing.T) {
tests := map[string]struct {
component radixv1.RadixCommonDeployComponent
expectedAnnotation map[string]string
}{
"network is not set": {
component: &radixv1.RadixDeployComponent{},
expectedAnnotation: make(map[string]string),
},
"network.ingress is not set": {
component: &radixv1.RadixDeployComponent{Network: &radixv1.Network{}},
expectedAnnotation: make(map[string]string),
},
"network.ingress.public is not set": {
component: &radixv1.RadixDeployComponent{Network: &radixv1.Network{Ingress: &radixv1.Ingress{}}},
expectedAnnotation: make(map[string]string),
},
"network.ingress.public.allow is not set": {
component: &radixv1.RadixDeployComponent{Network: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{}}}},
expectedAnnotation: make(map[string]string),
},
"network.ingress.public.allow is empty": {
component: &radixv1.RadixDeployComponent{Network: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{}}}}},
expectedAnnotation: make(map[string]string),
},
"network.ingress.public.allow single entry": {
component: &radixv1.RadixDeployComponent{Network: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("10.0.0.1")}}}}},
expectedAnnotation: map[string]string{"nginx.ingress.kubernetes.io/whitelist-source-range": "10.0.0.1"},
},
"network.ingress.public.allow multiple entries": {
component: &radixv1.RadixDeployComponent{Network: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("10.0.0.1"), radixv1.IPOrCIDR("10.10.10.10/30")}}}}},
expectedAnnotation: map[string]string{"nginx.ingress.kubernetes.io/whitelist-source-range": "10.0.0.1,10.10.10.10/30"},
},
}

sut := &ingressPublicAllowListAnnotationProvider{}
for testName, testSpec := range tests {
t.Run(testName, func(t *testing.T) {
actual, err := sut.GetAnnotations(testSpec.component, "")
assert.NoError(t, err)
if len(testSpec.expectedAnnotation) == 0 {
assert.Empty(t, actual)
} else {
assert.Equal(t, actual, testSpec.expectedAnnotation)
}
})
}

}

type OAuth2AnnotationsTestSuite struct {
suite.Suite
oauth2Config *defaults.MockOAuth2Config
Expand Down
5 changes: 4 additions & 1 deletion pkg/apis/ingress/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import (

// GetAuxOAuthProxyAnnotationProviders Gets aux OAuth proxy annotation providers
func GetAuxOAuthProxyAnnotationProviders() []AnnotationProvider {
return []AnnotationProvider{NewForceSslRedirectAnnotationProvider()}
return []AnnotationProvider{
NewForceSslRedirectAnnotationProvider(),
NewIngressPublicAllowListAnnotationProvider(),
}
}

// BuildOAuthProxyIngressForComponentIngress builds OAuth proxy ingress for RadixDeploy component ingress
Expand Down
Loading
Loading