From a068c1241d6ced4516a1806ac1ef90548aa7d02b Mon Sep 17 00:00:00 2001 From: Mohamed-Amine Bouqsimi Date: Tue, 6 Sep 2022 19:02:18 +0200 Subject: [PATCH] operator: Configure gateway to honor the global TLS security profile (#6870) --- operator/CHANGELOG.md | 1 + .../apis/config/v1/projectconfig_types.go | 29 +++++ .../apis/config/v1/zz_generated.deepcopy.go | 20 +++ .../loki-operator.clusterserviceversion.yaml | 1 + operator/cmd/loki-broker/main.go | 24 +++- operator/config/rbac/role.yaml | 1 + .../controllers/loki/lokistack_controller.go | 2 +- operator/go.mod | 4 +- operator/go.sum | 6 +- .../internal/tlsprofile/tlsprofile.go | 74 ++++++++++++ .../internal/tlsprofile/tlsprofile_test.go | 114 ++++++++++++++++++ .../handlers/lokistack_create_or_update.go | 11 ++ operator/internal/manifests/gateway.go | 14 ++- .../internal/manifests/gateway_tenants.go | 8 +- .../manifests/gateway_tenants_test.go | 23 ++-- operator/internal/manifests/gateway_test.go | 101 ++++++++++++++++ .../internal/manifests/openshift/configure.go | 13 +- .../manifests/openshift/opa_openshift.go | 5 +- operator/internal/manifests/options.go | 4 + 19 files changed, 430 insertions(+), 25 deletions(-) create mode 100644 operator/internal/handlers/internal/tlsprofile/tlsprofile.go create mode 100644 operator/internal/handlers/internal/tlsprofile/tlsprofile_test.go diff --git a/operator/CHANGELOG.md b/operator/CHANGELOG.md index d36dd519d4..1651ee9f1a 100644 --- a/operator/CHANGELOG.md +++ b/operator/CHANGELOG.md @@ -1,5 +1,6 @@ ## Main +- [6870](https://github.com/grafana/loki/pull/6870) **aminesnow**: Configure gateway to honor the global tlsSecurityProfile on Openshift - [6999](https://github.com/grafana/loki/pull/6999) **Red-GV**: Adding LokiStack Gateway alerts - [7000](https://github.com/grafana/loki/pull/7000) **xperimental**: Configure default node affinity for all pods - [6923](https://github.com/grafana/loki/pull/6923) **xperimental**: Reconcile owner reference for existing objects diff --git a/operator/apis/config/v1/projectconfig_types.go b/operator/apis/config/v1/projectconfig_types.go index e2fedbae19..e223d6fd42 100644 --- a/operator/apis/config/v1/projectconfig_types.go +++ b/operator/apis/config/v1/projectconfig_types.go @@ -73,6 +73,35 @@ type FeatureGates struct { // OpenShift contains a set of feature gates supported only on OpenShift. OpenShift OpenShiftFeatureGates `json:"openshift,omitempty"` + + // TLSProfile allows to chose a TLS security profile. + TLSProfile string `json:"tlsProfile,omitempty"` +} + +// TLSProfileType is a TLS security profile based on the Mozilla definitions: +// https://wiki.mozilla.org/Security/Server_Side_TLS +type TLSProfileType string + +const ( + // TLSProfileOldType is a TLS security profile based on: + // https://wiki.mozilla.org/Security/Server_Side_TLS#Old_backward_compatibility + TLSProfileOldType TLSProfileType = "Old" + // TLSProfileIntermediateType is a TLS security profile based on: + // https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29 + TLSProfileIntermediateType TLSProfileType = "Intermediate" + // TLSProfileModernType is a TLS security profile based on: + // https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility + TLSProfileModernType TLSProfileType = "Modern" +) + +// TLSProfileSpec is the desired behavior of a TLSProfileType. +type TLSProfileSpec struct { + // ciphers is used to specify the cipher algorithms that are negotiated + // during the TLS handshake. + Ciphers []string + // minTLSVersion is used to specify the minimal version of the TLS protocol + // that is negotiated during the TLS handshake. + MinTLSVersion string } //+kubebuilder:object:root=true diff --git a/operator/apis/config/v1/zz_generated.deepcopy.go b/operator/apis/config/v1/zz_generated.deepcopy.go index 3ff10850cd..316a973a93 100644 --- a/operator/apis/config/v1/zz_generated.deepcopy.go +++ b/operator/apis/config/v1/zz_generated.deepcopy.go @@ -65,3 +65,23 @@ func (in *ProjectConfig) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSProfileSpec) DeepCopyInto(out *TLSProfileSpec) { + *out = *in + if in.Ciphers != nil { + in, out := &in.Ciphers, &out.Ciphers + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSProfileSpec. +func (in *TLSProfileSpec) DeepCopy() *TLSProfileSpec { + if in == nil { + return nil + } + out := new(TLSProfileSpec) + in.DeepCopyInto(out) + return out +} diff --git a/operator/bundle/manifests/loki-operator.clusterserviceversion.yaml b/operator/bundle/manifests/loki-operator.clusterserviceversion.yaml index e14dab7c42..43f298c64a 100644 --- a/operator/bundle/manifests/loki-operator.clusterserviceversion.yaml +++ b/operator/bundle/manifests/loki-operator.clusterserviceversion.yaml @@ -972,6 +972,7 @@ spec: - apiGroups: - config.openshift.io resources: + - apiservers - dnses verbs: - get diff --git a/operator/cmd/loki-broker/main.go b/operator/cmd/loki-broker/main.go index b44b318592..c3c401b5c1 100644 --- a/operator/cmd/loki-broker/main.go +++ b/operator/cmd/loki-broker/main.go @@ -11,6 +11,7 @@ import ( "github.com/ViaQ/logerr/v2/log" "github.com/go-logr/logr" configv1 "github.com/grafana/loki/operator/apis/config/v1" + projectconfigv1 "github.com/grafana/loki/operator/apis/config/v1" lokiv1 "github.com/grafana/loki/operator/apis/loki/v1" "github.com/grafana/loki/operator/internal/manifests" "github.com/grafana/loki/operator/internal/manifests/storage" @@ -58,6 +59,8 @@ func (c *config) registerFlags(f *flag.FlagSet) { // Input and output file/dir options f.StringVar(&c.crFilepath, "custom-resource.path", "", "Path to a custom resource YAML file.") f.StringVar(&c.writeToDir, "output.write-dir", "", "write each file to the specified directory.") + // TLS profile option + f.StringVar(&c.featureFlags.TLSProfile, "tls-profile", "", "The TLS security Profile configuration.") } func (c *config) validateFlags(log logr.Logger) { @@ -126,14 +129,23 @@ func main() { os.Exit(1) } + if cfg.featureFlags.TLSProfile != "" && + cfg.featureFlags.TLSProfile != string(projectconfigv1.TLSProfileOldType) && + cfg.featureFlags.TLSProfile != string(projectconfigv1.TLSProfileIntermediateType) && + cfg.featureFlags.TLSProfile != string(projectconfigv1.TLSProfileModernType) { + logger.Error(err, "failed to parse TLS profile. Allowed values: 'Old', 'Intermediate', 'Modern'", "value", cfg.featureFlags.TLSProfile) + os.Exit(1) + } + // Convert config to manifest.Options opts := manifests.Options{ - Name: cfg.Name, - Namespace: cfg.Namespace, - Image: cfg.Image, - Stack: ls.Spec, - Gates: cfg.featureFlags, - ObjectStorage: cfg.objectStorage, + Name: cfg.Name, + Namespace: cfg.Namespace, + Image: cfg.Image, + Stack: ls.Spec, + Gates: cfg.featureFlags, + ObjectStorage: cfg.objectStorage, + TLSProfileType: projectconfigv1.TLSProfileType(cfg.featureFlags.TLSProfile), } if optErr := manifests.ApplyDefaultSettings(&opts); optErr != nil { diff --git a/operator/config/rbac/role.yaml b/operator/config/rbac/role.yaml index e3cc1f66c7..5dfac08b20 100644 --- a/operator/config/rbac/role.yaml +++ b/operator/config/rbac/role.yaml @@ -54,6 +54,7 @@ rules: - apiGroups: - config.openshift.io resources: + - apiservers - dnses verbs: - get diff --git a/operator/controllers/loki/lokistack_controller.go b/operator/controllers/loki/lokistack_controller.go index d42dab3e36..ea21d29dca 100644 --- a/operator/controllers/loki/lokistack_controller.go +++ b/operator/controllers/loki/lokistack_controller.go @@ -83,7 +83,7 @@ type LokiStackReconciler struct { // +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors;prometheusrules,verbs=get;list;watch;create;update;delete // +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;create;update // +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update -// +kubebuilder:rbac:groups=config.openshift.io,resources=dnses,verbs=get;list;watch +// +kubebuilder:rbac:groups=config.openshift.io,resources=dnses;apiservers,verbs=get;list;watch // +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch;create;update;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to diff --git a/operator/go.mod b/operator/go.mod index f248f7f45e..e29016b2f4 100644 --- a/operator/go.mod +++ b/operator/go.mod @@ -25,6 +25,7 @@ require github.com/ViaQ/logerr/v2 v2.0.0 require ( github.com/google/go-cmp v0.5.7 github.com/grafana/loki v1.6.2-0.20220708124813-b92f113cb096 + github.com/openshift/library-go v0.0.0-20220622115547-84d884f4c9f6 gopkg.in/yaml.v2 v2.4.0 ) @@ -148,7 +149,7 @@ require ( golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect - golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220222172238-00053529121e // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect @@ -165,6 +166,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 // indirect k8s.io/apiextensions-apiserver v0.24.0 // indirect + k8s.io/apiserver v0.24.0 // indirect k8s.io/component-base v0.24.0 // indirect k8s.io/klog/v2 v2.60.1 // indirect k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect diff --git a/operator/go.sum b/operator/go.sum index 10f9ed1536..62e4980f10 100644 --- a/operator/go.sum +++ b/operator/go.sum @@ -1074,6 +1074,8 @@ github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xA github.com/openshift/api v0.0.0-20220712151050-2647eb31dee7 h1:zjlaMHqzNrrm8bnltBnrKLwxALoLAH/8UAkBEESrEOg= github.com/openshift/api v0.0.0-20220712151050-2647eb31dee7/go.mod h1:LEnw1IVscIxyDnltE3Wi7bQb/QzIM8BfPNKoGA1Qlxw= github.com/openshift/build-machinery-go v0.0.0-20211213093930-7e33a7eb4ce3/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE= +github.com/openshift/library-go v0.0.0-20220622115547-84d884f4c9f6 h1:lmfmsIGq62lmj17qrZh4Gbbb86WvJw6pLhCNwNjB2Yk= +github.com/openshift/library-go v0.0.0-20220622115547-84d884f4c9f6/go.mod h1:AMZwYwSdbvALDl3QobEzcJ2IeDO7DYLsr42izKzh524= github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc= github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e h1:4cPxUYdgaGzZIT5/j0IfqOrrXmq6bG8AwvwisMXpdrg= github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= @@ -1586,8 +1588,9 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2089,6 +2092,7 @@ k8s.io/apiserver v0.18.3/go.mod h1:tHQRmthRPLUtwqsOnJJMoI8SW3lnoReZeE861lH8vUw= k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/apiserver v0.24.0 h1:GR7kGsjOMfilRvlG3Stxv/3uz/ryvJ/aZXc5pqdsNV0= k8s.io/apiserver v0.24.0/go.mod h1:WFx2yiOMawnogNToVvUYT9nn1jaIkMKj41ZYCVycsBA= k8s.io/client-go v0.24.3 h1:Nl1840+6p4JqkFWEW2LnMKU667BUxw03REfLAVhuKQY= k8s.io/client-go v0.24.3/go.mod h1:AAovolf5Z9bY1wIg2FZ8LPQlEdKHjLI7ZD4rw920BJw= diff --git a/operator/internal/handlers/internal/tlsprofile/tlsprofile.go b/operator/internal/handlers/internal/tlsprofile/tlsprofile.go new file mode 100644 index 0000000000..a906d718d4 --- /dev/null +++ b/operator/internal/handlers/internal/tlsprofile/tlsprofile.go @@ -0,0 +1,74 @@ +package tlsprofile + +import ( + "context" + + "github.com/ViaQ/logerr/v2/kverrors" + projectconfigv1 "github.com/grafana/loki/operator/apis/config/v1" + "github.com/grafana/loki/operator/internal/external/k8s" + openshiftv1 "github.com/openshift/api/config/v1" + "github.com/openshift/library-go/pkg/crypto" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// APIServerName is the apiserver resource name used to fetch it. +const APIServerName = "cluster" + +// GetSecurityProfileInfo gets the tls profile info to apply. +func GetSecurityProfileInfo(ctx context.Context, k k8s.Client, tlsProfileType projectconfigv1.TLSProfileType) (projectconfigv1.TLSProfileSpec, error) { + var tlsProfile openshiftv1.TLSSecurityProfile + + if tlsProfileType != "" { + tlsProfile = openshiftv1.TLSSecurityProfile{ + Type: openshiftv1.TLSProfileType(tlsProfileType), + } + } else { + tlsProfile = openshiftv1.TLSSecurityProfile{ + Type: openshiftv1.TLSProfileIntermediateType, + } + + var apiServer openshiftv1.APIServer + if err := k.Get(ctx, client.ObjectKey{Name: APIServerName}, &apiServer); err != nil { + if !apierrors.IsNotFound(err) { + return projectconfigv1.TLSProfileSpec{}, kverrors.Wrap(err, "failed to lookup apiServer") + } + } + + if apiServer.Spec.TLSSecurityProfile != nil { + tlsProfile = *apiServer.Spec.TLSSecurityProfile + } + } + + tlsMinVersion, ciphers := extractInfoFromTLSProfile(&tlsProfile) + return projectconfigv1.TLSProfileSpec{ + MinTLSVersion: tlsMinVersion, + Ciphers: ciphers, + }, nil +} + +func extractInfoFromTLSProfile(profile *openshiftv1.TLSSecurityProfile) (string, []string) { + var profileType openshiftv1.TLSProfileType + if profile == nil { + profileType = openshiftv1.TLSProfileIntermediateType + } else { + profileType = profile.Type + } + + var profileSpec *openshiftv1.TLSProfileSpec + if profileType == openshiftv1.TLSProfileCustomType { + if profile.Custom != nil { + profileSpec = &profile.Custom.TLSProfileSpec + } + } else { + profileSpec = openshiftv1.TLSProfiles[profileType] + } + + // nothing found / custom type set but no actual custom spec + if profileSpec == nil { + profileSpec = openshiftv1.TLSProfiles[openshiftv1.TLSProfileIntermediateType] + } + + // need to remap all Ciphers to their respective IANA names used by Go + return string(profileSpec.MinTLSVersion), crypto.OpenSSLToIANACipherSuites(profileSpec.Ciphers) +} diff --git a/operator/internal/handlers/internal/tlsprofile/tlsprofile_test.go b/operator/internal/handlers/internal/tlsprofile/tlsprofile_test.go new file mode 100644 index 0000000000..24b4c14905 --- /dev/null +++ b/operator/internal/handlers/internal/tlsprofile/tlsprofile_test.go @@ -0,0 +1,114 @@ +package tlsprofile_test + +import ( + "context" + "testing" + + projectconfigv1 "github.com/grafana/loki/operator/apis/config/v1" + "github.com/grafana/loki/operator/internal/external/k8s/k8sfakes" + "github.com/grafana/loki/operator/internal/handlers/internal/tlsprofile" + openshiftv1 "github.com/openshift/api/config/v1" + "github.com/stretchr/testify/assert" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + apiServer = openshiftv1.APIServer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + } + ciphersOld = []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + } + ciphersIntermediate = []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + } +) + +func TestGetSecurityProfileInfo(t *testing.T) { + type tt struct { + desc string + profile projectconfigv1.TLSProfileType + expected projectconfigv1.TLSProfileSpec + } + + tc := []tt{ + { + desc: "Old profile", + profile: projectconfigv1.TLSProfileOldType, + expected: projectconfigv1.TLSProfileSpec{ + MinTLSVersion: "VersionTLS10", + Ciphers: ciphersOld, + }, + }, + { + desc: "Intermediate profile", + profile: projectconfigv1.TLSProfileIntermediateType, + expected: projectconfigv1.TLSProfileSpec{ + MinTLSVersion: "VersionTLS12", + Ciphers: ciphersIntermediate, + }, + }, + { + desc: "Modern profile", + profile: projectconfigv1.TLSProfileModernType, + expected: projectconfigv1.TLSProfileSpec{ + MinTLSVersion: "VersionTLS13", + // Go lib crypto doesn't allow ciphers to be configured for TLS 1.3 + // (Read this and weep: https://github.com/golang/go/issues/29349) + Ciphers: []string{}, + }, + }, + } + + sw := &k8sfakes.FakeStatusWriter{} + k := &k8sfakes.FakeClient{} + + k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object) error { + if apiServer.Name == name.Name { + k.SetClientObject(object, &apiServer) + return nil + } + return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found") + } + + k.StatusStub = func() client.StatusWriter { return sw } + + for _, tc := range tc { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + info, err := tlsprofile.GetSecurityProfileInfo(context.TODO(), k, tc.profile) + assert.Nil(t, err) + assert.NotNil(t, info) + assert.EqualValues(t, tc.expected, info) + }) + } +} diff --git a/operator/internal/handlers/lokistack_create_or_update.go b/operator/internal/handlers/lokistack_create_or_update.go index 2ed40b3efc..344dfb5ad0 100644 --- a/operator/internal/handlers/lokistack_create_or_update.go +++ b/operator/internal/handlers/lokistack_create_or_update.go @@ -7,12 +7,14 @@ import ( "time" configv1 "github.com/grafana/loki/operator/apis/config/v1" + projectconfigv1 "github.com/grafana/loki/operator/apis/config/v1" lokiv1 "github.com/grafana/loki/operator/apis/loki/v1" lokiv1beta1 "github.com/grafana/loki/operator/apis/loki/v1beta1" "github.com/grafana/loki/operator/internal/external/k8s" "github.com/grafana/loki/operator/internal/handlers/internal/gateway" "github.com/grafana/loki/operator/internal/handlers/internal/rules" "github.com/grafana/loki/operator/internal/handlers/internal/storage" + "github.com/grafana/loki/operator/internal/handlers/internal/tlsprofile" "github.com/grafana/loki/operator/internal/manifests" storageoptions "github.com/grafana/loki/operator/internal/manifests/storage" "github.com/grafana/loki/operator/internal/metrics" @@ -224,6 +226,7 @@ func CreateOrUpdateLokiStack( Secrets: tenantSecrets, Configs: tenantConfigs, }, + TLSProfileType: projectconfigv1.TLSProfileType(fg.TLSProfile), } ll.Info("begin building manifests") @@ -240,6 +243,14 @@ func CreateOrUpdateLokiStack( } } + spec, err := tlsprofile.GetSecurityProfileInfo(ctx, k, opts.TLSProfileType) + if err != nil { + ll.Error(err, "failed to get security profile info") + return err + } + + opts.TLSProfileSpec = spec + objects, err := manifests.BuildAll(opts) if err != nil { ll.Error(err, "failed to build manifests") diff --git a/operator/internal/manifests/gateway.go b/operator/internal/manifests/gateway.go index 569d7687a5..7126819629 100644 --- a/operator/internal/manifests/gateway.go +++ b/operator/internal/manifests/gateway.go @@ -4,6 +4,7 @@ import ( "crypto/sha1" "fmt" "path" + "strings" "github.com/ViaQ/logerr/v2/kverrors" "github.com/imdario/mergo" @@ -41,16 +42,21 @@ func BuildGateway(opts Options) ([]client.Object, error) { objs := []client.Object{cm, dpl, svc, ing} + minTLSVersion := opts.TLSProfileSpec.MinTLSVersion + ciphersList := opts.TLSProfileSpec.Ciphers + ciphers := strings.Join(ciphersList, `,`) + if opts.Gates.HTTPEncryption { serviceName := serviceNameGatewayHTTP(opts.Name) - if err := configureGatewayMetricsPKI(&dpl.Spec.Template.Spec, serviceName); err != nil { + if err := configureGatewayMetricsPKI(&dpl.Spec.Template.Spec, serviceName, minTLSVersion, ciphers); err != nil { return nil, err } } if opts.Stack.Tenants != nil { mode := opts.Stack.Tenants.Mode - if err := configureGatewayDeploymentForMode(dpl, mode, opts.Gates, opts.Name, opts.Namespace); err != nil { + + if err := configureGatewayDeploymentForMode(dpl, mode, opts.Gates, opts.Name, opts.Namespace, minTLSVersion, ciphers); err != nil { return nil, err } @@ -343,7 +349,7 @@ func gatewayConfigOptions(opt Options) gateway.Options { } } -func configureGatewayMetricsPKI(podSpec *corev1.PodSpec, serviceName string) error { +func configureGatewayMetricsPKI(podSpec *corev1.PodSpec, serviceName, minTLSVersion, ciphers string) error { var gwIndex int for i, c := range podSpec.Containers { if c.Name == gatewayContainerName { @@ -378,6 +384,8 @@ func configureGatewayMetricsPKI(podSpec *corev1.PodSpec, serviceName string) err Args: []string{ fmt.Sprintf("--tls.internal.server.cert-file=%s", certFile), fmt.Sprintf("--tls.internal.server.key-file=%s", keyFile), + fmt.Sprintf("--tls.min-version=%s", minTLSVersion), + fmt.Sprintf("--tls.cipher-suites=%s", ciphers), }, } uriSchemeContainerSpec := corev1.Container{ diff --git a/operator/internal/manifests/gateway_tenants.go b/operator/internal/manifests/gateway_tenants.go index eaf73b4554..8ffc0a6349 100644 --- a/operator/internal/manifests/gateway_tenants.go +++ b/operator/internal/manifests/gateway_tenants.go @@ -55,7 +55,11 @@ func ApplyGatewayDefaultOptions(opts *Options) error { return nil } -func configureGatewayDeploymentForMode(d *appsv1.Deployment, mode lokiv1.ModeType, fg configv1.FeatureGates, stackName, stackNs string) error { +func configureGatewayDeploymentForMode( + d *appsv1.Deployment, mode lokiv1.ModeType, + fg configv1.FeatureGates, stackName, stackNs string, + minTLSVersion string, ciphers string, +) error { switch mode { case lokiv1.Static, lokiv1.Dynamic: return nil // nothing to configure @@ -79,6 +83,8 @@ func configureGatewayDeploymentForMode(d *appsv1.Deployment, mode lokiv1.ModeTyp secretName, serverName, gatewayHTTPPort, + minTLSVersion, + ciphers, ) } diff --git a/operator/internal/manifests/gateway_tenants_test.go b/operator/internal/manifests/gateway_tenants_test.go index 55efd35567..e48a9079de 100644 --- a/operator/internal/manifests/gateway_tenants_test.go +++ b/operator/internal/manifests/gateway_tenants_test.go @@ -258,11 +258,12 @@ func TestConfigureDeploymentForMode(t *testing.T) { "--logs.write.endpoint=http://example.com", fmt.Sprintf("--web.healthchecks.url=https://localhost:%d", gatewayHTTPPort), "--tls.client-auth-type=NoClientCert", - "--tls.min-version=VersionTLS12", "--tls.server.cert-file=/var/run/tls/http/tls.crt", "--tls.server.key-file=/var/run/tls/http/tls.key", "--tls.healthchecks.server-ca-file=/var/run/ca/service-ca.crt", fmt.Sprintf("--tls.healthchecks.server-name=%s", "test-gateway-http.test-ns.svc.cluster.local"), + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", }, VolumeMounts: []corev1.VolumeMount{ { @@ -291,7 +292,6 @@ func TestConfigureDeploymentForMode(t *testing.T) { Image: "quay.io/observatorium/opa-openshift:latest", Args: []string{ "--log.level=warn", - "--tls.min-version=VersionTLS12", "--opa.package=lokistack", "--opa.matcher=kubernetes_namespace_name", "--web.listen=:8082", @@ -378,6 +378,8 @@ func TestConfigureDeploymentForMode(t *testing.T) { "--logs.tail.endpoint=http://example.com", "--logs.write.endpoint=http://example.com", fmt.Sprintf("--web.healthchecks.url=http://localhost:%d", gatewayHTTPPort), + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", }, VolumeMounts: []corev1.VolumeMount{ { @@ -431,8 +433,9 @@ func TestConfigureDeploymentForMode(t *testing.T) { "--logs.tail.endpoint=http://example.com", "--logs.write.endpoint=http://example.com", fmt.Sprintf("--web.healthchecks.url=https://localhost:%d", gatewayHTTPPort), + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", "--tls.client-auth-type=NoClientCert", - "--tls.min-version=VersionTLS12", "--tls.server.cert-file=/var/run/tls/http/tls.crt", "--tls.server.key-file=/var/run/tls/http/tls.key", "--tls.healthchecks.server-ca-file=/var/run/ca/service-ca.crt", @@ -465,7 +468,6 @@ func TestConfigureDeploymentForMode(t *testing.T) { Image: "quay.io/observatorium/opa-openshift:latest", Args: []string{ "--log.level=warn", - "--tls.min-version=VersionTLS12", "--opa.package=lokistack", "--opa.matcher=kubernetes_namespace_name", "--web.listen=:8082", @@ -473,6 +475,8 @@ func TestConfigureDeploymentForMode(t *testing.T) { "--web.healthchecks.url=http://localhost:8082", "--tls.internal.server.cert-file=/var/run/tls/http/tls.crt", "--tls.internal.server.key-file=/var/run/tls/http/tls.key", + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", `--openshift.mappings=application=loki.grafana.com`, `--openshift.mappings=infrastructure=loki.grafana.com`, `--openshift.mappings=audit=loki.grafana.com`, @@ -566,6 +570,8 @@ func TestConfigureDeploymentForMode(t *testing.T) { "--logs.tail.endpoint=http://example.com", "--logs.write.endpoint=http://example.com", fmt.Sprintf("--web.healthchecks.url=http://localhost:%d", gatewayHTTPPort), + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", }, VolumeMounts: []corev1.VolumeMount{ { @@ -617,9 +623,10 @@ func TestConfigureDeploymentForMode(t *testing.T) { "--logs.tail.endpoint=https://example.com", "--logs.write.endpoint=https://example.com", fmt.Sprintf("--web.healthchecks.url=https://localhost:%d", gatewayHTTPPort), + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", "--logs.tls.ca-file=/var/run/ca/service-ca.crt", "--tls.client-auth-type=NoClientCert", - "--tls.min-version=VersionTLS12", "--tls.server.cert-file=/var/run/tls/http/tls.crt", "--tls.server.key-file=/var/run/tls/http/tls.key", "--tls.healthchecks.server-ca-file=/var/run/ca/service-ca.crt", @@ -657,7 +664,6 @@ func TestConfigureDeploymentForMode(t *testing.T) { Image: "quay.io/observatorium/opa-openshift:latest", Args: []string{ "--log.level=warn", - "--tls.min-version=VersionTLS12", "--opa.package=lokistack", "--opa.matcher=kubernetes_namespace_name", "--web.listen=:8082", @@ -665,6 +671,8 @@ func TestConfigureDeploymentForMode(t *testing.T) { "--web.healthchecks.url=http://localhost:8082", "--tls.internal.server.cert-file=/var/run/tls/http/tls.crt", "--tls.internal.server.key-file=/var/run/tls/http/tls.key", + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", `--openshift.mappings=application=loki.grafana.com`, `--openshift.mappings=infrastructure=loki.grafana.com`, `--openshift.mappings=audit=loki.grafana.com`, @@ -736,11 +744,12 @@ func TestConfigureDeploymentForMode(t *testing.T) { }, }, } + for _, tc := range tc { tc := tc t.Run(tc.desc, func(t *testing.T) { t.Parallel() - err := configureGatewayDeploymentForMode(tc.dpl, tc.mode, tc.featureGates, "test", "test-ns") + err := configureGatewayDeploymentForMode(tc.dpl, tc.mode, tc.featureGates, "test", "test-ns", "min-version", "cipher1,cipher2") require.NoError(t, err) require.Equal(t, tc.want, tc.dpl) }) diff --git a/operator/internal/manifests/gateway_test.go b/operator/internal/manifests/gateway_test.go index 04e78c8e65..a9b9e02e91 100644 --- a/operator/internal/manifests/gateway_test.go +++ b/operator/internal/manifests/gateway_test.go @@ -279,3 +279,104 @@ func TestBuildGateway_WithExtraObjectsForTenantMode_ReplacesIngressWithRoute(t * require.NotContains(t, kinds, "*v1.Ingress") require.Contains(t, kinds, "*v1.Route") } + +func TestBuildGateway_WithTLSProfile(t *testing.T) { + type tt struct { + desc string + mode lokiv1.ModeType + authZ *lokiv1.AuthorizationSpec + expectedArgs []string + } + + tc := []tt{ + { + desc: "static mode", + mode: lokiv1.Static, + authZ: &lokiv1.AuthorizationSpec{ + Roles: []lokiv1.RoleSpec{ + { + Name: "some-name", + Resources: []string{"metrics"}, + Tenants: []string{"test-a"}, + Permissions: []lokiv1.PermissionType{"read"}, + }, + }, + RoleBindings: []lokiv1.RoleBindingsSpec{ + { + Name: "test-a", + Subjects: []lokiv1.Subject{ + { + Name: "test@example.com", + Kind: "user", + }, + }, + Roles: []string{"read-write"}, + }, + }, + }, + expectedArgs: []string{ + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", + }, + }, + { + desc: "dynamic mode", + mode: lokiv1.Dynamic, + expectedArgs: []string{ + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", + }, + }, + { + desc: "openshift-logging mode", + mode: lokiv1.OpenshiftLogging, + expectedArgs: []string{ + "--tls.min-version=min-version", + "--tls.cipher-suites=cipher1,cipher2", + }, + }, + } + + options := Options{ + Name: "abcd", + Namespace: "efgh", + Gates: configv1.FeatureGates{ + LokiStackGateway: true, + HTTPEncryption: true, + TLSProfile: string(configv1.TLSProfileOldType), + }, + TLSProfileSpec: configv1.TLSProfileSpec{ + MinTLSVersion: "min-version", + Ciphers: []string{"cipher1", "cipher2"}, + }, + Stack: lokiv1.LokiStackSpec{ + Template: &lokiv1.LokiTemplateSpec{ + Gateway: &lokiv1.LokiComponentSpec{ + Replicas: rand.Int31(), + }, + }, + Tenants: &lokiv1.TenantsSpec{}, + }, + } + + for _, tc := range tc { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + options.Stack.Tenants.Mode = tc.mode + options.Stack.Tenants.Authorization = tc.authZ + objs, err := BuildGateway(options) + + require.NoError(t, err) + + d, ok := objs[1].(*appsv1.Deployment) + require.True(t, ok) + + for _, c := range d.Spec.Template.Spec.Containers { + require.Contains(t, c.Args, tc.expectedArgs[0]) + require.Contains(t, c.Args, tc.expectedArgs[1]) + } + }) + } + +} diff --git a/operator/internal/manifests/openshift/configure.go b/operator/internal/manifests/openshift/configure.go index 346564137d..8506665e65 100644 --- a/operator/internal/manifests/openshift/configure.go +++ b/operator/internal/manifests/openshift/configure.go @@ -46,6 +46,8 @@ func ConfigureGatewayDeployment( withTLS, withCertSigningService bool, secretName, serverName string, gatewayHTTPPort int, + minTLSVersion string, + ciphers string, ) error { var gwIndex int for i, c := range d.Spec.Template.Spec.Containers { @@ -99,7 +101,6 @@ func ConfigureGatewayDeployment( caFilePath := path.Join(caDir, caFile) gwArgs = append(gwArgs, "--tls.client-auth-type=NoClientCert", - "--tls.min-version=VersionTLS12", fmt.Sprintf("--tls.server.cert-file=%s", certFilePath), fmt.Sprintf("--tls.server.key-file=%s", keyFilePath), fmt.Sprintf("--tls.healthchecks.server-ca-file=%s", caFilePath), @@ -107,7 +108,6 @@ func ConfigureGatewayDeployment( gwContainer.ReadinessProbe.ProbeHandler.HTTPGet.Scheme = corev1.URISchemeHTTPS gwContainer.LivenessProbe.ProbeHandler.HTTPGet.Scheme = corev1.URISchemeHTTPS - gwContainer.Args = gwArgs // Create and mount TLS secrets volumes if not already created. if !withTLS { @@ -125,13 +125,20 @@ func ConfigureGatewayDeployment( ReadOnly: true, MountPath: tlsDir, }) + + // Add TLS profile info args since openshift gateway always uses TLS. + gwArgs = append(gwArgs, + fmt.Sprintf("--tls.min-version=%s", minTLSVersion), + fmt.Sprintf("--tls.cipher-suites=%s", ciphers)) } + gwContainer.Args = gwArgs + p := corev1.PodSpec{ ServiceAccountName: d.GetName(), Containers: []corev1.Container{ *gwContainer, - newOPAOpenShiftContainer(secretVolumeName, tlsDir, certFile, keyFile, withTLS), + newOPAOpenShiftContainer(secretVolumeName, tlsDir, certFile, keyFile, minTLSVersion, ciphers, withTLS), }, Volumes: gwVolumes, } diff --git a/operator/internal/manifests/openshift/opa_openshift.go b/operator/internal/manifests/openshift/opa_openshift.go index 2c24ef25b0..06f0a3dfac 100644 --- a/operator/internal/manifests/openshift/opa_openshift.go +++ b/operator/internal/manifests/openshift/opa_openshift.go @@ -19,7 +19,7 @@ const ( opaDefaultLabelMatcher = "kubernetes_namespace_name" ) -func newOPAOpenShiftContainer(secretVolumeName, tlsDir, certFile, keyFile string, withTLS bool) corev1.Container { +func newOPAOpenShiftContainer(secretVolumeName, tlsDir, certFile, keyFile, minTLSVersion, ciphers string, withTLS bool) corev1.Container { var ( image string args []string @@ -35,7 +35,6 @@ func newOPAOpenShiftContainer(secretVolumeName, tlsDir, certFile, keyFile string uriScheme = corev1.URISchemeHTTP args = []string{ "--log.level=warn", - "--tls.min-version=VersionTLS12", fmt.Sprintf("--opa.package=%s", opaDefaultPackage), fmt.Sprintf("--opa.matcher=%s", opaDefaultLabelMatcher), fmt.Sprintf("--web.listen=:%d", GatewayOPAHTTPPort), @@ -50,6 +49,8 @@ func newOPAOpenShiftContainer(secretVolumeName, tlsDir, certFile, keyFile string args = append(args, []string{ fmt.Sprintf("--tls.internal.server.cert-file=%s", certFilePath), fmt.Sprintf("--tls.internal.server.key-file=%s", keyFilePath), + fmt.Sprintf("--tls.min-version=%s", minTLSVersion), + fmt.Sprintf("--tls.cipher-suites=%s", ciphers), }...) uriScheme = corev1.URISchemeHTTPS diff --git a/operator/internal/manifests/options.go b/operator/internal/manifests/options.go index c61c86b2fd..fdf60a7695 100644 --- a/operator/internal/manifests/options.go +++ b/operator/internal/manifests/options.go @@ -2,6 +2,7 @@ package manifests import ( configv1 "github.com/grafana/loki/operator/apis/config/v1" + projectconfigv1 "github.com/grafana/loki/operator/apis/config/v1" lokiv1 "github.com/grafana/loki/operator/apis/loki/v1" lokiv1beta1 "github.com/grafana/loki/operator/apis/loki/v1beta1" "github.com/grafana/loki/operator/internal/manifests/internal" @@ -19,6 +20,9 @@ type Options struct { GatewayBaseDomain string ConfigSHA1 string + TLSProfileType projectconfigv1.TLSProfileType + TLSProfileSpec projectconfigv1.TLSProfileSpec + Gates configv1.FeatureGates Stack lokiv1.LokiStackSpec ResourceRequirements internal.ComponentResources