Repository: ory/kratos

Issue:
Configurable webhook header filter
### Preflight checklist

- [x] I could not find a solution in the existing issues, docs, nor discussions.
- [x] I agree to follow this project's [Code of Conduct](https://github.com/ory/kratos/blob/master/CODE_OF_CONDUCT.md).
- [x] I have read and am following this repository's [Contribution Guidelines](https://github.com/ory/kratos/blob/master/CONTRIBUTING.md).
- [x] I have joined the [Ory Community Slack](https://slack.ory.sh).
- [x] I am signed up to the [Ory Security Patch Newsletter](https://www.ory.sh/l/sign-up-newsletter).

### Ory Network Project

_No response_

### Describe your problem

I talked about this a bit on slack, but the header filter implemented by https://github.com/ory/kratos/pull/4048 is somewhat arbitrary and doesn't work for some configurations or use cases (for example, where you use `x-forwarded-for` instead of `true-client-ip` to get client IP, or if you need to process injected telemetry from a header).

### Describe your ideal solution

Ideally there is a config value for allowed headers, possibly defaulting to the current list to avoid making significant changes. 

### Workarounds or alternatives

For getting the client IP, I've updated my proxy configuration to set `True-Client-IP`. 

### Version

v1.3.1

### Additional Context

_No response_

Discussion:
@robert-sisutech: I'm on Kratos v.1.3.0.

I'm absolutely gutted that this header filtering was added without the possibility to add more headers to the allowlist in Kratos configuration. Recently updated Kratos to the latest version in our system and it broke some functionalities which relied on specific headers existing on the webhook requests as part of Kratos flows. Cannot downgrade as we need some latest Kratos features.
Requests to our system go through a reverse-proxy which decorates the request with some extra headers (ip address, ip country, plus some more implementation specific headers). I know we could transform the headers but the strict headers allowlist makes this almost impossible.

What was the benefit of strict allowlist for webhook request headers? 
Ideally yes there would be a way to allow more headers than the current strict headers allowlist. @aeneasr: > For getting the client IP, I've updated my proxy configuration to set True-Client-IP.

The workaround is a good option in my view.

@robert-sisutech is that workaround not working for you?

We have removed other headers due to security issues in our infra. At the moment there are no plans to change this@K3das: Is there any reason why a config option wouldn't work? This seems like a very instance-specific thing.@aeneasr: Not really (other than adding yet more config options), but it’s not a priority given that a workaround exists and we have no need for it!@K3das: @aeneasr would this be a per-webhook or instance-wide config? Doing it instance-wide would probably be a slightly easier implementation but I could imagine a scenario where someone wants to be selective about what hooks get what headers. @robert-sisutech: @aeneasr we have like around 5 headers we require to be on the request and that we would need to be able to read in Kratos' webhook request handler.
Your suggestion is not a workaround for our usecase. Although hacky, yeah the `True-Client-IP` header is great and possible to do for us but we're missing bunch of other headers which we cannot re-assign to any of the headers in the strict allowlist.

Adding strict filtering of headers for everyone using Ory Kratos due to your infra's security concerns isn't a really thoughtful decision in my opinion. Many of us still need access to other headers thus why I and I imagine some more developers would want the headers allowlist to be configurable or at least it should be possible to disable the strict headers filtering.

@aeneasr: > [@aeneasr](https://github.com/aeneasr) we have like around 5 headers we require to be on the request and that we would need to be able to read in Kratos' webhook request handler. Your suggestion is not a workaround for our usecase. Although hacky, yeah the `True-Client-IP` header is great and possible to do for us but we're missing bunch of other headers which we cannot re-assign to any of the headers in the strict allowlist.
> 
> Adding strict filtering of headers for everyone using Ory Kratos due to your infra's security concerns isn't a really thoughtful decision in my opinion. Many of us still need access to other headers thus why I and I imagine some more developers would want the headers allowlist to be configurable or at least it should be possible to disable the strict headers filtering.

Did you see my last reply?

Regarding your message pre-edit: We are listening to OSS community demands but as you know running such a large project isn’t free and our priorities are with people who pay us to maintain and advance this project (otherwise it would be long dead). It’s even more frustrating when other companies use our stuff, demand a lot, make a lot of money with their customers, but don’t give back except frustrated messages. So please don’t get frustrated but stay constructive.

By the way, if you rely on that many headers, maybe transient payloads are a better option for you.

> [@aeneasr](https://github.com/aeneasr) would this be a per-webhook or instance-wide config? Doing it instance-wide would probably be a slightly implementation but I could imagine a scenario where someone wants to be selective about what hooks get what headers.

Global should be fine, otherwise the config gets really convoluted. The biggest challenge is probably figuring out where to put the config key.@robert-sisutech: @aeneasr The aforementioned headers are something we don't have much control over. We could re-assign the headers but we cannot move them to transient payload while the request is in-flight. We don't add the headers ourself at the source of the request. The headers get added while the request is in flight in one part of our reverse-proxy/firewall/gateway of our infra. Transient payload is a good option if we can allow the data to be editable by end-user. In this case we don't want the end-user to have control of the data put in these headers so transient_payload unfortunately is not an option for us in this case. Transient payload however comes in very handy in some other flows we have.

I regret the tone in my pre-edit message. Edited it for a reason. I appreciate something like Ory Kratos exists and is open-source and available for everybody to use.

What would make the situation with strict headers allowlist better for some of us would be:

- flag in configuration to disable strict header filtering altogether (easier to implement)
- option in configuration to manually add more headers to the allowlist (bit more difficult to implement)


@aeneasr: Apology accepted, no problem :)

I feel making the headers configurable strikes a balance between security and customizability and would not need recompilation in case more headers are added to the allow list@Tan-Aki: We had the same issue.

Our head engineer did this for now.

![Image](https://github.com/user-attachments/assets/3a7838f7-1b06-4b97-87a2-0a42366681eb)
(re-adding the "X-Forwarded-For" header to the RequestHeaderAllowList)

We will remove that once 1.3.2 is out and we upgrade our Kratos instance.

I think this could be considered a breaking change and be added to the changelog documentation ? 

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a22d2f137..a2bbf8d3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -120,6 +120,7 @@ Closes https://github.com/ory-corp/cloud/issues/7176
     identifier of the original identity was not updated.
 
 * Cancel conditional passkey before trying again ([#4247](https://github.com/ory/kratos/issues/4247)) ([d9f6f75](https://github.com/ory/kratos/commit/d9f6f75b6a43aad996f6390f73616a2cf596c6e4))
+* Count MFA addresses in CountActiveMultiFactorCredentials for code method ([9860c9a](https://github.com/ory/kratos/commit/9860c9a4faa5bd5d725c742c4d4ce9473baa0963)), closes [ory/network#409](https://github.com/ory/network/issues/409)
 * Do not roll back transaction on partial identity insert error ([#4211](https://github.com/ory/kratos/issues/4211)) ([82660f0](https://github.com/ory/kratos/commit/82660f04e2f33d0aa86fccee42c90773a901d400))
 * Don't show oidc subject in login hints ([#4264](https://github.com/ory/kratos/issues/4264)) ([b95fd3f](https://github.com/ory/kratos/commit/b95fd3fa723521807824cad84e4a9ce812172311))
 * Duplicate autocomplete trigger ([6bbf915](https://github.com/ory/kratos/commit/6bbf91593a37e4973a86f610290ebab44df8dc81))
@@ -161,6 +162,7 @@ Closes https://github.com/ory-corp/cloud/issues/7176
     
     ```
 
+* Incorrect if switch in previous sceen case in two step registration ([f8ee403](https://github.com/ory/kratos/commit/f8ee40396a36a2e7a348c9cf983dec7db13814c5)), closes [#374](https://github.com/ory/kratos/issues/374)
 * Incorrect query plan ([#4218](https://github.com/ory/kratos/issues/4218)) ([7d0e78a](https://github.com/ory/kratos/commit/7d0e78a4f6631b0662beee3b8e9dd0d774b875ea))
 * Order-by clause and span names ([#4200](https://github.com/ory/kratos/issues/4200)) ([b6278af](https://github.com/ory/kratos/commit/b6278af5c7ed7fb845a71ad0e64f8b87402a8f4b))
 * Pass on correct context during verification ([#4151](https://github.com/ory/kratos/issues/4151)) ([7e0b500](https://github.com/ory/kratos/commit/7e0b500aada9c1931c759a43db7360e85afb57e3))
@@ -236,6 +238,7 @@ Closes https://github.com/ory-corp/cloud/issues/7176
 
 * Add a policy callback to customize OIDC credential linking ([#4302](https://github.com/ory/kratos/issues/4302)) ([e2f878a](https://github.com/ory/kratos/commit/e2f878a3ed25b4617bf6bd43b6e147d87d3b8ca2))
 * Add attributes to webhook events for better debugging ([#4206](https://github.com/ory/kratos/issues/4206)) ([00da05d](https://github.com/ory/kratos/commit/00da05da9f77bbfb68b364b3ba2a5d0a2d9e4f15))
+* Add captcha group to first-step registration ([eca4ae9](https://github.com/ory/kratos/commit/eca4ae9dcce37d03bbd1bf5f0cd492466c02acde))
 * Add context param to policy ([#4315](https://github.com/ory/kratos/issues/4315)) ([261596b](https://github.com/ory/kratos/commit/261596b7261c315b7d8291e886023c34fc9135c5))
 * Add explicit config flag for secure cookies ([#4180](https://github.com/ory/kratos/issues/4180)) ([2aabe12](https://github.com/ory/kratos/commit/2aabe12e5329acc807c495445999e5591bdf982b)):
 
@@ -270,6 +273,10 @@ Closes https://github.com/ory-corp/cloud/issues/7176
     
     See https://github.com/ory-corp/cloud/issues/7350
 
+* Add new Division ui node attributes ([235af52](https://github.com/ory/kratos/commit/235af527dea47b87ad0f18ff04f9b807e4639ae3)):
+
+    Division nodes may be used to hook dynamic scripts and are not actively used in the Ory Kratos open source.
+
 * Add oid as subject source for microsoft ([#4171](https://github.com/ory/kratos/issues/4171)) ([77beb4d](https://github.com/ory/kratos/commit/77beb4de5209cee0bea4b63dfec21d656cf64473)), closes [#4170](https://github.com/ory/kratos/issues/4170):
 
     In the case of Microsoft, using `sub` as an identifier can lead to problems. Because the use of OIDC at Microsoft is based on an app registration, the content of `sub` changes with every new app registration. `Sub` is therefore not uniquely related to the user. It is therefore not possible to transfer users from one app registration to another without further problems.
diff --git a/driver/config/.snapshots/TestDefaultWebhookHeaderAllowlist.json b/driver/config/.snapshots/TestDefaultWebhookHeaderAllowlist.json
new file mode 100644
index 000000000..6ec4b1e3d
--- /dev/null
+++ b/driver/config/.snapshots/TestDefaultWebhookHeaderAllowlist.json
@@ -0,0 +1,19 @@
+[
+  "Accept",
+  "Accept-Encoding",
+  "Accept-Language",
+  "Content-Length",
+  "Content-Type",
+  "Origin",
+  "Priority",
+  "Referer",
+  "Sec-Ch-Ua",
+  "Sec-Ch-Ua-Mobile",
+  "Sec-Ch-Ua-Platform",
+  "Sec-Fetch-Dest",
+  "Sec-Fetch-Mode",
+  "Sec-Fetch-Site",
+  "Sec-Fetch-User",
+  "True-Client-Ip",
+  "User-Agent"
+]
diff --git a/driver/config/config.go b/driver/config/config.go
index 366f9e371..f859393e0 100644
--- a/driver/config/config.go
+++ b/driver/config/config.go
@@ -208,6 +208,7 @@ const (
 	ViperKeyOAuth2ProviderOverrideReturnTo                   = "oauth2_provider.override_return_to"
 	ViperKeyClientHTTPNoPrivateIPRanges                      = "clients.http.disallow_private_ip_ranges"
 	ViperKeyClientHTTPPrivateIPExceptionURLs                 = "clients.http.private_ip_exception_urls"
+	ViperKeyWebhookHeaderAllowlist                           = "clients.web_hook.header_allowlist"
 	ViperKeyPreviewDefaultReadConsistencyLevel               = "preview.default_read_consistency_level"
 	ViperKeyVersion                                          = "version"
 	ViperKeyPasswordMigrationHook                            = "selfservice.methods.password.config.migrate_hook"
@@ -954,6 +955,28 @@ func (p *Config) SelfAdminURL(ctx context.Context) *url.URL {
 	return p.baseURL(ctx, ViperKeyAdminBaseURL, ViperKeyAdminHost, ViperKeyAdminPort, 4434)
 }
 
+func (p *Config) WebhookHeaderAllowlist(ctx context.Context) []string {
+	return p.GetProvider(ctx).StringsF(ViperKeyWebhookHeaderAllowlist, []string{
+		"Accept",
+		"Accept-Encoding",
+		"Accept-Language",
+		"Content-Length",
+		"Content-Type",
+		"Origin",
+		"Priority",
+		"Referer",
+		"Sec-Ch-Ua",
+		"Sec-Ch-Ua-Mobile",
+		"Sec-Ch-Ua-Platform",
+		"Sec-Fetch-Dest",
+		"Sec-Fetch-Mode",
+		"Sec-Fetch-Site",
+		"Sec-Fetch-User",
+		"True-Client-Ip",
+		"User-Agent",
+	})
+}
+
 func (p *Config) OAuth2ProviderHeader(ctx context.Context) http.Header {
 	hh := map[string]string{}
 	if err := p.GetProvider(ctx).Unmarshal(ViperKeyOAuth2ProviderHeader, &hh); err != nil {
diff --git a/embedx/config.schema.json b/embedx/config.schema.json
index 24e16743d..545c10567 100644
--- a/embedx/config.schema.json
+++ b/embedx/config.schema.json
@@ -43,7 +43,10 @@
       "description": "Ory Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).",
       "type": "string",
       "format": "uri-reference",
-      "examples": ["https://my-app.com/dashboard", "/dashboard"]
+      "examples": [
+        "https://my-app.com/dashboard",
+        "/dashboard"
+      ]
     },
     "selfServiceSessionRevokerHook": {
       "type": "object",
@@ -53,7 +56,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "selfServiceSessionIssuerHook": {
       "type": "object",
@@ -63,7 +68,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "selfServiceRequireVerifiedAddressHook": {
       "type": "object",
@@ -73,7 +80,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "selfServiceVerificationHook": {
       "type": "object",
@@ -83,7 +92,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "selfServiceShowVerificationUIHook": {
       "type": "object",
@@ -93,7 +104,9 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook"]
+      "required": [
+        "hook"
+      ]
     },
     "b2bSSOHook": {
       "type": "object",
@@ -107,7 +120,10 @@
         }
       },
       "additionalProperties": false,
-      "required": ["hook", "config"]
+      "required": [
+        "hook",
+        "config"
+      ]
     },
     "webHookAuthBasicAuthProperties": {
       "properties": {
@@ -127,11 +143,17 @@
             }
           },
           "additionalProperties": false,
-          "required": ["user", "password"]
+          "required": [
+            "user",
+            "password"
+          ]
         }
       },
       "additionalProperties": false,
-      "required": ["type", "config"]
+      "required": [
+        "type",
+        "config"
+      ]
     },
     "httpRequestConfig": {
       "type": "object",
@@ -139,7 +161,9 @@
         "url": {
           "title": "HTTP address of API endpoint",
           "description": "This URL will be used to send the emails to.",
-          "examples": ["https://example.com/api/v1/email"],
+          "examples": [
+            "https://example.com/api/v1/email"
+          ],
           "type": "string",
           "pattern": "^https?://"
         },
@@ -204,15 +228,25 @@
             "in": {
               "type": "string",
               "description": "How the api key should be transferred",
-              "enum": ["header", "cookie"]
+              "enum": [
+                "header",
+                "cookie"
+              ]
             }
           },
           "additionalProperties": false,
-          "required": ["name", "value", "in"]
+          "required": [
+            "name",
+            "value",
+            "in"
+          ]
         }
       },
       "additionalProperties": false,
-      "required": ["type", "config"]
+      "required": [
+        "type",
+        "config"
+      ]
     },
     "selfServiceWebHook": {
       "type": "object",
@@ -255,7 +289,10 @@
                     "const": true
                   }
                 },
-                "required": ["ignore", "parse"]
+                "required": [
+                  "ignore",
+                  "parse"
+                ]
               }
             },
             "url": {
@@ -328,30 +365,46 @@
                   "response": {
                     "properties": {
                       "ignore": {
-                        "enum": [true]
+                        "enum": [
+                          true
+                        ]
                       }
                     },
-                    "required": ["ignore"]
+                    "required": [
+                      "ignore"
+                    ]
                   }
                 },
-                "required": ["response"]
+                "required": [
+                  "response"
+                ]
               }
             },
             {
               "properties": {
                 "can_interrupt": {
-                  "enum": [false]
+                  "enum": [
+                    false
+                  ]
                 }
               },
-              "require": ["can_interrupt"]
+              "require": [
+                "can_interrupt"
+              ]
             }
           ],
           "additionalProperties": false,
-          "required": ["url", "method"]
+          "required": [
+            "url",
+            "method"
+          ]
         }
       },
       "additionalProperties": false,
-      "required": ["hook", "config"]
+      "required": [
+        "hook",
+        "config"
+      ]
     },
     "OIDCClaims": {
       "title": "OpenID Connect claims",
@@ -384,7 +437,9 @@
               "essential": true
             },
             "acr": {
-              "values": ["urn:mace:incommon:iap:silver"]
+              "values": [
+                "urn:mace:incommon:iap:silver"
+              ]
             }
           }
         }
@@ -432,7 +487,9 @@
       "properties": {
         "id": {
           "type": "string",
-          "examples": ["google"]
+          "examples": [
+            "google"
+          ]
         },
         "provider": {
           "title": "Provider",
@@ -463,7 +520,9 @@
             "x",
             "fedcm-test"
           ],
-          "examples": ["google"]
+          "examples": [
+            "google"
+          ]
         },
         "label": {
           "title": "Optional string which will be used when generating labels for UI buttons.",
@@ -478,17 +537,23 @@
         "issuer_url": {
           "type": "string",
           "format": "uri",
-          "examples": ["https://accounts.google.com"]
+          "examples": [
+            "https://accounts.google.com"
+          ]
         },
         "auth_url": {
           "type": "string",
           "format": "uri",
-          "examples": ["https://accounts.google.com/o/oauth2/v2/auth"]
+          "examples": [
+            "https://accounts.google.com/o/oauth2/v2/auth"
+          ]
         },
         "token_url": {
           "type": "string",
           "format": "uri",
-          "examples": ["https://www.googleapis.com/oauth2/v4/token"]
+          "examples": [
+            "https://www.googleapis.com/oauth2/v4/token"
+          ]
         },
         "mapper_url": {
           "title": "Jsonnet Mapper URL",
@@ -505,7 +570,10 @@
           "type": "array",
           "items": {
             "type": "string",
-            "examples": ["offline_access", "profile"]
+            "examples": [
+              "offline_access",
+              "profile"
+            ]
           }
         },
         "microsoft_tenant": {
@@ -524,21 +592,31 @@
           "title": "Microsoft subject source",
           "description": "Controls which source the subject identifier is taken from by microsoft provider. If set to `userinfo` (the default) then the identifier is taken from the `sub` field of OIDC ID token or data received from `/userinfo` standard OIDC endpoint. If set to `me` then the `id` field of data structure received from `https://graph.microsoft.com/v1.0/me` is taken as an identifier. If the value is `oid` then the the oid (Object ID) is taken to identify users across different services.",
           "type": "string",
-          "enum": ["userinfo", "me", "oid"],
+          "enum": [
+            "userinfo",
+            "me",
+            "oid"
+          ],
           "default": "userinfo",
-          "examples": ["userinfo"]
+          "examples": [
+            "userinfo"
+          ]
         },
         "apple_team_id": {
           "title": "Apple Developer Team ID",
           "description": "Apple Developer Team ID needed for generating a JWT token for client secret",
           "type": "string",
-          "examples": ["KP76DQS54M"]
+          "examples": [
+            "KP76DQS54M"
+          ]
         },
         "apple_private_key_id": {
           "title": "Apple Private Key Identifier",
           "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret",
           "type": "string",
-          "examples": ["UX56C66723"]
+          "examples": [
+            "UX56C66723"
+          ]
         },
         "apple_private_key": {
           "title": "Apple Private Key",
@@ -555,29 +633,43 @@
           "title": "Organization ID",
           "description": "The ID of the organization that this provider belongs to. Only effective in the Ory Network.",
           "type": "string",
-          "examples": ["12345678-1234-1234-1234-123456789012"]
+          "examples": [
+            "12345678-1234-1234-1234-123456789012"
+          ]
         },
         "additional_id_token_audiences": {
           "title": "Additional client ids allowed when using ID token submission",
           "type": "array",
           "items": {
             "type": "string",
-            "examples": ["12345678-1234-1234-1234-123456789012"]
+            "examples": [
+              "12345678-1234-1234-1234-123456789012"
+            ]
           }
         },
         "claims_source": {
           "title": "Claims source",
           "description": "Can be either `userinfo` (calls the userinfo endpoint to get the claims) or `id_token` (takes the claims from the id token). It defaults to `id_token`",
           "type": "string",
-          "enum": ["id_token", "userinfo"],
+          "enum": [
+            "id_token",
+            "userinfo"
+          ],
           "default": "id_token",
-          "examples": ["id_token", "userinfo"]
+          "examples": [
+            "id_token",
+            "userinfo"
+          ]
         },
         "pkce": {
           "title": "Proof Key for Code Exchange",
           "description": "PKCE controls if the OpenID Connect OAuth2 flow should use PKCE (Proof Key for Code Exchange). IMPORTANT: If you set this to `force`, you must whitelist a different return URL for your OAuth2 client in the provider's configuration. Instead of <base-url>/self-service/methods/oidc/callback/<provider>, you must use <base-url>/self-service/methods/oidc/callback",
           "type": "string",
-          "enum": ["auto", "never", "force"],
+          "enum": [
+            "auto",
+            "never",
+            "force"
+          ],
           "default": "auto"
         },
         "fedcm_config_url": {
@@ -585,17 +677,26 @@
           "description": "The URL where the FedCM IdP configuration is located for the provider. This is only effective in the Ory Network.",
           "type": "string",
           "format": "uri",
-          "examples": ["https://example.com/config.json"]
+          "examples": [
+            "https://example.com/config.json"
+          ]
         },
         "net_id_token_origin_header": {
           "title": "NetID Token Origin Header",
           "description": "Contains the orgin header to be used when exchanging a NetID FedCM token for an ID token",
           "type": "string",
-          "examples": ["https://example.com"]
+          "examples": [
+            "https://example.com"
+          ]
         }
       },
       "additionalProperties": false,
-      "required": ["id", "provider", "client_id", "mapper_url"],
+      "required": [
+        "id",
+        "provider",
+        "client_id",
+        "mapper_url"
+      ],
       "allOf": [
         {
           "if": {
@@ -604,17 +705,23 @@
                 "const": "microsoft"
               }
             },
-            "required": ["provider"]
+            "required": [
+              "provider"
+            ]
           },
           "then": {
-            "required": ["microsoft_tenant"]
+            "required": [
+              "microsoft_tenant"
+            ]
           },
           "else": {
             "not": {
               "properties": {
                 "microsoft_tenant": {}
               },
-              "required": ["microsoft_tenant"]
+              "required": [
+                "microsoft_tenant"
+              ]
             }
           }
         },
@@ -625,7 +732,9 @@
                 "const": "apple"
               }
             },
-            "required": ["provider"]
+            "required": [
+              "provider"
+            ]
           },
           "then": {
             "not": {
@@ -635,7 +744,9 @@
                   "minLength": 1
                 }
               },
-              "required": ["client_secret"]
+              "required": [
+                "client_secret"
+              ]
             },
             "required": [
               "apple_private_key_id",
@@ -644,7 +755,9 @@
             ]
           },
           "else": {
-            "required": ["client_secret"],
+            "required": [
+              "client_secret"
+            ],
             "allOf": [
               {
                 "not": {
@@ -654,7 +767,9 @@
                       "minLength": 1
                     }
                   },
-                  "required": ["apple_team_id"]
+                  "required": [
+                    "apple_team_id"
+                  ]
                 }
               },
               {
@@ -665,7 +780,9 @@
                       "minLength": 1
                     }
                   },
-                  "required": ["apple_private_key_id"]
+                  "required": [
+                    "apple_private_key_id"
+                  ]
                 }
               },
               {
@@ -676,7 +793,9 @@
                       "minLength": 1
                     }
                   },
