diff --git a/pkg/settings/cresettings/README.md b/pkg/settings/cresettings/README.md index 91ad77cb3a..caa8b70f78 100644 --- a/pkg/settings/cresettings/README.md +++ b/pkg/settings/cresettings/README.md @@ -50,6 +50,7 @@ flowchart GatewayHTTPPerNodeRate[\GatewayHTTPPerNodeRate/]:::rate GatewayConfidentialRelayGlobalRate[\GatewayConfidentialRelayGlobalRate/]:::rate GatewayConfidentialRelayPerNodeRate[\GatewayConfidentialRelayPerNodeRate/]:::rate + GatewayHTTPActionMtlsRequestRate[\GatewayHTTPActionMtlsRequestRate/]:::rate end %% TODO unused %% PerOrg.ZeroBalancePruningTimeout @@ -216,6 +217,9 @@ flowchart subgraph PerWorkflow.Secrets PerWorkflow.Secrets.CallLimit{{CallLimit}}:::bound end + subgraph PerOrg.HTTPAction + PerOrg.HTTPAction.MtlsRateLimit{{PerOrg.HTTPAction.MtlsRateLimit}}:::bound + end end subgraph vault VaultCiphertextSizeLimit{{VaultCiphertextSizeLimit}}:::bound diff --git a/pkg/settings/cresettings/defaults.json b/pkg/settings/cresettings/defaults.json index 15cd8f4aaa..6b18c23803 100644 --- a/pkg/settings/cresettings/defaults.json +++ b/pkg/settings/cresettings/defaults.json @@ -13,6 +13,7 @@ "GatewayHTTPPerNodeRate": "100rps:100", "GatewayConfidentialRelayGlobalRate": "50rps:10", "GatewayConfidentialRelayPerNodeRate": "10rps:10", + "GatewayHTTPActionMtlsRequestRate": "every30s:0", "TriggerRegistrationStatusUpdateTimeout": "0s", "BaseTriggerRetryInterval": "30s", "BaseTriggerMaxRetries": "20", @@ -38,7 +39,10 @@ "PerOrg": { "BaseTriggerRetransmitEnabled": "false", "WorkflowExecutionConcurrencyLimit": "100", - "ZeroBalancePruningTimeout": "24h0m0s" + "ZeroBalancePruningTimeout": "24h0m0s", + "HTTPAction": { + "MtlsRateLimit": "every30s:3" + } }, "PerOwner": { "WorkflowLimit": "1000", diff --git a/pkg/settings/cresettings/defaults.toml b/pkg/settings/cresettings/defaults.toml index f566bfa896..39d275d096 100644 --- a/pkg/settings/cresettings/defaults.toml +++ b/pkg/settings/cresettings/defaults.toml @@ -12,6 +12,7 @@ GatewayHTTPGlobalRate = '500rps:500' GatewayHTTPPerNodeRate = '100rps:100' GatewayConfidentialRelayGlobalRate = '50rps:10' GatewayConfidentialRelayPerNodeRate = '10rps:10' +GatewayHTTPActionMtlsRequestRate = 'every30s:0' TriggerRegistrationStatusUpdateTimeout = '0s' BaseTriggerRetryInterval = '30s' BaseTriggerMaxRetries = '20' @@ -40,6 +41,9 @@ BaseTriggerRetransmitEnabled = 'false' WorkflowExecutionConcurrencyLimit = '100' ZeroBalancePruningTimeout = '24h0m0s' +[PerOrg.HTTPAction] +MtlsRateLimit = 'every30s:3' + [PerOwner] WorkflowLimit = '1000' WorkflowExecutionConcurrencyLimit = '5' diff --git a/pkg/settings/cresettings/settings.go b/pkg/settings/cresettings/settings.go index 8c4ebfbc04..e164696407 100644 --- a/pkg/settings/cresettings/settings.go +++ b/pkg/settings/cresettings/settings.go @@ -67,6 +67,7 @@ var Default = Schema{ GatewayHTTPPerNodeRate: Rate(rate.Limit(100), 100), GatewayConfidentialRelayGlobalRate: Rate(rate.Limit(50), 10), GatewayConfidentialRelayPerNodeRate: Rate(rate.Limit(10), 10), + GatewayHTTPActionMtlsRequestRate: Rate(rate.Every(30*time.Second), 0), TriggerRegistrationStatusUpdateTimeout: Duration(0 * time.Second), BaseTriggerRetryInterval: Duration(30 * time.Second), BaseTriggerMaxRetries: Int(20), @@ -129,6 +130,9 @@ var Default = Schema{ BaseTriggerRetransmitEnabled: Bool(false), WorkflowExecutionConcurrencyLimit: Int(100), ZeroBalancePruningTimeout: Duration(24 * time.Hour), + HTTPAction: perOrgHTTPAction{ + MtlsRateLimit: Rate(rate.Every(30*time.Second), 3), + }, }, PerOwner: Owners{ WorkflowLimit: Int(1000), @@ -263,6 +267,7 @@ type Schema struct { GatewayHTTPPerNodeRate Setting[config.Rate] GatewayConfidentialRelayGlobalRate Setting[config.Rate] GatewayConfidentialRelayPerNodeRate Setting[config.Rate] + GatewayHTTPActionMtlsRequestRate Setting[config.Rate] TriggerRegistrationStatusUpdateTimeout Setting[time.Duration] BaseTriggerRetryInterval Setting[time.Duration] @@ -297,6 +302,7 @@ type Orgs struct { BaseTriggerRetransmitEnabled Setting[bool] WorkflowExecutionConcurrencyLimit Setting[int] `unit:"{workflow}"` ZeroBalancePruningTimeout Setting[time.Duration] + HTTPAction perOrgHTTPAction } type Owners struct { @@ -401,6 +407,9 @@ type httpAction struct { RequestSizeLimit Setting[config.Size] ResponseSizeLimit Setting[config.Size] } +type perOrgHTTPAction struct { + MtlsRateLimit Setting[config.Rate] +} type confidentialHTTP struct { CallLimit Setting[int] `unit:"{call}"` ConnectionTimeout Setting[time.Duration] diff --git a/pkg/settings/cresettings/settings_test.go b/pkg/settings/cresettings/settings_test.go index 9802c0e0b2..8e3569bdcd 100644 --- a/pkg/settings/cresettings/settings_test.go +++ b/pkg/settings/cresettings/settings_test.go @@ -70,7 +70,7 @@ func TestSchema_Unmarshal(t *testing.T) { "GatewayUnauthenticatedRequestRateLimit": "200rps:50", "GatewayUnauthenticatedRequestRateLimitPerIP": "1rps:100", "GatewayIncomingPayloadSizeLimit": "14kb", - "GatewayVaultManagementEnabled": "true", + "GatewayVaultManagementEnabled": "true", "GatewayConfidentialRelayGlobalRate": "20rps:7", "GatewayConfidentialRelayPerNodeRate": "4rps:2", "PerOrg": { diff --git a/pkg/types/gateway/action.go b/pkg/types/gateway/action.go index d11b65305b..6313be289e 100644 --- a/pkg/types/gateway/action.go +++ b/pkg/types/gateway/action.go @@ -23,6 +23,15 @@ type CacheSettings struct { ReadFromCache bool `json:"readFromCache,omitempty"` // If true, attempt to read a cached response for the request } +type Secret []byte + +func (s Secret) String() string { return "[REDACTED]" } + +type MtlsAuth struct { + PrivateKey Secret `json:"privateKey"` + Certificate []byte `json:"certificate"` +} + // OutboundHTTPRequest represents an HTTP request to be sent from workflow node to the gateway. type OutboundHTTPRequest struct { URL string `json:"url"` // URL to query, only http and https protocols are supported. @@ -35,6 +44,7 @@ type OutboundHTTPRequest struct { Body []byte `json:"body,omitempty"` // HTTP request body TimeoutMs uint32 `json:"timeoutMs,omitempty"` // Timeout in milliseconds CacheSettings CacheSettings `json:"cacheSettings"` // Best-effort cache control for the request + Mtls *MtlsAuth `json:"mtlsAuth,omitempty"` // Client certificate for mTLS requests // Maximum number of bytes to read from the response body. If the gateway max response size is smaller than this value, the gateway max response size will be used. MaxResponseBytes uint32 `json:"maxBytes,omitempty"` @@ -62,6 +72,12 @@ func (req OutboundHTTPRequest) Hash() string { s.Write([]byte(strconv.FormatUint(uint64(req.MaxResponseBytes), 10))) + if req.Mtls != nil { + s.Write(req.Mtls.PrivateKey) + s.Write(sep) + s.Write(req.Mtls.Certificate) + } + return hex.EncodeToString(s.Sum(nil)) } diff --git a/pkg/types/gateway/action_test.go b/pkg/types/gateway/action_test.go index 19f9f0389d..42db734fb7 100644 --- a/pkg/types/gateway/action_test.go +++ b/pkg/types/gateway/action_test.go @@ -19,6 +19,15 @@ func TestOutboundHTTPRequest_Hash(t *testing.T) { WorkflowOwner: "owner", MultiHeaders: map[string][]string{"A": {"1", "2"}}, } + baseWithMtls := OutboundHTTPRequest{ + Method: "GET", + URL: "https://example.com/", + WorkflowOwner: "owner", + Mtls: &MtlsAuth{ + PrivateKey: Secret("priv-key"), + Certificate: []byte("cert"), + }, + } tests := []struct { name string @@ -111,6 +120,80 @@ func TestOutboundHTTPRequest_Hash(t *testing.T) { }, sameHash: false, }, + { + name: "Same Mtls values yields same hash", + reqA: baseWithMtls, + reqB: OutboundHTTPRequest{ + Method: "GET", + URL: "https://example.com/", + WorkflowOwner: "owner", + Mtls: &MtlsAuth{ + PrivateKey: Secret("priv-key"), + Certificate: []byte("cert"), + }, + }, + sameHash: true, + }, + { + name: "Nil Mtls vs non-nil Mtls yields different hash", + reqA: OutboundHTTPRequest{ + Method: "GET", + URL: "https://example.com/", + WorkflowOwner: "owner", + }, + reqB: baseWithMtls, + sameHash: false, + }, + { + name: "Different Mtls PrivateKey yields different hash", + reqA: baseWithMtls, + reqB: OutboundHTTPRequest{ + Method: "GET", + URL: "https://example.com/", + WorkflowOwner: "owner", + Mtls: &MtlsAuth{ + PrivateKey: Secret("other-key"), + Certificate: []byte("cert"), + }, + }, + sameHash: false, + }, + { + name: "Different Mtls Certificate yields different hash", + reqA: baseWithMtls, + reqB: OutboundHTTPRequest{ + Method: "GET", + URL: "https://example.com/", + WorkflowOwner: "owner", + Mtls: &MtlsAuth{ + PrivateKey: Secret("priv-key"), + Certificate: []byte("other-cert"), + }, + }, + sameHash: false, + }, + { + name: "Shifting bytes between Mtls PrivateKey and Certificate yields different hash", + reqA: OutboundHTTPRequest{ + Method: "GET", + URL: "https://example.com/", + WorkflowOwner: "owner", + Mtls: &MtlsAuth{ + PrivateKey: Secret("ab"), + Certificate: []byte("cd"), + }, + }, + reqB: OutboundHTTPRequest{ + Method: "GET", + URL: "https://example.com/", + WorkflowOwner: "owner", + Mtls: &MtlsAuth{ + PrivateKey: Secret("abc"), + Certificate: []byte("d"), + }, + }, + sameHash: false, + }, } for _, tt := range tests {