Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a94c75b
INFOPLAT-2963 Adds TTL to loop config
hendoxc Oct 10, 2025
76d8492
INFOPLAT-2963 Wires up `Keystore` as `Signer` impl for beholder headers
hendoxc Oct 10, 2025
0ca9acd
Deferred signer
hendoxc Oct 10, 2025
243ee13
Makes lazy signer an interface
hendoxc Oct 14, 2025
a8180d8
Removes keystore mentions from loop server
hendoxc Oct 15, 2025
54deac7
Uses Initial provided headers to the lazy signer
hendoxc Oct 15, 2025
0f81da9
Makes configuration more clear
hendoxc Oct 15, 2025
303df4c
Simplify server beholder auth config
hendoxc Oct 15, 2025
d48f7b2
Adjust `Signer` interface to use `keyID string`
hendoxc Oct 15, 2025
6d982fc
Example of setting signer
hendoxc Oct 15, 2025
2abd8cf
`fmt`
hendoxc Oct 15, 2025
addb026
Sort `Auth` fields
hendoxc Oct 15, 2025
fa3d656
Reduces stuttering in interface
hendoxc Oct 15, 2025
743b596
Address comments
hendoxc Oct 15, 2025
716eed0
Sets config mechanism dependent on authheadertll
hendoxc Oct 15, 2025
b087dad
Merge branch 'main' into INFOPLAT-2963-beholder-rotating-auth-headers…
hendoxc Oct 15, 2025
2b77990
Wire up beholder client in relayer
hendoxc Oct 16, 2025
08a8370
Merge branch 'INFOPLAT-2963-beholder-rotating-auth-headers-loop-impl'…
hendoxc Oct 16, 2025
7a0497f
Merge branch 'main' into INFOPLAT-2963-beholder-rotating-auth-headers…
hendoxc Oct 16, 2025
2cf0fcf
Simply setting signer
hendoxc Oct 16, 2025
6b1456f
Merge branch 'INFOPLAT-2963-beholder-rotating-auth-headers-loop-impl'…
hendoxc Oct 16, 2025
e0b201d
Updates `SetSigner` comment
hendoxc Oct 16, 2025
6781ddd
Removes deferred beholder signer wire up
hendoxc Oct 16, 2025
b2413bd
Merge branch 'main' into INFOPLAT-2963-beholder-rotating-auth-headers…
hendoxc Oct 17, 2025
8f9ed7a
Fixes merge conflict
hendoxc Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/loop/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const (
envTelemetryTraceSampleRatio = "CL_TELEMETRY_TRACE_SAMPLE_RATIO"
envTelemetryAuthHeader = "CL_TELEMETRY_AUTH_HEADER"
envTelemetryAuthPubKeyHex = "CL_TELEMETRY_AUTH_PUB_KEY_HEX"
envTelemetryAuthHeadersTTL = "CL_TELEMETRY_AUTH_HEADERS_TTL"
envTelemetryEmitterBatchProcessor = "CL_TELEMETRY_EMITTER_BATCH_PROCESSOR"
envTelemetryEmitterExportTimeout = "CL_TELEMETRY_EMITTER_EXPORT_TIMEOUT"
envTelemetryEmitterExportInterval = "CL_TELEMETRY_EMITTER_EXPORT_INTERVAL"
Expand Down Expand Up @@ -114,6 +115,7 @@ type EnvConfig struct {
TelemetryTraceSampleRatio float64
TelemetryAuthHeaders map[string]string
TelemetryAuthPubKeyHex string
TelemetryAuthHeadersTTL time.Duration
TelemetryEmitterBatchProcessor bool
TelemetryEmitterExportTimeout time.Duration
TelemetryEmitterExportInterval time.Duration
Expand Down Expand Up @@ -184,6 +186,7 @@ func (e *EnvConfig) AsCmdEnv() (env []string) {
add(envTelemetryAuthHeader+k, v)
}
add(envTelemetryAuthPubKeyHex, e.TelemetryAuthPubKeyHex)
add(envTelemetryAuthHeadersTTL, e.TelemetryAuthHeadersTTL.String())
add(envTelemetryEmitterBatchProcessor, strconv.FormatBool(e.TelemetryEmitterBatchProcessor))
add(envTelemetryEmitterExportTimeout, e.TelemetryEmitterExportTimeout.String())
add(envTelemetryEmitterExportInterval, e.TelemetryEmitterExportInterval.String())
Expand Down Expand Up @@ -331,6 +334,10 @@ func (e *EnvConfig) parse() error {
e.TelemetryTraceSampleRatio = getFloat64OrZero(envTelemetryTraceSampleRatio)
e.TelemetryAuthHeaders = getMap(envTelemetryAuthHeader)
e.TelemetryAuthPubKeyHex = os.Getenv(envTelemetryAuthPubKeyHex)
e.TelemetryAuthHeadersTTL, err = getDuration(envTelemetryAuthHeadersTTL)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", envTelemetryAuthHeadersTTL, err)
}
e.TelemetryEmitterBatchProcessor, err = getBool(envTelemetryEmitterBatchProcessor)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", envTelemetryEmitterBatchProcessor, err)
Expand Down
49 changes: 49 additions & 0 deletions pkg/loop/lazy_keystore_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package loop

import (
"context"
"fmt"
"sync"

"github.com/smartcontractkit/chainlink-common/pkg/types/keystore"
)

// lazyKeystoreSigner is a thread-safe wrapper that allows the keystore
// to be set after the signer is created. This enables beholder to start
// with rotating auth configured, but the actual keystore can be injected later.
type lazyKeystoreSigner struct {
mu sync.RWMutex
keystore keystore.Keystore
}

func newLazyKeystoreSigner() *lazyKeystoreSigner {
return &lazyKeystoreSigner{}
}

// Sign implements the beholder.Signer interface
func (l *lazyKeystoreSigner) Sign(ctx context.Context, keyID []byte, data []byte) ([]byte, error) {
l.mu.RLock()
ks := l.keystore
l.mu.RUnlock()

if ks == nil {
return nil, fmt.Errorf("keystore not yet available for signing")
}

return ks.Sign(ctx, keyID, data)
}

// SetKeystore updates the underlying keystore. This is thread-safe and can be
// called at any time, even after beholder has been initialized.
func (l *lazyKeystoreSigner) SetKeystore(ks keystore.Keystore) {
l.mu.Lock()
defer l.mu.Unlock()
l.keystore = ks
}

// HasKeystore returns true if a keystore has been set
func (l *lazyKeystoreSigner) HasKeystore() bool {
l.mu.RLock()
defer l.mu.RUnlock()
return l.keystore != nil
}
54 changes: 52 additions & 2 deletions pkg/loop/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
"github.com/smartcontractkit/chainlink-common/pkg/sqlutil"
"github.com/smartcontractkit/chainlink-common/pkg/sqlutil/pg"
"github.com/smartcontractkit/chainlink-common/pkg/types/keystore"
)

// NewStartedServer returns a started Server.
Expand Down Expand Up @@ -78,6 +79,10 @@ type Server struct {
checker *services.HealthChecker
LimitsFactory limits.Factory
otelViews []sdkmetric.View
// Keystore is an optional keystore that can be used for beholder auth signing
Keystore keystore.Keystore
// lazySigner allows keystore to be injected after beholder initialization
lazySigner *lazyKeystoreSigner
}

func newServer(loggerName string, otelViews []sdkmetric.View) (*Server, error) {
Expand Down Expand Up @@ -133,8 +138,6 @@ func (s *Server) start() error {
OtelExporterGRPCEndpoint: s.EnvConfig.TelemetryEndpoint,
ResourceAttributes: append(attributes, s.EnvConfig.TelemetryAttributes.AsStringAttributes()...),
TraceSampleRatio: s.EnvConfig.TelemetryTraceSampleRatio,
AuthHeaders: s.EnvConfig.TelemetryAuthHeaders,
AuthPublicKeyHex: s.EnvConfig.TelemetryAuthPubKeyHex,
EmitterBatchProcessor: s.EnvConfig.TelemetryEmitterBatchProcessor,
EmitterExportTimeout: s.EnvConfig.TelemetryEmitterExportTimeout,
EmitterExportInterval: s.EnvConfig.TelemetryEmitterExportInterval,
Expand All @@ -147,6 +150,36 @@ func (s *Server) start() error {
ChipIngressInsecureConnection: s.EnvConfig.ChipIngressInsecureConnection,
}

// Configure beholder auth with lazy keystore injection support
if s.EnvConfig.TelemetryAuthPubKeyHex != "" && s.EnvConfig.TelemetryAuthHeadersTTL > 0 {
// Use lazy signer pattern - allows keystore to be injected after startup
s.lazySigner = newLazyKeystoreSigner()

// If keystore is already available, set it immediately
if s.Keystore != nil {
s.lazySigner.SetKeystore(s.Keystore)
s.Logger.Infow("Using keystore for beholder rotating auth", "ttl", s.EnvConfig.TelemetryAuthHeadersTTL)
} else {
s.Logger.Infow("Beholder rotating auth configured, waiting for keystore injection", "ttl", s.EnvConfig.TelemetryAuthHeadersTTL)
}

beholderCfg.AuthKeySigner = s.lazySigner
beholderCfg.AuthPublicKeyHex = s.EnvConfig.TelemetryAuthPubKeyHex
beholderCfg.AuthHeadersTTL = s.EnvConfig.TelemetryAuthHeadersTTL
} else {
// Fall back to static auth headers if rotating auth not configured
if len(s.EnvConfig.TelemetryAuthHeaders) > 0 {
s.Logger.Info("Using static auth headers for beholder")
beholderCfg.AuthHeaders = s.EnvConfig.TelemetryAuthHeaders
beholderCfg.AuthPublicKeyHex = s.EnvConfig.TelemetryAuthPubKeyHex
} else if s.EnvConfig.TelemetryAuthPubKeyHex != "" {
// Static auth with pub key only
s.Logger.Info("Using static auth with public key for beholder")
beholderCfg.AuthHeaders = s.EnvConfig.TelemetryAuthHeaders
beholderCfg.AuthPublicKeyHex = s.EnvConfig.TelemetryAuthPubKeyHex
}
}

// note: due to the OTEL specification, all histogram buckets
// must be defined when the beholder client is created
beholderCfg.MetricViews = append(beholderCfg.MetricViews, s.otelViews...)
Expand Down Expand Up @@ -237,6 +270,23 @@ func (s *Server) MustRegister(c services.HealthReporter) {

func (s *Server) Register(c services.HealthReporter) error { return s.checker.Register(c) }

// UpdateKeystore injects or updates the keystore used for beholder auth signing.
// This can be called at any time, even after the server has started and beholder
// has been initialized. The next time beholder needs to sign (after TTL expires),
// it will use the new keystore.
func (s *Server) UpdateKeystore(ks keystore.Keystore) {
s.Keystore = ks
if s.lazySigner != nil {
wasSet := s.lazySigner.HasKeystore()
s.lazySigner.SetKeystore(ks)
if wasSet {
s.Logger.Info("Keystore updated for beholder rotating auth")
} else {
s.Logger.Info("Keystore injected for beholder rotating auth")
}
}
}

// Stop closes resources and flushes logs.
func (s *Server) Stop() {
if s.dbStatsReporter != nil {
Expand Down
Loading