-                  "required": ["apple_private_key"]
+                  "required": [
+                    "apple_private_key"
+                  ]
                 }
               }
             ]
@@ -862,7 +981,10 @@
       "title": "Required Authenticator Assurance Level",
       "description": "Sets what Authenticator Assurance Level (used for 2FA) is required to access this feature. If set to `highest_available` then this endpoint requires the highest AAL the identity has set up. If set to `aal1` then the identity can access this feature without 2FA.",
       "type": "string",
-      "enum": ["aal1", "highest_available"],
+      "enum": [
+        "aal1",
+        "highest_available"
+      ],
       "default": "highest_available"
     },
     "selfServiceAfterSettings": {
@@ -1058,7 +1180,9 @@
         "path": {
           "title": "Path to PEM-encoded Fle",
           "type": "string",
-          "examples": ["path/to/file.pem"]
+          "examples": [
+            "path/to/file.pem"
+          ]
         },
         "base64": {
           "title": "Base64 Encoded Inline",
@@ -1106,7 +1230,9 @@
               "$ref": "#/definitions/emailCourierTemplate"
             }
           },
-          "required": ["email"]
+          "required": [
+            "email"
+          ]
         },
         "valid": {
           "additionalProperties": false,
@@ -1119,7 +1245,9 @@
               "$ref": "#/definitions/smsCourierTemplate"
             }
           },
