Skip to content

Commit 1c5531b

Browse files
feat(storage): Add SigV4 authentication support for Elasticsearch/OpenSearch storage backends (#7611)
This PR enables Jaeger to use AWS Managed Elasticsearch/OpenSearch for trace and metrics storage by adding SigV4 HTTP authentication support to Elasticsearch and OpenSearch backends. ## Summary of changes **Configuration** - Add `jaeger_storage.backends.<name>.<elasticsearch|opensearch>.auth_extension.authenticator` to reference an OpenTelemetry HTTP authenticator extension by name - Add `jaeger_storage.metric_backends.<name>.<elasticsearch|opensearch>.auth_extension.authenticator` for metric storage backends **Elasticsearch/OpenSearch backends** - Thread the resolved HTTP authenticator through the factory chain (v1/v2 trace storage and metrics storage) - Wrap the HTTP RoundTripper used by ES/OS clients with the extension's RoundTripper (applies SigV4 signing when using `sigv4authextension` ) - Updated `GetHTTPRoundTripper()` to accept and apply the HTTP authenticator ## Configuration example ```yaml extensions: sigv4auth: region: us-east-1 service: es # or 'aoss' for OpenSearch Serverless # credentials/assume-role configuration per the extension's documentation service: extensions: [sigv4auth] jaeger_storage: backends: es-aws: elasticsearch: servers: ["https://my-domain.us-east-1.es.amazonaws.com/"] auth_extension: authenticator: sigv4auth indices: spans: shards: 5 replicas: 1 metric_backends: es-metrics: elasticsearch: servers: ["https://my-domain.us-east-1.es.amazonaws.com/"] auth_extension: authenticator: sigv4auth ``` ## Implementation - ES/OS backends now support optional HTTP authenticators via `auth_extension.authenticator` - The extension's RoundTripper wraps the base transport for SigV4 signing - Supports trace and metrics storage for Elasticsearch 7.x/8.x and OpenSearch ## Scope - Adds authentication support to: - Elasticsearch trace storage (v1 and v2) - OpenSearch trace storage (v1 and v2) - Elasticsearch metrics storage - OpenSearch metrics storage - Backward compatible - authentication is optional ## Related issue Part of #7468 --------- Signed-off-by: SoumyaRaikwar <[email protected]> Signed-off-by: Soumya Raikwar <[email protected]> Co-authored-by: Yuri Shkuro <[email protected]>
1 parent fac4fea commit 1c5531b

File tree

16 files changed

+645
-116
lines changed

16 files changed

+645
-116
lines changed

cmd/jaeger/internal/extension/jaegerstorage/config.go

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,27 +51,9 @@ type TraceBackend struct {
5151
ClickHouse *clickhouse.Configuration `mapstructure:"clickhouse"`
5252
}
5353

54-
// AuthConfig represents authentication configuration for metric backends.
55-
//
56-
// The Authenticator field expects the ID (name) of an HTTP authenticator
57-
// extension that is registered in the running binary and implements
58-
// go.opentelemetry.io/collector/extension/extensionauth.HTTPClient.
59-
//
60-
// Valid values:
61-
// - "sigv4auth" in the stock Jaeger binary (built-in).
62-
// - Any other extension name is valid only if that authenticator extension
63-
// is included in the build; otherwise Jaeger will error at startup when
64-
// resolving the extension.
65-
// - Empty/omitted means no auth (default behavior).
66-
type AuthConfig struct {
67-
// Authenticator is the name (ID) of the HTTP authenticator extension to use.
68-
Authenticator string `mapstructure:"authenticator"`
69-
}
70-
71-
// PrometheusConfiguration wraps the base Prometheus configuration with auth support.
7254
type PrometheusConfiguration struct {
73-
promcfg.Configuration `mapstructure:",squash"`
74-
Auth *AuthConfig `mapstructure:"auth,omitempty"`
55+
Configuration promcfg.Configuration `mapstructure:",squash"`
56+
Authentication escfg.Authentication `mapstructure:"auth"`
7557
}
7658

7759
// MetricBackend contains configuration for a single metric storage backend.

cmd/jaeger/internal/extension/jaegerstorage/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ metric_backends:
116116
`)
117117
cfg := createDefaultConfig().(*Config)
118118
require.NoError(t, conf.Unmarshal(cfg))
119-
assert.NotEmpty(t, cfg.MetricBackends["some_metrics_storage"].Prometheus.ServerURL)
119+
assert.NotEmpty(t, cfg.MetricBackends["some_metrics_storage"].Prometheus.Configuration.ServerURL)
120120
}
121121

122122
func TestConfigDefaultElasticsearchAsMetricsBackend(t *testing.T) {

cmd/jaeger/internal/extension/jaegerstorage/extension.go

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"go.opentelemetry.io/collector/extension/extensionauth"
1515

1616
"github.com/jaegertracing/jaeger/internal/metrics"
17+
"github.com/jaegertracing/jaeger/internal/storage/elasticsearch/config"
1718
esmetrics "github.com/jaegertracing/jaeger/internal/storage/metricstore/elasticsearch"
1819
"github.com/jaegertracing/jaeger/internal/storage/metricstore/prometheus"
1920
"github.com/jaegertracing/jaeger/internal/storage/v1"
@@ -135,9 +136,9 @@ func findExtension(host component.Host) (Extension, error) {
135136
return ext, nil
136137
}
137138

138-
func newStorageExt(config *Config, telset component.TelemetrySettings) *storageExt {
139+
func newStorageExt(cfg *Config, telset component.TelemetrySettings) *storageExt {
139140
return &storageExt{
140-
config: config,
141+
config: cfg,
141142
telset: telset,
142143
factories: make(map[string]tracestore.Factory),
143144
metricsFactories: make(map[string]storage.MetricStoreFactory),
@@ -184,19 +185,30 @@ func (s *storageExt) Start(ctx context.Context, host component.Host) error {
184185
case cfg.Elasticsearch != nil:
185186
esTelset := telset
186187
esTelset.Metrics = scopedMetricsFactory(storageName, "elasticsearch", "tracestore")
188+
httpAuth, authErr := s.resolveAuthenticator(host, cfg.Elasticsearch.Authentication, "elasticsearch", storageName)
189+
if authErr != nil {
190+
return authErr
191+
}
187192
factory, err = es.NewFactory(
188193
ctx,
189194
*cfg.Elasticsearch,
190195
esTelset,
196+
httpAuth,
191197
)
198+
192199
case cfg.Opensearch != nil:
193200
osTelset := telset
194201
osTelset.Metrics = scopedMetricsFactory(storageName, "opensearch", "tracestore")
195-
factory, err = es.NewFactory(
196-
ctx,
202+
httpAuth, authErr := s.resolveAuthenticator(host, cfg.Opensearch.Authentication, "opensearch", storageName)
203+
if authErr != nil {
204+
return authErr
205+
}
206+
factory, err = es.NewFactory(ctx,
197207
*cfg.Opensearch,
198208
osTelset,
209+
httpAuth,
199210
)
211+
200212
case cfg.ClickHouse != nil:
201213
chTelset := telset
202214
chTelset.Metrics = scopedMetricsFactory(storageName, "clickhouse", "tracestore")
@@ -223,46 +235,43 @@ func (s *storageExt) Start(ctx context.Context, host component.Host) error {
223235
case cfg.Prometheus != nil:
224236
promTelset := telset
225237
promTelset.Metrics = scopedMetricsFactory(metricStorageName, "prometheus", "metricstore")
226-
227-
// Resolve authenticator if configured
228-
var httpAuthenticator extensionauth.HTTPClient
229-
if cfg.Prometheus.Auth != nil && cfg.Prometheus.Auth.Authenticator != "" {
230-
httpAuthenticator, err = s.getAuthenticator(host, cfg.Prometheus.Auth.Authenticator)
231-
if err != nil {
232-
return fmt.Errorf("failed to get HTTP authenticator '%s' for metric storage '%s': %w",
233-
cfg.Prometheus.Auth.Authenticator, metricStorageName, err)
234-
}
235-
s.telset.Logger.Sugar().Infof("HTTP auth configured for metric storage '%s' with authenticator '%s'",
236-
metricStorageName, cfg.Prometheus.Auth.Authenticator)
238+
httpAuth, authErr := s.resolveAuthenticator(host, cfg.Prometheus.Authentication, "prometheus metrics", metricStorageName)
239+
if authErr != nil {
240+
return authErr
237241
}
238-
239-
// Create factory with optional authenticator (nil if not configured)
240242
metricStoreFactory, err = prometheus.NewFactoryWithConfig(
241243
cfg.Prometheus.Configuration,
242244
promTelset,
243-
httpAuthenticator,
245+
httpAuth,
244246
)
245-
if err != nil {
246-
return fmt.Errorf("failed to initialize metrics storage '%s': %w", metricStorageName, err)
247-
}
248247

249248
case cfg.Elasticsearch != nil:
250249
esTelset := telset
251250
esTelset.Metrics = scopedMetricsFactory(metricStorageName, "elasticsearch", "metricstore")
251+
httpAuth, authErr := s.resolveAuthenticator(host, cfg.Elasticsearch.Authentication, "elasticsearch metrics", metricStorageName)
252+
if authErr != nil {
253+
return authErr
254+
}
252255
metricStoreFactory, err = esmetrics.NewFactory(
253256
ctx,
254257
*cfg.Elasticsearch,
255258
esTelset,
259+
httpAuth,
256260
)
257261

258262
case cfg.Opensearch != nil:
259263
osTelset := telset
260264
osTelset.Metrics = scopedMetricsFactory(metricStorageName, "opensearch", "metricstore")
261-
metricStoreFactory, err = esmetrics.NewFactory(
262-
ctx,
265+
httpAuth, authErr := s.resolveAuthenticator(host, cfg.Opensearch.Authentication, "opensearch metrics", metricStorageName)
266+
if authErr != nil {
267+
return authErr
268+
}
269+
metricStoreFactory, err = esmetrics.NewFactory(ctx,
263270
*cfg.Opensearch,
264271
osTelset,
272+
httpAuth,
265273
)
274+
266275
default:
267276
err = fmt.Errorf("no metric backend configuration provided for '%s'", metricStorageName)
268277
}
@@ -305,11 +314,14 @@ func (s *storageExt) MetricStorageFactory(name string) (storage.MetricStoreFacto
305314
return mf, ok
306315
}
307316

308-
// getAuthenticator retrieves an HTTP authenticator extension from the host by name
309-
// authentication extension ID, or nil if no extension is configured.
317+
// getAuthenticator retrieves an HTTP authenticator extension from the host by name.
310318
func (*storageExt) getAuthenticator(host component.Host, authenticatorName string) (extensionauth.HTTPClient, error) {
319+
if authenticatorName == "" {
320+
return nil, nil
321+
}
322+
311323
for id, ext := range host.GetExtensions() {
312-
if id.Name() == authenticatorName {
324+
if id.String() == authenticatorName || id.Name() == authenticatorName {
313325
if httpAuth, ok := ext.(extensionauth.HTTPClient); ok {
314326
return httpAuth, nil
315327
}
@@ -318,3 +330,18 @@ func (*storageExt) getAuthenticator(host component.Host, authenticatorName strin
318330
}
319331
return nil, fmt.Errorf("authenticator extension '%s' not found", authenticatorName)
320332
}
333+
334+
// resolveAuthenticator is a helper to resolve and validate HTTP authenticator for a backend
335+
func (s *storageExt) resolveAuthenticator(host component.Host, authCfg config.Authentication, backendType, backendName string) (extensionauth.HTTPClient, error) {
336+
if authCfg.AuthenticatorID.String() == "" {
337+
return nil, nil
338+
}
339+
340+
httpAuth, err := s.getAuthenticator(host, authCfg.AuthenticatorID.String())
341+
if err != nil {
342+
return nil, fmt.Errorf("failed to get HTTP authenticator for %s backend '%s': %w", backendType, backendName, err)
343+
}
344+
s.telset.Logger.Sugar().Infof("HTTP auth configured for %s backend '%s' with authenticator '%s'",
345+
backendType, backendName, authCfg.AuthenticatorID.String())
346+
return httpAuth, nil
347+
}

0 commit comments

Comments
 (0)