diff --git a/.chloggen/juliaj_telemetrygen-tls-mtls-support.yaml b/.chloggen/juliaj_telemetrygen-tls-mtls-support.yaml new file mode 100644 index 000000000000..aeb4a81fdb3c --- /dev/null +++ b/.chloggen/juliaj_telemetrygen-tls-mtls-support.yaml @@ -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: [] diff --git a/Makefile b/Makefile index f7f505bc78f6..a635b9463e49 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/telemetrygen/README.md b/cmd/telemetrygen/README.md index f13788b27e95..71b7a8026852 100644 --- a/cmd/telemetrygen/README.md +++ b/cmd/telemetrygen/README.md @@ -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 diff --git a/cmd/telemetrygen/internal/common/config.go b/cmd/telemetrygen/internal/common/config.go index 8ec9a8acfac2..154b2fd4a43f 100644 --- a/cmd/telemetrygen/internal/common/config.go +++ b/cmd/telemetrygen/internal/common/config.go @@ -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) @@ -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") } diff --git a/cmd/telemetrygen/internal/common/tls_utils.go b/cmd/telemetrygen/internal/common/tls_utils.go new file mode 100644 index 000000000000..d9678079a188 --- /dev/null +++ b/cmd/telemetrygen/internal/common/tls_utils.go @@ -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 +} diff --git a/cmd/telemetrygen/internal/logs/exporter.go b/cmd/telemetrygen/internal/logs/exporter.go index b8b3601e50af..187cfe78e9db 100644 --- a/cmd/telemetrygen/internal/logs/exporter.go +++ b/cmd/telemetrygen/internal/logs/exporter.go @@ -14,6 +14,8 @@ 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 { @@ -21,20 +23,42 @@ type exporter interface { } 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 } diff --git a/cmd/telemetrygen/internal/metrics/exporter.go b/cmd/telemetrygen/internal/metrics/exporter.go index d320e292447c..5d77caf4eae8 100644 --- a/cmd/telemetrygen/internal/metrics/exporter.go +++ b/cmd/telemetrygen/internal/metrics/exporter.go @@ -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( @@ -21,18 +25,24 @@ 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), @@ -40,11 +50,17 @@ func httpExporterOptions(cfg *Config) []otlpmetrichttp.Option { 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 } diff --git a/cmd/telemetrygen/internal/metrics/metrics.go b/cmd/telemetrygen/internal/metrics/metrics.go index 0061d64b3695..8289287e3793 100644 --- a/cmd/telemetrygen/internal/metrics/metrics.go +++ b/cmd/telemetrygen/internal/metrics/metrics.go @@ -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 } diff --git a/cmd/telemetrygen/internal/traces/config.go b/cmd/telemetrygen/internal/traces/config.go index 55e020e899d3..aaa1ab08815e 100644 --- a/cmd/telemetrygen/internal/traces/config.go +++ b/cmd/telemetrygen/internal/traces/config.go @@ -20,7 +20,8 @@ type Config struct { StatusCode string Batch bool LoadSize int - SpanDuration time.Duration + + SpanDuration time.Duration } // Flags registers config flags. diff --git a/cmd/telemetrygen/internal/traces/exporter.go b/cmd/telemetrygen/internal/traces/exporter.go index 4161ccc3d1f1..dbcf55d7ab6e 100644 --- a/cmd/telemetrygen/internal/traces/exporter.go +++ b/cmd/telemetrygen/internal/traces/exporter.go @@ -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( @@ -21,18 +25,24 @@ 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), @@ -40,11 +50,17 @@ func httpExporterOptions(cfg *Config) []otlptracehttp.Option { 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 } diff --git a/cmd/telemetrygen/internal/traces/traces.go b/cmd/telemetrygen/internal/traces/traces.go index a84491a378fb..576346ad56aa 100644 --- a/cmd/telemetrygen/internal/traces/traces.go +++ b/cmd/telemetrygen/internal/traces/traces.go @@ -34,16 +34,31 @@ func Start(cfg *Config) error { var exp *otlptrace.Exporter if cfg.UseHTTP { + var exporterOpts []otlptracehttp.Option + logger.Info("starting HTTP exporter") - exp, err = otlptracehttp.New(context.Background(), httpExporterOptions(cfg)...) + exporterOpts, err = httpExporterOptions(cfg) + if err != nil { + return err + } + exp, err = otlptracehttp.New(context.Background(), exporterOpts...) + if err != nil { + return fmt.Errorf("failed to obtain OTLP HTTP exporter: %w", err) + } } else { + var exporterOpts []otlptracegrpc.Option + logger.Info("starting gRPC exporter") - exp, err = otlptracegrpc.New(context.Background(), grpcExporterOptions(cfg)...) + exporterOpts, err = grpcExporterOptions(cfg) + if err != nil { + return err + } + exp, err = otlptracegrpc.New(context.Background(), exporterOpts...) + if err != nil { + return fmt.Errorf("failed to obtain OTLP gRPC exporter: %w", err) + } } - if err != nil { - return fmt.Errorf("failed to obtain OTLP exporter: %w", err) - } defer func() { logger.Info("stopping the exporter") if tempError := exp.Shutdown(context.Background()); tempError != nil { diff --git a/examples/secure-tracing/README.md b/examples/secure-tracing/README.md new file mode 100644 index 000000000000..9b0312d4638e --- /dev/null +++ b/examples/secure-tracing/README.md @@ -0,0 +1,117 @@ +# Build A Secure Trace Collection Pipeline + +Implementing robust security measures is essential for any tracing ingestion service and infrastructure. This is an illustrative example of a secure setup encompassing the following features for trace ingestion: + +- Data Encryption with OTLP receiver supporting TLS. Transport Layer Security (TLS) is employed to encrypt traces in transit between the application and the OpenTelemetry (OTLP) endpoint, fortifying data security. +- Client Authentication via [Envoy](https://www.envoyproxy.io/docs/envoy/latest/start/start), a high-performance proxy. +Even though we can configure OTLP receiver with mTLS for client authentication, authorization is not supported by OpenTelemetry Collector. This is one of the reasons that we use Envoy for client authentication. It allows us to easily add authorization, ensuring that only authorized clients can submit traces to the ingestion service. + +In this example, we also include a test via telementrygen: a tool provided from this repository for generating synthetic telemetry data, which helps verify the security features and overall functionality of the set up. + + +## Data Encryption via TLS +The OpenTelemetry Collector has detailed [documentation](https://github.com/open-telemetry/opentelemetry-collector/blob/main/config/configtls/README.md) on how to configure TLS. In this example, we enable TLS for receivers which leverages server configuration. + +Example +``` +receivers: + otlp: + protocols: + grpc: + endpoint: mysite.local:55690 + tls: + cert_file: server.crt + key_file: server.key +``` + +## Client Authentication +Envoy [sandbox for TLS](https://www.envoyproxy.io/docs/envoy/latest/start/sandboxes/tls) is a good source for reference. A few elements in following configuration are good to call out, +- `require_client_certificate` is set true +- The `matcher` under `validation_context` expects a client certificate with `group-id` as well as `tenat-id` which provides more granular control. + +``` +typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + require_client_certificate: true + common_tls_context: + # h2 If the listener is going to support both HTTP/2 and HTTP/1.1. + alpn_protocols: "h2" + tls_certificates: + - certificate_chain: + filename: "/etc/envoy.crt" + private_key: + filename: "/etc/envoy.key" + validation_context: + match_typed_subject_alt_names: + - san_type: URI + matcher: + Match tenant by two level info: group id and tenant-id. + exact: "aprn:trace-client:certmgr:::group-x:/ig/5003178/uv/tenant-a" + trusted_ca: + filename: "/etc/ca.crt" +``` + +## Setup Environment +### Generate Certificates +To generate various self-signed certificates, including those for Envoy and the OpenTelemetry Collector receiver, as well as tracing client certificate, we utilize the widely renowned open-source tool [OpenSSL](https://www.openssl.org/source/), OpenSSL 3.1.0 14 was tested. + +In the `certs` folder, you can find a set of `.ext` files which define the properties for a certificate. A `MakeFile` is provided to facilate the process. + +``` +$ cd certs +$ make clean && make all +``` +### Bring up services +We use docker compose to bring up Envoy and OpenTelemetry Collection Pipeline. Make sure the current folder is `secure-tracing`, + +``` +$ docker compose up +``` +From the console window, verify that `Envoy` and `Collector` are up and running. If you see error similar to following, + +``` +secure-tracing-otel-collector-1 | Error: cannot start pipelines: failed to load TLS config: failed to load TLS cert and key: read /etc/otel-collector.crt: is a directory +``` +It's most likely due to missing certificates. Follow the steps from section above to generate certificates. + +## Run test + +### Compile telemetrygen +From the root of this repository, +``` +$ cd cmd/telemetrygen +$ go build . +$ cp telemetrygen ../../examples/secure-tracing +``` + +### Send a trace +From the root of this repository, +``` +$ cd examples/secure-tracing +$ chmod +x telemetrygen +$ ./telemetrygen traces --traces 1 --otlp-endpoint 127.0.0.1:10000 --ca-cert 'certs/ca.crt' --mtls --client-cert 'certs/tracing-client.crt' --client-key 'certs/tracing-client.key' +``` + +Verify traces are captured and sent to console running `docker compose up`, similar to following ... +``` +secure-tracing-otel-collector-1 | -> service.name: Str(telemetrygen) +secure-tracing-otel-collector-1 | ScopeSpans #0 +secure-tracing-otel-collector-1 | ScopeSpans SchemaURL: +secure-tracing-otel-collector-1 | InstrumentationScope telemetrygen +secure-tracing-otel-collector-1 | Span #0 +secure-tracing-otel-collector-1 | Trace ID : 0fe7ca900fda938ce918f8b2d82d6ae9 +secure-tracing-otel-collector-1 | Parent ID : 1011b3f973049923 +secure-tracing-otel-collector-1 | ID : 65f3cfe524375dd4 +secure-tracing-otel-collector-1 | Name : okey-dokey +secure-tracing-otel-collector-1 | Kind : Server +... +secure-tracing-otel-collector-1 | Attributes: +secure-tracing-otel-collector-1 | -> net.peer.ip: Str(1.2.3.4) +secure-tracing-otel-collector-1 | -> peer.service: Str(telemetrygen-client) +... + +``` +## Shutdown Services +``` +$ docker compose down +``` diff --git a/examples/secure-tracing/certs/Makefile b/examples/secure-tracing/certs/Makefile new file mode 100644 index 000000000000..54185145249f --- /dev/null +++ b/examples/secure-tracing/certs/Makefile @@ -0,0 +1,39 @@ + +all: ca.crt envoy.crt tracing-client.crt otel-collector.crt + +clean: + rm -rf *.key *.crt + rm -f *.key *.crt *.csr ca.srl + +ca.key: + openssl genrsa -out ca.key 2048 + +ca.crt: ca.key + openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 -out ca.crt -config ca.ext + +envoy.key: + openssl genrsa -out envoy.key 2048 + +envoy.csr: envoy.key + openssl req -new -key envoy.key -out envoy.csr -config envoy.ext -extensions 'v3_req' + +envoy.crt: envoy.csr + openssl x509 -req -in envoy.csr -CA ca.crt -CAkey ca.key -out envoy.crt -CAcreateserial -days 365 -sha256 -extfile envoy.ext -extensions 'v3_req' + +tracing-client.key: + openssl genrsa -out tracing-client.key 2048 + +tracing-client.csr: tracing-client.key + openssl req -new -key tracing-client.key -out tracing-client.csr -config tracing-client.ext -extensions 'v3_req' + +tracing-client.crt: tracing-client.csr + openssl x509 -req -in tracing-client.csr -CA ca.crt -CAkey ca.key -out tracing-client.crt -CAcreateserial -days 365 -sha256 -extfile tracing-client.ext -extensions 'v3_req' + +otel-collector.key: + openssl genrsa -out otel-collector.key 2048 + +otel-collector.csr: otel-collector.key + openssl req -new -key otel-collector.key -out otel-collector.csr -config otel-collector.ext -extensions 'v3_req' + +otel-collector.crt: otel-collector.csr + openssl x509 -req -in otel-collector.csr -CA ca.crt -CAkey ca.key -out otel-collector.crt -CAcreateserial -days 365 -sha256 -extfile otel-collector.ext -extensions 'v3_req' diff --git a/examples/secure-tracing/certs/ca.ext b/examples/secure-tracing/certs/ca.ext new file mode 100644 index 000000000000..ad89873ecd3c --- /dev/null +++ b/examples/secure-tracing/certs/ca.ext @@ -0,0 +1,11 @@ +[req] +distinguished_name = req_distinguished_name +prompt = no + +[req_distinguished_name] +C = US +ST = CA +L = Cupertino +O = Example +OU = Local Development +CN = ca diff --git a/examples/secure-tracing/certs/envoy.ext b/examples/secure-tracing/certs/envoy.ext new file mode 100644 index 000000000000..09f2fcfeb8f3 --- /dev/null +++ b/examples/secure-tracing/certs/envoy.ext @@ -0,0 +1,22 @@ +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = US +ST = CA +L = Cupertino +O = Example +OU = Local Development +CN = envoy-example + +[v3_req] +keyUsage = keyEncipherment, dataEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = envoy-example +DNS.2 = localhost +IP.1 = 127.0.0.1 diff --git a/examples/secure-tracing/certs/otel-collector.ext b/examples/secure-tracing/certs/otel-collector.ext new file mode 100644 index 000000000000..bcce792808c8 --- /dev/null +++ b/examples/secure-tracing/certs/otel-collector.ext @@ -0,0 +1,23 @@ +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = US +ST = CA +L = Cupertino +O = Example +OU = Local Development +CN = otel-collector-example + +[v3_req] +keyUsage = keyEncipherment, dataEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = otel-collector +DNS.2 = otel-collector.default.svc.cluster.local +DNS.3 = localhost +IP.1 = 127.0.0.1 diff --git a/examples/secure-tracing/certs/tracing-client.ext b/examples/secure-tracing/certs/tracing-client.ext new file mode 100644 index 000000000000..8dc741bdd9dd --- /dev/null +++ b/examples/secure-tracing/certs/tracing-client.ext @@ -0,0 +1,20 @@ +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = US +ST = CA +L = Cupertino +O = Example +OU = Local Development +CN = traces-client + +[v3_req] +keyUsage = keyEncipherment, digitalSignature +extendedKeyUsage = clientAuth +subjectAltName = @alt_names + +[alt_names] +URI.1 = trace-client:group-x:tenant-a diff --git a/examples/secure-tracing/docker-compose.yaml b/examples/secure-tracing/docker-compose.yaml new file mode 100644 index 000000000000..0a57c85a849f --- /dev/null +++ b/examples/secure-tracing/docker-compose.yaml @@ -0,0 +1,26 @@ +version: '3' +services: + envoy: + image: envoyproxy/envoy-alpine:v1.21-latest + command: ["/usr/local/bin/envoy", "-c", "/etc/envoy-config.yaml", "-l", "debug"] + ports: + - "10000:10000" + - "9901:9901" + volumes: + - ./certs/envoy.crt:/etc/envoy.crt + - ./certs/envoy.key:/etc/envoy.key + - ./certs/ca.crt:/etc/ca.crt + - ./envoy-config.yaml:/etc/envoy-config.yaml + otel-collector: + image: otel/opentelemetry-collector:0.91.0 + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./certs/otel-collector.crt:/etc/otel-collector.crt + - ./certs/otel-collector.key:/etc/otel-collector.key + - ./certs/ca.crt:/etc/ca.crt + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "13133:13133" # health_check extension + - "55690" # OTLP gRPC receiver + depends_on: + - envoy diff --git a/examples/secure-tracing/envoy-config.yaml b/examples/secure-tracing/envoy-config.yaml new file mode 100644 index 000000000000..951782dff6e7 --- /dev/null +++ b/examples/secure-tracing/envoy-config.yaml @@ -0,0 +1,70 @@ +admin: + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 9901 +static_resources: + listeners: + - name: listener_0 + address: + socket_address: { address: 0.0.0.0, port_value: 10000 } + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: { prefix: "/" } + route: { cluster: collector_service } + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + require_client_certificate: true + common_tls_context: + # h2 If the listener is going to support both HTTP/2 and HTTP/1.1. + alpn_protocols: "h2" + tls_certificates: + - certificate_chain: + filename: "/etc/envoy.crt" + private_key: + filename: "/etc/envoy.key" + validation_context: + match_typed_subject_alt_names: + - san_type: URI + matcher: + # Match tenant by two levels: group-id and tenant-id. + exact: "trace-client:group-x:tenant-a" + trusted_ca: + filename: "/etc/ca.crt" + + clusters: + - name: collector_service + connect_timeout: 25s + type: LOGICAL_DNS + lb_policy: ROUND_ROBIN + http2_protocol_options: { } + load_assignment: + cluster_name: collector_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: otel-collector + port_value: 55690 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext diff --git a/examples/secure-tracing/otel-collector-config.yaml b/examples/secure-tracing/otel-collector-config.yaml new file mode 100644 index 000000000000..11010690d60a --- /dev/null +++ b/examples/secure-tracing/otel-collector-config.yaml @@ -0,0 +1,23 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:55690 + tls: + cert_file: /etc/otel-collector.crt + key_file: /etc/otel-collector.key + +processors: + batch: + +exporters: + logging: + verbosity: detailed + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [logging] +