-          "required": ["email"]
+          "required": [
+            "email"
+          ]
         }
       }
     },
@@ -1190,7 +1318,9 @@
     "selfservice": {
       "type": "object",
       "additionalProperties": false,
-      "required": ["default_browser_return_url"],
+      "required": [
+        "default_browser_return_url"
+      ],
       "properties": {
         "default_browser_return_url": {
           "$ref": "#/definitions/defaultReturnTo"
@@ -1225,20 +1355,30 @@
                   "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/user/settings"],
+                  "examples": [
+                    "https://my-app.com/user/settings"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/settings"
                 },
                 "lifespan": {
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "privileged_session_max_age": {
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "required_aal": {
                   "$ref": "#/definitions/featureRequiredAal"
@@ -1287,14 +1427,20 @@
                   "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/signup"],
+                  "examples": [
+                    "https://my-app.com/signup"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/registration"
                 },
                 "lifespan": {
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "before": {
                   "$ref": "#/definitions/selfServiceBeforeRegistration"
@@ -1319,20 +1465,29 @@
                   "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/login"],
+                  "examples": [
+                    "https://my-app.com/login"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/login"
                 },
                 "lifespan": {
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "style": {
                   "title": "Login Flow Style",
                   "description": "The style of the login flow. If set to `unified` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.",
                   "type": "string",
-                  "enum": ["unified", "identifier_first"],
+                  "enum": [
+                    "unified",
+                    "identifier_first"
+                  ],
                   "default": "unified"
                 },
                 "before": {
@@ -1359,7 +1514,9 @@
                   "description": "URL where the Ory Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/verify"],
+                  "examples": [
+                    "https://my-app.com/verify"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/verification"
                 },
                 "after": {
@@ -1371,7 +1528,11 @@
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "before": {
                   "$ref": "#/definitions/selfServiceBeforeVerification"
@@ -1380,7 +1541,10 @@
                   "title": "Verification Strategy",
                   "description": "The strategy to use for verification requests",
                   "type": "string",
-                  "enum": ["link", "code"],
+                  "enum": [
+                    "link",
+                    "code"
+                  ],
                   "default": "code"
                 },
                 "notify_unknown_recipients": {
@@ -1407,7 +1571,9 @@
                   "description": "URL where the Ory Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/verify"],
+                  "examples": [
+                    "https://my-app.com/verify"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/recovery"
                 },
                 "after": {
@@ -1419,7 +1585,11 @@
                   "type": "string",
                   "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                   "default": "1h",
-                  "examples": ["1h", "1m", "1s"]
+                  "examples": [
+                    "1h",
+                    "1m",
+                    "1s"
+                  ]
                 },
                 "before": {
                   "$ref": "#/definitions/selfServiceBeforeRecovery"
@@ -1428,7 +1598,10 @@
                   "title": "Recovery Strategy",
                   "description": "The strategy to use for recovery requests",
                   "type": "string",
-                  "enum": ["link", "code"],
+                  "enum": [
+                    "link",
+                    "code"
+                  ],
                   "default": "code"
                 },
                 "notify_unknown_recipients": {
@@ -1448,7 +1621,9 @@
                   "description": "URL where the Ory Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
                   "type": "string",
                   "format": "uri-reference",
-                  "examples": ["https://my-app.com/kratos-error"],
+                  "examples": [
+                    "https://my-app.com/kratos-error"
+                  ],
                   "default": "https://www.ory.sh/kratos/docs/fallback/error"
                 }
               }
@@ -1477,19 +1652,25 @@
                             "type": "string",
                             "description": "The ID of the organization.",
                             "format": "uuid",
-                            "examples": ["00000000-0000-0000-0000-000000000000"]
+                            "examples": [
+                              "00000000-0000-0000-0000-000000000000"
+                            ]
                           },
                           "label": {
                             "type": "string",
                             "description": "The label of the organization.",
-                            "examples": ["ACME SSO"]
+                            "examples": [
+                              "ACME SSO"
+                            ]
                           },
                           "domains": {
                             "type": "array",
                             "items": {
                               "type": "string",
                               "format": "hostname",
-                              "examples": ["my-app.com"],
+                              "examples": [
+                                "my-app.com"
+                              ],
                               "description": "If this domain matches the email's domain, this provider is shown."
                             }
                           }
@@ -1529,14 +1710,20 @@
                     "base_url": {
                       "title": "Override the base URL which should be used as the base for recovery and verification links.",
                       "type": "string",
-                      "examples": ["https://my-app.com"]
+                      "examples": [
+                        "https://my-app.com"
+                      ]
                     },
                     "lifespan": {
                       "title": "How long a link is valid for",
                       "type": "string",
                       "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                       "default": "1h",
-                      "examples": ["1h", "1m", "1s"]
+                      "examples": [
+                        "1h",
+                        "1m",
+                        "1s"
+                      ]
                     }
                   }
                 }
@@ -1604,13 +1791,16 @@
                       "type": "string",
                       "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
                       "default": "1h",
-                      "examples": ["1h", "1m", "1s"]
+                      "examples": [
+                        "1h",
+                        "1m",
+                        "1s"
+                      ]
                     },
                     "missing_credential_fallback_enabled": {
                       "type": "boolean",
                       "title": "Enable Code OTP as a Fallback",
                       "description": "Enabling this allows users to sign in with the code method, even if their identity schema or their credentials are not set up to use the code method. If enabled, a verified address (such as an email) will be used to send the code to the user. Use with caution and only if actually needed.",
-
                       "default": false
                     }
                   }
@@ -1789,13 +1979,17 @@
                           "type": "string",
                           "title": "Relying Party Display Name",
                           "description": "An name to help the user identify this RP.",
-                          "examples": ["Ory Foundation"]
+                          "examples": [
+                            "Ory Foundation"
+                          ]
                         },
                         "id": {
                           "type": "string",
                           "title": "Relying Party Identifier",
                           "description": "The id must be a subset of the domain currently in the browser.",
-                          "examples": ["ory.sh"]
+                          "examples": [
+                            "ory.sh"
+                          ]
                         },
                         "origin": {
                           "type": "string",
@@ -1803,7 +1997,9 @@
                           "description": "An explicit RP origin. If left empty, this defaults to `id`, prepended with the current protocol schema (HTTP or HTTPS).",
                           "format": "uri",
                           "deprecationMessage": "This field is deprecated. Use `origins` instead.",
-                          "examples": ["https://www.ory.sh"]
+                          "examples": [
+                            "https://www.ory.sh"
+                          ]
                         },
                         "origins": {
                           "type": "array",
@@ -1824,13 +2020,18 @@
                           "description": "An icon to help the user identify this RP.",
                           "format": "uri",
                           "deprecationMessage": "This field is deprecated and ignored due to security considerations.",
-                          "examples": ["https://www.ory.sh/an-icon.png"]
+                          "examples": [
+                            "https://www.ory.sh/an-icon.png"
+                          ]
                         }
                       },
                       "type": "object",
                       "oneOf": [
                         {
-                          "required": ["id", "display_name"],
+                          "required": [
+                            "id",
+                            "display_name"
+                          ],
                           "properties": {
                             "origin": {
                               "not": {}
@@ -1841,7 +2042,11 @@
                           }
                         },
                         {
-                          "required": ["id", "display_name", "origin"],
+                          "required": [
+                            "id",
+                            "display_name",
+                            "origin"
+                          ],
                           "properties": {
                             "origin": {
                               "type": "string"
@@ -1852,7 +2057,11 @@
                           }
                         },
                         {
-                          "required": ["id", "display_name", "origins"],
+                          "required": [
+                            "id",
+                            "display_name",
+                            "origins"
+                          ],
                           "properties": {
                             "origin": {
                               "not": {}
@@ -1877,10 +2086,14 @@
                     "const": true
                   }
                 },
-                "required": ["enabled"]
+                "required": [
+                  "enabled"
+                ]
               },
               "then": {
-                "required": ["config"]
+                "required": [
+                  "config"
+                ]
               }
             },
             "passkey": {
@@ -1903,13 +2116,17 @@
                           "type": "string",
                           "title": "Relying Party Display Name",
                           "description": "A name to help the user identify this RP.",
-                          "examples": ["Ory Foundation"]
+                          "examples": [
+                            "Ory Foundation"
+                          ]
                         },
                         "id": {
                           "type": "string",
                           "title": "Relying Party Identifier",
                           "description": "The id must be a subset of the domain currently in the browser.",
-                          "examples": ["ory.sh"]
+                          "examples": [
+                            "ory.sh"
+                          ]
                         },
                         "origins": {
                           "type": "array",
@@ -1925,7 +2142,10 @@
                         }
                       },
                       "type": "object",
-                      "required": ["display_name", "id"]
+                      "required": [
+                        "display_name",
+                        "id"
+                      ]
                     }
                   },
                   "additionalProperties": false
