diff --git a/aggregator/pkg/middlewares/hmac_auth_middleware.go b/aggregator/pkg/middlewares/hmac_auth_middleware.go index 6a6768f1b..14b68619b 100644 --- a/aggregator/pkg/middlewares/hmac_auth_middleware.go +++ b/aggregator/pkg/middlewares/hmac_auth_middleware.go @@ -60,8 +60,8 @@ func (m *HMACAuthMiddleware) Intercept(ctx context.Context, req any, info *grpc. return nil, status.Error(codes.Unauthenticated, "invalid credentials") } - if len(client.Secrets) == 0 { - m.logger.Errorf("Client %s has no secrets configured", client.ClientID) + if len(client.APIKeys) == 0 { + m.logger.Errorf("Client %s has no API keys configured", client.ClientID) return nil, status.Error(codes.Internal, "authentication configuration error") } @@ -80,7 +80,7 @@ func (m *HMACAuthMiddleware) Intercept(ctx context.Context, req any, info *grpc. stringToSign := hmac.GenerateStringToSign(hmac.HTTPMethodPost, info.FullMethod, bodyHash, apiKey, timestamp) - if !hmac.ValidateSignature(stringToSign, providedSignature, client.Secrets) { + if !hmac.ValidateSignature(stringToSign, providedSignature, client.APIKeys) { m.logger.Warnf("Authentication failed for client %s: invalid signature", client.ClientID) return nil, status.Error(codes.Unauthenticated, "invalid signature") } diff --git a/aggregator/pkg/middlewares/hmac_auth_middleware_test.go b/aggregator/pkg/middlewares/hmac_auth_middleware_test.go index 94f3efa3c..6681f72a0 100644 --- a/aggregator/pkg/middlewares/hmac_auth_middleware_test.go +++ b/aggregator/pkg/middlewares/hmac_auth_middleware_test.go @@ -50,9 +50,9 @@ func createTestAPIKeyConfig() *model.APIKeyConfig { Description: "Test client 1", Enabled: true, IsAdmin: false, - Secrets: map[string]string{ - "current": testSecretCurrent1, - "previous": "secret-old-1", + APIKeys: map[string]string{ + testAPIKey1: testSecretCurrent1, + "previous-key-1": "secret-old-1", // For key rotation testing }, }, "test-api-key-2": { @@ -60,8 +60,8 @@ func createTestAPIKeyConfig() *model.APIKeyConfig { Description: "Test client 2", Enabled: true, IsAdmin: false, - Secrets: map[string]string{ - "current": "secret-current-2", + APIKeys: map[string]string{ + "test-api-key-2": "secret-current-2", }, }, testAdminAPIKey: { @@ -69,8 +69,8 @@ func createTestAPIKeyConfig() *model.APIKeyConfig { Description: "Test admin client", Enabled: true, IsAdmin: true, - Secrets: map[string]string{ - "current": testAdminSecretCurrent, + APIKeys: map[string]string{ + testAdminAPIKey: testAdminSecretCurrent, }, }, }, diff --git a/aggregator/pkg/model/configuration.go b/aggregator/pkg/model/configuration.go index 056a92fe4..687dc0fed 100644 --- a/aggregator/pkg/model/configuration.go +++ b/aggregator/pkg/model/configuration.go @@ -2,8 +2,8 @@ package model import ( "bytes" - "encoding/json" "errors" + "flag" "fmt" "math/big" "os" @@ -13,6 +13,11 @@ import ( "github.com/ethereum/go-ethereum/common" ) +// isRunningInTest returns true if the code is running under `go test`. +func isRunningInTest() bool { + return flag.Lookup("test.v") != nil +} + // Signer represents a participant in the commit verification process. type Signer struct { ParticipantID string `toml:"participantID"` @@ -124,17 +129,36 @@ type ServerConfig struct { // APIClient represents a configured client for API access. type APIClient struct { - ClientID string `toml:"clientId"` - Description string `toml:"description,omitempty"` - Enabled bool `toml:"enabled"` - IsAdmin bool `toml:"isAdmin,omitempty"` - Secrets map[string]string `toml:"secrets,omitempty"` - Groups []string `toml:"groups,omitempty"` + ClientID string `toml:"clientId"` + Description string `toml:"description,omitempty"` + Enabled bool `toml:"enabled"` + IsAdmin bool `toml:"isAdmin,omitempty"` + // APIKeys maps API keys to their corresponding secrets + // This allows multiple active API key/secret pairs for rotation + APIKeys map[string]string `toml:"-"` + Groups []string `toml:"groups,omitempty"` +} + +// ClientEnvVarPair represents an API key and secret environment variable pair. +type ClientEnvVarPair struct { + APIKeyEnv string `toml:"api_key_env"` + SecretEnv string `toml:"secret_env"` +} + +// APIClientMetadata represents client metadata loaded from TOML configuration. +type APIClientMetadata struct { + Description string `toml:"description,omitempty"` + Groups []string `toml:"groups,omitempty"` + Enabled bool `toml:"enabled"` + Admin bool `toml:"admin,omitempty"` + // Environment variable configuration for API key/secret pairs + // This allows arbitrary environment variable names and supports multiple pairs for rotation + KeyPairEnvVars []ClientEnvVarPair `toml:"key_pair_env_vars,omitempty"` } // APIKeyConfig represents the configuration for API key management. type APIKeyConfig struct { - // Clients maps API keys to client configurations + // Clients maps client IDs to client configurations Clients map[string]*APIClient `toml:"clients"` } @@ -316,11 +340,17 @@ type BeholderConfig struct { // GetClientByAPIKey returns the client configuration for a given API key. func (c *APIKeyConfig) GetClientByAPIKey(apiKey string) (*APIClient, bool) { - client, exists := c.Clients[apiKey] - if !exists || !client.Enabled { - return nil, false + // Search through all clients to find the one with this API key + for _, client := range c.Clients { + if !client.Enabled { + continue + } + // Check if this client has the requested API key + if _, exists := client.APIKeys[apiKey]; exists { + return client, true + } } - return client, true + return nil, false } // ValidateAPIKey validates an API key against the configuration. @@ -344,20 +374,23 @@ func (c *APIKeyConfig) ValidateAPIKey(apiKey string) error { // AggregatorConfig is the root configuration for the pb. type AggregatorConfig struct { // CommitteeID are just arbitrary names for different committees this is a concept internal to the aggregator - Committees map[CommitteeID]*Committee `toml:"committees"` - Server ServerConfig `toml:"server"` - Storage *StorageConfig `toml:"storage"` - APIKeys APIKeyConfig `toml:"-"` - ChainStatuses ChainStatusConfig `toml:"chainStatuses"` - Aggregation AggregationConfig `toml:"aggregation"` - OrphanRecovery OrphanRecoveryConfig `toml:"orphanRecovery"` - RateLimiting RateLimitingConfig `toml:"rateLimiting"` - HealthCheck HealthCheckConfig `toml:"healthCheck"` - DisableValidation bool `toml:"disableValidation"` - StubMode bool `toml:"stubQuorumValidation"` - Monitoring MonitoringConfig `toml:"monitoring"` - PyroscopeURL string `toml:"pyroscope_url"` - MaxAnonymousGetMessageSinceRange int64 `toml:"maxAnonymousGetMessageSinceRange"` + Committees map[CommitteeID]*Committee `toml:"committees"` + Server ServerConfig `toml:"server"` + Storage *StorageConfig `toml:"storage"` + // Client credentials loaded from environment variables + APIKeys APIKeyConfig `toml:"-"` + // TOML configuration for client metadata + APIClients map[string]*APIClientMetadata `toml:"apiClients"` + ChainStatuses ChainStatusConfig `toml:"chainStatuses"` + Aggregation AggregationConfig `toml:"aggregation"` + OrphanRecovery OrphanRecoveryConfig `toml:"orphanRecovery"` + RateLimiting RateLimitingConfig `toml:"rateLimiting"` + HealthCheck HealthCheckConfig `toml:"healthCheck"` + DisableValidation bool `toml:"disableValidation"` + StubMode bool `toml:"stubQuorumValidation"` + Monitoring MonitoringConfig `toml:"monitoring"` + PyroscopeURL string `toml:"pyroscope_url"` + MaxAnonymousGetMessageSinceRange int64 `toml:"maxAnonymousGetMessageSinceRange"` } // SetDefaults sets default values for the configuration. @@ -407,16 +440,34 @@ func (c *AggregatorConfig) SetDefaults() { // ValidateAPIKeyConfig validates the API key configuration. func (c *AggregatorConfig) ValidateAPIKeyConfig() error { - // Validate each API key configuration - for apiKey, client := range c.APIKeys.Clients { - if strings.TrimSpace(apiKey) == "" { - return errors.New("api key cannot be empty") + // Validate each client configuration + for clientID, client := range c.APIKeys.Clients { + if strings.TrimSpace(clientID) == "" { + return errors.New("client ID cannot be empty") } if client == nil { - return fmt.Errorf("client configuration for api key '%s' cannot be nil", apiKey) + return fmt.Errorf("client configuration for client ID '%s' cannot be nil", clientID) } if strings.TrimSpace(client.ClientID) == "" { - return fmt.Errorf("client id for api key '%s' cannot be empty", apiKey) + return fmt.Errorf("client ID field for client '%s' cannot be empty", clientID) + } + if client.ClientID != clientID { + return fmt.Errorf("client ID mismatch: map key '%s' does not match client.ClientID '%s'", clientID, client.ClientID) + } + + // Validate that enabled clients have at least one API key + if client.Enabled && len(client.APIKeys) == 0 { + return fmt.Errorf("enabled client '%s' must have at least one API key", clientID) + } + + // Validate API keys and secrets + for apiKey, secret := range client.APIKeys { + if strings.TrimSpace(apiKey) == "" { + return fmt.Errorf("API key cannot be empty for client '%s'", clientID) + } + if strings.TrimSpace(secret) == "" { + return fmt.Errorf("secret cannot be empty for API key '%s' of client '%s'", apiKey, clientID) + } } // Validate group references @@ -469,6 +520,15 @@ func (c *AggregatorConfig) ValidateStorageConfig() error { return errors.New("storage.pageSize cannot exceed 1000") } + // Validate that PostgreSQL storage has a connection URL + // In test environments, allow dynamic configuration of storage URL + if c.Storage.StorageType == StorageTypePostgreSQL && c.Storage.ConnectionURL == "" { + if !isRunningInTest() { + return errors.New("PostgreSQL storage requires a connection URL") + } + // In tests, we allow storage URL to be set up dynamically after config loading + } + return nil } @@ -531,25 +591,20 @@ func (c *AggregatorConfig) Validate() error { } func (c *AggregatorConfig) LoadFromEnvironment() error { - if c.Storage.StorageType == StorageTypePostgreSQL { + // Load storage connection URL if using PostgreSQL and not already configured + if c.Storage.StorageType == StorageTypePostgreSQL && c.Storage.ConnectionURL == "" { storageURL := os.Getenv("AGGREGATOR_STORAGE_CONNECTION_URL") - if storageURL == "" { - return errors.New("AGGREGATOR_STORAGE_CONNECTION_URL environment variable is required") + if storageURL != "" { + c.Storage.ConnectionURL = storageURL } - c.Storage.ConnectionURL = storageURL + // Note: Empty storage URL is allowed for tests where storage is configured dynamically } - apiKeysJSON := os.Getenv("AGGREGATOR_API_KEYS_JSON") - if apiKeysJSON == "" { - return errors.New("AGGREGATOR_API_KEYS_JSON environment variable is required") + // Load API clients from individual environment variables + if err := c.loadAPIClientsFromEnvironment(); err != nil { + return fmt.Errorf("failed to load API clients from environment: %w", err) } - var apiKeyConfig APIKeyConfig - if err := json.Unmarshal([]byte(apiKeysJSON), &apiKeyConfig); err != nil { - return fmt.Errorf("failed to parse AGGREGATOR_API_KEYS_JSON: %w", err) - } - c.APIKeys = apiKeyConfig - if c.RateLimiting.Storage.Type == RateLimiterStoreTypeRedis { if err := c.loadRateLimiterRedisConfigFromEnvironment(); err != nil { return fmt.Errorf("failed to load rate limiter redis config from environment: %w", err) @@ -559,6 +614,72 @@ func (c *AggregatorConfig) LoadFromEnvironment() error { return nil } +func (c *AggregatorConfig) loadAPIClientsFromEnvironment() error { + // Initialize APIKeys if nil + if c.APIKeys.Clients == nil { + c.APIKeys.Clients = make(map[string]*APIClient) + } + + // If APIKeys are already populated (e.g., in tests), skip environment loading + if len(c.APIKeys.Clients) > 0 { + return nil + } + + if len(c.APIClients) == 0 { + return nil + } + + // For each client defined in metadata, load their API keys and secrets from environment + for clientID, metadata := range c.APIClients { + // Create the client configuration from metadata + client := &APIClient{ + ClientID: clientID, + Description: metadata.Description, + Groups: metadata.Groups, + Enabled: metadata.Enabled, + IsAdmin: metadata.Admin, + APIKeys: make(map[string]string), + } + + foundKeys := false + + // Require explicit environment variable configuration + if len(metadata.KeyPairEnvVars) == 0 { + return fmt.Errorf("client '%s' has no key_pair_env_vars configured - explicit environment variable configuration is required", clientID) + } + + for _, envVarPair := range metadata.KeyPairEnvVars { + if envVarPair.APIKeyEnv == "" || envVarPair.SecretEnv == "" { + return fmt.Errorf("client '%s' has incomplete environment variable configuration: both api_key_env and secret_env must be specified", clientID) + } + + apiKey := os.Getenv(envVarPair.APIKeyEnv) + secret := os.Getenv(envVarPair.SecretEnv) + + if apiKey != "" && secret != "" { + client.APIKeys[apiKey] = secret + foundKeys = true + } else if apiKey == "" || secret == "" { + // If only one is set, that's an error + return fmt.Errorf("both %s and %s must be set together for client '%s'", envVarPair.APIKeyEnv, envVarPair.SecretEnv, clientID) + } + // If both are empty, we just skip this pair (allows optional backup keys) + } + + // Require at least one API key/secret pair for enabled clients + if client.Enabled && !foundKeys { + return fmt.Errorf("enabled client '%s' has no API key/secret pairs in environment variables", clientID) + } + + // Only store clients that have at least one API key or are explicitly disabled + if foundKeys || !client.Enabled { + c.APIKeys.Clients[clientID] = client + } + } + + return nil +} + func (c *AggregatorConfig) loadRateLimiterRedisConfigFromEnvironment() error { redisAddress := os.Getenv("AGGREGATOR_REDIS_ADDRESS") if redisAddress == "" { diff --git a/aggregator/testconfig/default/aggregator.toml b/aggregator/testconfig/default/aggregator.toml index 37ec1fea7..555aeab2c 100644 --- a/aggregator/testconfig/default/aggregator.toml +++ b/aggregator/testconfig/default/aggregator.toml @@ -96,3 +96,34 @@ LogStreamingEnabled = true MetricReaderInterval = 5 TraceSampleRatio = 1.0 TraceBatchTimeout = 10 + +# API Client Metadata Configuration +# This section defines static metadata for API clients +# The actual API keys and secrets are loaded from environment variables + +[apiClients.verifier_1] +description = "Development default verifier node 1" +groups = ["verifiers"] +enabled = true +admin = false +key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_1_API_KEY", secret_env = "AGGREGATOR_VERIFIER_1_SECRET" } +] + +[apiClients.verifier_2] +description = "Development default verifier node 2" +groups = ["verifiers"] +enabled = true +admin = false +key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_2_API_KEY", secret_env = "AGGREGATOR_VERIFIER_2_SECRET" } +] + +[apiClients.monitoring] +description = "Monitoring and infrastructure client" +groups = ["monitoring"] +enabled = true +admin = false +key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_MONITORING_API_KEY", secret_env = "AGGREGATOR_MONITORING_SECRET" } +] diff --git a/aggregator/testconfig/secondary/aggregator.toml b/aggregator/testconfig/secondary/aggregator.toml index a93bfb4e1..42e94ed93 100644 --- a/aggregator/testconfig/secondary/aggregator.toml +++ b/aggregator/testconfig/secondary/aggregator.toml @@ -96,3 +96,34 @@ LogStreamingEnabled = true MetricReaderInterval = 5 TraceSampleRatio = 1.0 TraceBatchTimeout = 10 + +# API Client Metadata Configuration +# This section defines static metadata for API clients +# The actual API keys and secrets are loaded from environment variables + +[apiClients.verifier_1] +description = "Development secondary verifier node 1" +groups = ["verifiers"] +enabled = true +admin = false +key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_1_API_KEY", secret_env = "AGGREGATOR_VERIFIER_1_SECRET" } +] + +[apiClients.verifier_2] +description = "Development secondary verifier node 2" +groups = ["verifiers"] +enabled = true +admin = false +key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_2_API_KEY", secret_env = "AGGREGATOR_VERIFIER_2_SECRET" } +] + +[apiClients.monitoring] +description = "Monitoring and infrastructure client" +groups = ["monitoring"] +enabled = true +admin = false +key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_MONITORING_API_KEY", secret_env = "AGGREGATOR_MONITORING_SECRET" } +] diff --git a/aggregator/testconfig/tertiary/aggregator.toml b/aggregator/testconfig/tertiary/aggregator.toml index 28b2fc30f..07c9f4ecd 100644 --- a/aggregator/testconfig/tertiary/aggregator.toml +++ b/aggregator/testconfig/tertiary/aggregator.toml @@ -96,3 +96,34 @@ LogStreamingEnabled = true MetricReaderInterval = 5 TraceSampleRatio = 1.0 TraceBatchTimeout = 10 + +# API Client Metadata Configuration +# This section defines static metadata for API clients +# The actual API keys and secrets are loaded from environment variables + +[apiClients.verifier_1] +description = "Development tertiary verifier node 1" +groups = ["verifiers"] +enabled = true +admin = false +key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_1_API_KEY", secret_env = "AGGREGATOR_VERIFIER_1_SECRET" } +] + +[apiClients.verifier_2] +description = "Development tertiary verifier node 2" +groups = ["verifiers"] +enabled = true +admin = false +key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_2_API_KEY", secret_env = "AGGREGATOR_VERIFIER_2_SECRET" } +] + +[apiClients.monitoring] +description = "Monitoring and infrastructure client" +groups = ["monitoring"] +enabled = true +admin = false +key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_MONITORING_API_KEY", secret_env = "AGGREGATOR_MONITORING_SECRET" } +] diff --git a/aggregator/tests/chain_status_integration_test.go b/aggregator/tests/chain_status_integration_test.go index 44a22eeb3..0526b04a5 100644 --- a/aggregator/tests/chain_status_integration_test.go +++ b/aggregator/tests/chain_status_integration_test.go @@ -4,6 +4,8 @@ package tests import ( "context" "fmt" + "os" + "strings" "sync" "testing" "time" @@ -55,48 +57,92 @@ func WithChainStatusTestClients() ConfigOption { testClients = append(testClients, clientID) } - // Configure regular test clients + // Initialize APIClients metadata map + if cfg.APIClients == nil { + cfg.APIClients = make(map[string]*model.APIClientMetadata) + } + + // Configure metadata for regular test clients for _, clientID := range testClients { - secret := "secret-" + clientID - cfg.APIKeys.Clients[clientID] = &model.APIClient{ - ClientID: clientID, + apiKeyEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_API_KEY" + secretEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_SECRET" + + cfg.APIClients[clientID] = &model.APIClientMetadata{ Description: "Test client for " + clientID, + Groups: []string{}, Enabled: true, - IsAdmin: false, - Secrets: map[string]string{ - "current": secret, + Admin: false, + KeyPairEnvVars: []model.ClientEnvVarPair{ + { + APIKeyEnv: apiKeyEnv, + SecretEnv: secretEnv, + }, }, } } - // Configure admin clients + // Configure metadata for admin clients for _, clientID := range adminClients { - secret := "secret-" + clientID - cfg.APIKeys.Clients[clientID] = &model.APIClient{ - ClientID: clientID, + apiKeyEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_API_KEY" + secretEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_SECRET" + + cfg.APIClients[clientID] = &model.APIClientMetadata{ Description: "Admin test client for " + clientID, + Groups: []string{}, Enabled: true, - IsAdmin: true, - Secrets: map[string]string{ - "current": secret, + Admin: true, + KeyPairEnvVars: []model.ClientEnvVarPair{ + { + APIKeyEnv: apiKeyEnv, + SecretEnv: secretEnv, + }, }, } } - // Configure verifier clients for admin tests + // Configure metadata for verifier clients for _, clientID := range verifierClients { - secret := "secret-" + clientID - cfg.APIKeys.Clients[clientID] = &model.APIClient{ - ClientID: clientID, + apiKeyEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_API_KEY" + secretEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_SECRET" + + cfg.APIClients[clientID] = &model.APIClientMetadata{ Description: "Verifier test client for " + clientID, + Groups: []string{"verifiers"}, Enabled: true, - IsAdmin: false, - Secrets: map[string]string{ - "current": secret, + Admin: false, + KeyPairEnvVars: []model.ClientEnvVarPair{ + { + APIKeyEnv: apiKeyEnv, + SecretEnv: secretEnv, + }, }, } } + // Set up environment variables for all test clients + for _, clientID := range testClients { + apiKeyEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_API_KEY" + secretEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_SECRET" + _ = os.Setenv(apiKeyEnv, "key-"+clientID) + _ = os.Setenv(secretEnv, "secret-"+clientID) + } + + // Set up environment variables for admin clients + for _, clientID := range adminClients { + apiKeyEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_API_KEY" + secretEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_SECRET" + _ = os.Setenv(apiKeyEnv, "key-"+clientID) + _ = os.Setenv(secretEnv, "secret-"+clientID) + } + + // Set up environment variables for verifier clients + for _, clientID := range verifierClients { + apiKeyEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_API_KEY" + secretEnv := "AGGREGATOR_" + strings.ToUpper(strings.ReplaceAll(clientID, "-", "_")) + "_SECRET" + _ = os.Setenv(apiKeyEnv, "key-"+clientID) + _ = os.Setenv(secretEnv, "secret-"+clientID) + } + return cfg, clientCfg } } @@ -110,13 +156,13 @@ func TestChainStatusClientIsolation(t *testing.T) { defer cleanup() // Create separate clients with different credentials - client1, _, cleanup1 := CreateAuthenticatedClient(t, listener, WithClientAuth("isolation-client-1", "secret-isolation-client-1")) + client1, _, cleanup1 := CreateAuthenticatedClient(t, listener, WithClientAuth("key-isolation-client-1", "secret-isolation-client-1")) defer cleanup1() - client2, _, cleanup2 := CreateAuthenticatedClient(t, listener, WithClientAuth("isolation-client-2", "secret-isolation-client-2")) + client2, _, cleanup2 := CreateAuthenticatedClient(t, listener, WithClientAuth("key-isolation-client-2", "secret-isolation-client-2")) defer cleanup2() - client3, _, cleanup3 := CreateAuthenticatedClient(t, listener, WithClientAuth("isolation-client-3", "secret-isolation-client-3")) + client3, _, cleanup3 := CreateAuthenticatedClient(t, listener, WithClientAuth("key-isolation-client-3", "secret-isolation-client-3")) defer cleanup3() // Client 1 stores chain status @@ -189,7 +235,7 @@ func TestChainStatusClientIsolation(t *testing.T) { clients := make([]*clientInfo, numClients) for i := 0; i < numClients; i++ { clientID := "same-chain-client-" + string(rune('A'+i)) - aggClient, _, clientCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth(clientID, "secret-"+clientID)) + aggClient, _, clientCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-"+clientID, "secret-"+clientID)) clients[i] = &clientInfo{ client: aggClient, clientID: clientID, @@ -233,9 +279,9 @@ func TestChainStatusClientIsolation(t *testing.T) { defer cleanup() // Create two separate clients - clientA, _, cleanupA := CreateAuthenticatedClient(t, listener, WithClientAuth("update-client-A", "secret-update-client-A")) + clientA, _, cleanupA := CreateAuthenticatedClient(t, listener, WithClientAuth("key-update-client-A", "secret-update-client-A")) defer cleanupA() - clientB, _, cleanupB := CreateAuthenticatedClient(t, listener, WithClientAuth("update-client-B", "secret-update-client-B")) + clientB, _, cleanupB := CreateAuthenticatedClient(t, listener, WithClientAuth("key-update-client-B", "secret-update-client-B")) defer cleanupB() // Both clients store initial data @@ -339,7 +385,7 @@ func TestChainStatusConcurrency(t *testing.T) { clients := make([]*clientInfo, numClients) for i := 0; i < numClients; i++ { clientID := "concurrent-client-" + string(rune('A'+i%26)) + string(rune('A'+i/26)) - aggClient, _, clientCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth(clientID, "secret-"+clientID)) + aggClient, _, clientCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-"+clientID, "secret-"+clientID)) clients[i] = &clientInfo{ client: aggClient, clientID: clientID, @@ -545,9 +591,9 @@ func TestChainStatusClientIsolation_DynamoDB(t *testing.T) { defer cleanup() // Create two separate clients - client1, _, cleanup1 := CreateAuthenticatedClient(t, listener, WithClientAuth("ddb-isolation-client-1", "secret-ddb-isolation-client-1")) + client1, _, cleanup1 := CreateAuthenticatedClient(t, listener, WithClientAuth("key-ddb-isolation-client-1", "secret-ddb-isolation-client-1")) defer cleanup1() - client2, _, cleanup2 := CreateAuthenticatedClient(t, listener, WithClientAuth("ddb-isolation-client-2", "secret-ddb-isolation-client-2")) + client2, _, cleanup2 := CreateAuthenticatedClient(t, listener, WithClientAuth("key-ddb-isolation-client-2", "secret-ddb-isolation-client-2")) defer cleanup2() // Client 1 stores chain status @@ -658,11 +704,11 @@ func TestChainStatusAdminAPI(t *testing.T) { defer cleanup() // Create verifier client - verifierClient, _, verifierCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("verifier-client-1", "secret-verifier-client-1")) + verifierClient, _, verifierCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-verifier-client-1", "secret-verifier-client-1")) defer verifierCleanup() // Create admin client - adminClient, _, adminCleanup := CreateAdminAuthenticatedClient(t, listener, "admin-client-1", "secret-admin-client-1", "") + adminClient, _, adminCleanup := CreateAdminAuthenticatedClient(t, listener, "key-admin-client-1", "secret-admin-client-1", "") defer adminCleanup() // Verifier stores initial chain status @@ -681,7 +727,7 @@ func TestChainStatusAdminAPI(t *testing.T) { require.Len(t, verifierResp.Statuses, 2, "verifier should see their data") // Admin overrides verifier data using on-behalf-of - adminOverrideClient, _, adminOverrideCleanup := CreateAdminAuthenticatedClient(t, listener, "admin-client-1", "secret-admin-client-1", "verifier-client-1") + adminOverrideClient, _, adminOverrideCleanup := CreateAdminAuthenticatedClient(t, listener, "key-admin-client-1", "secret-admin-client-1", "verifier-client-1") defer adminOverrideCleanup() overrideReq := &pb.WriteChainStatusRequest{ @@ -724,7 +770,7 @@ func TestChainStatusAdminAPI(t *testing.T) { defer cleanup() // Create two verifier clients - verifier1Client, _, verifier1Cleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("verifier-client-1", "secret-verifier-client-1")) + verifier1Client, _, verifier1Cleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-verifier-client-1", "secret-verifier-client-1")) defer verifier1Cleanup() // We don't need to create verifier2Client, we only need to test the attack attempt @@ -740,7 +786,7 @@ func TestChainStatusAdminAPI(t *testing.T) { // Try to create a "fake admin" client using verifier 2 credentials with admin header // This should fail because verifier 2 is not an admin - fakeAdminClient, _, fakeAdminCleanup := CreateAdminAuthenticatedClient(t, listener, "verifier-client-2", "secret-verifier-client-2", "verifier-client-1") + fakeAdminClient, _, fakeAdminCleanup := CreateAdminAuthenticatedClient(t, listener, "key-verifier-client-2", "secret-verifier-client-2", "verifier-client-1") defer fakeAdminCleanup() attackReq := &pb.WriteChainStatusRequest{ @@ -767,11 +813,11 @@ func TestChainStatusAdminAPI(t *testing.T) { defer cleanup() // Create verifier client - verifierClient, _, verifierCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("verifier-client-1", "secret-verifier-client-1")) + verifierClient, _, verifierCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-verifier-client-1", "secret-verifier-client-1")) defer verifierCleanup() // Admin sets extreme configuration values on behalf of verifier - adminOverrideClient, _, adminOverrideCleanup := CreateAdminAuthenticatedClient(t, listener, "admin-client-1", "secret-admin-client-1", "verifier-client-1") + adminOverrideClient, _, adminOverrideCleanup := CreateAdminAuthenticatedClient(t, listener, "key-admin-client-1", "secret-admin-client-1", "verifier-client-1") defer adminOverrideCleanup() extremeReq := &pb.WriteChainStatusRequest{ @@ -809,11 +855,11 @@ func TestChainStatusAdminAPI(t *testing.T) { defer cleanup() // Create multiple verifier clients - verifier1Client, _, verifier1Cleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("verifier-client-1", "secret-verifier-client-1")) + verifier1Client, _, verifier1Cleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-verifier-client-1", "secret-verifier-client-1")) defer verifier1Cleanup() - verifier2Client, _, verifier2Cleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("verifier-client-2", "secret-verifier-client-2")) + verifier2Client, _, verifier2Cleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-verifier-client-2", "secret-verifier-client-2")) defer verifier2Cleanup() - verifier3Client, _, verifier3Cleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("verifier-client-3", "secret-verifier-client-3")) + verifier3Client, _, verifier3Cleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-verifier-client-3", "secret-verifier-client-3")) defer verifier3Cleanup() // Each verifier stores initial data @@ -828,7 +874,7 @@ func TestChainStatusAdminAPI(t *testing.T) { } // Admin overrides only verifier 2's data - adminOverrideClient, _, adminOverrideCleanup := CreateAdminAuthenticatedClient(t, listener, "admin-client-1", "secret-admin-client-1", "verifier-client-2") + adminOverrideClient, _, adminOverrideCleanup := CreateAdminAuthenticatedClient(t, listener, "key-admin-client-1", "secret-admin-client-1", "verifier-client-2") defer adminOverrideCleanup() overrideReq := &pb.WriteChainStatusRequest{ @@ -866,11 +912,11 @@ func TestChainStatusAdminAPI(t *testing.T) { defer cleanup() // Create admin client without on-behalf-of header - adminClient, _, adminCleanup := CreateAdminAuthenticatedClient(t, listener, "admin-client-1", "secret-admin-client-1", "") + adminClient, _, adminCleanup := CreateAdminAuthenticatedClient(t, listener, "key-admin-client-1", "secret-admin-client-1", "") defer adminCleanup() // Create verifier client - verifierClient, _, verifierCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("verifier-client-1", "secret-verifier-client-1")) + verifierClient, _, verifierCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-verifier-client-1", "secret-verifier-client-1")) defer verifierCleanup() // Verifier stores data @@ -912,11 +958,11 @@ func TestChainStatusAdminAPI(t *testing.T) { defer cleanup() // Create verifier client - verifierClient, _, verifierCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("verifier-client-1", "secret-verifier-client-1")) + verifierClient, _, verifierCleanup := CreateAuthenticatedClient(t, listener, WithClientAuth("key-verifier-client-1", "secret-verifier-client-1")) defer verifierCleanup() // Create admin client - adminOverrideClient, _, adminOverrideCleanup := CreateAdminAuthenticatedClient(t, listener, "admin-client-1", "secret-admin-client-1", "verifier-client-1") + adminOverrideClient, _, adminOverrideCleanup := CreateAdminAuthenticatedClient(t, listener, "key-admin-client-1", "secret-admin-client-1", "verifier-client-1") defer adminOverrideCleanup() // Verifier stores initial data with 10 chain selectors diff --git a/aggregator/tests/utils.go b/aggregator/tests/utils.go index 9ecc94e23..d12430ef9 100644 --- a/aggregator/tests/utils.go +++ b/aggregator/tests/utils.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "net" + "os" + "strings" "testing" "time" @@ -31,6 +33,21 @@ var ( defaultSecret = "test-secret-key" ) +// setupTestEnvironmentVariables sets up environment variables for testing. +func setupTestEnvironmentVariables(t *testing.T, apiKey, secret string) { + // Set up environment variables for the default test client + // In test context, we can safely ignore errors from environment variable setting + _ = os.Setenv("AGGREGATOR_TEST_CLIENT_API_KEY", apiKey) + _ = os.Setenv("AGGREGATOR_TEST_CLIENT_SECRET", secret) + + // Clean up environment variables when test completes + t.Cleanup(func() { + // In test context, we can safely ignore errors from environment variable cleanup + _ = os.Unsetenv("AGGREGATOR_TEST_CLIENT_API_KEY") + _ = os.Unsetenv("AGGREGATOR_TEST_CLIENT_SECRET") + }) +} + // ClientConfig holds configuration for test client behavior. type ClientConfig struct { SkipAuth bool @@ -80,14 +97,16 @@ func WithShardCount(shardCount int) ConfigOption { func WithAPIKeyAuth(apiKey, secret string) ConfigOption { return func(cfg *model.AggregatorConfig, clientCfg *ClientConfig) (*model.AggregatorConfig, *ClientConfig) { - cfg.APIKeys.Clients[apiKey] = &model.APIClient{ - ClientID: apiKey, - Description: "Custom test client", - Enabled: true, - Secrets: map[string]string{ - "current": secret, - }, - } + // Set environment variables for this custom client + // Use a normalized client ID for environment variable naming + clientID := "custom_test_client" + envAPIKey := fmt.Sprintf("AGGREGATOR_%s_API_KEY", strings.ToUpper(clientID)) + envSecret := fmt.Sprintf("AGGREGATOR_%s_SECRET", strings.ToUpper(clientID)) + + // In test context, we can safely ignore errors from environment variable setting + _ = os.Setenv(envAPIKey, apiKey) + _ = os.Setenv(envSecret, secret) + return cfg, clientCfg } } @@ -165,7 +184,7 @@ func CreateServerOnly(t *testing.T, options ...ConfigOption) (*bufconn.Listener, // Use SugaredLogger for better API sugaredLggr := logger.Sugared(lggr) - // Create base config with DynamoDB storage as default + // Create base config with PostgreSQL storage as default config := &model.AggregatorConfig{ Server: model.ServerConfig{ Address: ":50051", @@ -179,6 +198,20 @@ func CreateServerOnly(t *testing.T, options ...ConfigOption) (*bufconn.Listener, APIKeys: model.APIKeyConfig{ Clients: make(map[string]*model.APIClient), }, + // Add explicit API client metadata for tests + APIClients: map[string]*model.APIClientMetadata{ + "test_client": { + Description: "Test client for integration tests", + Enabled: true, + Admin: false, + KeyPairEnvVars: []model.ClientEnvVarPair{ + { + APIKeyEnv: "AGGREGATOR_TEST_CLIENT_API_KEY", + SecretEnv: "AGGREGATOR_TEST_CLIENT_SECRET", + }, + }, + }, + }, RateLimiting: model.RateLimitingConfig{ Enabled: true, Storage: model.RateLimiterStoreConfig{ @@ -197,25 +230,24 @@ func CreateServerOnly(t *testing.T, options ...ConfigOption) (*bufconn.Listener, }, } - config.APIKeys.Clients[defaultAPIKey] = &model.APIClient{ - ClientID: "test-client", - Description: "Test client for integration tests", - Enabled: true, - Secrets: map[string]string{ - "current": defaultSecret, - }, - } - clientConfig := &ClientConfig{ SkipAuth: false, APIKey: defaultAPIKey, Secret: defaultSecret, } + // Set up default test environment variables for API clients + setupTestEnvironmentVariables(t, defaultAPIKey, defaultSecret) + for _, option := range options { config, clientConfig = option(config, clientConfig) } + // Load API clients from environment variables + if err := config.LoadFromEnvironment(); err != nil { + return nil, nil, fmt.Errorf("failed to load configuration from environment: %w", err) + } + // Setup storage based on final configuration var cleanupStorage func() diff --git a/build/devenv/env.toml b/build/devenv/env.toml index c10724325..b605956d4 100644 --- a/build/devenv/env.toml +++ b/build/devenv/env.toml @@ -261,40 +261,21 @@ Type = "always" redis_address = "default-aggregator-redis:6379" redis_password = "" redis_db = "0" - api_keys_json = ''' -{ - "clients": { - "dev-api-key-verifier-1": { - "clientId": "default-verifier-1", - "description": "Development default verifier 1", - "enabled": true, - "groups": ["verifiers"], - "secrets": { - "primary": "dev-secret-verifier-1" - } - }, - "dev-api-key-verifier-2": { - "clientId": "default-verifier-2", - "description": "Development default verifier 2", - "enabled": true, - "groups": ["verifiers"], - "secrets": { - "primary": "dev-secret-verifier-2" - } - }, - "dev-api-key-monitoring": { - "clientId": "monitoring", - "description": "Monitoring and infrastructure client", - "enabled": true, - "groups": ["monitoring"], - "secrets": { - "primary": "dev-secret-monitoring" - } - } - } -} -''' - + # Client credentials using map-based structure supporting key rotation + # Each client can have multiple key_pair_env_vars for arbitrary environment variable naming + [aggregator.env.clients] + [aggregator.env.clients.verifier_1] + key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_1_API_KEY", secret_env = "AGGREGATOR_VERIFIER_1_SECRET", api_key_value = "dev-api-key-verifier-1", secret_value = "dev-secret-verifier-1" } + ] + [aggregator.env.clients.verifier_2] + key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_2_API_KEY", secret_env = "AGGREGATOR_VERIFIER_2_SECRET", api_key_value = "dev-api-key-verifier-2", secret_value = "dev-secret-verifier-2" } + ] + [aggregator.env.clients.monitoring] + key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_MONITORING_API_KEY", secret_env = "AGGREGATOR_MONITORING_SECRET", api_key_value = "dev-monitoring-api-key", secret_value = "dev-monitoring-secret" } + ] [[aggregator]] committee_name = "secondary" image = "aggregator:dev" @@ -316,39 +297,21 @@ Type = "always" redis_address = "secondary-aggregator-redis:6379" redis_password = "" redis_db = "0" - api_keys_json = ''' -{ - "clients": { - "dev-api-key-secondary-verifier-1": { - "clientId": "secondary-verifier-1", - "description": "Development secondary verifier 1", - "enabled": true, - "groups": ["verifiers"], - "secrets": { - "primary": "dev-secret-secondary-verifier-1" - } - }, - "dev-api-key-secondary-verifier-2": { - "clientId": "secondary-verifier-2", - "description": "Development secondary verifier 2", - "enabled": true, - "groups": ["verifiers"], - "secrets": { - "primary": "dev-secret-secondary-verifier-2" - } - }, - "dev-api-key-monitoring": { - "clientId": "monitoring", - "description": "Monitoring and infrastructure client", - "enabled": true, - "groups": ["monitoring"], - "secrets": { - "primary": "dev-secret-monitoring" - } - } - } -} -''' + # Client credentials using map-based structure supporting key rotation + # Each client can have multiple key_pair_env_vars for arbitrary environment variable naming + [aggregator.env.clients] + [aggregator.env.clients.verifier_1] + key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_1_API_KEY", secret_env = "AGGREGATOR_VERIFIER_1_SECRET", api_key_value = "dev-api-key-secondary-verifier-1", secret_value = "dev-secret-secondary-verifier-1" } + ] + [aggregator.env.clients.verifier_2] + key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_2_API_KEY", secret_env = "AGGREGATOR_VERIFIER_2_SECRET", api_key_value = "dev-api-key-secondary-verifier-2", secret_value = "dev-secret-secondary-verifier-2" } + ] + [aggregator.env.clients.monitoring] + key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_MONITORING_API_KEY", secret_env = "AGGREGATOR_MONITORING_SECRET", api_key_value = "dev-secondary-monitoring-api-key", secret_value = "dev-secondary-monitoring-secret" } + ] [[aggregator]] committee_name = "tertiary" @@ -371,39 +334,21 @@ Type = "always" redis_address = "tertiary-aggregator-redis:6379" redis_password = "" redis_db = "0" - api_keys_json = ''' -{ - "clients": { - "dev-api-key-tertiary-verifier-1": { - "clientId": "tertiary-verifier-1", - "description": "Development tertiary verifier 1", - "enabled": true, - "groups": ["verifiers"], - "secrets": { - "primary": "dev-secret-tertiary-verifier-1" - } - }, - "dev-api-key-tertiary-verifier-2": { - "clientId": "tertiary-verifier-2", - "description": "Development tertiary verifier 2", - "enabled": true, - "groups": ["verifiers"], - "secrets": { - "primary": "dev-secret-tertiary-verifier-2" - } - }, - "dev-api-key-monitoring": { - "clientId": "monitoring", - "description": "Monitoring and infrastructure client", - "enabled": true, - "groups": ["monitoring"], - "secrets": { - "primary": "dev-secret-monitoring" - } - } - } -} -''' + # Client credentials using map-based structure supporting key rotation + # Each client can have multiple key_pair_env_vars for arbitrary environment variable naming + [aggregator.env.clients] + [aggregator.env.clients.verifier_1] + key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_1_API_KEY", secret_env = "AGGREGATOR_VERIFIER_1_SECRET", api_key_value = "dev-api-key-tertiary-verifier-1", secret_value = "dev-secret-tertiary-verifier-1" } + ] + [aggregator.env.clients.verifier_2] + key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_VERIFIER_2_API_KEY", secret_env = "AGGREGATOR_VERIFIER_2_SECRET", api_key_value = "dev-api-key-tertiary-verifier-2", secret_value = "dev-secret-tertiary-verifier-2" } + ] + [aggregator.env.clients.monitoring] + key_pair_env_vars = [ + { api_key_env = "AGGREGATOR_MONITORING_API_KEY", secret_env = "AGGREGATOR_MONITORING_SECRET", api_key_value = "dev-tertiary-monitoring-api-key", secret_value = "dev-tertiary-monitoring-secret" } + ] [[nodesets]] name = "don" diff --git a/build/devenv/services/aggregator.go b/build/devenv/services/aggregator.go index 2670bbb74..8d5edebdf 100644 --- a/build/devenv/services/aggregator.go +++ b/build/devenv/services/aggregator.go @@ -50,7 +50,23 @@ type AggregatorEnvConfig struct { RedisAddress string `toml:"redis_address"` RedisPassword string `toml:"redis_password"` RedisDB string `toml:"redis_db"` - APIKeysJSON string `toml:"api_keys_json"` + // Client credentials - supports arbitrary number of clients + // Map key is the client identifier, value contains API key and secret + Clients map[string]ClientCredentials `toml:"clients"` +} + +// ClientCredentials holds API credentials for a client with support for multiple key pairs. +type ClientCredentials struct { + // Support multiple environment variable pairs for rotation + KeyPairEnvVars []ClientEnvVarPair `toml:"key_pair_env_vars,omitempty"` +} + +// ClientEnvVarPair represents environment variable names and values for API key and secret. +type ClientEnvVarPair struct { + APIKeyEnv string `toml:"api_key_env"` + SecretEnv string `toml:"secret_env"` + APIKeyValue string `toml:"api_key_value,omitempty"` + SecretValue string `toml:"secret_value,omitempty"` } type AggregatorInput struct { @@ -251,10 +267,17 @@ func NewAggregator(in *AggregatorInput) (*AggregatorOutput, error) { } envVars["AGGREGATOR_STORAGE_CONNECTION_URL"] = in.Env.StorageConnectionURL - if in.Env.APIKeysJSON == "" { - return nil, fmt.Errorf("AGGREGATOR_API_KEYS_JSON is required in env config") + // Set client environment variables using the new map-based approach with rotation support + for _, creds := range in.Env.Clients { + // Support multiple environment variable pairs from metadata + if len(creds.KeyPairEnvVars) > 0 { + for _, envVarPair := range creds.KeyPairEnvVars { + // Use the explicit environment variable names and values from metadata + envVars[envVarPair.APIKeyEnv] = envVarPair.APIKeyValue + envVars[envVarPair.SecretEnv] = envVarPair.SecretValue + } + } } - envVars["AGGREGATOR_API_KEYS_JSON"] = in.Env.APIKeysJSON if in.Env.RedisAddress == "" { return nil, fmt.Errorf("AGGREGATOR_REDIS_ADDRESS is required in env config") diff --git a/build/devenv/tests/services/aggregator_test.go b/build/devenv/tests/services/aggregator_test.go index 8908902df..4fc376f92 100644 --- a/build/devenv/tests/services/aggregator_test.go +++ b/build/devenv/tests/services/aggregator_test.go @@ -33,7 +33,38 @@ func TestServiceAggregator(t *testing.T) { RedisAddress: "default-aggregator-redis:6379", RedisPassword: "", RedisDB: "0", - APIKeysJSON: `{"clients":{"test-key":{"clientId":"test","enabled":true,"groups":[],"secrets":{"primary":"test-secret"}}}}`, + Clients: map[string]services.ClientCredentials{ + "verifier_1": { + KeyPairEnvVars: []services.ClientEnvVarPair{ + { + APIKeyEnv: "AGGREGATOR_VERIFIER_1_API_KEY", + SecretEnv: "AGGREGATOR_VERIFIER_1_SECRET", + APIKeyValue: "dev-api-key-verifier-1", + SecretValue: "dev-secret-verifier-1", + }, + }, + }, + "verifier_2": { + KeyPairEnvVars: []services.ClientEnvVarPair{ + { + APIKeyEnv: "AGGREGATOR_VERIFIER_2_API_KEY", + SecretEnv: "AGGREGATOR_VERIFIER_2_SECRET", + APIKeyValue: "dev-api-key-verifier-2", + SecretValue: "dev-secret-verifier-2", + }, + }, + }, + "monitoring": { + KeyPairEnvVars: []services.ClientEnvVarPair{ + { + APIKeyEnv: "AGGREGATOR_MONITORING_API_KEY", + SecretEnv: "AGGREGATOR_MONITORING_SECRET", + APIKeyValue: "dev-monitoring-api-key", + SecretValue: "dev-monitoring-secret", + }, + }, + }, + }, }, }) require.NoError(t, err)