diff --git a/.chloggen/fips.yaml b/.chloggen/fips.yaml new file mode 100755 index 0000000000..ec572de643 --- /dev/null +++ b/.chloggen/fips.yaml @@ -0,0 +1,19 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action) +component: collector + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add flag to disable components when operator runs on FIPS enabled cluster. + +# One or more tracking issues related to the change +issues: [3315] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: | + Flag `--fips-disabled-components=receiver.otlp,exporter.otlp,processor.batch,extension.oidc` can be used to disable + components when operator runs on FIPS enabled cluster. The operator uses `/proc/sys/crypto/fips_enabled` to check + if FIPS is enabled. diff --git a/apis/v1beta1/collector_webhook.go b/apis/v1beta1/collector_webhook.go index 4e783a01df..b1f8ddd91e 100644 --- a/apis/v1beta1/collector_webhook.go +++ b/apis/v1beta1/collector_webhook.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/open-telemetry/opentelemetry-operator/internal/config" + "github.com/open-telemetry/opentelemetry-operator/internal/fips" ta "github.com/open-telemetry/opentelemetry-operator/internal/manifests/targetallocator/adapters" "github.com/open-telemetry/opentelemetry-operator/internal/rbac" ) @@ -48,6 +49,7 @@ type CollectorWebhook struct { reviewer *rbac.Reviewer metrics *Metrics bv BuildValidator + fips fips.FIPSCheck } func (c CollectorWebhook) Default(_ context.Context, obj runtime.Object) error { @@ -290,6 +292,13 @@ func (c CollectorWebhook) Validate(ctx context.Context, r *OpenTelemetryCollecto return warnings, fmt.Errorf("the OpenTelemetry Collector mode is set to %s, which does not support the attribute 'deploymentUpdateStrategy'", r.Spec.Mode) } + if c.fips != nil { + components := r.Spec.Config.GetEnabledComponents() + if notAllowedComponents := c.fips.DisabledComponents(components[KindReceiver], components[KindExporter], components[KindProcessor], components[KindExtension]); notAllowedComponents != nil { + return nil, fmt.Errorf("the collector configuration contains not FIPS compliant components: %s. Please remove it from the config", notAllowedComponents) + } + } + return warnings, nil } @@ -423,6 +432,7 @@ func NewCollectorWebhook( reviewer *rbac.Reviewer, metrics *Metrics, bv BuildValidator, + fips fips.FIPSCheck, ) *CollectorWebhook { return &CollectorWebhook{ logger: logger, @@ -431,11 +441,12 @@ func NewCollectorWebhook( reviewer: reviewer, metrics: metrics, bv: bv, + fips: fips, } } -func SetupCollectorWebhook(mgr ctrl.Manager, cfg config.Config, reviewer *rbac.Reviewer, metrics *Metrics, bv BuildValidator) error { - cvw := NewCollectorWebhook(mgr.GetLogger().WithValues("handler", "CollectorWebhook", "version", "v1beta1"), mgr.GetScheme(), cfg, reviewer, metrics, bv) +func SetupCollectorWebhook(mgr ctrl.Manager, cfg config.Config, reviewer *rbac.Reviewer, metrics *Metrics, bv BuildValidator, fipsCheck fips.FIPSCheck) error { + cvw := NewCollectorWebhook(mgr.GetLogger().WithValues("handler", "CollectorWebhook", "version", "v1beta1"), mgr.GetScheme(), cfg, reviewer, metrics, bv, fipsCheck) return ctrl.NewWebhookManagedBy(mgr). For(&OpenTelemetryCollector{}). WithValidator(cvw). diff --git a/apis/v1beta1/collector_webhook_test.go b/apis/v1beta1/collector_webhook_test.go index 64ffff48ea..9ce9cd3c90 100644 --- a/apis/v1beta1/collector_webhook_test.go +++ b/apis/v1beta1/collector_webhook_test.go @@ -113,6 +113,7 @@ func TestValidate(t *testing.T) { getReviewer(test.shouldFailSar), nil, bv, + nil, ) t.Run(tt.name, func(t *testing.T) { tt := tt @@ -494,6 +495,7 @@ func TestCollectorDefaultingWebhook(t *testing.T) { getReviewer(test.shouldFailSar), nil, bv, + nil, ) ctx := context.Background() err := cvw.Default(ctx, &test.otelcol) @@ -1285,6 +1287,7 @@ func TestOTELColValidatingWebhook(t *testing.T) { getReviewer(test.shouldFailSar), nil, bv, + nil, ) ctx := context.Background() warnings, err := cvw.ValidateCreate(ctx, &test.otelcol) @@ -1352,6 +1355,7 @@ func TestOTELColValidateUpdateWebhook(t *testing.T) { getReviewer(test.shouldFailSar), nil, bv, + nil, ) ctx := context.Background() warnings, err := cvw.ValidateUpdate(ctx, &test.otelcolOld, &test.otelcolNew) diff --git a/apis/v1beta1/config.go b/apis/v1beta1/config.go index b34601bf05..0eb9af57e9 100644 --- a/apis/v1beta1/config.go +++ b/apis/v1beta1/config.go @@ -112,6 +112,10 @@ func (c *Config) GetEnabledComponents() map[ComponentKind]map[string]interface{} KindExporter: {}, KindExtension: {}, } + for _, extension := range c.Service.Extensions { + toReturn[KindExtension][extension] = struct{}{} + } + for _, pipeline := range c.Service.Pipelines { if pipeline == nil { continue diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 55a3cf3446..4e56fb16de 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -102,6 +102,10 @@ type mockAutoDetect struct { RBACPermissionsFunc func(ctx context.Context) (autoRBAC.Availability, error) } +func (m *mockAutoDetect) FIPSEnabled(ctx context.Context) bool { + return false +} + func (m *mockAutoDetect) PrometheusCRsAvailability() (prometheus.Availability, error) { if m.PrometheusCRsAvailabilityFunc != nil { return m.PrometheusCRsAvailabilityFunc() @@ -178,7 +182,7 @@ func TestMain(m *testing.M) { } reviewer := rbac.NewReviewer(clientset) - if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer, nil, nil); err != nil { + if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer, nil, nil, nil); err != nil { fmt.Printf("failed to SetupWebhookWithManager: %v", err) os.Exit(1) } diff --git a/internal/autodetect/fips/fipsautodetect.go b/internal/autodetect/fips/fipsautodetect.go new file mode 100644 index 0000000000..2d54dd8305 --- /dev/null +++ b/internal/autodetect/fips/fipsautodetect.go @@ -0,0 +1,39 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fips + +import ( + "errors" + "os" + "strings" +) + +const fipsFile = "/proc/sys/crypto/fips_enabled" + +// IsFipsEnabled checks whether FIPS is enabled on the platform. +func IsFipsEnabled() bool { + // check if file exists + if _, err := os.Stat(fipsFile); errors.Is(err, os.ErrNotExist) { + return false + } + content, err := os.ReadFile(fipsFile) + if err != nil { + // file cannot be read, enable FIPS to avoid any violations + return true + } + contentStr := string(content) + contentStr = strings.TrimSpace(contentStr) + return contentStr == "1" +} diff --git a/internal/autodetect/main.go b/internal/autodetect/main.go index 8682a6c27d..27c368f3f5 100644 --- a/internal/autodetect/main.go +++ b/internal/autodetect/main.go @@ -22,6 +22,7 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/rest" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/fips" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus" autoRBAC "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac" @@ -35,6 +36,7 @@ type AutoDetect interface { OpenShiftRoutesAvailability() (openshift.RoutesAvailability, error) PrometheusCRsAvailability() (prometheus.Availability, error) RBACPermissions(ctx context.Context) (autoRBAC.Availability, error) + FIPSEnabled(ctx context.Context) bool } type autoDetect struct { @@ -122,3 +124,7 @@ func (a *autoDetect) RBACPermissions(ctx context.Context) (autoRBAC.Availability return autoRBAC.Available, nil } + +func (a *autoDetect) FIPSEnabled(_ context.Context) bool { + return fips.IsFipsEnabled() +} diff --git a/internal/config/main_test.go b/internal/config/main_test.go index 1f3886f776..08882a0392 100644 --- a/internal/config/main_test.go +++ b/internal/config/main_test.go @@ -82,6 +82,10 @@ type mockAutoDetect struct { RBACPermissionsFunc func(ctx context.Context) (rbac.Availability, error) } +func (m *mockAutoDetect) FIPSEnabled(_ context.Context) bool { + return false +} + func (m *mockAutoDetect) OpenShiftRoutesAvailability() (openshift.RoutesAvailability, error) { if m.OpenShiftRoutesAvailabilityFunc != nil { return m.OpenShiftRoutesAvailabilityFunc() diff --git a/internal/fips/fipscheck.go b/internal/fips/fipscheck.go new file mode 100644 index 0000000000..499df12fcc --- /dev/null +++ b/internal/fips/fipscheck.go @@ -0,0 +1,77 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fips + +import ( + "strings" +) + +type FIPSCheck interface { + // DisabledComponents checks if a submitted components are denied or not. + DisabledComponents(receivers map[string]interface{}, exporters map[string]interface{}, processors map[string]interface{}, extensions map[string]interface{}) []string +} + +// FipsCheck holds configuration for FIPS deny list. +type fipsCheck struct { + receivers map[string]bool + exporters map[string]bool + processors map[string]bool + extensions map[string]bool +} + +// NewFipsCheck creates new FipsCheck. +func NewFipsCheck(receivers, exporters, processors, extensions []string) FIPSCheck { + return &fipsCheck{ + receivers: listToMap(receivers), + exporters: listToMap(exporters), + processors: listToMap(processors), + extensions: listToMap(extensions), + } +} + +func listToMap(list []string) map[string]bool { + m := map[string]bool{} + for _, v := range list { + m[v] = true + } + return m +} + +func (fips fipsCheck) DisabledComponents(receivers map[string]interface{}, exporters map[string]interface{}, processors map[string]interface{}, extensions map[string]interface{}) []string { + var disabled []string + if comp := isDisabled(fips.receivers, receivers); comp != "" { + disabled = append(disabled, comp) + } + if comp := isDisabled(fips.exporters, exporters); comp != "" { + disabled = append(disabled, comp) + } + if comp := isDisabled(fips.processors, processors); comp != "" { + disabled = append(disabled, comp) + } + if comp := isDisabled(fips.extensions, extensions); comp != "" { + disabled = append(disabled, comp) + } + return disabled +} + +func isDisabled(denyList map[string]bool, cfg map[string]interface{}) string { + for id := range cfg { + component := strings.Split(id, "/")[0] + if denyList[component] { + return component + } + } + return "" +} diff --git a/internal/fips/fipscheck_test.go b/internal/fips/fipscheck_test.go new file mode 100644 index 0000000000..c52d7b1e3d --- /dev/null +++ b/internal/fips/fipscheck_test.go @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fips + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFipsCheck(t *testing.T) { + fipsCheck := NewFipsCheck([]string{"rec1", "rec2"}, []string{"exp1"}, []string{"processor"}, []string{"ext1"}) + blocked := fipsCheck.DisabledComponents( + map[string]interface{}{"otlp": true, "rec1/my": true}, + map[string]interface{}{"exp1": true}, + map[string]interface{}{"processor": true}, + map[string]interface{}{"ext1": true}) + + assert.Equal(t, []string{"rec1", "exp1", "processor", "ext1"}, blocked) +} diff --git a/internal/webhook/podmutation/webhookhandler_suite_test.go b/internal/webhook/podmutation/webhookhandler_suite_test.go index 8448762f5d..7490a57579 100644 --- a/internal/webhook/podmutation/webhookhandler_suite_test.go +++ b/internal/webhook/podmutation/webhookhandler_suite_test.go @@ -105,7 +105,7 @@ func TestMain(m *testing.M) { } reviewer := rbac.NewReviewer(clientset) - if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer, nil, nil); err != nil { + if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer, nil, nil, nil); err != nil { fmt.Printf("failed to SetupWebhookWithManager: %v", err) os.Exit(1) } diff --git a/main.go b/main.go index 55c754f2fb..1d3471898f 100644 --- a/main.go +++ b/main.go @@ -53,6 +53,7 @@ import ( "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus" "github.com/open-telemetry/opentelemetry-operator/internal/config" + "github.com/open-telemetry/opentelemetry-operator/internal/fips" collectorManifests "github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector" openshiftDashboards "github.com/open-telemetry/opentelemetry-operator/internal/openshift/dashboards" "github.com/open-telemetry/opentelemetry-operator/internal/rbac" @@ -141,6 +142,7 @@ func main() { encodeLevelKey string encodeTimeKey string encodeLevelFormat string + fipsDisabledComponents string ) pflag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") @@ -180,6 +182,7 @@ func main() { pflag.StringVar(&encodeLevelKey, "zap-level-key", "level", "The level key to be used in the customized Log Encoder") pflag.StringVar(&encodeTimeKey, "zap-time-key", "timestamp", "The time key to be used in the customized Log Encoder") pflag.StringVar(&encodeLevelFormat, "zap-level-format", "uppercase", "The level format to be used in the customized Log Encoder") + pflag.StringVar(&fipsDisabledComponents, "fips-disabled-components", "uppercase", "Disabled collector components when operator runs on FIPS enabled platform. Example flag value =receiver.foo,receiver.bar,exporter.baz") pflag.IntVar(&webhookPort, "webhook-port", 9443, "The port the webhook endpoint binds to.") pflag.Parse() @@ -438,7 +441,13 @@ func main() { return warnings } - if err = otelv1beta1.SetupCollectorWebhook(mgr, cfg, reviewer, crdMetrics, bv); err != nil { + var fipsCheck fips.FIPSCheck + if ad.FIPSEnabled(ctx) { + receivers, exporters, processors, extensions := parseFipsFlag(fipsDisabledComponents) + logger.Info("Fips disabled components", "receivers", receivers, "exporters", exporters, "processors", processors, "extensions", extensions) + fipsCheck = fips.NewFipsCheck(receivers, exporters, processors, extensions) + } + if err = otelv1beta1.SetupCollectorWebhook(mgr, cfg, reviewer, crdMetrics, bv, fipsCheck); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "OpenTelemetryCollector") os.Exit(1) } @@ -535,3 +544,31 @@ func tlsConfigSetting(cfg *tls.Config, tlsOpt tlsConfig) { } cfg.CipherSuites = cipherSuiteIDs } + +func parseFipsFlag(fipsFlag string) ([]string, []string, []string, []string) { + split := strings.Split(fipsFlag, ",") + var receivers []string + var exporters []string + var processors []string + var extensions []string + for _, val := range split { + val = strings.TrimSpace(val) + typeAndName := strings.Split(val, ".") + if len(typeAndName) == 2 { + componentType := typeAndName[0] + name := typeAndName[1] + + switch componentType { + case "receiver": + receivers = append(receivers, name) + case "exporter": + exporters = append(exporters, name) + case "processor": + processors = append(processors, name) + case "extension": + extensions = append(extensions, name) + } + } + } + return receivers, exporters, processors, extensions +} diff --git a/pkg/collector/upgrade/suite_test.go b/pkg/collector/upgrade/suite_test.go index 89a56d8b40..c5e5cdbd23 100644 --- a/pkg/collector/upgrade/suite_test.go +++ b/pkg/collector/upgrade/suite_test.go @@ -105,7 +105,7 @@ func TestMain(m *testing.M) { } reviewer := rbac.NewReviewer(clientset) - if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer, nil, nil); err != nil { + if err = v1beta1.SetupCollectorWebhook(mgr, config.New(), reviewer, nil, nil, nil); err != nil { fmt.Printf("failed to SetupWebhookWithManager: %v", err) os.Exit(1) }