From 8cfc84166271f0b2ba855803f16bd7c175b6c342 Mon Sep 17 00:00:00 2001 From: Cedric Cordenier Date: Mon, 18 May 2026 11:36:53 +0100 Subject: [PATCH] CRE-4343/CRE-4352: Add mtls support in OutboundHTTPRequest + feature flag for mtls --- pkg/settings/cresettings/README.md | 3 + pkg/settings/cresettings/defaults.json | 5 +- pkg/settings/cresettings/defaults.toml | 3 + pkg/settings/cresettings/settings.go | 19 ++++-- pkg/settings/cresettings/settings_test.go | 2 +- pkg/types/gateway/action.go | 16 +++++ pkg/types/gateway/action_test.go | 83 +++++++++++++++++++++++ 7 files changed, 123 insertions(+), 8 deletions(-) diff --git a/pkg/settings/cresettings/README.md b/pkg/settings/cresettings/README.md index 47bfac10f3..67b0180dd2 100644 --- a/pkg/settings/cresettings/README.md +++ b/pkg/settings/cresettings/README.md @@ -214,6 +214,9 @@ flowchart subgraph PerWorkflow.Secrets PerWorkflow.Secrets.CallLimit{{CallLimit}}:::bound end + subgraph PerOrg.HTTPAction + PerOrg.HTTPAction.MtlsAuthAllowed[/PerOrg.HTTPAction.MtlsAuthAllowed/]:::gate + end end subgraph vault VaultCiphertextSizeLimit{{VaultCiphertextSizeLimit}}:::bound diff --git a/pkg/settings/cresettings/defaults.json b/pkg/settings/cresettings/defaults.json index 1aec2f8ad7..89adf555f6 100644 --- a/pkg/settings/cresettings/defaults.json +++ b/pkg/settings/cresettings/defaults.json @@ -36,7 +36,10 @@ "PerOrg": { "BaseTriggerRetransmitEnabled": "false", "WorkflowExecutionConcurrencyLimit": "100", - "ZeroBalancePruningTimeout": "24h0m0s" + "ZeroBalancePruningTimeout": "24h0m0s", + "HTTPAction": { + "MtlsAuthAllowed": "false" + } }, "PerOwner": { "WorkflowLimit": "1000", diff --git a/pkg/settings/cresettings/defaults.toml b/pkg/settings/cresettings/defaults.toml index 9499ca3d37..21b8f10269 100644 --- a/pkg/settings/cresettings/defaults.toml +++ b/pkg/settings/cresettings/defaults.toml @@ -38,6 +38,9 @@ BaseTriggerRetransmitEnabled = 'false' WorkflowExecutionConcurrencyLimit = '100' ZeroBalancePruningTimeout = '24h0m0s' +[PerOrg.HTTPAction] +MtlsAuthAllowed = 'false' + [PerOwner] WorkflowLimit = '1000' WorkflowExecutionConcurrencyLimit = '5' diff --git a/pkg/settings/cresettings/settings.go b/pkg/settings/cresettings/settings.go index 932defcc2a..4a03b36356 100644 --- a/pkg/settings/cresettings/settings.go +++ b/pkg/settings/cresettings/settings.go @@ -123,9 +123,12 @@ var Default = Schema{ VaultMaxPerOracleUnexpiredBlobCount: Int(1000), PerOrg: Orgs{ - BaseTriggerRetransmitEnabled: Bool(false), + BaseTriggerRetransmitEnabled: Bool(false), WorkflowExecutionConcurrencyLimit: Int(100), ZeroBalancePruningTimeout: Duration(24 * time.Hour), + HTTPAction: perOrgHTTPAction{ + MtlsAuthAllowed: Bool(false), + }, }, PerOwner: Owners{ WorkflowLimit: Int(1000), @@ -260,10 +263,10 @@ type Schema struct { GatewayConfidentialRelayPerNodeRate Setting[config.Rate] TriggerRegistrationStatusUpdateTimeout Setting[time.Duration] - BaseTriggerRetryInterval Setting[time.Duration] - BaseTriggerMaxRetries Setting[int] `unit:"{attempt}"` - BaseTriggerPruneAge Setting[time.Duration] - BaseTriggerMaxSendsPerTick Setting[int] `unit:"{event}"` + BaseTriggerRetryInterval Setting[time.Duration] + BaseTriggerMaxRetries Setting[int] `unit:"{attempt}"` + BaseTriggerPruneAge Setting[time.Duration] + BaseTriggerMaxSendsPerTick Setting[int] `unit:"{event}"` VaultCiphertextSizeLimit Setting[config.Size] VaultShareSizeLimit Setting[config.Size] @@ -289,9 +292,10 @@ type Schema struct { PerWorkflow Workflows `scope:"workflow"` } type Orgs struct { - BaseTriggerRetransmitEnabled Setting[bool] + BaseTriggerRetransmitEnabled Setting[bool] WorkflowExecutionConcurrencyLimit Setting[int] `unit:"{workflow}"` ZeroBalancePruningTimeout Setting[time.Duration] + HTTPAction perOrgHTTPAction } type Owners struct { @@ -396,6 +400,9 @@ type httpAction struct { RequestSizeLimit Setting[config.Size] ResponseSizeLimit Setting[config.Size] } +type perOrgHTTPAction struct { + MtlsAuthAllowed Setting[bool] +} 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 3165650741..64197c0957 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 {