From 20d5e869756e817e162f16877ad92f4f127bb5ed Mon Sep 17 00:00:00 2001 From: HaimKortovich Date: Wed, 13 May 2026 13:04:31 -0500 Subject: [PATCH] refactor auth --- config/dev.exs | 11 + config/runtime.exs | 4 +- flake.nix | 1 + lib/policy_service/application.ex | 6 +- .../plugs/authentication_plug.ex | 187 ----------------- .../plugs/authorization_plug.ex | 188 ------------------ .../plugs/authorize_roles.ex | 88 ++++++++ .../plugs/claims_extractor.ex | 127 ------------ .../plugs/extract_organization_id.ex | 22 ++ .../plugs/require_organization_id.ex | 27 +++ lib/policy_service_web/router.ex | 34 +++- ops/chart/values.yaml | 6 + 12 files changed, 183 insertions(+), 518 deletions(-) delete mode 100644 lib/policy_service_web/plugs/authentication_plug.ex delete mode 100644 lib/policy_service_web/plugs/authorization_plug.ex create mode 100644 lib/policy_service_web/plugs/authorize_roles.ex delete mode 100644 lib/policy_service_web/plugs/claims_extractor.ex create mode 100644 lib/policy_service_web/plugs/extract_organization_id.ex create mode 100644 lib/policy_service_web/plugs/require_organization_id.ex diff --git a/config/dev.exs b/config/dev.exs index fc6d18a..d12fb3a 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -73,3 +73,14 @@ config :phoenix, :stacktrace_depth, 20 config :phoenix, :plug_init_mode, :runtime config :open_api_spex, :cache_adapter, OpenApiSpex.Plug.NoneCache + +config :policy_service, :zitadel, + issuer: System.get_env("ZITADEL_ISSUER", "https://id.corredorconnect.com"), + client_id: System.get_env("ZITADEL_CLIENT_ID"), + client_secret: System.get_env("ZITADEL_CLIENT_SECRET"), + roles_claim: "urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles", + required_scopes: [ + "openid", + "profile", + "urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles" + ] diff --git a/config/runtime.exs b/config/runtime.exs index 8268e11..3dfe318 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -45,11 +45,11 @@ config :policy_service, :zitadel, issuer: System.get_env("ZITADEL_ISSUER", "https://id.corredorconnect.com"), client_id: System.get_env("ZITADEL_CLIENT_ID"), client_secret: System.get_env("ZITADEL_CLIENT_SECRET"), + roles_claim: "urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles", required_scopes: [ "openid", "profile", - "urn:zitadel:iam:user:resourceowner", - "urn:zitadel:iam:org:projects:roles" + "urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles" ] # ## Using releases diff --git a/flake.nix b/flake.nix index 4b12bc8..a9c209d 100644 --- a/flake.nix +++ b/flake.nix @@ -48,6 +48,7 @@ elixir-ls kubernetes-helm git + nodejs ]; }; } diff --git a/lib/policy_service/application.ex b/lib/policy_service/application.ex index b5774f2..96a0edd 100644 --- a/lib/policy_service/application.ex +++ b/lib/policy_service/application.ex @@ -3,12 +3,12 @@ defmodule PolicyService.Application do # for more information on OTP Applications @moduledoc false + @zitadel Application.fetch_env!(:policy_service, :zitadel) + use Application @impl true def start(_type, _args) do - zitadel_config = Application.get_env(:policy_service, :zitadel, []) - children = [ PolicyService.CommandedApp, PolicyService.Handlers.QuoteRequestHandler, @@ -22,7 +22,7 @@ defmodule PolicyService.Application do {Phoenix.PubSub, name: PolicyService.PubSub, pool_size: 1}, {Oidcc.ProviderConfiguration.Worker, %{ - issuer: Keyword.get(zitadel_config, :issuer), + issuer: @zitadel[:issuer], name: PolicyService.ZitadelProvider }}, PolicyServiceWeb.Endpoint diff --git a/lib/policy_service_web/plugs/authentication_plug.ex b/lib/policy_service_web/plugs/authentication_plug.ex deleted file mode 100644 index 5e98dee..0000000 --- a/lib/policy_service_web/plugs/authentication_plug.ex +++ /dev/null @@ -1,187 +0,0 @@ -defmodule PolicyServiceWeb.Plugs.AuthenticationPlug do - @moduledoc """ - Authentication plug for validating JWT tokens using Zitadel. - - This plug validates JWT tokens using the oidcc library and extracts - user claims for use throughout the application. - """ - - import Plug.Conn - require Logger - - @doc """ - Initializes the authentication plug. - - ## Options - - :provider - The OIDCC provider configuration worker name (required) - """ - def init(opts) do - provider = Keyword.fetch!(opts, :provider) - - zitadel_config = Application.get_env(:policy_service, :zitadel, []) - - %{ - provider: provider, - client_id: Keyword.get(zitadel_config, :client_id), - client_secret: Keyword.get(zitadel_config, :client_secret), - required_scopes: Keyword.get(zitadel_config, :required_scopes, []) - } - end - - @doc """ - Authenticates the request by validating the JWT token. - - Extracts the Authorization header, validates the JWT token using - Zitadel's JWKS, and assigns the extracted claims to the connection. - """ - def call(conn, config) do - with {:ok, token} <- extract_token(conn), - {:ok, validated_token} <- validate_token(token, config), - {:ok, claims} <- extract_claims(validated_token) do - conn - |> assign(:authenticated, true) - |> assign(:current_user, claims) - |> assign(:user_id, claims.user_id) - |> assign(:org_id, claims.org_id) - |> assign(:roles, claims.roles) - |> assign(:scopes, claims.scopes) - else - {:error, :missing_token} -> - handle_missing_token(conn) - - {:error, :invalid_token} = error -> - handle_invalid_token(conn, error) - - {:error, :expired_token} = error -> - handle_expired_token(conn, error) - - {:error, :insufficient_scopes} = error -> - handle_insufficient_scopes(conn, error) - - {:error, reason} = error -> - handle_authentication_error(conn, error, reason) - end - end - - defp extract_token(conn) do - case get_req_header(conn, "authorization") do - ["Bearer " <> token] when token != "" -> - {:ok, token} - - ["Bearer " <> token] when token == "" -> - {:error, :missing_token} - - ["bearer " <> token] when token != "" -> - {:ok, token} - - [] -> - {:error, :missing_token} - - _ -> - {:error, :invalid_token} - end - end - - defp validate_token(token, config) do - try do - case Oidcc.Token.validate_jwt( - token, - config.provider, - %{} - ) do - {:ok, validated_token} -> - validate_required_scopes(validated_token, config) - - {:error, :invalid_token} -> - Logger.warning("Invalid JWT token provided") - {:error, :invalid_token} - - {:error, :expired_token} -> - Logger.warning("Expired JWT token provided") - {:error, :expired_token} - - {:error, reason} -> - Logger.error("Token validation failed: #{inspect(reason)}") - {:error, :invalid_token} - end - rescue - error -> - Logger.error("Token validation error: #{inspect(error)}") - {:error, :invalid_token} - end - end - - defp validate_required_scopes(validated_token, config) do - required_scopes = config.required_scopes || [] - - if required_scopes == [] do - {:ok, validated_token} - else - token_scopes = PolicyServiceWeb.Plugs.ClaimsExtractor.get_scopes(validated_token.claims) - - if has_all_required_scopes?(token_scopes, required_scopes) do - {:ok, validated_token} - else - missing_scopes = required_scopes -- token_scopes - Logger.warning("Token missing required scopes: #{inspect(missing_scopes)}") - {:error, :insufficient_scopes} - end - end - end - - defp has_all_required_scopes?(token_scopes, required_scopes) do - Enum.all?(required_scopes, fn scope -> scope in token_scopes end) - end - - defp extract_claims(validated_token) do - try do - claims = PolicyServiceWeb.Plugs.ClaimsExtractor.extract_claims(validated_token) - PolicyServiceWeb.Plugs.ClaimsExtractor.validate_claims!(validated_token.claims) - {:ok, claims} - rescue - error -> - Logger.error("Failed to extract claims: #{inspect(error)}") - {:error, :invalid_claims} - end - end - - defp handle_missing_token(conn) do - conn - |> put_resp_content_type("application/json") - |> send_resp(401, Jason.encode!(%{error: "Missing authentication token"})) - |> halt() - end - - defp handle_invalid_token(conn, _error) do - conn - |> put_resp_content_type("application/json") - |> send_resp(401, Jason.encode!(%{error: "Invalid authentication token"})) - |> halt() - end - - defp handle_expired_token(conn, _error) do - conn - |> put_resp_content_type("application/json") - |> send_resp(401, Jason.encode!(%{error: "Authentication token has expired"})) - |> halt() - end - - defp handle_insufficient_scopes(conn, _error) do - conn - |> put_resp_content_type("application/json") - |> send_resp( - 403, - Jason.encode!(%{error: "Insufficient permissions - required scopes not granted"}) - ) - |> halt() - end - - defp handle_authentication_error(conn, _error, reason) do - Logger.error("Authentication error: #{inspect(reason)}") - - conn - |> put_resp_content_type("application/json") - |> send_resp(401, Jason.encode!(%{error: "Authentication failed"})) - |> halt() - end -end diff --git a/lib/policy_service_web/plugs/authorization_plug.ex b/lib/policy_service_web/plugs/authorization_plug.ex deleted file mode 100644 index d0b6265..0000000 --- a/lib/policy_service_web/plugs/authorization_plug.ex +++ /dev/null @@ -1,188 +0,0 @@ -defmodule PolicyServiceWeb.Plugs.AuthorizationPlug do - @moduledoc """ - Authorization plug for enforcing role-based access control. - - This plug checks if authenticated users have the required roles - and scopes to access protected resources. - """ - - import Plug.Conn - require Logger - - @doc """ - Initializes the authorization plug. - - ## Options - - :required_roles - List of roles that can access the resource (optional) - - :required_scopes - List of scopes required to access the resource (optional) - - :resource_owner_check - Function to check if user owns the resource (optional) - """ - def init(opts) do - required_permission = Keyword.get(opts, :required_permission, nil) - required_scopes = Keyword.get(opts, :required_scopes, []) - resource_owner_check = Keyword.get(opts, :resource_owner_check, nil) - - %{ - required_permission: required_permission, - required_scopes: required_scopes, - resource_owner_check: resource_owner_check - } - end - - @doc """ - Authorizes the request by checking roles and scopes. - - Verifies that the authenticated user has the required roles and scopes - to access the requested resource. - """ - def call(conn, config) do - user_roles = conn.assigns[:roles] || [] - user_scopes = conn.assigns[:scopes] || [] - user_id = conn.assigns[:user_id] - org_id = conn.assigns[:org_id] - - with :ok <- check_roles(user_roles, config.required_roles), - :ok <- check_scopes(user_scopes, config.required_scopes), - :ok <- check_resource_ownership(conn, config.resource_owner_check, user_id, org_id) do - conn - else - {:error, :insufficient_role} -> - handle_insufficient_role(conn, config.required_roles) - - {:error, :insufficient_scope} -> - handle_insufficient_scope(conn, config.required_scopes) - - {:error, :not_owner} -> - handle_not_owner(conn) - end - end - - defp check_roles(_user_roles, required_roles) when required_roles == [] do - :ok - end - - defp check_roles(user_roles, required_permission) do - if has_any_role?(user_roles, required_permission) do - :ok - else - Logger.warning( - "User with roles #{inspect(user_roles)} lacks required permission: #{inspect(required_permission)}" - ) - - {:error, :insufficient_role} - end - end - - defp check_scopes(_user_scopes, required_scopes) when required_scopes == [] do - :ok - end - - defp check_scopes(user_scopes, required_scopes) do - if has_all_scopes?(user_scopes, required_scopes) do - :ok - else - Logger.warning( - "User with scopes #{inspect(user_scopes)} lacks required scopes: #{inspect(required_scopes)}" - ) - - {:error, :insufficient_scope} - end - end - - defp check_resource_ownership(_conn, nil, _user_id, _org_id) do - :ok - end - - defp check_resource_ownership(conn, check_function, user_id, org_id) do - try do - if check_function.(conn, user_id, org_id) do - :ok - else - {:error, :not_owner} - end - rescue - error -> - Logger.error("Resource ownership check failed: #{inspect(error)}") - {:error, :not_owner} - end - end - - @doc """ - Checks if the user has any of the required roles. - - Supports role hierarchy where admin has all permissions. - """ - def has_any_role?(user_roles, required_roles) do - cond do - "admin" in user_roles -> - true - - Enum.any?(required_roles, fn role -> role in user_roles end) -> - true - - true -> - false - end - end - - @doc """ - Checks if the user has all of the required scopes. - """ - def has_all_scopes?(user_scopes, required_scopes) do - Enum.all?(required_scopes, fn scope -> scope in user_scopes end) - end - - @doc """ - Checks if the user can access a specific resource based on ownership. - - Admins can access any resource. Other users can only access their own resources. - """ - def can_access_resource?(user_roles, resource_user_id, current_user_id) do - cond do - "admin" in user_roles -> - true - - resource_user_id == current_user_id -> - true - - true -> - false - end - end - - defp handle_insufficient_role(conn, required_roles) do - conn - |> put_resp_content_type("application/json") - |> send_resp( - 403, - Jason.encode!(%{ - error: "Insufficient permissions", - required_roles: required_roles - }) - ) - |> halt() - end - - defp handle_insufficient_scope(conn, required_scopes) do - conn - |> put_resp_content_type("application/json") - |> send_resp( - 403, - Jason.encode!(%{ - error: "Insufficient permissions", - required_scopes: required_scopes - }) - ) - |> halt() - end - - defp handle_not_owner(conn) do - conn - |> put_resp_content_type("application/json") - |> send_resp( - 403, - Jason.encode!(%{error: "You do not have permission to access this resource"}) - ) - |> halt() - end -end diff --git a/lib/policy_service_web/plugs/authorize_roles.ex b/lib/policy_service_web/plugs/authorize_roles.ex new file mode 100644 index 0000000..f970380 --- /dev/null +++ b/lib/policy_service_web/plugs/authorize_roles.ex @@ -0,0 +1,88 @@ +defmodule PolicyServiceWeb.Plugs.AuthorizeRoles do + @moduledoc """ + Authorize request based on Zitadel role permissions. + + After token introspection, checks if the user holds any of the + `required_permissions` roles for the organization identified by + `X-Organization-Id` header. + + The Zitadel roles claim structure is: + %{"urn:zitadel:iam:org:project::roles": { + "": { + "": "" + }, + "": { + "": "" + } + }} + """ + + @behaviour Plug + + import Plug.Conn + + @impl Plug + def init(opts), + do: + opts + |> Keyword.validate!([ + :roles_claim + ]) + + @impl Plug + def call(conn, opts) do + IO.inspect(conn.private) + + required_permissions = + conn.private[Phoenix.Router.Route] + |> Map.get(:options, %{}) + |> Map.get(:required_permissions, []) + + if authorized?(conn, opts.roles_claim, required_permissions) do + conn + else + conn + |> put_resp_content_type("application/json") + |> halt() + |> send_resp( + :forbidden, + %{error: "Forbidden", reason: "Missing required role"} + ) + end + end + + defp authorized?(conn, roles_claim, required_permissions) do + org_id = conn.private[PolicyServiceWeb.Plugs.ExtractOrganizationId] + + with true <- org_id_given?(org_id), + roles_map <- get_roles_map(conn, roles_claim), + true <- has_any_role?(roles_map, org_id, required_permissions) do + true + else + _ -> false + end + end + + defp org_id_given?(org_id), do: not is_nil(org_id) + + defp get_roles_map(conn, roles_claim) do + case conn.private[Oidcc.Plug.IntrospectToken] do + %{claims: %{^roles_claim => %{} = roles_map}} -> + role = Map.get(roles_map, roles_claim, %{}) + role + + %{claims: claims} when is_map(claims) -> + Map.get(claims, roles_claim, %{}) + + _ -> + %{} + end + end + + defp has_any_role?(roles_map, org_id, required_permissions) do + Enum.any?(required_permissions, fn role -> + role_orgs = Map.get(roles_map, role, %{}) + Map.has_key?(role_orgs, org_id) + end) + end +end diff --git a/lib/policy_service_web/plugs/claims_extractor.ex b/lib/policy_service_web/plugs/claims_extractor.ex deleted file mode 100644 index 3b36074..0000000 --- a/lib/policy_service_web/plugs/claims_extractor.ex +++ /dev/null @@ -1,127 +0,0 @@ -defmodule PolicyServiceWeb.Plugs.ClaimsExtractor do - @moduledoc """ - Extracts and normalizes Zitadel claims from validated JWT tokens. - - This module handles the extraction of organization ID, user ID, roles, - and scopes from Zitadel standard claims and normalizes them for use - throughout the application. - """ - - @doc """ - Extracts and normalizes claims from a validated OIDCC token. - - ## Parameters - - token: The validated OIDCC token struct - - ## Returns - A map containing normalized claims: - - org_id: Organization ID from Zitadel claims - - user_id: User ID from Zitadel claims - - roles: List of user roles - - scopes: List of granted scopes - """ - def extract_claims(token) do - claims = token.claims - - %{ - org_id: get_org_id(claims), - user_id: get_user_id(claims), - roles: get_roles(claims), - scopes: get_scopes(claims) - } - end - - @doc """ - Extracts organization ID from Zitadel claims. - - Uses the Zitadel-specific claim 'urn:zitadel:iam:user:resourceowner:id' - or falls back to the standard 'azp' (authorized party) claim. - """ - def get_org_id(claims) do - claims["urn:zitadel:iam:user:resourceowner:id"] || - claims["azp"] || - "default" - end - - @doc """ - Extracts user ID from Zitadel claims. - - Uses the standard 'sub' (subject) claim as the primary user identifier. - """ - def get_user_id(claims) do - claims["sub"] || - raise "Missing required user_id claim" - end - - @doc """ - Extracts roles from Zitadel claims. - - Zitadel provides roles in the 'urn:zitadel:iam:org:project:roles' claim - with a nested structure containing role keys and organization context. - """ - def get_roles(claims) do - project_roles = claims["urn:zitadel:iam:org:project:roles"] || [] - extract_roles_from_project_roles(project_roles) - end - - @doc """ - Extracts roles from Zitadel's nested project roles structure. - - The structure is typically: - [ - %{"role1" => %{"id" => "org1", "primaryDomain" => "domain1"}}, - %{"role2" => %{"id" => "org2", "primaryDomain" => "domain2"}} - ] - """ - def extract_roles_from_project_roles(project_roles) when is_list(project_roles) do - project_roles - |> Enum.flat_map(fn role_map -> - role_map - |> Map.keys() - |> Enum.reject(&(&1 == "id" || &1 == "primaryDomain")) - end) - |> Enum.map(&String.downcase/1) - |> Enum.uniq() - end - - def extract_roles_from_project_roles(_), do: [] - - @doc """ - Extracts scopes from Zitadel claims. - - Uses the standard 'scope' claim and splits it into a list. - """ - def get_scopes(claims) do - scope_string = claims["scope"] || "" - - scope_string - |> String.split(" ") - |> Enum.reject(&(&1 == "")) - end - - @doc """ - Checks if the given claims contain a specific role. - """ - def has_role?(claims, role) when is_binary(role) do - roles = get_roles(claims) - role_lower = String.downcase(role) - role_lower in roles - end - - @doc """ - Checks if the given claims contain a specific scope. - """ - def has_scope?(claims, scope) when is_binary(scope) do - scopes = get_scopes(claims) - scope in scopes - end - - @doc """ - Validates that all required claims are present. - """ - def validate_claims!(claims) do - _user_id = get_user_id(claims) - _org_id = get_org_id(claims) - :ok - end -end diff --git a/lib/policy_service_web/plugs/extract_organization_id.ex b/lib/policy_service_web/plugs/extract_organization_id.ex new file mode 100644 index 0000000..9992786 --- /dev/null +++ b/lib/policy_service_web/plugs/extract_organization_id.ex @@ -0,0 +1,22 @@ +defmodule PolicyServiceWeb.Plugs.ExtractOrganizationId do + @moduledoc """ + Extract `X-Organization-Id` request header. + + Stores the organization identifier in conn.private[__MODULE__] for downstream authorization checks. + """ + + @behaviour Plug + + import Plug.Conn, only: [get_req_header: 2, put_private: 3] + + @impl Plug + def init(_opts), do: %{} + + @impl Plug + def call(conn, _opts) do + case get_req_header(conn, "x-organization-id") do + [org_id | _rest] -> put_private(conn, __MODULE__, org_id) + [] -> put_private(conn, __MODULE__, nil) + end + end +end diff --git a/lib/policy_service_web/plugs/require_organization_id.ex b/lib/policy_service_web/plugs/require_organization_id.ex new file mode 100644 index 0000000..d88e62c --- /dev/null +++ b/lib/policy_service_web/plugs/require_organization_id.ex @@ -0,0 +1,27 @@ +defmodule PolicyServiceWeb.Plugs.RequireOrganizationId do + @moduledoc """ + Ensure `X-Organization-Id` header is provided. + + This plug must be used after `PolicyServiceWeb.Plugs.ExtractOrganizationId`. + """ + + @behaviour Plug + + import Plug.Conn, only: [get_req_header: 2, halt: 1, send_resp: 3] + + @impl Plug + def init(_opts), do: %{} + + @impl Plug + def call(conn, _opts) do + case get_req_header(conn, "x-organization-id") do + [] -> + conn + |> halt() + |> send_resp(:bad_request, "The organization id is required") + + [_org_id] -> + conn + end + end +end diff --git a/lib/policy_service_web/router.ex b/lib/policy_service_web/router.ex index 95f0615..7e49879 100644 --- a/lib/policy_service_web/router.ex +++ b/lib/policy_service_web/router.ex @@ -4,17 +4,26 @@ defmodule PolicyServiceWeb.Router do alias PolicyServiceWeb.PolicyController alias PolicyServiceWeb.HealthController + @zitadel Application.fetch_env!(:policy_service, :zitadel) + pipeline :api do plug OpenApiSpex.Plug.PutApiSpec, module: PolicyServiceWeb.ApiSpec end - pipeline :authenticated do - plug PolicyServiceWeb.Plugs.AuthenticationPlug, - provider: PolicyService.ZitadelProvider - end + pipeline :authorize do + plug Oidcc.Plug.ExtractAuthorization + plug Oidcc.Plug.RequireAuthorization - pipeline :authorized do - plug PolicyServiceWeb.Plugs.AuthorizationPlug + plug PolicyServiceWeb.Plugs.RequireOrganizationId + plug PolicyServiceWeb.Plugs.ExtractOrganizationId + + plug Oidcc.Plug.IntrospectToken, + provider: PolicyService.ZitadelProvider, + client_id: @zitadel[:client_id], + client_secret: @zitadel[:client_secret] + + plug PolicyServiceWeb.Plugs.AuthorizeRoles, + roles_claim: @zitadel[:roles_claim] end get "/health", HealthController, :health @@ -26,14 +35,17 @@ defmodule PolicyServiceWeb.Router do get "/openapi", OpenApiSpex.Plug.RenderSpec, [] scope "/v1" do - pipe_through [:authenticated, :authorized] + pipe_through [:authorize] - get "/policies", PolicyController, :index, required_permission: "policy:read" - get "/policies/:application_id", PolicyController, :show, required_permission: "policy:read" - post "/policies", PolicyController, :create, required_permission: "policy:create_request" + get "/policies", PolicyController, :index, required_permission: ["policy:read"] + + get "/policies/:application_id", PolicyController, :show, + required_permissions: ["policy:read"] + + post "/policies", PolicyController, :create, required_permissions: ["policy:create_request"] post "/policies/:application_id/accept", PolicyController, :accept, - required_permission: "policy:submit_solicitation" + required_permission: ["policy:submit_solicitation"] end end diff --git a/ops/chart/values.yaml b/ops/chart/values.yaml index 0057520..13a0bc3 100644 --- a/ops/chart/values.yaml +++ b/ops/chart/values.yaml @@ -86,6 +86,12 @@ controllers: secretKeyRef: name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-apiapp-client-secret' key: clientSecret + + ZITADEL_PROJECT_ID: + valueFrom: + secretKeyRef: + name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-apiapp-client-secret' + key: projectId probes: liveness: enabled: true