From 741c4724b135a0c69530e1ad38600d0523cff729 Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:48:36 -0800 Subject: [PATCH] config: add support for certificate configuration (#6376) Part of https://github.com/open-telemetry/opentelemetry-go-contrib/issues/6351 --------- Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> --- .golangci.yml | 6 ++++ CHANGELOG.md | 1 + config/go.mod | 2 +- config/testdata/bad_cert.crt | 1 + config/testdata/ca.crt | 20 +++++++++++ config/v0.3.0/config.go | 20 +++++++++++ config/v0.3.0/log.go | 18 ++++++++++ config/v0.3.0/log_test.go | 70 ++++++++++++++++++++++++++++++++++++ config/v0.3.0/metric.go | 17 +++++++++ config/v0.3.0/metric_test.go | 69 +++++++++++++++++++++++++++++++++++ config/v0.3.0/trace.go | 18 ++++++++++ config/v0.3.0/trace_test.go | 70 ++++++++++++++++++++++++++++++++++++ 12 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 config/testdata/bad_cert.crt create mode 100644 config/testdata/ca.crt diff --git a/.golangci.yml b/.golangci.yml index a86971538da..a1edee29a6c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -70,6 +70,12 @@ issues: - gosec - perfsprint - usestdlibvars + # Ignoring gosec G402: TLS MinVersion too low + # as the https://pkg.go.dev/crypto/tls#Config handles MinVersion default well. + - path: config/*.go + text: "G402: TLS MinVersion too low." + linters: + - gosec include: # revive exported should have comment or be unexported. - EXC0012 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d6b0ec8fc3..a6d3c0e83ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm This module provides an OpenTelemetry logging bridge for `github.com/go-logr/logr`. (#6386) - Added SNS instrumentation in `go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws`. (#6388) - Use a `sync.Pool` for metric options in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`. (#6394) +- Added support for configuring `Certificate` field when configuring OTLP exporters in `go.opentelemetry.io/contrib/config`. (#6376) ### Changed diff --git a/config/go.mod b/config/go.mod index 1412a25e94c..f9b705ec6f1 100644 --- a/config/go.mod +++ b/config/go.mod @@ -22,6 +22,7 @@ require ( go.opentelemetry.io/otel/sdk/log v0.8.0 go.opentelemetry.io/otel/sdk/metric v1.32.0 go.opentelemetry.io/otel/trace v1.32.0 + google.golang.org/grpc v1.68.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -47,6 +48,5 @@ require ( golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/grpc v1.68.1 // indirect google.golang.org/protobuf v1.35.2 // indirect ) diff --git a/config/testdata/bad_cert.crt b/config/testdata/bad_cert.crt new file mode 100644 index 00000000000..dbbd2b8576f --- /dev/null +++ b/config/testdata/bad_cert.crt @@ -0,0 +1 @@ +This is intentionally not a PEM formatted cert file. diff --git a/config/testdata/ca.crt b/config/testdata/ca.crt new file mode 100644 index 00000000000..2272f84e64d --- /dev/null +++ b/config/testdata/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNjCCAh4CCQC0I5IQT7eziDANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJB +VTESMBAGA1UECAwJQXVzdHJhbGlhMQ8wDQYDVQQHDAZTeWRuZXkxEjAQBgNVBAoM +CU15T3JnTmFtZTEVMBMGA1UEAwwMTXlDb21tb25OYW1lMB4XDTIyMDgwMzA0MTky +MVoXDTMyMDczMTA0MTkyMVowXTELMAkGA1UEBhMCQVUxEjAQBgNVBAgMCUF1c3Ry +YWxpYTEPMA0GA1UEBwwGU3lkbmV5MRIwEAYDVQQKDAlNeU9yZ05hbWUxFTATBgNV +BAMMDE15Q29tbW9uTmFtZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AMhGP0dy3zvkdx9zI+/XVjPOWlER0OUp7Sgzidc3nLOk42+bH4ofIVNtOFVqlNKi +O1bImu238VdBhd6R5IZZ1ZdIMcCeDgSJYu2X9wA3m4PKz8IdXo5ly2OHghhmCvqG +WxgqDj5wPXiczQwuf1EcDMtRWbXJ6Z/XH1U68R/kRdNLkiZ2LwtjoQpis5XYckLL +CrdF+AL6GeDIe0Mh9QGs26Vux+2kvaOGNUWRPE6Wt4GkqyKqmzYfR9HbflJ4xHT2 +I+jE1lg+jMBeom7z8Z90RE4GGcHjO+Vens/88r5EAjTnFj1Kb5gL2deSHY1m/++R +Z/kRyg+zQJyw4fAzlAA4+VkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAM3gRdTKX +eGwGYVmmKqA2vTxeigQYLHml7OSopcWj2wJfxfp49HXPRuvgpQn9iubxO3Zmhd83 +2X1E+T0A8oy5CfxgpAhHb3lY0jm3TjKXm6m+dSODwL3uND8tX+SqR8sRTFxPvPuo +pmvhdTZoRI3EzIiHLTgCuSU25JNP/vrVoKk0JvCkDYTU/WcVfj0v95DTMoWR4JGz +mtBwrgD0EM2XRw5ZMc7sMPli1gqmCbCQUrDZ+rPB78WDCBILBd8Cz75qYTUp98BY +akJyBckdJHAdyEQYDKa9HpmpexOO7IhSXCTEN1DEBgpZgEi/lBDRG/b0OzenUUgt +LUABtWt3pNQ9HA== +-----END CERTIFICATE----- diff --git a/config/v0.3.0/config.go b/config/v0.3.0/config.go index 44030d4f612..b54c60479c2 100644 --- a/config/v0.3.0/config.go +++ b/config/v0.3.0/config.go @@ -5,7 +5,10 @@ package config // import "go.opentelemetry.io/contrib/config/v0.3.0" import ( "context" + "crypto/tls" + "crypto/x509" "errors" + "os" "gopkg.in/yaml.v3" @@ -155,3 +158,20 @@ func toStringMap(pairs []NameStringValuePair) map[string]string { } return output } + +// createTLSConfig creates a tls.Config from a raw certificate bytes +// to verify a server certificate. +func createTLSConfig(certFile string) (*tls.Config, error) { + b, err := os.ReadFile(certFile) + if err != nil { + return nil, err + } + cp := x509.NewCertPool() + if ok := cp.AppendCertsFromPEM(b); !ok { + return nil, errors.New("failed to append certificate to the cert pool") + } + + return &tls.Config{ + RootCAs: cp, + }, nil +} diff --git a/config/v0.3.0/log.go b/config/v0.3.0/log.go index 40ad54dbfd1..42b1b209207 100644 --- a/config/v0.3.0/log.go +++ b/config/v0.3.0/log.go @@ -10,6 +10,8 @@ import ( "net/url" "time" + "google.golang.org/grpc/credentials" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" @@ -154,6 +156,14 @@ func otlpHTTPLogExporter(ctx context.Context, otlpConfig *OTLP) (sdklog.Exporter opts = append(opts, otlploghttp.WithHeaders(toStringMap(otlpConfig.Headers))) } + if otlpConfig.Certificate != nil { + creds, err := createTLSConfig(*otlpConfig.Certificate) + if err != nil { + return nil, fmt.Errorf("could not create client tls credentials: %w", err) + } + opts = append(opts, otlploghttp.WithTLSClientConfig(creds)) + } + return otlploghttp.New(ctx, opts...) } @@ -196,5 +206,13 @@ func otlpGRPCLogExporter(ctx context.Context, otlpConfig *OTLP) (sdklog.Exporter opts = append(opts, otlploggrpc.WithHeaders(toStringMap(otlpConfig.Headers))) } + if otlpConfig.Certificate != nil { + creds, err := credentials.NewClientTLSFromFile(*otlpConfig.Certificate, "") + if err != nil { + return nil, fmt.Errorf("could not create client tls credentials: %w", err) + } + opts = append(opts, otlploggrpc.WithTLSCredentials(creds)) + } + return otlploggrpc.New(ctx, opts...) } diff --git a/config/v0.3.0/log_test.go b/config/v0.3.0/log_test.go index f04d521eb50..7d96fee647d 100644 --- a/config/v0.3.0/log_test.go +++ b/config/v0.3.0/log_test.go @@ -6,7 +6,9 @@ package config import ( "context" "errors" + "fmt" "net/url" + "path/filepath" "reflect" "testing" @@ -221,6 +223,40 @@ func TestLogProcessor(t *testing.T) { }, wantProcessor: sdklog.NewBatchProcessor(otlpGRPCExporter), }, + { + name: "batch/otlp-grpc-good-ca-certificate", + processor: LogRecordProcessor{ + Batch: &BatchLogRecordProcessor{ + Exporter: LogRecordExporter{ + OTLP: &OTLP{ + Protocol: ptr("grpc"), + Endpoint: ptr("localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "ca.crt")), + }, + }, + }, + }, + wantProcessor: sdklog.NewBatchProcessor(otlpGRPCExporter), + }, + { + name: "batch/otlp-grpc-bad-ca-certificate", + processor: LogRecordProcessor{ + Batch: &BatchLogRecordProcessor{ + Exporter: LogRecordExporter{ + OTLP: &OTLP{ + Protocol: ptr("grpc"), + Endpoint: ptr("localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "bad_cert.crt")), + }, + }, + }, + }, + wantErr: fmt.Errorf("could not create client tls credentials: %w", errors.New("credentials: failed to append certificates")), + }, { name: "batch/otlp-grpc-exporter-no-scheme", processor: LogRecordProcessor{ @@ -313,6 +349,40 @@ func TestLogProcessor(t *testing.T) { }, wantProcessor: sdklog.NewBatchProcessor(otlpHTTPExporter), }, + { + name: "batch/otlp-http-good-ca-certificate", + processor: LogRecordProcessor{ + Batch: &BatchLogRecordProcessor{ + Exporter: LogRecordExporter{ + OTLP: &OTLP{ + Protocol: ptr("http/protobuf"), + Endpoint: ptr("localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "ca.crt")), + }, + }, + }, + }, + wantProcessor: sdklog.NewBatchProcessor(otlpHTTPExporter), + }, + { + name: "batch/otlp-http-bad-ca-certificate", + processor: LogRecordProcessor{ + Batch: &BatchLogRecordProcessor{ + Exporter: LogRecordExporter{ + OTLP: &OTLP{ + Protocol: ptr("http/protobuf"), + Endpoint: ptr("localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "bad_cert.crt")), + }, + }, + }, + }, + wantErr: fmt.Errorf("could not create client tls credentials: %w", errors.New("failed to append certificate to the cert pool")), + }, { name: "batch/otlp-http-exporter-with-path", processor: LogRecordProcessor{ diff --git a/config/v0.3.0/metric.go b/config/v0.3.0/metric.go index c551a5b91fe..e88c02349c9 100644 --- a/config/v0.3.0/metric.go +++ b/config/v0.3.0/metric.go @@ -17,6 +17,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "google.golang.org/grpc/credentials" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -181,6 +182,14 @@ func otlpHTTPMetricExporter(ctx context.Context, otlpConfig *OTLPMetric) (sdkmet } } + if otlpConfig.Certificate != nil { + creds, err := createTLSConfig(*otlpConfig.Certificate) + if err != nil { + return nil, fmt.Errorf("could not create client tls credentials: %w", err) + } + opts = append(opts, otlpmetrichttp.WithTLSClientConfig(creds)) + } + return otlpmetrichttp.New(ctx, opts...) } @@ -236,6 +245,14 @@ func otlpGRPCMetricExporter(ctx context.Context, otlpConfig *OTLPMetric) (sdkmet } } + if otlpConfig.Certificate != nil { + creds, err := credentials.NewClientTLSFromFile(*otlpConfig.Certificate, "") + if err != nil { + return nil, fmt.Errorf("could not create client tls credentials: %w", err) + } + opts = append(opts, otlpmetricgrpc.WithTLSCredentials(creds)) + } + return otlpmetricgrpc.New(ctx, opts...) } diff --git a/config/v0.3.0/metric_test.go b/config/v0.3.0/metric_test.go index b6c6d095fa8..aa01665831f 100644 --- a/config/v0.3.0/metric_test.go +++ b/config/v0.3.0/metric_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/url" + "path/filepath" "reflect" "testing" "time" @@ -215,6 +216,40 @@ func TestReader(t *testing.T) { }, wantReader: sdkmetric.NewPeriodicReader(otlpGRPCExporter), }, + { + name: "periodic/otlp-grpc-good-ca-certificate", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: PushMetricExporter{ + OTLP: &OTLPMetric{ + Protocol: ptr("grpc"), + Endpoint: ptr("https://localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "ca.crt")), + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpGRPCExporter), + }, + { + name: "periodic/otlp-grpc-bad-ca-certificate", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: PushMetricExporter{ + OTLP: &OTLPMetric{ + Protocol: ptr("grpc"), + Endpoint: ptr("https://localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "bad_cert.crt")), + }, + }, + }, + }, + wantErr: fmt.Errorf("could not create client tls credentials: %w", errors.New("credentials: failed to append certificates")), + }, { name: "periodic/otlp-grpc-exporter-no-endpoint", reader: MetricReader{ @@ -408,6 +443,40 @@ func TestReader(t *testing.T) { }, wantReader: sdkmetric.NewPeriodicReader(otlpHTTPExporter), }, + { + name: "periodic/otlp-http-good-ca-certificate", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: PushMetricExporter{ + OTLP: &OTLPMetric{ + Protocol: ptr("http/protobuf"), + Endpoint: ptr("https://localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "ca.crt")), + }, + }, + }, + }, + wantReader: sdkmetric.NewPeriodicReader(otlpHTTPExporter), + }, + { + name: "periodic/otlp-http-bad-ca-certificate", + reader: MetricReader{ + Periodic: &PeriodicMetricReader{ + Exporter: PushMetricExporter{ + OTLP: &OTLPMetric{ + Protocol: ptr("http/protobuf"), + Endpoint: ptr("https://localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "bad_cert.crt")), + }, + }, + }, + }, + wantErr: fmt.Errorf("could not create client tls credentials: %w", errors.New("failed to append certificate to the cert pool")), + }, { name: "periodic/otlp-http-exporter-with-path", reader: MetricReader{ diff --git a/config/v0.3.0/trace.go b/config/v0.3.0/trace.go index f1e6552cde3..10d2473912f 100644 --- a/config/v0.3.0/trace.go +++ b/config/v0.3.0/trace.go @@ -10,6 +10,8 @@ import ( "net/url" "time" + "google.golang.org/grpc/credentials" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" @@ -127,6 +129,14 @@ func otlpGRPCSpanExporter(ctx context.Context, otlpConfig *OTLP) (sdktrace.SpanE opts = append(opts, otlptracegrpc.WithHeaders(toStringMap(otlpConfig.Headers))) } + if otlpConfig.Certificate != nil { + creds, err := credentials.NewClientTLSFromFile(*otlpConfig.Certificate, "") + if err != nil { + return nil, fmt.Errorf("could not create client tls credentials: %w", err) + } + opts = append(opts, otlptracegrpc.WithTLSCredentials(creds)) + } + return otlptracegrpc.New(ctx, opts...) } @@ -164,6 +174,14 @@ func otlpHTTPSpanExporter(ctx context.Context, otlpConfig *OTLP) (sdktrace.SpanE opts = append(opts, otlptracehttp.WithHeaders(toStringMap(otlpConfig.Headers))) } + if otlpConfig.Certificate != nil { + creds, err := createTLSConfig(*otlpConfig.Certificate) + if err != nil { + return nil, fmt.Errorf("could not create client tls credentials: %w", err) + } + opts = append(opts, otlptracehttp.WithTLSClientConfig(creds)) + } + return otlptracehttp.New(ctx, opts...) } diff --git a/config/v0.3.0/trace_test.go b/config/v0.3.0/trace_test.go index fb6e9b14cdc..8e0ef27a479 100644 --- a/config/v0.3.0/trace_test.go +++ b/config/v0.3.0/trace_test.go @@ -6,7 +6,9 @@ package config import ( "context" "errors" + "fmt" "net/url" + "path/filepath" "reflect" "testing" @@ -261,6 +263,40 @@ func TestSpanProcessor(t *testing.T) { }, wantProcessor: sdktrace.NewBatchSpanProcessor(otlpGRPCExporter), }, + { + name: "batch/otlp-grpc-good-ca-certificate", + processor: SpanProcessor{ + Batch: &BatchSpanProcessor{ + Exporter: SpanExporter{ + OTLP: &OTLP{ + Protocol: ptr("grpc"), + Endpoint: ptr("localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "ca.crt")), + }, + }, + }, + }, + wantProcessor: sdktrace.NewBatchSpanProcessor(otlpGRPCExporter), + }, + { + name: "batch/otlp-grpc-bad-ca-certificate", + processor: SpanProcessor{ + Batch: &BatchSpanProcessor{ + Exporter: SpanExporter{ + OTLP: &OTLP{ + Protocol: ptr("grpc"), + Endpoint: ptr("localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "bad_cert.crt")), + }, + }, + }, + }, + wantErr: fmt.Errorf("could not create client tls credentials: %w", errors.New("credentials: failed to append certificates")), + }, { name: "batch/otlp-grpc-exporter-no-scheme", processor: SpanProcessor{ @@ -353,6 +389,40 @@ func TestSpanProcessor(t *testing.T) { }, wantProcessor: sdktrace.NewBatchSpanProcessor(otlpHTTPExporter), }, + { + name: "batch/otlp-http-good-ca-certificate", + processor: SpanProcessor{ + Batch: &BatchSpanProcessor{ + Exporter: SpanExporter{ + OTLP: &OTLP{ + Protocol: ptr("http/protobuf"), + Endpoint: ptr("localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "ca.crt")), + }, + }, + }, + }, + wantProcessor: sdktrace.NewBatchSpanProcessor(otlpHTTPExporter), + }, + { + name: "batch/otlp-http-bad-ca-certificate", + processor: SpanProcessor{ + Batch: &BatchSpanProcessor{ + Exporter: SpanExporter{ + OTLP: &OTLP{ + Protocol: ptr("http/protobuf"), + Endpoint: ptr("localhost:4317"), + Compression: ptr("gzip"), + Timeout: ptr(1000), + Certificate: ptr(filepath.Join("..", "testdata", "bad_cert.crt")), + }, + }, + }, + }, + wantErr: fmt.Errorf("could not create client tls credentials: %w", errors.New("failed to append certificate to the cert pool")), + }, { name: "batch/otlp-http-exporter-with-path", processor: SpanProcessor{