Using ACLs

This section describes the practical aspects of configuring Access Control Lists (ACLs) with the Java/Scala SDKs, if you are not sure what ACLs are or how they work, see Access Control Lists first.

Configuring ACLs

Kalix ACLs consist of a list of principal matchers that are allowed to invoke a method, and a list of principal matchers that should be denied to invoke a method. For a request to be allowed, at least one principal associated with a request must be matched by at least one principal matcher in the allow list, and no principals associated with the request may match any principal matchers in the deny list.

Here is an example ACL on a method:

rpc MyMethod(MyRequest) returns (MyResponse) {
  option (kalix.method).acl = {
    allow: { service: "*" }
    deny: { service: "my-service" }
  };
};

The above ACL allows all services except the service called my-service. To allow all traffic:

option (kalix.method).acl.allow = { principal: ALL };

To allow only traffic from the internet:

option (kalix.method).acl.allow = { principal: INTERNET };

To allow traffic from service-a and service-b:

option (kalix.method).acl = {
  allow: { service: "service-a" }
  allow: { service: "service-b" }
};

To block all traffic, an ACL with no allows can be configured:

option (kalix.method).acl = {};

Sharing ACLs between methods

The above examples show how to configure an ACL on a method. ACLs can also be shared between all methods on a gRPC service by specifying them on the service:

service MyService {
  option (kalix.service).acl.allow = { principal: INTERNET };
  ...
}

The service’s ACL can be overridden by individual methods, by specifying the ACL on the method. Note that an ACL defined on a method completely overrides an ACL defined on a service. It does not add to it. So for example, in the following service:

service MyService {
  option (kalix.service).acl.allow = { service: "service-a" };
  rpc MyMethod(MyRequest) returns (MyResponse) {
    option (kalix.method).acl.allow = { service: "service-b" };
  };
}

The MyMethod rpc method will allow calls by service-b, but not by service-a.

Configuring the default policy

The default policy can be configured by specifying a file-level annotation, for example, to set a default policy of allowing all local services:

option (kalix.file).acl = {
  allow: { service: "*" }
};

An ACL declared at the file level is used as the default for all gRPC services that don’t declare their own explicit ACL, regardless of whether they appear in the same file or not. There must only be one file level ACL in the whole service, multiple file level ACLs will result in an error at discovery time, as there cannot be multiple defaults declared.

Customizing the deny code

When a request is denied, by default, a gRPC error code of 7, PERMISSION_DENIED, is sent. This gets transcoded to an HTTP status code of 403, forbidden, when using HTTP transcoding. The code that is returned when a request is denied can be customised using the deny_code property. The deny code must be a valid gRPC error code, which is any integer from 1 to 16, described here.

For example, to make Kalix reply with 5, NOT_FOUND:

  rpc MyMethod(MyRequest) returns (MyResponse) {
    option (kalix.method).acl = {
      allow: {service: "*"}
      deny_code: 5
    };
  };

Deny codes, if not specified on an ACL, are inherited from the service, or the default, so updating the deny_code in the default ACL policy will set it for all methods:

option (kalix.file).acl = {
  deny_code: 5
};

ACLs on eventing methods

Any method with an eventing.in annotation on it will not automatically inherit either the default or its gRPC service’s ACL, rather, all outside communication will be blocked, since it’s assumed that a method that subscribes to an event stream must only be intended to be invoked in response to events on that stream. This can be overridden, allowing invocation by gRPC, by explicitly defining an ACL on that method:

  rpc MyMethod(MyRequest) returns (MyResponse) {
    option (kalix.method).eventing.in.topic = "my-topic";
    option (kalix.method).acl.allow = {service: "*"};
  };

Backoffice and self invocations

Invocations of methods from the same service, or from the backoffice using the kalix service proxy command, are always permitted, regardless of what ACLs are defined on them.

Local development with ACLs

When testing or running in development, by default, all calls to your service will be accessible as ACLs are disabled by default.

Enabling ACLs in local development

When running a service during local development, it may be useful to enable ACL check. This can be done by setting the ACL_ENABLED environment variable to true in your docker-compose.yml file:

