How-To: Apply workflow access policies

Restrict which applications can invoke workflow and activity operations on a target application

Using workflow access policies, you can control which calling applications are permitted to invoke specific workflow operations on a target application. A WorkflowAccessPolicy is a Kubernetes CRD (or YAML resource in self-hosted mode) that is evaluated on the callee side. You can scope it to one or more target applications with scopes, or omit scopes to apply the policy to all applications.

Workflow access policies are a pure allow-list. A request is permitted if, and only if, some rule in some loaded policy matches the caller, the operation, and the workflow or activity name. With no policies loaded, all calls are allowed (open by default), preserving backward compatibility. Self-calls (where the caller App ID is the same as the target App ID) are always allowed, regardless of policy contents.

Prerequisites

  • Dapr installed with mTLS enabled. mTLS is required for cross-app enforcement because the caller’s identity is extracted from the SPIFFE ID embedded in the mTLS client certificate.

Terminology

Caller and target applications

Workflow access policies describe what caller applications are allowed to do against a target application:

  • The target application is the application that hosts the workflow or activity being scheduled. The policy is enforced inside the target’s Dapr sidecar (callee-side enforcement). A policy applies to a target through the spec.scopes field, which lists the target App IDs the policy applies to.
  • The caller application is the application that is invoking the workflow or activity. For cross-app calls, the caller’s App ID is taken from the SPIFFE identity in the mTLS client certificate. For same-sidecar (self) calls, the local App ID is used directly.

The SPIFFE ID embedded in the mTLS certificate has the format spiffe://<trustdomain>/ns/<namespace>/<appid>. The App ID and namespace are extracted from this identity when a workflow access policy is evaluated.

Workflow and activity name matching

Workflow and activity names in policy rules are matched as either exact names or glob patterns. Glob matching follows Go’s path.Match semantics:

  • * matches any sequence of non-separator characters
  • ? matches any single non-separator character
  • [abc] matches any character in the set (a character class)

Operations

Workflow and activity rules grant the listed callers permission to schedule the named workflow or activity. A parent workflow on one app can schedule a child workflow or activity on a target app; the target’s policy decides whether the call is permitted.

  • Workflow rules require an operations field. Set it to [schedule].
  • Activity rules don’t have an operations field. Activities only support scheduling.

CRD specification

The example below shows every field in a workflow access policy. The policy is applied to orders-target in the production namespace (via scopes). It grants the frontend and ops-console applications (the callers) permission to schedule OrderWF, schedule any workflow whose name starts with Report, and schedule the ChargePayment activity and any activity whose name starts with RefundEvent.

apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
  name: orders-policy
  namespace: production
scopes:
  - orders-target
spec:
  rules:
    - callers:
        - appID: frontend
        - appID: ops-console
      workflows:
        - name: OrderWF
          operations: [schedule]
        - name: "Report*"
          operations: [schedule]
      activities:
        - name: ChargePayment
        - name: "RefundEvent*"

Spec fields

Fields are listed in the order they appear in the YAML document.

FieldRequiredTypeDescription
scopesNlistTarget App IDs this policy applies to. If omitted or empty, the policy applies to all applications. The policy is always enforced on the callee (target) side.
rulesNlistAllow-list of rules. A call is permitted if any rule matches. If rules is omitted or empty while a policy is loaded for the target, all cross-app calls are denied.
rules[].callersYlistList of caller objects this rule applies to. Must contain at least one entry. Every caller must be listed explicitly (with the exception of self-calls, which are always allowed).
rules[].callers[].appIDYstringThe Dapr App ID of the calling application. The caller must be in the same namespace as the target; cross-namespace workflow calls are always denied and are not supported.
rules[].workflowsN*listWorkflow rules granted to the matched callers.
rules[].workflows[].nameYstringExact name or glob pattern of the workflow.
rules[].workflows[].operationsYlistSet to [schedule]. The CRD also accepts terminate, raise, pause, resume, purge, get, rerun for forward compatibility; these have no effect today because the matching public workflow APIs do not route cross-app.
rules[].activitiesN*listActivity rules granted to the matched callers.
rules[].activities[].nameYstringExact name or glob pattern of the activity. Activities only support the schedule operation, so there is no operations field.

* At least one of workflows or activities must be present in each rule.

Policy semantics

  1. No policies loaded: All workflow and activity requests are allowed. This preserves backward compatibility when no policies exist.
  2. One or more policies loaded: The target defaults to deny. A cross-app schedule is permitted only if some rule matches the caller and the workflow or activity name.
  3. Self-calls are always allowed: If the caller App ID is the same as the target App ID, the request is permitted regardless of policy contents. This means a target app does not need to list itself in its own policy to schedule its own workflows or activities (including the internal reminder-based execution path).
  4. Cross-namespace workflow calls are always denied. Cross-namespace workflows are not supported. A policy is namespaced and applies to target apps in its own namespace via scopes. The caller must be in the same namespace as the target; calls from any other namespace are always rejected, regardless of whether policies are loaded and even if the caller App ID appears in a rule.
  5. mTLS is required for cross-app enforcement: if any policy is loaded and mTLS is not active, cross-app calls are denied because the caller’s SPIFFE identity cannot be verified.
  6. Glob matching: *, ?, and character classes work on both workflow and activity names.

Enforcement paths