@@ -1937,10 +2157,14 @@
                     "const": true
                   }
                 },
-                "required": ["enabled"]
+                "required": [
+                  "enabled"
+                ]
               },
               "then": {
-                "required": ["config"]
+                "required": [
+                  "config"
+                ]
               }
             },
             "oidc": {
@@ -1963,7 +2187,9 @@
                       "title": "Base URL for OAuth2 Redirect URIs",
                       "description": "Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.",
                       "format": "uri",
-                      "examples": ["https://auth.myexample.org/"]
+                      "examples": [
+                        "https://auth.myexample.org/"
+                      ]
                     },
                     "providers": {
                       "title": "OpenID Connect and OAuth2 Providers",
@@ -2071,7 +2297,9 @@
                       "$ref": "#/definitions/smsCourierTemplate"
                     }
                   },
-                  "required": ["email"]
+                  "required": [
+                    "email"
+                  ]
                 }
               }
             },
@@ -2090,7 +2318,9 @@
                       "$ref": "#/definitions/smsCourierTemplate"
                     }
                   },
-                  "required": ["email"]
+                  "required": [
+                    "email"
+                  ]
                 }
               }
             }
@@ -2100,13 +2330,18 @@
           "type": "string",
           "title": "Override message templates",
           "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.",
-          "examples": ["/conf/courier-templates"]
+          "examples": [
+            "/conf/courier-templates"
+          ]
         },
         "message_retries": {
           "description": "Defines the maximum number of times the sending of a message is retried after it failed before it is marked as abandoned",
           "type": "integer",
           "default": 5,
-          "examples": [10, 60]
+          "examples": [
+            10,
+            60
+          ]
         },
         "worker": {
           "description": "Configures the dispatch worker.",
@@ -2129,7 +2364,10 @@
           "title": "Delivery Strategy",
           "description": "Defines how emails will be sent, either through SMTP (default) or HTTP.",
           "type": "string",
-          "enum": ["smtp", "http"],
+          "enum": [
+            "smtp",
+            "http"
+          ],
           "default": "smtp"
         },
         "http": {
@@ -2186,7 +2424,9 @@
               "title": "SMTP Sender Name",
               "description": "The recipient of an email will see this as the sender name.",
               "type": "string",
-              "examples": ["Bob"]
+              "examples": [
+                "Bob"
+              ]
             },
             "headers": {
               "title": "SMTP Headers",
@@ -2223,19 +2463,26 @@
                 "title": "Channel id",
                 "description": "The channel id. Corresponds to the .via property of the identity schema for recovery, verification, etc. Currently only sms is supported.",
                 "maxLength": 32,
-                "enum": ["sms"]
+                "enum": [
+                  "sms"
+                ]
               },
               "type": {
                 "type": "string",
                 "title": "Channel type",
                 "description": "The channel type. Currently only http is supported.",
-                "enum": ["http"]
+                "enum": [
+                  "http"
+                ]
               },
               "request_config": {
                 "$ref": "#/definitions/httpRequestConfig"
               }
             },
