From cb173523f4dca67db4d3c11de5cb80330643eebb Mon Sep 17 00:00:00 2001 From: Aleksey Savaitan Date: Wed, 4 Sep 2024 14:42:22 +0300 Subject: [PATCH] [#13] support tls over grpc for otlp_grpc exporter type Signed-off-by: Aleksey Savaitan --- testdata/testdata.go | 25 ++++ testdata/tracing/invalid_empty_root_ca.pem | 0 testdata/tracing/invalid_root_ca.pem | 1 + ...valid_google_globalsign_r4_rsa_root_ca.pem | 12 ++ .../valid_google_gts_r4_ecdsa_root_ca.pem | 13 ++ tracing/config.go | 11 +- tracing/config_test.go | 36 ++++++ tracing/setup.go | 13 +- tracing/setup_test.go | 119 ++++++++++++++++++ 9 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 testdata/testdata.go create mode 100644 testdata/tracing/invalid_empty_root_ca.pem create mode 100644 testdata/tracing/invalid_root_ca.pem create mode 100644 testdata/tracing/valid_google_globalsign_r4_rsa_root_ca.pem create mode 100644 testdata/tracing/valid_google_gts_r4_ecdsa_root_ca.pem create mode 100644 tracing/setup_test.go diff --git a/testdata/testdata.go b/testdata/testdata.go new file mode 100644 index 0000000..29d0909 --- /dev/null +++ b/testdata/testdata.go @@ -0,0 +1,25 @@ +package testdata + +import ( + "path/filepath" + "runtime" +) + +// basepath is the root directory of this package. +var basepath string + +func init() { + _, currentFile, _, _ := runtime.Caller(0) + basepath = filepath.Dir(currentFile) +} + +// Path returns the absolute path the given relative file or directory path, +// relative to the frostfs-observability/testdata directory in the user's GOPATH. +// If rel is already absolute, it is returned unmodified. +func Path(rel string) string { + if filepath.IsAbs(rel) { + return rel + } + + return filepath.Join(basepath, rel) +} diff --git a/testdata/tracing/invalid_empty_root_ca.pem b/testdata/tracing/invalid_empty_root_ca.pem new file mode 100644 index 0000000..e69de29 diff --git a/testdata/tracing/invalid_root_ca.pem b/testdata/tracing/invalid_root_ca.pem new file mode 100644 index 0000000..2aa3af9 --- /dev/null +++ b/testdata/tracing/invalid_root_ca.pem @@ -0,0 +1 @@ +invalid content \ No newline at end of file diff --git a/testdata/tracing/valid_google_globalsign_r4_rsa_root_ca.pem b/testdata/tracing/valid_google_globalsign_r4_rsa_root_ca.pem new file mode 100644 index 0000000..7670528 --- /dev/null +++ b/testdata/tracing/valid_google_globalsign_r4_rsa_root_ca.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- diff --git a/testdata/tracing/valid_google_gts_r4_ecdsa_root_ca.pem b/testdata/tracing/valid_google_gts_r4_ecdsa_root_ca.pem new file mode 100644 index 0000000..fdb2d1b --- /dev/null +++ b/testdata/tracing/valid_google_gts_r4_ecdsa_root_ca.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- diff --git a/tracing/config.go b/tracing/config.go index 8bb5db5..2cb3dfb 100644 --- a/tracing/config.go +++ b/tracing/config.go @@ -1,6 +1,9 @@ package tracing -import "fmt" +import ( + "crypto/x509" + "fmt" +) // Exporter is type of tracing target. type Exporter string @@ -18,6 +21,8 @@ type Config struct { Exporter Exporter // Endpoint is collector endpoint for OTLP exporters. Endpoint string + // ServerCaCertPool is cert pool of to remote server CA certificate. Use for TLS setup. + ServerCaCertPool *x509.CertPool // Service is service name that will be used in tracing. // Mandatory. @@ -64,6 +69,10 @@ func (c *Config) hasChange(other *Config) bool { return !c.serviceInfoEqual(other) } + if other.Exporter == OTLPgRPCExporter && !c.ServerCaCertPool.Equal(other.ServerCaCertPool) { + return true + } + return c.Exporter != other.Exporter || c.Endpoint != other.Endpoint || !c.serviceInfoEqual(other) diff --git a/tracing/config_test.go b/tracing/config_test.go index 2cbd8e0..5046f50 100644 --- a/tracing/config_test.go +++ b/tracing/config_test.go @@ -1,7 +1,12 @@ package tracing import ( + "crypto/x509" + "os" "testing" + + "git.frostfs.info/TrueCloudLab/frostfs-observability/testdata" + "github.com/stretchr/testify/require" ) func TestConfig_validate(t *testing.T) { @@ -232,6 +237,28 @@ func TestConfig_hasChange(t *testing.T) { Version: "v1.0.0", }, }, + { + name: "use tls root ca certificate for grpc", + want: true, + config: Config{ + Enabled: true, + Exporter: OTLPgRPCExporter, + Endpoint: "localhost:4717", + Service: "test", + InstanceID: "s01", + Version: "v1.0.0", + ServerCaCertPool: readCertPoolByPath(t, "tracing/valid_google_globalsign_r4_rsa_root_ca.pem"), + }, + other: Config{ + Enabled: true, + Exporter: OTLPgRPCExporter, + Endpoint: "localhost:4717", + Service: "test", + InstanceID: "s01", + Version: "v1.0.0", + ServerCaCertPool: readCertPoolByPath(t, "tracing/valid_google_gts_r4_ecdsa_root_ca.pem"), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -241,3 +268,12 @@ func TestConfig_hasChange(t *testing.T) { }) } } + +func readCertPoolByPath(t *testing.T, path string) *x509.CertPool { + ca, err := os.ReadFile(testdata.Path(path)) + require.NoError(t, err) + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(ca) + require.True(t, ok) + return roots +} diff --git a/tracing/setup.go b/tracing/setup.go index 2fca75b..ae969e3 100644 --- a/tracing/setup.go +++ b/tracing/setup.go @@ -2,6 +2,7 @@ package tracing import ( "context" + "crypto/x509" "fmt" "sync" "sync/atomic" @@ -15,8 +16,11 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.17.0" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" + "google.golang.org/grpc/credentials" ) +var ErrEmptyServerRootCaPool = fmt.Errorf("empty server root ca cert pool") + var ( // tracingLock protects provider, done, config and tracer from concurrent update. // These fields change when the config is updated or the application is shutdown. @@ -135,7 +139,14 @@ func getExporter(ctx context.Context, cfg *Config) (sdktrace.SpanExporter, error case NoOpExporter: return tracetest.NewNoopExporter(), nil case OTLPgRPCExporter: - return otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(cfg.Endpoint), otlptracegrpc.WithInsecure()) + securityOption := otlptracegrpc.WithInsecure() + if cfg.ServerCaCertPool != nil { + if cfg.ServerCaCertPool.Equal(x509.NewCertPool()) { + return nil, fmt.Errorf("failed to setup tracing: %w", ErrEmptyServerRootCaPool) + } + securityOption = otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(cfg.ServerCaCertPool, "")) + } + return otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(cfg.Endpoint), securityOption) } } diff --git a/tracing/setup_test.go b/tracing/setup_test.go new file mode 100644 index 0000000..4b0488b --- /dev/null +++ b/tracing/setup_test.go @@ -0,0 +1,119 @@ +package tracing_test + +import ( + "context" + "crypto/x509" + "os" + "testing" + + "git.frostfs.info/TrueCloudLab/frostfs-observability/testdata" + "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing" + "github.com/stretchr/testify/require" +) + +func TestSetup(t *testing.T) { + tests := []struct { + name string + config tracing.Config + want bool + expErr error + }{ + { + name: "setup stdout exporter", + config: tracing.Config{ + Enabled: true, + Exporter: tracing.StdoutExporter, + Service: "service-name", + }, + want: true, + expErr: nil, + }, + { + name: "setup noop exporter", + config: tracing.Config{ + Enabled: true, + Exporter: tracing.NoOpExporter, + Service: "service-name", + }, + + want: true, + expErr: nil, + }, + { + name: "setup otlp_grpc insecure exporter", + config: tracing.Config{ + Enabled: true, + Exporter: tracing.OTLPgRPCExporter, + Service: "service-name", + Endpoint: "test-endpoint.com:4317", + }, + want: true, + expErr: nil, + }, + { + name: "setup otlp_grpc secure exporter with valid rsa root ca certificate", + config: tracing.Config{ + Enabled: true, + Exporter: tracing.OTLPgRPCExporter, + Service: "service-name", + Endpoint: "test-endpoint.com:4317", + ServerCaCertPool: readCertPoolByPath(t, "tracing/valid_google_globalsign_r4_rsa_root_ca.pem"), + }, + want: true, + expErr: nil, + }, + { + name: "setup otlp_grpc secure exporter with valid ecdsa root ca certificate", + config: tracing.Config{ + Enabled: true, + Exporter: tracing.OTLPgRPCExporter, + Service: "service-name", + Endpoint: "test-endpoint.com:4317", + ServerCaCertPool: readCertPoolByPath(t, "tracing/valid_google_gts_r4_ecdsa_root_ca.pem"), + }, + want: true, + expErr: nil, + }, + { + name: "setup otlp_grpc secure exporter with invalid root ca certificate", + config: tracing.Config{ + Enabled: true, + Exporter: tracing.OTLPgRPCExporter, + Service: "service-name", + Endpoint: "test-endpoint.com:4317", + ServerCaCertPool: readCertPoolByPath(t, "tracing/invalid_root_ca.pem"), + }, + want: false, + expErr: tracing.ErrEmptyServerRootCaPool, + }, + { + name: "setup otlp_grpc secure exporter with empty root ca certificate", + config: tracing.Config{ + Enabled: true, + Exporter: tracing.OTLPgRPCExporter, + Service: "service-name", + Endpoint: "test-endpoint.com:4317", + ServerCaCertPool: readCertPoolByPath(t, "tracing/invalid_empty_root_ca.pem"), + }, + want: false, + expErr: tracing.ErrEmptyServerRootCaPool, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tracing.Setup(context.Background(), tt.config) + require.ErrorIs(t, err, tt.expErr) + if got != tt.want { + t.Errorf("Setup config = %v, want %v", got, tt.want) + } + }) + } +} + +func readCertPoolByPath(t *testing.T, path string) *x509.CertPool { + ca, err := os.ReadFile(testdata.Path(path)) + require.NoError(t, err) + roots := x509.NewCertPool() + _ = roots.AppendCertsFromPEM(ca) + return roots +}