Workflow access policies are enforced inside the orchestrator and activity actors, under the actor lock, after the workflow’s internal state has been loaded. This eliminates any time-of-check-to-time-of-use race between resolving a workflow’s name and dispatching the operation.

The cross-app paths covered today are scheduling a child workflow or activity on another app: a parent workflow on the calling app reaches the target app’s workflow/activity actor, which evaluates the policy before dispatching. The same enforcement point also blocks cross-app callers attempting non-subject actor methods or trying to inject reminders into a target actor.

Example policies

Scenario 1: Restrict who can schedule a cross-app workflow

Allow orchestrator-app to schedule OrderWF on the order-service application in the default namespace. No other applications can schedule this workflow, with the exception of order-service itself (self-calls are always allowed).

apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
  name: order-service-policy
  namespace: default
scopes:
  - order-service
spec:
  rules:
    - callers:
        - appID: orchestrator-app
      workflows:
        - name: OrderWF
          operations: [schedule]

Scenario 2: Glob-matched workflow scheduling

Allow analytics-app to schedule any workflow whose name begins with Report on the reporting-service application.

apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
  name: reporting-glob
  namespace: default
scopes:
  - reporting-service
spec:
  rules:
    - callers:
        - appID: analytics-app
      workflows:
        - name: "Report*"
          operations: [schedule]

Scenario 3: Cross-app activities (multi-application workflows)

When using multi-application workflows, the target application does not need to list itself in the callers to execute its own activities. Self-calls are always allowed, so the policy only describes which other apps may schedule activities on the target. In the policy below, orchestrator-app can schedule the TrainModel and ValidateModel activities on the ml-worker application. No other applications can. The orchestrator-app must be in the same namespace as ml-worker, because cross-namespace workflows are not supported and are always denied.

apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
  name: ml-worker-policy
  namespace: production
scopes:
  - ml-worker
spec:
  rules:
    - callers:
        - appID: orchestrator-app
      activities:
        - name: TrainModel
        - name: ValidateModel

ml-worker can still schedule TrainModel and ValidateModel on itself without appearing in the rule because it is the local app.

Scenario 4: Mixed workflow and activity access for a single caller

A single rule can grant a caller scheduling access to both workflows and activities. Here the api-gateway application can schedule the ChargeCustomer workflow, the ChargePayment activity, and any activity whose name starts with Refund on the payments-service application.

apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
  name: payments-policy
  namespace: default
scopes:
  - payments-service
spec:
  rules:
    - callers:
        - appID: api-gateway
      workflows:
        - name: ChargeCustomer
          operations: [schedule]
      activities:
        - name: ChargePayment
        - name: "Refund*"

Scenario 5: Namespace-wide deny by default

Much like a Kubernetes NetworkPolicy that selects every pod in a namespace to default-deny ingress, you can apply a single policy to every application in a namespace. Omit scopes so the policy applies to all applications, and define no rules (an empty or omitted rules list). Because policies are a pure allow-list, a loaded policy with nothing to match denies every cross-app request. Self-calls are still always allowed, so each app can continue to schedule its own workflows and activities (including the internal reminder-based execution path).

apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
  name: default-deny
  namespace: production
spec:
  rules: []

This is the strictest posture for a namespace: no application can schedule a workflow or activity on any other application in production, and every cross-app call is denied. Start from this default-deny baseline and layer on additional, narrowly scoped policies (following the earlier scenarios) to grant access to specific callers as needed. Because policies are a pure allow-list, the rules from those additional policies combine, opening up only the access they explicitly grant.

Production best practices

  • Use deny by default. Loading any WorkflowAccessPolicy for a target automatically denies cross-app requests that are not explicitly listed. For the strictest baseline, start from a namespace-wide default-deny policy (see Scenario 5) and add narrowly scoped policies only as needed. Keep policies minimal and review them when adding new workflows.
  • Use glob patterns conservatively. Patterns like * can grant broader access than intended. Prefer exact names where possible, and use glob patterns only for stable name families.
  • Enable mTLS. mTLS is required for cross-app enforcement. Without mTLS, cross-app requests are denied when any policy is loaded.
  • Audit denial logs. Dapr logs a warning whenever a request is denied by a workflow access policy. Use these logs to spot misconfiguration and unauthorized callers.
  • Use scopes to target the policy. Apply each policy only to the apps that should enforce it, reducing the surface area each daprd has to load.

Self-hosted setup

In self-hosted mode, place the workflow access policy YAML in the resources directory ($HOME/.dapr/components by default, or the path passed via --resources-path).

apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
  name: my-policy
scopes:
  - my-app
spec:
  rules:
    - callers:
        - appID: frontend
      workflows:
        - name: MyWorkflow
          operations: [schedule]

For cross-app enforcement, mTLS must be enabled by running Sentry locally. See Setup & configure mTLS certificates for self hosted for details on configuring mTLS in self-hosted mode.

Kubernetes setup

In Kubernetes, apply the WorkflowAccessPolicy CRD with kubectl:

kubectl apply -f workflow-access-policy.yaml

The Dapr operator watches for WorkflowAccessPolicy resources and distributes them to the appropriate sidecars based on the scopes field. mTLS is enabled by default in Kubernetes mode.

Hot-reload support

Workflow access policies are hot-reloaded in both Kubernetes and self-hosted modes. Creating, updating, or deleting a policy takes effect without restarting the Dapr sidecar.