-            "required": ["id", "request_config"],
+            "required": [
+              "id",
+              "request_config"
+            ],
             "additionalProperties": false
           }
         }
@@ -2286,7 +2533,10 @@
           "type": "string",
           "title": "Default Read Consistency Level",
           "description": "The default consistency level to use when reading from the database. Defaults to `strong` to not break existing API contracts. Only set this to `eventual` if you can accept that other read APIs will suddenly return eventually consistent results. It is only effective in Ory Network.",
-          "enum": ["strong", "eventual"],
+          "enum": [
+            "strong",
+            "eventual"
+          ],
           "default": "strong"
         }
       }
@@ -2314,7 +2564,9 @@
               "description": "The URL where the admin endpoint is exposed at.",
               "type": "string",
               "format": "uri",
-              "examples": ["https://kratos.private-network:4434/"]
+              "examples": [
+                "https://kratos.private-network:4434/"
+              ]
             },
             "host": {
               "title": "Admin Host",
@@ -2328,7 +2580,9 @@
               "type": "integer",
               "minimum": 1,
               "maximum": 65535,
-              "examples": [4434],
+              "examples": [
+                4434
+              ],
               "default": 4434
             },
             "socket": {
@@ -2387,7 +2641,9 @@
                     ]
                   },
                   "uniqueItems": true,
