# Secure service invocation with access control

When one app calls another over service invocation, Catalyst routes the request and authenticates the caller by its ID. By default, any app in a Catalyst project can invoke any other app. Service-invocation access control lets you lock this down: you declare which caller apps may invoke a target app — and which methods — and Catalyst denies everything else.

Access control is expressed as an access control list (ACL) inside a Catalyst `Configuration` resource that you attach to the target app. Unlike [workflow access policies](https://docs.diagrid.io/operate/project-operations/policies/workflows), there is no dedicated access-policy command — you manage a `Configuration` with the `diagrid configuration` commands (or a YAML manifest) and attach it to an app with `diagrid app update`.

## How access control works

A `Configuration`'s `accessControl` block has two parts:

- **`defaultAction`** — the global default, `allow` or `deny`, applied when no caller policy matches.
- **`policies`** — a list of per-caller rules. Each names a caller `appId` and can narrow access to specific **operations** (request paths and HTTP verbs).

Catalyst evaluates the caller's authenticated ID against these rules at the service-invocation boundary, before the request reaches the target app. A denied call is rejected with `403 Forbidden`, and the target app never sees it.

```mermaid
---
title: Service invocation access control
---
flowchart LR
  CALLER(Orchestrator App)
  subgraph Catalyst
    CID(orchestrator-app)
    ACL{Access control}:::decision
    OID(order-app)
  end
  TARGET(Order App)

  CALLER-->CID
  CID--"POST /orders"-->ACL
  ACL--"allowed"-->OID
  OID-->TARGET

  classDef decision stroke:#ed8936
```

Three behaviors to keep in mind:

- **No access control means open.** If the target app has no `Configuration` with an `accessControl` block, every app in the project can invoke it. Attaching a deny-by-default configuration is what turns enforcement on.
- **Most specific rule wins.** A matching `operations` rule takes precedence over the caller's `defaultAction`, which takes precedence over the global `defaultAction`.
- **IDs are authenticated, not asserted.** A caller cannot spoof another app's ID — Catalyst authenticates it cryptographically during service invocation, so an allow-list is a real trust boundary.

## Create and attach a policy

The fastest way to create a policy is with the `--default-action` and `--policy` flags:

```bash
diagrid configuration create order-app-acl \
  --project my-project \
  --default-action deny \
  --policy orchestrator-app:allow \
  --policy reporting-app:allow
```

This creates a deny-by-default configuration that allows only `orchestrator-app` and `reporting-app`. Attach it to the target app so it takes effect on inbound traffic:

```bash
diagrid app update order-app --project my-project --app-config order-app-acl
```

Now any other app that tries to invoke `order-app` is rejected with `403 Forbidden`. The `--policy appID:action` flag is all-or-nothing per caller — it allows or denies every method. To restrict callers to specific methods, define the configuration as a YAML manifest.

## Fine-grained rules with a YAML manifest

To grant access to specific paths and verbs, declare `operations` on a caller and apply the manifest:

```yaml
# order-app-acl.yaml
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: order-app-acl
spec:
  accessControl:
    defaultAction: deny
    policies:
      - appId: orchestrator-app
        operations:
          - name: /orders
            httpVerb: ["POST"]
            action: allow
          - name: /orders/*
            httpVerb: ["GET"]
            action: allow
```

```bash
diagrid apply -f order-app-acl.yaml --project my-project
diagrid app update order-app --project my-project --app-config order-app-acl
```

`diagrid apply` creates the configuration or replaces it in place, so you can re-run it as you iterate on the manifest. Here `orchestrator-app` can `POST /orders` and `GET` any `/orders/{id}` on `order-app`, and nothing else — every other caller and method is denied. Match the request path with `name`: an exact path like `/orders`, a single-segment wildcard like `/orders/*`, or a multi-segment wildcard like `/orders/**/items`. Set `httpVerb` to the methods the rule covers, or `["*"]` for any method.

## Start from deny by default

For the strictest posture, apply a configuration whose global default is `deny` with no caller policies, then attach it. Every cross-app call to the target app is denied until you add an allow rule:

```yaml
# deny-all.yaml
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: order-app-acl
spec:
  accessControl:
    defaultAction: deny
```

```bash
diagrid apply -f deny-all.yaml --project my-project
diagrid app update order-app --project my-project --app-config order-app-acl
```

From this baseline, layer in callers by editing the manifest and re-running `diagrid apply -f`. Apply one configuration per target app and keep its `accessControl` rules scoped to that app's callers.

## Manage configurations

List the configurations in a project:

```bash
diagrid configuration list --project my-project
```

Inspect a configuration, including its full ACL:

```bash
diagrid configuration get order-app-acl --project my-project -o yaml
```

Update a configuration with flags. The flags replace the corresponding fields, so the example below replaces all caller policies with a single grant for `orchestrator-app`:

```bash
diagrid configuration update order-app-acl \
  --project my-project \
  --policy orchestrator-app:allow
```

To change fine-grained rules, edit the YAML manifest and re-run `diagrid apply -f order-app-acl.yaml`. To stop enforcing access control, delete the configuration:

```bash
diagrid configuration delete order-app-acl --project my-project
```

## Verify a policy

To confirm a policy is enforced, invoke the target app from a caller that no rule allows. The caller receives a `403 Forbidden` and the request never reaches `order-app`. Calls that match an allow rule continue to succeed.

:::note
Access control changes take 10–15 seconds to propagate after you apply the configuration or attach it to an app. If a caller is allowed or denied unexpectedly, wait and retry, and confirm the ID in the policy exactly matches the caller's ID (case-sensitive).
:::

## Next steps

- Secure cross-app workflows with [workflow access policies](https://docs.diagrid.io/operate/project-operations/policies/workflows)
- See the [`Configuration` manifest reference](https://docs.diagrid.io/references/policies#configuration) for every `accessControl` field
- Learn how [Catalyst secures the platform end to end](https://docs.diagrid.io/concepts/security)