kalix-proxy:
  image: gcr.io/kalix-public/kalix-proxy:latest
  command: -Dconfig.resource=dev-mode.conf -Dkalix.proxy.eventing.support=google-pubsub-emulator
  ports:
    - "9000:9000"
  environment:
    USER_FUNCTION_HOST: ${USER_FUNCTION_HOST:-host.docker.internal}
    USER_FUNCTION_PORT: 8080
    ACL_ENABLED: 'true'

Once you enable ACL, calls to the proxy are treated as if they are coming from the Internet.

If the ACL is configured to only allow calls from other Kalix services, you will have to impersonate a local service. This can be done by setting the Impersonate-Kalix-Service header on the requests you make.

Service identification in local development

If running multiple services in local development, you may want to run with ACLs enabled to verify that they work for cross-service communication. In order to do this, you need to ensure that when services communicate with each other, they are able to identify themselves to one another. This can be done by setting the SERVICE_NAME environment variable in your docker-compose.yml file:

kalix-proxy:
  image: gcr.io/kalix-public/kalix-proxy:latest
  command: -Dconfig.resource=dev-mode.conf -Dkalix.proxy.eventing.support=google-pubsub-emulator
  ports:
    - "9000:9000"
  environment:
    USER_FUNCTION_HOST: ${USER_FUNCTION_HOST:-host.docker.internal}
    USER_FUNCTION_PORT: 8080
    SERVICE_NAME: my-service-name

Note that in local development, the services don’t actually authenticate with each other, they only pass their identity in a header. It is assumed in local development that a client can be trusted to set that header correctly.

Programmatically accessing principals

The current principal associated with a request can be accessed by reading metadata headers. If the request came from another service, the _kalix-src-svc header will be set to the name of the service that made the request. Kalix guarantees that this header will only be present from an authenticated principal, it can’t be spoofed.

For internet, self and backoffice requests, the _kalix-src header will be set to internet, self and backoffice respectively. Backoffice requests are requests that have been made using the kalix service proxy command, they are authenticated and authorized to ensure only developers of your project can make them.

Default ACL in project templates

If no ACLs is defined at all in a Kalix service Kalix will allow requests from both other services and the internet to all components of a Kalix service.

The Kalix quickstarts and Maven archetypes the sbt g8-template all include a less permissive ACL for the entire service, to not accidentally make services available to the public internet, just like the one described in the next section.

Defining an ACL for the entire Kalix Service

A default ACL for the entire Kalix Service can be defined by placing a kalix_policy.proto file among the protobuf descriptors of the service. It should only contain a kalix.file(acl) annotation:

Java
src/main/proto/com/example/kalix_policy.proto
syntax = "proto3";

package com.example;

import "kalix/annotations.proto"; (1)

option (kalix.file).acl = {
  allow: { service: "*" } (2)
};
1 Import the needed Kalix annotations from kalix/annotations.proto
2 Allow access from all other services, but not the public internet
Scala
src/main/proto/com/example/kalix_policy.proto
syntax = "proto3";

package com.example;

import "kalix/annotations.proto"; (1)

option (kalix.file).acl = {
  allow: { service: "*" } (2)
};
1 Import the needed Kalix annotations from kalix/annotations.proto
2 Allow access from all other services, but not the public internet

This is the default ACL included in the quickstarts and project templates, it allows calls from any other Kalix service deployed in the same project, but denies access from the internet.

Inspecting the principal inside a service

Checking the ACLs are in general done for you by Kalix, however in some cases programmatic access to the principal of a call can be useful.

Accessing the principal of a call inside a service is possible through the request metadata Metadata.principals(). The Metadata for a call is available through the context (actionContext, commandContext) of the component.

ACLs when running unit tests

In the generated unit test testkits, the ACLs are ignored.

ACLs when running integration tests

When running integration tests, ACLs are disabled by default but can be explicitly enabled per test by running the test with Settings.withAclEnabled.

For integration tests that call other services that have ACLs limiting access to specific service names Settings.withServiceName allows specifying what the service identifies itself as to other services.

KalixTestKit.getGrpcClientForPrincipal makes it possible to get an integration test client that is authenticated with specific credentials for calling a service with ACLs.