-                  "default": ["*"],
+                  "default": [
+                    "*"
+                  ],
                   "examples": [
                     [
                       "https://example.com",
@@ -2399,7 +2655,13 @@
                 "allowed_methods": {
                   "type": "array",
                   "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.",
-                  "default": ["POST", "GET", "PUT", "PATCH", "DELETE"],
+                  "default": [
+                    "POST",
+                    "GET",
+                    "PUT",
+                    "PATCH",
+                    "DELETE"
+                  ],
                   "items": {
                     "type": "string",
                     "enum": [
@@ -2433,7 +2695,9 @@
                 "exposed_headers": {
                   "type": "array",
                   "description": "Sets which headers are safe to expose to the API of a CORS API specification.",
-                  "default": ["Content-Type"],
+                  "default": [
+                    "Content-Type"
+                  ],
                   "items": {
                     "type": "string"
                   }
@@ -2476,7 +2740,9 @@
               "type": "integer",
               "minimum": 1,
               "maximum": 65535,
-              "examples": [4433],
+              "examples": [
+                4433
+              ],
               "default": 4433
             },
             "socket": {
@@ -2526,7 +2792,10 @@
         "format": {
           "description": "The log format can either be text or JSON.",
           "type": "string",
-          "enum": ["json", "text"]
+          "enum": [
+            "json",
+            "text"
+          ]
         }
       },
       "additionalProperties": false
@@ -2567,7 +2836,9 @@
               "id": {
                 "title": "The schema's ID.",
                 "type": "string",
-                "examples": ["employee"]
+                "examples": [
+                  "employee"
+                ]
               },
               "url": {
                 "type": "string",
@@ -2581,11 +2852,16 @@
                 ]
               }
             },
-            "required": ["id", "url"]
+            "required": [
+              "id",
+              "url"
+            ]
           }
         }
       },
