Skip to content

Commit

Permalink
[cmd/telemetrygen] Add TLS/mTLS support to telemetrygen (open-telemet…
Browse files Browse the repository at this point in the history
…ry#29681)

**Description:** 
Implementing robust security measures is essential for any tracing
ingestion service and infrastructure. This adds TLS/mTLS support to
telemetrygen traces so that it can be used to test the security
implementation. To illustrate the usage, a new example, `secure-tracing`
is added to examples collection.

**Link to tracking Issue:** <Issue number if applicable>

**Testing:** 
Tested with the `secure-tracing` example.

**Documentation:** 
- telemetrygen README has been updated with a link to secure-tracing. 
- secure-tracing README contains setup of a testing environment and test
script via telemetrygen
  • Loading branch information
Juliaj authored and cparkins committed Jan 10, 2024
1 parent c0058fd commit 2c0974f
Show file tree
Hide file tree
Showing 20 changed files with 592 additions and 24 deletions.
27 changes: 27 additions & 0 deletions .chloggen/juliaj_telemetrygen-tls-mtls-support.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# 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. filelogreceiver)
component: cmd/telemetrygen

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: This updates telemetrygen with TLS/mTLS options to test the security of telemetry ingestion services and infrastructure for secure communication. To illustrate the usage, a new example, secure-tracing is added to examples collection.

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [29681]

# (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:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ otel-from-lib:
.PHONY: build-examples
build-examples:
docker-compose -f examples/demo/docker-compose.yaml build
cd examples/secure-tracing/certs && $(MAKE) clean && $(MAKE) all && docker-compose -f ../docker-compose.yaml build
docker-compose -f exporter/splunkhecexporter/example/docker-compose.yml build

.PHONY: deb-rpm-package
Expand Down
2 changes: 2 additions & 0 deletions cmd/telemetrygen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Or, to generate a specific number of traces:
telemetrygen traces --otlp-insecure --traces 1
```

To send traces in secure connection, see [examples/secure-tracing](../../examples/secure-tracing/)

Check `telemetrygen traces --help` for all the options.

### Logs
Expand Down
20 changes: 20 additions & 0 deletions cmd/telemetrygen/internal/common/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ type Config struct {
Headers KeyValue
ResourceAttributes KeyValue
TelemetryAttributes KeyValue

// OTLP TLS configuration
CaFile string

// OTLP mTLS configuration
ClientAuth ClientAuth
}

type ClientAuth struct {
Enabled bool
ClientCertFile string
ClientKeyFile string
}

// Endpoint returns the appropriate endpoint URL based on the selected communication mode (gRPC or HTTP)
Expand Down Expand Up @@ -125,4 +137,12 @@ func (c *Config) CommonFlags(fs *pflag.FlagSet) {
c.TelemetryAttributes = make(map[string]string)
fs.Var(&c.TelemetryAttributes, "telemetry-attributes", "Custom telemetry attributes to use. The value is expected in the format \"key=\\\"value\\\"\". "+
"Flag may be repeated to set multiple attributes (e.g --telemetry-attributes \"key1=\\\"value1\\\"\" --telemetry-attributes \"key2=\\\"value2\\\"\")")

// TLS CA configuration
fs.StringVar(&c.CaFile, "ca-cert", "", "Trusted Certificate Authority to verify server certificate")

// mTLS configuration
fs.BoolVar(&c.ClientAuth.Enabled, "mtls", false, "Whether to require client authentication for mTLS")
fs.StringVar(&c.ClientAuth.ClientCertFile, "client-cert", "", "Client certificate file")
fs.StringVar(&c.ClientAuth.ClientKeyFile, "client-key", "", "Client private key file")
}
77 changes: 77 additions & 0 deletions cmd/telemetrygen/internal/common/tls_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package common

import (
"crypto/tls"
"crypto/x509"
"errors"
"os"

"google.golang.org/grpc/credentials"
)

// caPool loads CA certificate from a file and returns a CertPool.
// The certPool is used to set RootCAs in certificate verification.
func caPool(caFile string) (*x509.CertPool, error) {
pool := x509.NewCertPool()
if caFile != "" {
data, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
if !pool.AppendCertsFromPEM(data) {
return nil, errors.New("failed to add CA certificate to root CA pool")
}
}
return pool, nil
}

func GetTLSCredentialsForGRPCExporter(caFile string, cAuth ClientAuth) (credentials.TransportCredentials, error) {

pool, err := caPool(caFile)
if err != nil {
return nil, err
}

creds := credentials.NewTLS(&tls.Config{
RootCAs: pool,
})

// Configuration for mTLS
if cAuth.Enabled {
keypair, err := tls.LoadX509KeyPair(cAuth.ClientCertFile, cAuth.ClientKeyFile)
if err != nil {
return nil, err
}
creds = credentials.NewTLS(&tls.Config{
RootCAs: pool,
Certificates: []tls.Certificate{keypair},
})
}

return creds, nil
}

func GetTLSCredentialsForHTTPExporter(caFile string, cAuth ClientAuth) (*tls.Config, error) {
pool, err := caPool(caFile)
if err != nil {
return nil, err
}

tlsCfg := tls.Config{
RootCAs: pool,
}

// Configuration for mTLS
if cAuth.Enabled {
keypair, err := tls.LoadX509KeyPair(cAuth.ClientCertFile, cAuth.ClientKeyFile)
if err != nil {
return nil, err
}
tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert
tlsCfg.Certificates = []tls.Certificate{keypair}
}
return &tlsCfg, nil
}
40 changes: 32 additions & 8 deletions cmd/telemetrygen/internal/logs/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,51 @@ import (
"go.opentelemetry.io/collector/pdata/plog/plogotlp"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

"github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen/internal/common"
)

type exporter interface {
export(plog.Logs) error
}

func newExporter(ctx context.Context, cfg *Config) (exporter, error) {

// Exporter with HTTP
if cfg.UseHTTP {
if cfg.Insecure {
return &httpClientExporter{
client: http.DefaultClient,
cfg: cfg,
}, nil
}
creds, err := common.GetTLSCredentialsForHTTPExporter(cfg.CaFile, cfg.ClientAuth)
if err != nil {
return nil, fmt.Errorf("failed to get TLS credentials: %w", err)
}
return &httpClientExporter{
client: http.DefaultClient,
client: &http.Client{Transport: &http.Transport{TLSClientConfig: creds}},
cfg: cfg,
}, nil
}

if !cfg.Insecure {
return nil, fmt.Errorf("'telemetrygen logs' only supports insecure gRPC")
}
// only support grpc in insecure mode
clientConn, err := grpc.DialContext(ctx, cfg.Endpoint(), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
// Exporter with GRPC
var err error
var clientConn *grpc.ClientConn
if cfg.Insecure {
clientConn, err = grpc.DialContext(ctx, cfg.Endpoint(), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
} else {
creds, err := common.GetTLSCredentialsForGRPCExporter(cfg.CaFile, cfg.ClientAuth)
if err != nil {
return nil, fmt.Errorf("failed to get TLS credentials: %w", err)
}
clientConn, err = grpc.DialContext(ctx, cfg.Endpoint(), grpc.WithTransportCredentials(creds))
if err != nil {
return nil, err
}
}
return &gRPCClientExporter{client: plogotlp.NewGRPCClient(clientConn)}, nil
}
Expand Down
24 changes: 20 additions & 4 deletions cmd/telemetrygen/internal/metrics/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
package metrics

import (
"fmt"

"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"google.golang.org/grpc"

"github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen/internal/common"
)

// grpcExporterOptions creates the configuration options for a gRPC-based OTLP metric exporter.
// It configures the exporter with the provided endpoint, connection security settings, and headers.
func grpcExporterOptions(cfg *Config) []otlpmetricgrpc.Option {
func grpcExporterOptions(cfg *Config) ([]otlpmetricgrpc.Option, error) {
grpcExpOpt := []otlpmetricgrpc.Option{
otlpmetricgrpc.WithEndpoint(cfg.Endpoint()),
otlpmetricgrpc.WithDialOption(
Expand All @@ -21,30 +25,42 @@ func grpcExporterOptions(cfg *Config) []otlpmetricgrpc.Option {

if cfg.Insecure {
grpcExpOpt = append(grpcExpOpt, otlpmetricgrpc.WithInsecure())
} else {
credentials, err := common.GetTLSCredentialsForGRPCExporter(cfg.CaFile, cfg.ClientAuth)
if err != nil {
return nil, fmt.Errorf("failed to get TLS credentials: %w", err)
}
grpcExpOpt = append(grpcExpOpt, otlpmetricgrpc.WithTLSCredentials(credentials))
}

if len(cfg.Headers) > 0 {
grpcExpOpt = append(grpcExpOpt, otlpmetricgrpc.WithHeaders(cfg.Headers))
}

return grpcExpOpt
return grpcExpOpt, nil
}

// httpExporterOptions creates the configuration options for an HTTP-based OTLP metric exporter.
// It configures the exporter with the provided endpoint, URL path, connection security settings, and headers.
func httpExporterOptions(cfg *Config) []otlpmetrichttp.Option {
func httpExporterOptions(cfg *Config) ([]otlpmetrichttp.Option, error) {
httpExpOpt := []otlpmetrichttp.Option{
otlpmetrichttp.WithEndpoint(cfg.Endpoint()),
otlpmetrichttp.WithURLPath(cfg.HTTPPath),
}

if cfg.Insecure {
httpExpOpt = append(httpExpOpt, otlpmetrichttp.WithInsecure())
} else {
tlsCfg, err := common.GetTLSCredentialsForHTTPExporter(cfg.CaFile, cfg.ClientAuth)
if err != nil {
return nil, fmt.Errorf("failed to get TLS credentials: %w", err)
}
httpExpOpt = append(httpExpOpt, otlpmetrichttp.WithTLSClientConfig(tlsCfg))
}

if len(cfg.Headers) > 0 {
httpExpOpt = append(httpExpOpt, otlpmetrichttp.WithHeaders(cfg.Headers))
}

return httpExpOpt
return httpExpOpt, nil
}
22 changes: 20 additions & 2 deletions cmd/telemetrygen/internal/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,29 @@ func Start(cfg *Config) error {
expFunc := func() (sdkmetric.Exporter, error) {
var exp sdkmetric.Exporter
if cfg.UseHTTP {
var exporterOpts []otlpmetrichttp.Option

logger.Info("starting HTTP exporter")
exp, err = otlpmetrichttp.New(context.Background(), httpExporterOptions(cfg)...)
exporterOpts, err = httpExporterOptions(cfg)
if err != nil {
return nil, err
}
exp, err = otlpmetrichttp.New(context.Background(), exporterOpts...)
if err != nil {
return nil, fmt.Errorf("failed to obtain OTLP HTTP exporter: %w", err)
}
} else {
var exporterOpts []otlpmetricgrpc.Option

logger.Info("starting gRPC exporter")
exp, err = otlpmetricgrpc.New(context.Background(), grpcExporterOptions(cfg)...)
exporterOpts, err = grpcExporterOptions(cfg)
if err != nil {
return nil, err
}
exp, err = otlpmetricgrpc.New(context.Background(), exporterOpts...)
if err != nil {
return nil, fmt.Errorf("failed to obtain OTLP gRPC exporter: %w", err)
}
}
return exp, err
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/telemetrygen/internal/traces/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ type Config struct {
StatusCode string
Batch bool
LoadSize int
SpanDuration time.Duration

SpanDuration time.Duration
}

// Flags registers config flags.
Expand Down
24 changes: 20 additions & 4 deletions cmd/telemetrygen/internal/traces/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
package traces

import (
"fmt"

"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"google.golang.org/grpc"

"github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen/internal/common"
)

// grpcExporterOptions creates the configuration options for a gRPC-based OTLP trace exporter.
// It configures the exporter with the provided endpoint, connection security settings, and headers.
func grpcExporterOptions(cfg *Config) []otlptracegrpc.Option {
func grpcExporterOptions(cfg *Config) ([]otlptracegrpc.Option, error) {
grpcExpOpt := []otlptracegrpc.Option{
otlptracegrpc.WithEndpoint(cfg.Endpoint()),
otlptracegrpc.WithDialOption(
Expand All @@ -21,30 +25,42 @@ func grpcExporterOptions(cfg *Config) []otlptracegrpc.Option {

if cfg.Insecure {
grpcExpOpt = append(grpcExpOpt, otlptracegrpc.WithInsecure())
} else {
credentials, err := common.GetTLSCredentialsForGRPCExporter(cfg.CaFile, cfg.ClientAuth)
if err != nil {
return nil, fmt.Errorf("failed to get TLS credentials: %w", err)
}
grpcExpOpt = append(grpcExpOpt, otlptracegrpc.WithTLSCredentials(credentials))
}

if len(cfg.Headers) > 0 {
grpcExpOpt = append(grpcExpOpt, otlptracegrpc.WithHeaders(cfg.Headers))
}

return grpcExpOpt
return grpcExpOpt, nil
}

// httpExporterOptions creates the configuration options for an HTTP-based OTLP trace exporter.
// It configures the exporter with the provided endpoint, URL path, connection security settings, and headers.
func httpExporterOptions(cfg *Config) []otlptracehttp.Option {
func httpExporterOptions(cfg *Config) ([]otlptracehttp.Option, error) {
httpExpOpt := []otlptracehttp.Option{
otlptracehttp.WithEndpoint(cfg.Endpoint()),
otlptracehttp.WithURLPath(cfg.HTTPPath),
}

if cfg.Insecure {
httpExpOpt = append(httpExpOpt, otlptracehttp.WithInsecure())
} else {
tlsCfg, err := common.GetTLSCredentialsForHTTPExporter(cfg.CaFile, cfg.ClientAuth)
if err != nil {
return nil, fmt.Errorf("failed to get TLS credentials: %w", err)
}
httpExpOpt = append(httpExpOpt, otlptracehttp.WithTLSClientConfig(tlsCfg))
}

if len(cfg.Headers) > 0 {
httpExpOpt = append(httpExpOpt, otlptracehttp.WithHeaders(cfg.Headers))
}

return httpExpOpt
return httpExpOpt, nil
}
Loading

0 comments on commit 2c0974f

Please sign in to comment.