From a9c59da516006b518640800217854f0ae468fbf2 Mon Sep 17 00:00:00 2001 From: Svetlana Brennan <50715937+svetlanabrennan@users.noreply.github.com> Date: Tue, 17 May 2022 01:24:31 -0500 Subject: [PATCH] feat(trace-otlp-grpc): configure security with env vars (#2827) * feat(trace-otlp-grpc): add insecure configs Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): add unit tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): wip add certificate and tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix security config and unit tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): add env and certificate tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): wip certificate tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix lint error Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): wip add additional security setting checks Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): update default url to http scheme Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): wip add tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): wip refactor function around insecure setting Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): wip update returned security setting for some use cases Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): update certificate and add tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): wip certificate tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): add diag tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): update default url Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): add changelog item Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): add grpc scheme check and update test Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): add grpc scheme test Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix metrics default url Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): update readme Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix lint Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix changelog and get security from env func Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): wip troubleshoot Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix readme Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): refactor code and fix lint Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): remove grpc scheme and grpc scheme tests Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): fix credentials for failing test afer main merge Signed-off-by: Svetlana Brennan * feat(trace-otlp-grpc): move changelog to unreleased section Signed-off-by: Svetlana Brennan * Use exact match for protocol check to avoid leaking cases like httpx Co-authored-by: Chengzhong Wu Co-authored-by: Daniel Dyla --- experimental/CHANGELOG.md | 1 + .../exporter-trace-otlp-grpc/README.md | 31 ++++-- .../src/OTLPTraceExporter.ts | 7 +- .../test/OTLPTraceExporter.test.ts | 14 +-- .../README.md | 21 +++- .../src/OTLPMetricExporter.ts | 8 +- .../test/OTLPMetricExporter.test.ts | 4 +- .../otlp-grpc-exporter-base/src/index.ts | 2 +- .../otlp-grpc-exporter-base/src/util.ts | 104 +++++++++++++++++- .../otlp-grpc-exporter-base/test/util.test.ts | 102 ++++++++++++++++- .../src/utils/environment.ts | 26 ++++- 11 files changed, 277 insertions(+), 43 deletions(-) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index a703efc970..51ea849844 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to experimental packages in this project will be documented * feat(exporters): update proto version and use otlp-transformer #2929 @pichlermarc * fix(sdk-metrics-base): misbehaving aggregation temporality selector tolerance #2958 @legendecas +* feat(trace-otlp-grpc): configure security with env vars #2827 @svetlanabrennan * feat(sdk-metrics-base): async instruments callback timeout #2742 @legendecas ### :bug: (Bug Fix) diff --git a/experimental/packages/exporter-trace-otlp-grpc/README.md b/experimental/packages/exporter-trace-otlp-grpc/README.md index 0ff58a0afa..1b70cad913 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/README.md +++ b/experimental/packages/exporter-trace-otlp-grpc/README.md @@ -27,8 +27,8 @@ const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/sdk const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc'); const collectorOptions = { - // url is optional and can be omitted - default is localhost:4317 - url: ':', + // url is optional and can be omitted - default is http://localhost:4317 + url: 'http://:', }; const provider = new BasicTracerProvider(); @@ -51,8 +51,8 @@ const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/sdk const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc'); const collectorOptions = { - // url is optional and can be omitted - default is localhost:4317 - url: ':', + // url is optional and can be omitted - default is http://localhost:4317 + url: 'http://:', credentials: grpc.credentials.createSsl(), }; @@ -91,8 +91,8 @@ const metadata = new grpc.Metadata(); metadata.set('k', 'v'); const collectorOptions = { - // url is optional and can be omitted - default is localhost:4317 - url: ':', + // url is optional and can be omitted - default is http://localhost:4317 + url: 'http://:', metadata, // // an optional grpc.Metadata object to be sent with each request }; @@ -135,8 +135,8 @@ By default no compression will be used. To use compression, set it programmatica const { CompressionAlgorithm } = require('@opentelemetry/exporter-trace-otlp-grpc'); const collectorOptions = { - // url is optional and can be omitted - default is localhost:4317 - url: ':', + // url is optional and can be omitted - default is http://localhost:4317 + url: 'http://:', metadata, // // an optional grpc.Metadata object to be sent with each request compression: CompressionAlgorithm.GZIP, }; @@ -149,11 +149,20 @@ const exporter = new OTLPTraceExporter(collectorOptions); | Environment variable | Description | |----------------------|-------------| - | OTEL_EXPORTER_OTLP_TRACES_TIMEOUT | The maximum waiting time, in milliseconds, allowed to send each OTLP trace batch. Default is 10000. | - | OTEL_EXPORTER_OTLP_TIMEOUT | The maximum waiting time, in milliseconds, allowed to send each OTLP trace and metric batch. Default is 10000. | | OTEL_EXPORTER_OTLP_TRACES_COMPRESSION | The compression type to use on OTLP trace requests. Options include gzip. By default no compression will be used. | | OTEL_EXPORTER_OTLP_COMPRESSION | The compression type to use on OTLP trace, metric, and log requests. Options include gzip. By default no compression will be used. | - > The per-signal environment variables (`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT`) takes precedence and non-per-signal environment variable (`OTEL_EXPORTER_OTLP_TIMEOUT`). + | OTEL_EXPORTER_OTLP_TRACES_INSECURE | Whether to enable client transport security for the exporter's gRPC connection for trace requests. This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme. Options include true or false. By default insecure is false which creates a secure connection. | + | OTEL_EXPORTER_OTLP_INSECURE | Whether to enable client transport security for the exporter's gRPC connection for trace, metric and log requests. This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme. Options include true or false. By default insecure is false which creates a secure connection. | + | OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE | The path to the file containing trusted root certificate to use when verifying an OTLP trace server's TLS credentials. By default the host platform's trusted root certificate is used.| + | OTEL_EXPORTER_OTLP_CERTIFICATE | The path to the file containing trusted root certificate to use when verifying an OTLP trace, metric, or log server's TLS credentials. By default the host platform's trusted root certificate is used. | + | OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP trace client's TLS credentials. Must provide a client certificate/chain when providing a private client key. By default no client key file is used. | + | OTEL_EXPORTER_OTLP_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP trace, metric or log client's TLS credentials. Must provide a client certificate/chain when providing a private client key. By default no client key file is used. | + | OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE | The path to the file containing trusted client certificate/chain for clients private key to use when verifying an OTLP trace server's TLS credentials. Must provide a private client key when providing a certificate/chain. By default no chain file is used. | + | OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE | The path to the file containing trusted client certificate/chain for clients private key to use when verifying an OTLP trace, metric and log server's TLS credentials. Must provide a private client key when providing a certificate/chain. By default no chain file is used. | + | OTEL_EXPORTER_OTLP_TRACES_TIMEOUT | The maximum waiting time, in milliseconds, allowed to send each OTLP trace batch. Default is 10000. | + | OTEL_EXPORTER_OTLP_TIMEOUT | The maximum waiting time, in milliseconds, allowed to send each OTLP trace and metric batch. Default is 10000. | + + > Settings configured programmatically take precedence over environment variables. Per-signal environment variables take precedence over non-per-signal environment variables. ## Running opentelemetry-collector locally to see the traces diff --git a/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts index 5ea0829dc5..baafc4773a 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts @@ -21,12 +21,11 @@ import { OTLPGRPCExporterConfigNode, OTLPGRPCExporterNodeBase, ServiceClientType, - validateAndNormalizeUrl + validateAndNormalizeUrl, + DEFAULT_COLLECTOR_URL } from '@opentelemetry/otlp-grpc-exporter-base'; import { createExportTraceServiceRequest, IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer'; -const DEFAULT_COLLECTOR_URL = 'localhost:4317'; - /** * OTLP Trace Exporter for Node */ @@ -55,7 +54,7 @@ export class OTLPTraceExporter ? validateAndNormalizeUrl(getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0 ? validateAndNormalizeUrl(getEnv().OTEL_EXPORTER_OTLP_ENDPOINT) - : DEFAULT_COLLECTOR_URL; + : validateAndNormalizeUrl(DEFAULT_COLLECTOR_URL); } getServiceClientType() { diff --git a/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts index 03e8be4c6e..d1052e9edc 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts @@ -123,9 +123,9 @@ const testCollectorExporter = (params: TestParams) => fs.readFileSync('./test/certs/client.key'), fs.readFileSync('./test/certs/client.crt') ) - : undefined; + : grpc.credentials.createInsecure(); collectorExporter = new OTLPTraceExporter({ - url: 'grpcs://' + address, + url: 'https://' + address, credentials, metadata: params.metadata, }); @@ -207,7 +207,7 @@ const testCollectorExporter = (params: TestParams) => fs.readFileSync('./test/certs/client.key'), fs.readFileSync('./test/certs/client.crt') ) - : undefined; + : grpc.credentials.createInsecure(); const collectorExporterWithTimeout = new OTLPTraceExporter({ url: 'grpcs://' + address, @@ -236,9 +236,9 @@ const testCollectorExporter = (params: TestParams) => fs.readFileSync('./test/certs/client.key'), fs.readFileSync('./test/certs/client.crt') ) - : undefined; + : grpc.credentials.createInsecure(); collectorExporter = new OTLPTraceExporter({ - url: 'grpcs://' + address, + url: 'https://' + address, credentials, metadata: params.metadata, compression: CompressionAlgorithm.GZIP, @@ -286,11 +286,11 @@ const testCollectorExporter = (params: TestParams) => fs.readFileSync('./test/certs/client.key'), fs.readFileSync('./test/certs/client.crt') ) - : undefined; + : grpc.credentials.createInsecure(); envSource.OTEL_EXPORTER_OTLP_COMPRESSION = 'gzip'; collectorExporter = new OTLPTraceExporter({ - url: 'grpcs://' + address, + url: 'https://' + address, credentials, metadata: params.metadata, }); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/README.md b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/README.md index 8d514d0e82..e85afe9b33 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/README.md +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/README.md @@ -26,8 +26,8 @@ The OTLPMetricsExporter in Node expects the URL to only be the hostname. It will const { MeterProvider, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics-base'); const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc'); const collectorOptions = { - // url is optional and can be omitted - default is grpc://localhost:4317 - url: 'grpc://:', + // url is optional and can be omitted - default is http://localhost:4317 + url: 'http://:', }; const exporter = new OTLPMetricExporter(collectorOptions); @@ -48,6 +48,23 @@ const counter = meter.createCounter('metric_name'); counter.add(10, { 'key': 'value' }); ``` +## Environment Variable Configuration + + | Environment variable | Description | + |----------------------|-------------| + | OTEL_EXPORTER_OTLP_METRICS_COMPRESSION | The compression type to use on OTLP metric requests. Options include gzip. By default no compression will be used. | + | OTEL_EXPORTER_OTLP_COMPRESSION | The compression type to use on OTLP trace, metric, and log requests. Options include gzip. By default no compression will be used. | + | OTEL_EXPORTER_OTLP_METRICS_INSECURE | Whether to enable client transport security for the exporter's gRPC connection for metric requests. This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme. Options include true or false. By default insecure is false which creates a secure connection. | + | OTEL_EXPORTER_OTLP_INSECURE | Whether to enable client transport security for the exporter's gRPC connection for trace, metric and log requests. This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme. Options include true or false. By default insecure is false which creates a secure connection. | + | OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE | The path to the file containing trusted root certificate to use when verifying an OTLP metric server's TLS credentials. By default the host platform's trusted root certificate is used.| + | OTEL_EXPORTER_OTLP_CERTIFICATE | The path to the file containing trusted root certificate to use when verifying an OTLP trace, metric, or log server's TLS credentials. By default the host platform's trusted root certificate is used. | + | OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP metric client's TLS credentials. Must provide a client certificate/chain when providing a private client key. By default no client key file is used. | + | OTEL_EXPORTER_OTLP_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP trace, metric or log client's TLS credentials. Must provide a client certificate/chain when providing a private client key. By default no client key file is used. | + | OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE | The path to the file containing trusted client certificate/chain for clients private key to use when verifying an OTLP metric server's TLS credentials. Must provide a private client key when providing a certificate/chain. By default no chain file is used. | + | OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE | The path to the file containing trusted client certificate/chain for clients private key to use when verifying an OTLP trace, metric and log server's TLS credentials. Must provide a private client key when providing a certificate/chain. By default no chain file is used. | + + > Settings configured programmatically take precedence over environment variables. Per-signal environment variables take precedence over non-per-signal environment variables. + ## Running opentelemetry-collector locally to see the metrics 1. Go to `examples/otlp-exporter-node` diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts index f7f45114be..71e6e9b105 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts @@ -24,15 +24,13 @@ import { OTLPGRPCExporterConfigNode, OTLPGRPCExporterNodeBase, ServiceClientType, - validateAndNormalizeUrl + validateAndNormalizeUrl, + DEFAULT_COLLECTOR_URL } from '@opentelemetry/otlp-grpc-exporter-base'; import { baggageUtils, getEnv } from '@opentelemetry/core'; import { Metadata } from '@grpc/grpc-js'; import { createExportMetricsServiceRequest, IExportMetricsServiceRequest } from '@opentelemetry/otlp-transformer'; -const DEFAULT_COLLECTOR_URL = 'localhost:4317'; - - class OTLPMetricExporterProxy extends OTLPGRPCExporterNodeBase { constructor(config: OTLPGRPCExporterConfigNode & OTLPMetricExporterOptions= defaultOptions) { @@ -59,7 +57,7 @@ class OTLPMetricExporterProxy extends OTLPGRPCExporterNodeBase 0 ? validateAndNormalizeUrl(getEnv().OTEL_EXPORTER_OTLP_ENDPOINT) - : DEFAULT_COLLECTOR_URL; + : validateAndNormalizeUrl(DEFAULT_COLLECTOR_URL); } convert(metrics: ResourceMetrics[]): IExportMetricsServiceRequest { diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts index b03a612436..3792c3e8da 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts @@ -123,9 +123,9 @@ const testOTLPMetricExporter = (params: TestParams) => fs.readFileSync('./test/certs/client.key'), fs.readFileSync('./test/certs/client.crt') ) - : undefined; + : grpc.credentials.createInsecure(); collectorExporter = new OTLPMetricExporter({ - url: 'grpcs://' + address, + url: 'https://' + address, credentials, metadata: params.metadata, temporalityPreference: AggregationTemporality.CUMULATIVE diff --git a/experimental/packages/otlp-grpc-exporter-base/src/index.ts b/experimental/packages/otlp-grpc-exporter-base/src/index.ts index 6ddb52ade2..8dfb3543e7 100644 --- a/experimental/packages/otlp-grpc-exporter-base/src/index.ts +++ b/experimental/packages/otlp-grpc-exporter-base/src/index.ts @@ -16,4 +16,4 @@ export * from './OTLPGRPCExporterNodeBase'; export { ServiceClientType, OTLPGRPCExporterConfigNode } from './types'; -export { validateAndNormalizeUrl, GrpcCompressionAlgorithm } from './util'; +export { DEFAULT_COLLECTOR_URL, validateAndNormalizeUrl, GrpcCompressionAlgorithm } from './util'; diff --git a/experimental/packages/otlp-grpc-exporter-base/src/util.ts b/experimental/packages/otlp-grpc-exporter-base/src/util.ts index ad25335a79..313618fa72 100644 --- a/experimental/packages/otlp-grpc-exporter-base/src/util.ts +++ b/experimental/packages/otlp-grpc-exporter-base/src/util.ts @@ -21,16 +21,19 @@ import { getEnv, globalErrorHandler } from '@opentelemetry/core'; import * as path from 'path'; import { OTLPGRPCExporterNodeBase } from './OTLPGRPCExporterNodeBase'; import { URL } from 'url'; +import * as fs from 'fs'; import { GRPCQueueItem, OTLPGRPCExporterConfigNode, ServiceClientType, } from './types'; import { CompressionAlgorithm, ExportServiceError, OTLPExporterError } from '@opentelemetry/otlp-exporter-base'; +export const DEFAULT_COLLECTOR_URL = 'http://localhost:4317'; + export function onInit( collector: OTLPGRPCExporterNodeBase, config: OTLPGRPCExporterConfigNode ): void { collector.grpcQueue = []; - const credentials: grpc.ChannelCredentials = - config.credentials || grpc.credentials.createInsecure(); + + const credentials: grpc.ChannelCredentials = configureSecurity(config.credentials, collector.url); const includeDirs = [path.resolve(__dirname, '..', 'protos')]; @@ -120,14 +123,107 @@ export function validateAndNormalizeUrl(url: string): string { 'URL path should not be set when using grpc, the path part of the URL will be ignored.' ); } - if (target.protocol !== '' && !target.protocol?.match(/(http|grpc)s?/)) { + if (target.protocol !== '' && !target.protocol?.match(/^(http)s?:$/)) { diag.warn( - 'URL protocol should be http(s):// or grpc(s)://. Using grpc://.' + 'URL protocol should be http(s)://. Using http://.' ); } return target.host; } +export function configureSecurity(credentials: grpc.ChannelCredentials | undefined, endpoint: string): + grpc.ChannelCredentials { + + let insecure: boolean; + + if (credentials) { + return credentials; + } else if (endpoint.startsWith('https://')) { + insecure = false; + } else if (endpoint.startsWith('http://') || endpoint === DEFAULT_COLLECTOR_URL) { + insecure = true; + } else { + insecure = getSecurityFromEnv(); + } + + if (insecure) { + return grpc.credentials.createInsecure(); + } else { + return useSecureConnection(); + } +} + +function getSecurityFromEnv(): boolean { + const definedInsecure = + getEnv().OTEL_EXPORTER_OTLP_TRACES_INSECURE || + getEnv().OTEL_EXPORTER_OTLP_INSECURE; + + if (definedInsecure) { + return definedInsecure.toLowerCase() === 'true'; + } else { + return false; + } +} + +export function useSecureConnection(): grpc.ChannelCredentials { + const rootCertPath = retrieveRootCert(); + const privateKeyPath = retrievePrivateKey(); + const certChainPath = retrieveCertChain(); + + return grpc.credentials.createSsl(rootCertPath, privateKeyPath, certChainPath); +} + +function retrieveRootCert(): Buffer | undefined { + const rootCertificate = + getEnv().OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE || + getEnv().OTEL_EXPORTER_OTLP_CERTIFICATE; + + if (rootCertificate) { + try { + return fs.readFileSync(path.resolve(process.cwd(), rootCertificate)); + } catch { + diag.warn('Failed to read root certificate file'); + return undefined; + } + } else { + return undefined; + } +} + +function retrievePrivateKey(): Buffer | undefined { + const clientKey = + getEnv().OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY || + getEnv().OTEL_EXPORTER_OTLP_CLIENT_KEY; + + if (clientKey) { + try { + return fs.readFileSync(path.resolve(process.cwd(), clientKey)); + } catch { + diag.warn('Failed to read client certificate private key file'); + return undefined; + } + } else { + return undefined; + } +} + +function retrieveCertChain(): Buffer | undefined { + const clientChain = + getEnv().OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE || + getEnv().OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE; + + if (clientChain) { + try { + return fs.readFileSync(path.resolve(process.cwd(), clientChain)); + } catch { + diag.warn('Failed to read client certificate chain file'); + return undefined; + } + } else { + return undefined; + } +} + function toGrpcCompression(compression: CompressionAlgorithm): GrpcCompressionAlgorithm { if(compression === CompressionAlgorithm.NONE) return GrpcCompressionAlgorithm.NONE; diff --git a/experimental/packages/otlp-grpc-exporter-base/test/util.test.ts b/experimental/packages/otlp-grpc-exporter-base/test/util.test.ts index cae1de6ce3..c583317895 100644 --- a/experimental/packages/otlp-grpc-exporter-base/test/util.test.ts +++ b/experimental/packages/otlp-grpc-exporter-base/test/util.test.ts @@ -18,7 +18,8 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; import { diag } from '@opentelemetry/api'; -import { validateAndNormalizeUrl, configureCompression, GrpcCompressionAlgorithm } from '../src/util'; +import * as grpc from '@grpc/grpc-js'; +import { validateAndNormalizeUrl, configureCompression, GrpcCompressionAlgorithm, configureSecurity, useSecureConnection, DEFAULT_COLLECTOR_URL } from '../src/util'; import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base'; // Tests added to detect breakage released in #2130 @@ -35,19 +36,19 @@ describe('validateAndNormalizeUrl()', () => { expected: 'api.datacat.io:1234', }, { - name: 'grpc://host:port should trim off protocol', - input: 'grpc://api.datacat.io:1234', + name: 'https://host:port should trim off protocol', + input: 'https://api.datacat.io:1234', expected: 'api.datacat.io:1234', }, { name: 'bad protocol should warn but return host:port', input: 'badproto://api.datacat.io:1234', expected: 'api.datacat.io:1234', - warn: 'URL protocol should be http(s):// or grpc(s)://. Using grpc://.', + warn: 'URL protocol should be http(s)://. Using http://.', }, { name: 'path on end of url should warn but return host:port', - input: 'grpc://api.datacat.io:1234/a/b/c', + input: 'http://api.datacat.io:1234/a/b/c', expected: 'api.datacat.io:1234', warn: 'URL path should not be set when using grpc, the path part of the URL will be ignored.', }, @@ -59,7 +60,7 @@ describe('validateAndNormalizeUrl()', () => { }, { name: ':// in path is valid when a protocol is specified', - input: 'grpc://api.datacat.io/a/b://c', + input: 'http://api.datacat.io/a/b://c', expected: 'api.datacat.io', warn: 'URL path should not be set when using grpc, the path part of the URL will be ignored.', }, @@ -81,6 +82,95 @@ describe('validateAndNormalizeUrl()', () => { }); }); +describe('utils - configureSecurity', () => { + const envSource = process.env; + it('should return insecure channel when using all defaults', () => { + const credentials = configureSecurity(undefined, DEFAULT_COLLECTOR_URL); + assert.ok(credentials._isSecure() === false); + }); + it('should return user defined channel credentials', () => { + const userDefinedCredentials = grpc.credentials.createSsl(); + const credentials = configureSecurity(userDefinedCredentials, 'http://foo.bar'); + + assert.ok(userDefinedCredentials === credentials); + assert.ok(credentials._isSecure() === true); + }); + it('should return secure channel when endpoint contains https scheme - no matter insecure env settings,', () => { + envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE='true'; + const credentials = configureSecurity(undefined, 'https://foo.bar'); + assert.ok(credentials._isSecure() === true); + delete envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE; + }); + + it('should return insecure channel when endpoint contains http scheme and no insecure env settings', () => { + const credentials = configureSecurity(undefined, 'http://foo.bar'); + assert.ok(credentials._isSecure() === false); + }); + it('should return secure channel when endpoint does not contain scheme and no insecure env settings', () => { + const credentials = configureSecurity(undefined, 'foo.bar'); + assert.ok(credentials._isSecure() === true); + }); + it('should return insecure channel when endpoint contains http scheme and insecure env set to false', () => { + envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE='false'; + const credentials = configureSecurity(undefined, 'http://foo.bar'); + assert.ok(credentials._isSecure() === false); + delete envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE; + }); + it('should return insecure channel when endpoint contains http scheme and insecure env set to true', () => { + envSource.OTEL_EXPORTER_OTLP_INSECURE='true'; + const credentials = configureSecurity(undefined, 'http://localhost'); + assert.ok(credentials._isSecure() === false); + delete envSource.OTEL_EXPORTER_OTLP_INSECURE; + }); + it('should return secure channel when endpoint does not contain scheme and insecure env set to false', () => { + envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE='false'; + const credentials = configureSecurity(undefined, 'foo.bar'); + assert.ok(credentials._isSecure() === true); + delete envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE; + }); + it('should return insecure channel when endpoint does not contain scheme and insecure env set to true', () => { + envSource.OTEL_EXPORTER_OTLP_INSECURE='true'; + const credentials = configureSecurity(undefined, 'foo.bar'); + assert.ok(credentials._isSecure() === false); + delete envSource.OTEL_EXPORTER_OTLP_INSECURE; + }); +}); + +describe('useSecureConnection', () => { + const envSource = process.env; + it('should return secure connection using all credentials', () => { + envSource.OTEL_EXPORTER_OTLP_CERTIFICATE='./test/certs/ca.crt'; + envSource.OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY='./test/certs/client.key'; + envSource.OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE='./test/certs/client.crt'; + + const credentials = useSecureConnection(); + assert.ok(credentials._isSecure() === true); + + delete envSource.OTEL_EXPORTER_OTLP_CERTIFICATE; + delete envSource.OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY; + delete envSource.OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE; + }); + it('should return secure connection using only root certificate', () => { + envSource.OTEL_EXPORTER_OTLP_CERTIFICATE='./test/certs/ca.crt'; + const credentials = useSecureConnection(); + assert.ok(credentials._isSecure() === true); + delete envSource.OTEL_EXPORTER_OTLP_CERTIFICATE; + }); + it('should warn user when file cannot be read and use default root certificate', () => { + envSource.OTEL_EXPORTER_OTLP_CERTIFICATE='./wrongpath/test/certs/ca.crt'; + const diagWarn = sinon.stub(diag, 'warn'); + const credentials = useSecureConnection(); + const args = diagWarn.args[0]; + + assert.strictEqual(args[0], 'Failed to read root certificate file'); + sinon.assert.calledOnce(diagWarn); + assert.ok(credentials._isSecure() === true); + + delete envSource.OTEL_EXPORTER_OTLP_CERTIFICATE; + diagWarn.restore(); + }); +}); + describe('configureCompression', () => { const envSource = process.env; it('should return none for compression', () => { diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index bb3b123ffe..def0fe4846 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -87,9 +87,21 @@ export type ENVIRONMENT = { OTEL_TRACES_EXPORTER?: string; OTEL_TRACES_SAMPLER_ARG?: string; OTEL_TRACES_SAMPLER?: string; + OTEL_EXPORTER_OTLP_INSECURE?: string, + OTEL_EXPORTER_OTLP_TRACES_INSECURE?: string, + OTEL_EXPORTER_OTLP_METRICS_INSECURE?: string, + OTEL_EXPORTER_OTLP_CERTIFICATE?: string, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE?: string, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE?: string, OTEL_EXPORTER_OTLP_COMPRESSION?: string, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION?: string, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION?: string + OTEL_EXPORTER_OTLP_CLIENT_KEY?: string, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY?: string, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY?: string, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE?: string, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE?: string, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE?: string } & ENVIRONMENT_NUMBERS & ENVIRONMENT_LISTS; @@ -144,9 +156,21 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_TRACES_EXPORTER: 'none', OTEL_TRACES_SAMPLER: TracesSamplerValues.ParentBasedAlwaysOn, OTEL_TRACES_SAMPLER_ARG: '', + OTEL_EXPORTER_OTLP_INSECURE: '', + OTEL_EXPORTER_OTLP_TRACES_INSECURE: '', + OTEL_EXPORTER_OTLP_METRICS_INSECURE: '', + OTEL_EXPORTER_OTLP_CERTIFICATE: '', + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: '', + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: '', OTEL_EXPORTER_OTLP_COMPRESSION: '', OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: '', - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: '' + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: '', + OTEL_EXPORTER_OTLP_CLIENT_KEY: '', + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: '', + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: '', + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: '', + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: '', + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: '' }; /**