-      "required": ["schemas"],
+      "required": [
+        "schemas"
+      ],
       "additionalProperties": false
     },
     "secrets": {
@@ -2634,7 +2910,10 @@
           "description": "One of the values: argon2, bcrypt.\nAny other hashes will be migrated to the set algorithm once an identity authenticates using their password.",
           "type": "string",
           "default": "bcrypt",
-          "enum": ["argon2", "bcrypt"]
+          "enum": [
+            "argon2",
+            "bcrypt"
+          ]
         },
         "argon2": {
           "title": "Configuration for the Argon2id hasher.",
@@ -2690,7 +2969,9 @@
           "title": "Configuration for the Bcrypt hasher. Minimum is 4 when --dev flag is used and 12 otherwise.",
           "type": "object",
           "additionalProperties": false,
-          "required": ["cost"],
+          "required": [
+            "cost"
+          ],
           "properties": {
             "cost": {
               "type": "integer",
@@ -2712,7 +2993,11 @@
           "description": "One of the values: noop, aes, xchacha20-poly1305",
           "type": "string",
           "default": "noop",
-          "enum": ["noop", "aes", "xchacha20-poly1305"]
+          "enum": [
+            "noop",
+            "aes",
+            "xchacha20-poly1305"
+          ]
         }
       }
     },
@@ -2741,7 +3026,11 @@
           "title": "HTTP Cookie Same Site Configuration",
           "description": "Sets the session and CSRF cookie SameSite.",
           "type": "string",
-          "enum": ["Strict", "Lax", "None"],
+          "enum": [
+            "Strict",
+            "Lax",
+            "None"
+          ],
           "default": "Lax"
         }
       },
@@ -2771,7 +3060,9 @@
                   "patternProperties": {
                     "[a-zA-Z0-9-_.]+": {
                       "type": "object",
-                      "required": ["jwks_url"],
+                      "required": [
+                        "jwks_url"
+                      ],
                       "properties": {
                         "ttl": {
                           "type": "string",
@@ -2804,7 +3095,11 @@
           "type": "string",
           "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
           "default": "24h",
-          "examples": ["1h", "1m", "1s"]
+          "examples": [
+            "1h",
+            "1m",
+            "1s"
+          ]
         },
         "cookie": {
           "type": "object",
@@ -2840,7 +3135,11 @@
               "title": "Session Cookie SameSite Configuration",
               "description": "Sets the session cookie SameSite. Overrides `cookies.same_site`.",
               "type": "string",
-              "enum": ["Strict", "Lax", "None"]
+              "enum": [
+                "Strict",
+                "Lax",
+                "None"
+              ]
             }
           },
           "additionalProperties": false
@@ -2850,7 +3149,11 @@
           "description": "Sets when a session can be extended. Settings this value to `24h` will prevent the session from being extended before until 24 hours before it expires. This setting prevents excessive writes to the database. We highly recommend setting this value.",
           "type": "string",
           "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$",
-          "examples": ["1h", "1m", "1s"]
+          "examples": [
+            "1h",
+            "1m",
+            "1s"
+          ]
         }
       }
     },
@@ -2874,7 +3177,9 @@
       "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.",
       "type": "string",
       "pattern": "^(v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|$",
-      "examples": ["v0.5.0-alpha.1"]
+      "examples": [
+        "v0.5.0-alpha.1"
+      ]
     },
     "dev": {
       "type": "boolean"
@@ -2898,7 +3203,9 @@
       "type": "integer",
       "minimum": 0,
       "maximum": 65535,
-      "examples": [4434],
+      "examples": [
+        4434
+      ],
       "default": 0
     },
     "config": {
@@ -2934,6 +3241,40 @@
               },
               "default": []
             }
+          },
+          "web_hook": {
+            "title": "Global web_hook HTTP client configuration",
+            "description": "Configure the global HTTP client of the web_hook action.",
+            "type": "object",
+            "properties": {
+              "header_allowlist": {
+                "title": "Allowed request headers",
+                "description": "List of request headers that are forwarded to the web hook target in canonical form.",
+                "type": "array",
+                "items": {
+                  "type": "string"
+                },
+                "default": [
+                  "Accept",
+                  "Accept-Encoding",
+                  "Accept-Language",
+                  "Content-Length",
+                  "Content-Type",
+                  "Origin",
+                  "Priority",
+                  "Referer",
+                  "Sec-Ch-Ua",
+                  "Sec-Ch-Ua-Mobile",
+                  "Sec-Ch-Ua-Platform",
+                  "Sec-Fetch-Dest",
+                  "Sec-Fetch-Mode",
+                  "Sec-Fetch-Site",
+                  "Sec-Fetch-User",
+                  "True-Client-Ip",
+                  "User-Agent"
+                ]
+              }
+            }
           }
         }
       }
@@ -3004,10 +3345,14 @@
                             "const": true
                           }
                         },
-                        "required": ["enabled"]
+                        "required": [
+                          "enabled"
+                        ]
                       }
                     },
-                    "required": ["verification"]
+                    "required": [
+                      "verification"
+                    ]
                   },
                   {
                     "properties": {
@@ -3017,21 +3362,31 @@
                             "const": true
                           }
                         },
-                        "required": ["enabled"]
+                        "required": [
+                          "enabled"
+                        ]
                       }
                     },
-                    "required": ["recovery"]
+                    "required": [
+                      "recovery"
+                    ]
                   }
                 ]
               }
             },
-            "required": ["flows"]
+            "required": [
+              "flows"
+            ]
           }
         },
-        "required": ["selfservice"]
+        "required": [
+          "selfservice"
+        ]
       },
       "then": {
-        "required": ["courier"]
+        "required": [
+          "courier"
+        ]
       }
     },
     {
@@ -3050,21 +3405,33 @@
                 ]
               }
             },
-            "required": ["algorithm"]
+            "required": [
+              "algorithm"
+            ]
           }
         },
-        "required": ["ciphers"]
+        "required": [
+          "ciphers"
+        ]
       },
       "then": {
-        "required": ["secrets"],
+        "required": [
+          "secrets"
+        ],
         "properties": {
           "secrets": {
-            "required": ["cipher"]
+            "required": [
+              "cipher"
+            ]
           }
         }
       }
     }
   ],
-  "required": ["identity", "dsn", "selfservice"],
+  "required": [
+    "identity",
+    "dsn",
+    "selfservice"
+  ],
   "additionalProperties": false
 }
diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go
index d756338b1..a362e8caa 100644
--- a/selfservice/hook/web_hook.go
+++ b/selfservice/hook/web_hook.go
@@ -25,6 +25,7 @@ import (
 	grpccodes "google.golang.org/grpc/codes"
 
 	"github.com/ory/herodot"
+	"github.com/ory/kratos/driver/config"
 	"github.com/ory/kratos/identity"
 	"github.com/ory/kratos/request"
 	"github.com/ory/kratos/schema"
@@ -74,6 +75,7 @@ type (
 		x.HTTPClientProvider
 		x.TracingProvider
 		jsonnetsecure.VMProvider
+		config.Provider
 	}
 
 	templateContext struct {
@@ -359,7 +361,7 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error {
 			attribute.Bool("webhook.response.parse", parseResponse),
 		)
 
-		removeDisallowedHeaders(data)
+		removeDisallowedHeaders(data, e.deps.Config().WebhookHeaderAllowlist(ctx))
 
 		req, err := builder.BuildRequest(ctx, data)
 		if errors.Is(err, request.ErrCancel) {
@@ -429,32 +431,15 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error {
 	return nil
 }
 
-// RequestHeaderAllowList contains the allowed request headers that are forwarded
-// to the web hook target in canonical form (textproto.CanonicalMIMEHeaderKey).
-var RequestHeaderAllowList = map[string]struct{}{
-	"Accept":             {},
-	"Accept-Encoding":    {},
-	"Accept-Language":    {},
-	"Content-Length":     {},
-	"Content-Type":       {},
-	"Origin":             {},
-	"Priority":           {},
-	"Referer":            {},
-	"Sec-Ch-Ua":          {},
-	"Sec-Ch-Ua-Mobile":   {},
-	"Sec-Ch-Ua-Platform": {},
-	"Sec-Fetch-Dest":     {},
-	"Sec-Fetch-Mode":     {},
-	"Sec-Fetch-Site":     {},
-	"Sec-Fetch-User":     {},
-	"True-Client-Ip":     {},
-	"User-Agent":         {},
-}
+func removeDisallowedHeaders(data *templateContext, headerAllowlist []string) {
+	allowedMap := make(map[string]struct{})
+	for _, header := range headerAllowlist {
+		allowedMap[header] = struct{}{}
+	}
 
-func removeDisallowedHeaders(data *templateContext) {
 	headers := maps.Clone(data.RequestHeaders)
 	maps.DeleteFunc(headers, func(key string, _ []string) bool {
-		_, found := RequestHeaderAllowList[textproto.CanonicalMIMEHeaderKey(key)]
+		_, found := allowedMap[textproto.CanonicalMIMEHeaderKey(key)]
 		return !found
 	})
 	data.RequestHeaders = headers



Test patch:
diff --git a/driver/config/config_test.go b/driver/config/config_test.go
index 0b65f6c14..24ce81ff7 100644
--- a/driver/config/config_test.go
+++ b/driver/config/config_test.go
@@ -460,6 +460,13 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) {
 	assert.Equal(t, "https://example.org/bar", p.SelfServiceLinkMethodBaseURL(ctx).String())
 }
 
+func TestDefaultWebhookHeaderAllowlist(t *testing.T) {
+	t.Parallel()
+	ctx := context.Background()
+	p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
+	snapshotx.SnapshotT(t, p.WebhookHeaderAllowlist(ctx))
+}
+
 func TestViperProvider_Secrets(t *testing.T) {
 	t.Parallel()
 	ctx := context.Background()
diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go
index 700835309..16888917e 100644
--- a/selfservice/hook/web_hook_integration_test.go
+++ b/selfservice/hook/web_hook_integration_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/sirupsen/logrus/hooks/test"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"github.com/tidwall/gjson"
 	"github.com/tidwall/sjson"
 	"go.opentelemetry.io/otel/attribute"
 	sdktrace "go.opentelemetry.io/otel/sdk/trace"
@@ -60,14 +61,39 @@ var transientPayload = json.RawMessage(`{
 }`)
 
 func TestWebHooks(t *testing.T) {
-	_, reg := internal.NewFastRegistryWithMocks(t)
+	ctx := context.Background()
+	conf, reg := internal.NewFastRegistryWithMocks(t)
 	logger := logrusx.New("kratos", "test")
+
+	conf.Set(ctx, config.ViperKeyWebhookHeaderAllowlist, []string{
+		"Accept",
+		"Accept-Encoding",
+		"Accept-Language",
+		"Content-Length",
+		"Content-Type",
+		"Origin",
+		"Priority",
+		"Referer",
+		"Sec-Ch-Ua",
+		"Sec-Ch-Ua-Mobile",
+		"Sec-Ch-Ua-Platform",
+		"Sec-Fetch-Dest",
+		"Sec-Fetch-Mode",
+		"Sec-Fetch-Site",
+		"Sec-Fetch-User",
+		"True-Client-Ip",
+		"User-Agent",
+		"Valid-Header",
+	})
+
 	whDeps := struct {
 		x.SimpleLoggerWithClient
 		*jsonnetsecure.TestProvider
+		config.Provider
 	}{
-		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
+		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(ctx), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
 		jsonnetsecure.NewTestProvider(t),
+		reg,
 	}
 	type WebHookRequest struct {
 		Body    string
@@ -337,7 +363,8 @@ func TestWebHooks(t *testing.T) {
 								Header: map[string][]string{
 									"Some-Header":    {"Some-Value"},
 									"User-Agent":     {"Foo-Bar-Browser"},
-									"Invalid-Header": {"ignored"},
+									"Invalid-Header": {"should be ignored"},
+									"Valid-Header":   {"should not be ignored"},
 									"Cookie":         {"Some-Cookie-1=Some-Cookie-Value; Some-Cookie-2=Some-other-Cookie-Value", "Some-Cookie-3=Third-Cookie-Value"},
 								},
 								RequestURI: "/some_end_point",
@@ -393,6 +420,11 @@ func TestWebHooks(t *testing.T) {
 								// According to the HTTP spec any request method, but TRACE is allowed to
 								// have a body. Even this is a really bad practice for some of them, like for
 								// GET
+								assert.Zero(t, gjson.Get(whr.Body, "headers.Invalid-Header"))
+								assert.NotZero(t, gjson.Get(whr.Body, "headers.Valid-Header"))
+								whr.Body, err = sjson.Delete(whr.Body, "headers.Valid-Header")
+								assert.NoError(t, err)
+
 								assert.JSONEq(t, tc.expectedBody(req, f, s), whr.Body)
 							} else {
 								assert.Emptyf(t, whr.Body, "HTTP %s is not allowed to have a body", method)
@@ -1005,9 +1037,11 @@ func TestDisallowPrivateIPRanges(t *testing.T) {
 	whDeps := struct {
 		x.SimpleLoggerWithClient
 		*jsonnetsecure.TestProvider
+		config.Provider
 	}{
 		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
 		jsonnetsecure.NewTestProvider(t),
+		reg,
 	}
 
 	req := &http.Request{
@@ -1075,9 +1109,11 @@ func TestAsyncWebhook(t *testing.T) {
 	whDeps := struct {
 		x.SimpleLoggerWithClient
 		*jsonnetsecure.TestProvider
+		config.Provider
 	}{
 		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
 		jsonnetsecure.NewTestProvider(t),
+		reg,
 	}
 
 	req := &http.Request{
@@ -1155,9 +1191,11 @@ func TestWebhookEvents(t *testing.T) {
 	whDeps := struct {
 		x.SimpleLoggerWithClient
 		*jsonnetsecure.TestProvider
+		config.Provider
 	}{
 		x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
 		jsonnetsecure.NewTestProvider(t),
+		reg,
 	}
 
 	req := &http.Request{