Securing a Spring Boot Application with Cerbos

Semaphore
16 min readAug 21, 2024

--

Access control is essential for application security. It ensures that only authorized users can access specific resources or perform certain actions. Effective access control prevents unauthorized access, safeguards sensitive data, and maintains application integrity.

In the realm of microservices architectures, managing authorization logic across various services can become a complex task. Cerbos offers a solution by centralizing authorization policies. This simplifies the process because you can make decisions from any part of the application. You can avoid a maintenance burden as your application evolves.

In this tutorial, we will explore Cerbos, a modern, open-source solution, for managing and enforcing access control policies. While popular tools like Okta excel at user authentication and basic access control, applications with complex permission structures require a more granular approach. Cerbos offers a flexible method for defining and applying detailed access control rules. It supports various frameworks and languages, such as Python and Java.

In this article, you will learn:

  • How Cerbos works and what benefits it offers.
  • How to secure a Spring Boot application using Cerbos policies.
  • How to test Cerbos policies in the Semaphore CI pipeline.

Prerequisites

To ensure smooth learning, this tutorial assumes you have:

  • Basic Docker knowledge, including containers and Docker Compose.
  • Fundamentals of Spring Boot, such as working with JPA entities and REST APIs.
  • An integrated development environment (IDE) like IntelliJ IDEA.

Let’s get started!

Understanding Cerbos

The core concept of Cerbos is to define all access control rules in a central location in a human-readable format. When you update these rules, Cerbos applies the changes across all instances serving your application. This way, you don’t need to release any code changes.

Cerbos consists of two main components:

Benefits of Using Cerbos

  • Developers can concentrate on building core application features while delegating access rule management to product owners or security teams.
  • You can deploy Cerbos as a separate microservice.
  • Cerbos makes authorization logic easier. It replaces complex, hard-coded permission checks with a single call to the Cerbos policy engine.

For example, look at this Java boiler-plate code:

With Cerbos, you can simplify it to the absolute minimum:

What Are Cerbos Policies

You can define Cerbos policies in YAML or JSON files. These policies consist of two main parts:

  • Resources: Specify an entity within your application that requires protection through access rules. For example, an employee record with personal details, salary, etc.
  • Rules: Determine who can access each resource and what actions they can perform. For instance, colleagues from the HR department can access and manage employee records.

Cerbos provides several ways to create policies:

  • Cerbos playground: An interactive interface for defining and testing policies.
  • Cerbos Hub: A managed platform for policy creation and management.
  • Manual Configuration: Defining policies directly in YAML files.

In this tutorial, we will use the Cerbos Playground to define our policies.

Integrating Cerbos into a Spring Boot Application

Project Scenario

In our demo application, we will define access control policies for an employee management system. The system has two primary roles: regular employees and HR personnel. The policies will ensure that regular employees can only view their own profiles. HR personnel will have full control over all employee records.

Prepare the Project

Create a new Maven project in your favorite IDE. For example, name it CerbosSpringBootDemo.

Create the Cerbos policies

Visit the Cerbos playground.

Create two custom roles: employee and hr. The resource we will manage is the employee’s profile.

  • The employee role has read-only access to the profile.
  • The hr role has full access to perform all actions.

Finally, press Generate.

You can then see what the policies look like:

On the left panel, you can see the YAML files that contain the policies.

We are interested in the resource_policies folder. Let’s copy the contents of these files and save them to our local machine.

Create a new folder in the root of your project called cerbos-policies. The name can be different but note that Cerbos will look for a folder called policies when running in the container. Create a sub-folder called testdata. Make sure you have the following structure:

├── cerbos-policies
│ ├── profile_test.yml│ ├── profile.yaml│ └── testdata│ ├── principals.yaml│ └── resources.yaml
  • principals.yaml:
principals:
employee#1:
id: employee#1
roles:
- employee
attr: {}
hr#2:
id: hr#2
roles:
- hr
attr: {}

This file defines the principals (users) for the policy tests, specifying their IDs, roles, and attributes.

  • resources.yaml:
resources:
profile#1:
id: profile#1
kind: profile
attr: {}

This file defines the resources that will be subject to the policies being tested.

  • profile.yaml:
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: profile
version: default
rules:
- actions:
- create
effect: EFFECT_ALLOW
roles:
- user
- admin
- hr
- actions:
- read
effect: EFFECT_ALLOW
roles:
- user
- admin
- employee
- hr
- actions:
- update
effect: EFFECT_ALLOW
roles:
- user
- admin
- hr
- actions:
- delete
effect: EFFECT_ALLOW
roles:
- admin
- hr

This policy file specifies the resource as the profile and sets rules that allow an employee with the hr role to perform all actions. It restricts employees with the employee role to read their own profiles only.

  • profile_test.yaml:
name: profileTestSuite
description: Tests for verifying the profile resource policy
tests:
- name: profile actions
input:
principals:
- employee#1
- hr#2
resources:
- profile#1
actions:
- create
- read
- update
- delete
expected:
- resource: profile#1
principal: employee#1
actions:
create: EFFECT_DENY
read: EFFECT_ALLOW
update: EFFECT_DENY
delete: EFFECT_DENY
- resource: profile#1
principal: hr#2
actions:
create: EFFECT_ALLOW
read: EFFECT_ALLOW
update: EFFECT_ALLOW
delete: EFFECT_ALLOW

This YAML file defines a set of tests for verifying the access control policy related to the profile resource. It specifies the actions that different principals (users with roles) can perform on the resource and what the expected outcomes should be. Note that Cerbos expects the file suffix _test.

Prepare the Infrastructure for Local Development

You can integrate Cerbos into your stack in two main ways:

  • By downloading and installing the binaries.
  • Using a docker container.

We will pull the official Docker image and run it via docker-compose. We will also deploy our custom policies to the running Cerbos PDP instance.

Create a docker-compose.yml file in the root of your project with the following content:

version: "2.1"
services:
my-cerbos-container:
container_name: my-cerbos-container
image: ghcr.io/cerbos/cerbos:0.34.0
ports:
- "3592:3592"
- "3593:3593"
volumes:
- ./cerbos-policies:/policies
expose:
- "3592"
- "3593"
command: compile /policies

Let’s break down the docker-compose.yml file to understand what each section does:

  • image: Specifies the Docker image for this service, which is the Cerbos PDP version 0.34.0 from GitHub Container Registry.
  • container_name: Names the container my-cerbos-container for easy identification.
  • ports: Maps ports 3592 and 3593 on the host machine to the same ports in the container, allowing external access.
  • expose: Indicates that ports 3592 and 3593 should be exposed, making them available for linked services.
  • volumes: Mounts the ./cerbos-policies directory from the host machine to the /policies directory in the container, providing the Cerbos PDP access to your policy files.
  • command: Executes the Cerbos command compile /policies to run the tests in the profile_test.yaml file.

Create the Spring Boot Application

Add the following dependencies to the parent pom.xml file:

<dependencies>
<!-- Spring boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.3</version>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
<!-- Cerbos dependencies -->
<dependency>
<groupId>dev.cerbos</groupId>
<artifactId>cerbos-sdk-java</artifactId>
<version>0.12.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-core</artifactId>
<version>1.64.0</version>
</dependency>
<!-- Other dev tools -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
</dependencies>

Below is a summary of each dependency:

  • spring-boot-starter-web: Provides the core web functionalities to build a Spring Boot web application.
  • spring-boot-starter-data-jpa: Adds support for JPA, enabling easy database interaction with repositories.
  • jakarta. persistence-api: Supplies the Jakarta Persistence API which is essential for object-relational mapping and managing relational data in Java applications.
  • h2: Includes the H2 database engine, a lightweight in-memory database useful for development, testing, and small-scale applications.
  • cerbos-sdk-java: Provides the Cerbos SDK for Java, which facilitates integration with the Cerbos authorization system to handle access control policies.
  • grpc-core: Supports the core functionality for gRPC (Google Remote Procedure Call), which Cerbos uses for communication.
  • logback-classic: Implements logging capabilities using Logback.
  • lombok: Offers annotations to reduce boilerplate code in Java, such as generating getters, setters, and constructors automatically.

Configure the application.yml file:

spring:
datasource:
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
h2:
console:
enabled: true
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create
show-sql: true
logging:
level:
root: warn
org.cerbos.demo: debug

Key points:

  • ddl-auto: create: Configures Hibernate to create the database schema and tables at startup. This is helpful for development and testing but should be changed for production.
  • h2:console:enabled:true: Enables the H2 console. You can access it at localhost:8080/h2-console when the application starts.
  • show-sql: true: Enables the logging of SQL statements generated by Hibernate.
  • logging.level: Reduces the root level output and sets only the necessary org.cerbos.demo package to debug.

Let’s configure the Cerbos client. Create a new class CerbosConfig.java:

@Configuration
public class CerbosConfig {
    @Bean
public CerbosBlockingClient cerbosClient() throws CerbosClientBuilder.InvalidClientConfigurationException {
LoadBalancerRegistry.getDefaultRegistry().register(new io.grpc.internal.PickFirstLoadBalancerProvider());
NameResolverRegistry.getDefaultRegistry().register(new io.grpc.internal.DnsNameResolverProvider());
return new CerbosClientBuilder("localhost:3593").withPlaintext().buildBlockingClient();
}
}

This configuration sets up and registers a CerbosBlockingClient bean in the Spring application context. This allows the application to connect to the PDP to check access control policies. It uses load balancer and name resolver registrations to ensure the gRPC client can find and connect to the PDP server.

You can find more details and custom options for the CerbosClient in the Cerbos-Java GitHub repository.

Let’s create the model for our application. Create a new class Employee.java that represents the employees table:

@Entity
@Table(name = "employees")
@Data
public class Employee {
    @Id
@Column(name = "employee_id" , nullable = false)
private Long employeeId;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private Double salary;
@Column(nullable = false)
private String role;
}

This entity contains usual employee information.

Create the Profile.java that represents the profiles table:

@Entity
@Table(name = "profiles")
@Data
public class Profile {
    @Id
private Long id;
@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "employee_id")
private Employee employee;
}

This entity contains the employee details.

Key points:

@OneToOne(cascade = CascadeType.PERSIST): Defines a one-to-one relationship between the Profile and Employee entities. The cascade = CascadeType.PERSIST attribute means that when you persist a Profile entity, you also persist the associated Employeeentity.

Let’s create the JPA repositories: EmployeeRepository.java and ProfileRepository.java. They will allow access to the entities.

public interface EmployeeRepository extends JpaRepository<Employee, Long> {

}
public interface ProfileRepository extends JpaRepository<Profile, Long> {

}

We will build the backend of the Spring boot application and test the endpoints by sending cURL requests.

Create a ProfileController.java for the REST communication:

@RestController
@RequestMapping("/api/profile")
@RequiredArgsConstructor
@Slf4j
public class ProfileController {
    private final EmployeeRepository employeeRepository;
private final ProfileRepository profileRepository;
private final CerbosBlockingClient cerbosBlockingClient;
@GetMapping("/get/{profileId}/{employeeId}")
public ResponseEntity<String> getProfile(@PathVariable String profileId, @PathVariable String employeeId) {
Profile profile = getProfileOrNotFound(profileId);
if (profile == null) {
return handleProfileNotFound();
}
Employee employee = getEmployeeOrNotFound(employeeId);
if (employee == null) {
return handleEmployeeNotFound();
}
Principal principal = Principal.newInstance(employeeId, employee.getRole())
.withAttribute("id", AttributeValue.stringValue(employeeId));
Resource resource = Resource.newInstance("profile", profileId)
.withAttribute("owner", AttributeValue.stringValue(String.valueOf(profile.getEmployee().getEmployeeId())));
if (!isAllowed("read", principal, resource)) {
log.debug("Not allowed to read profile");
return handleForbidden();
}
return ResponseEntity.ok().body(profile.getEmployee().toString());
}
@DeleteMapping("/delete/{profileId}/{employeeId}")
public ResponseEntity<String> deleteProfile(@PathVariable String profileId, @PathVariable String employeeId) {
Profile profile = getProfileOrNotFound(profileId);
if (profile == null) {
return handleProfileNotFound();
}
Employee employee = getEmployeeOrNotFound(employeeId);
if (employee == null) {
return handleEmployeeNotFound();
}
Principal principal = Principal.newInstance(employeeId, employee.getRole());
Resource resource = Resource.newInstance("profile", profileId);
if (!isAllowed("delete", principal, resource)) {
log.debug("Not allowed to delete profile");
return handleForbidden();
}
try {
profileRepository.deleteById(Long.valueOf(profileId));
boolean isDeleted = profileRepository.findById(Long.valueOf(profileId)).isEmpty();
return isDeleted ? ResponseEntity.ok().body("Profile deleted successfully") : ResponseEntity.internalServerError().body("Failed to delete profile");
} catch (Exception e) {
return handleInternalServerError(e);
}
}
private boolean isAllowed(String action, Principal principal, Resource resource) {
return cerbosBlockingClient.check(principal, resource, action).isAllowed(action);
}
private Profile getProfileOrNotFound(String profileId) {
return profileRepository.findById(Long.parseLong(profileId)).orElse(null);
}
private Employee getEmployeeOrNotFound(String employeeId) {
return employeeRepository.findById(Long.parseLong(employeeId)).orElse(null);
}
private ResponseEntity<String> handleProfileNotFound() {
return ResponseEntity.badRequest().body("Profile not found");
}
private ResponseEntity<String> handleEmployeeNotFound() {
return ResponseEntity.badRequest().body("Employee not found");
}
private ResponseEntity<String> handleForbidden() {
return ResponseEntity.status(403).body("Forbidden");
}
private ResponseEntity<String> handleInternalServerError(Exception e) {
log.error("Error processing request: ", e);
return ResponseEntity.internalServerError().body("Error processing request: " + e.getMessage());
}
}

Key aspects and functionality:

The controller fetches data from the JPA repositories. It performs authorization checks using Cerbos and handles both successful and failed operations.

GET /api/profile/get/{profileId}/{employeeId}:
  • Creates Principal and Resource objects to represent the current user and the profile resource.
  • Uses cerbosBlockingClient to check if the user is allowed to perform the “read” action on the resource.
  • Returns 403 Forbidden if the access is denied, otherwise returns the profile details.
DELETE /api/profile/get/{profileId}/{employeeId}:
  • Performs the same steps as above, but this time it checks if the user can delete the profile.

Finally, let’s create the Main.java class:

@SpringBootConfiguration
@ComponentScan(basePackages = "org.cerbos.demo")
@EnableJpaRepositories
@EnableAutoConfiguration
@Slf4j
public class Main {
    static ConfigurableApplicationContext appCtx;    public static void main(String[] args) {
var app = new SpringApplication(Main.class);
appCtx = app.run(args);
}
@Bean
CommandLineRunner commandLineRunner(EmployeeRepository employeeRepository, ProfileRepository profileRepository) {
return args -> {
populateDb(employeeRepository, profileRepository);
};
}
void populateDb(EmployeeRepository employeeRepository, ProfileRepository profileRepository) {
Employee employee1 = new Employee();
employee1.setEmployeeId(123L);
employee1.setName("John Doe");
employee1.setEmail("john.doe@me.com");
employee1.setRole("employee");
employee1.setSalary(1500.0);
Employee employee2 = new Employee();
employee2.setEmployeeId(321L);
employee2.setName("Marie Smith");
employee2.setEmail("marie.smith@me.com");
employee2.setRole("employee");
employee2.setSalary(2000.0);
Employee hr = new Employee();
hr.setEmployeeId(456L);
hr.setName("Andrew Anderson");
hr.setEmail("andrew.anderson@me.com");
hr.setRole("hr");
hr.setSalary(1000.0);
employeeRepository.save(employee1);
employeeRepository.save(employee2);
employeeRepository.save(hr);
Profile profile = new Profile();
profile.setId(111L);
profile.setEmployee(employee1);
profileRepository.save(profile);
Profile profile2 = new Profile();
profile2.setId(222L);
profile2.setEmployee(employee2);
profileRepository.save(profile2);
Profile profile3 = new Profile();
profile3.setId(333L);
profile3.setEmployee(hr);
profileRepository.save(profile3);
log.debug("Saved data to db");
}
}

Here, we created a CommandLineRunner bean that populates the database with sample data when the application starts.

Testing the Application

I have the following folder structure so far:

├── cerbos-policies
│ ├── profile_test.yml
│ ├── profile.yaml
│ └── testdata
│ ├── principals.yaml
│ └── resources.yaml
├── docker-compose.yml
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── org
│ │ │ └── cerbos
│ │ │ └── demo
│ │ │ ├── config
│ │ │ │ └── CerbosConfig.java
│ │ │ ├── controller
│ │ │ │ └── ProfileController.java
│ │ │ ├── Main.java
│ │ │ ├── model
│ │ │ │ ├── Employee.java
│ │ │ │ └── Profile.java
│ │ │ └── repository
│ │ │ ├── EmployeeRepository.java
│ │ │ └── ProfileRepository.java
│ │ └── resources
│ │ └── application.yml

Of course, your Java package names can be different. Ensure you follow the structure for cerbos-policies.

First, you can test the Cerbos policies locally. Open a Terminal at the root of the project and run the Cerbos container:

docker-compose up

You should see a result like this:

my-cerbos-container | Test results

my-cerbos-container | └──profileTestSuite (profile_test.yml) [32 OK]

my-cerbos-container |

my-cerbos-container | 32 tests executed [32 OK]

my-cerbos-container exited with code 0

As you can see, the tests are successful. Later, we will run the tests automatically in our CI pipeline. We needed this step to make sure that the policies were correct. Comment out this line ( command: compile /policies) to keep the container running.

Run docker-compose up again.

Check that Cerbos is up and running by opening http://localhost:3592/ in your browser.

You should see a page like this:

Start the Spring Boot application from your IDE.

Since we enabled the H2 Console, we can see that the database contains the expected dummy data:

Let’s try to access the profile with ID 111 as an employee with ID 123:

$ curl -X GET http://localhost:8080/api/profile/get/111/123
    Employee(employeeId=123, name=John Doe, email=john.doe@me.com, salary=1500.0, role=employee)

The profile belongs to this employee, so it works as expected.

Now let’s try the same, but this time using the ID of the HR:

$ curl -X GET http://localhost:8080/api/profile/get/111/456
    Employee(employeeId=123, name=John Doe, email=john.doe@me.com, salary=1500.0, role=employee)

As expected, it shows the details because the HR has access to all actions.

Now, let’s try the DELETE request:

$ curl -X DELETE http://localhost:8080/api/profile/delete/111/123

Forbidden

As anticipated, the employee is not allowed to delete their profile.

Let’s try with the hr role:

$ curl -X DELETE http://localhost:8080/api/profile/delete/111/456

Profile deleted successfully

So far, everything works fine.

However, what will happen if an employee attempts to access someone else’s profile?

Let’s try to access the profile of employee1 with the employee ID of employee2:

$ curl -X GET http://localhost:8080/api/profile/get/111/321

Employee(employeeId=123, name=John Doe, email=john.doe@me.com, salary=1500.0, role=employee)

Since we haven’t defined any custom rules, employees can currently access each other’s profiles. This is not the desired behavior, as we want to keep the information secure.

Cerbos addresses this issue using Conditions, which utilize Common Expression Language (CEL) syntax. You can add attributes to the principal and the resources to evaluate and enforce specific access conditions. For instance, you can check if a user’s address is within a certain geographic location, etc.

request:  
principal:
id: john
roles:
- employee
attr:
geography: GB

Checking the condition:

condition:
match:
all:
of:
- expr: >
"GB" in R.attr.geographies
- expr: P.attr.geography == "GB"

Let’s refine our Cerbos policies by incorporating conditions.

Modify the resources.yaml to add a new attribute called owner:

resources:
profile#1:
id: profile#1
kind: profile
attr:
owner: 123

Add this line to the profile.yaml for the read action:

- actions:
- read
effect: EFFECT_ALLOW
roles: ["*"]
condition:
match:
expr: (request.resource.attr.owner == request.principal.id) || ('hr' in request.principal.roles)

The policy allows any role to access the resource, provided they meet the condition specified in the match expression. This condition allows access if either the principal is the owner of the resource or holds the hr role.

We also need to adjust the Java code. Extend the code that gets the Principal and Resource in the getProfile() method with this:

Principal principal = Principal.newInstance(employeeId, employee.getRole()).withAttribute("id", AttributeValue.stringValue(employeeId));

Resource resource = Resource.newInstance("profile", profileId).withAttribute("owner",AttributeValue.stringValue(String.valueOf(profile.getEmployee().getEmployeeId())));
  • Principal: Represents the user requesting access. The “id” attribute helps to identify and match the principal in access control decisions.
  • Resource: Represents the entity being accessed. The “owner” attribute specifies the resource’s owner and facilitates access control checks.

This setup allows Cerbos to enforce policies based on these attributes. For example, it ensures that only the resource owner or users with specific roles (like ‘hr’) can access or modify the resource.

The PDP receives these values and determines if the action is allowed based on the predefined policy.

The initial test cases will no longer work after these changes. We can extend the test suites by introducing multiple user resources and principals. Replace the content of the profile_test.yml with this:

name: profileTestSuite
description: Tests for verifying the profile resource policy
principals:
hr:
id: hr1
roles:
- hr
employee1:
id: emp1
roles:
- employee
employee2:
id: emp2
roles:
- employee
employee3:
id: emp3
roles:
- employee
resources:
profile:
kind: profile
id: emp1
attr:
owner: emp1
profile2:
kind: profile
id: emp2
attr:
owner: emp2
tests:
- name: profile actions
input:
principals:
- employee1
- employee2
- employee3
- hr
resources:
- profile
- profile2
actions:
- create
- read
- update
- delete
expected:
- resource: profile
principal: employee1
actions:
create: EFFECT_DENY
read: EFFECT_ALLOW
update: EFFECT_DENY
delete: EFFECT_DENY
- resource: profile2
principal: employee2
actions:
create: EFFECT_DENY
read: EFFECT_ALLOW
update: EFFECT_DENY
delete: EFFECT_DENY
- resource: profile
principal: employee3
actions:
create: EFFECT_DENY
read: EFFECT_DENY
update: EFFECT_DENY
delete: EFFECT_DENY
- resource: profile
principal: hr
actions:
create: EFFECT_ALLOW
read: EFFECT_ALLOW
update: EFFECT_ALLOW
delete: EFFECT_ALLOW
- resource: profile2
principal: hr
actions:
create: EFFECT_ALLOW
read: EFFECT_ALLOW
update: EFFECT_ALLOW
delete: EFFECT_ALLOW

Automating Policy Tests with Semaphore CI

Automating policy tests in the CI/CD pipeline is a best practice. This means any misconfigured rules will cause the tests to fail so that you can react quickly.

In this section, you’ll learn how to integrate and run Cerbos policy tests within the Semaphore CI pipeline.

Semaphore requires a sempahore.yml file. This configuration file specifies the tasks and workflows that Semaphore CI should execute. It outlines the steps for building your application, running tests, and deploying changes. Create a new directory in the root of your project called .semaphore. Create the semaphore.yml file inside it:

version: v1.0
name: Cerbos Policy Execution
agent:
machine:
type: e1-standard-2
os_image: ubuntu2004
blocks:
- name: Compile Policies
task:
jobs:
- name: Compile
commands:
- checkout
- docker run -it --name my-cerbos-container -v ./cerbos-policies:/policies -p 3592:3592 ghcr.io/cerbos/cerbos:0.34.0 compile /policies
- docker logs my-cerbos-container

The semaphore.yml file performs the following tasks:

  • Sets up a virtual machine with ubuntu2004 image.
  • Runs the Docker container to compile the policies using the Cerbos command compile /policies.
  • Retrieves the container logs for review in the job’s output.

To run the tests in the pipeline, you need a free Semaphore account. Visit the signup page and choose either GitHub or Bitbucket for registration. In this tutorial, I will use GitHub.

Next, create a new project by clicking the “+ Create new” button.

I will connect my repository with Semaphore, which I used for this tutorial.

Semaphore will automatically initialize the project shortly. You can add more people to the project if you wish.

The final step is to create the workflow for the pipeline. With the semaphore.yml file already in place, you can use it directly for the workflow.

Let’s push something to the repository to trigger the pipeline.

For example, I added a README.md file to my project.

Shortly after the push, you’ll see that the pipeline will run the tests.

To see the details, click on the Compile job log.

As we added the docker logs command, we can access the container’s logs. There, you can see that all the tests were successful.

Let’s deliberately misconfigure the policies. For example, I’ll change the expected result for the hr role from EFFECT_ALLOW to EFFECT_DENY in one of the actions:

- resource: profile
principal: hr
actions:
create: EFFECT_ALLOW
read: EFFECT_DENY
update: EFFECT_ALLOW
delete: EFFECT_ALLOW

Push the changes.

As expected, the job failed:

You can see in the logs which test failed:

Conclusion

In this tutorial, you learned what Cerbos is, and how to secure a Spring Boot application with Cerbos policies. You discovered how to create granular policies based on attributes to manage access control effectively.

Additionally, you set up automated policy testing with Semaphore CI. You can integrate this step into your existing pipeline.

By following this guide, you now have a solid foundation in leveraging Cerbos to enhance the security of your applications.

While this tutorial covered the basics of Cerbos, there are many more features to explore. For a deeper understanding, I encourage you to dive into the comprehensive Cerbos documentation.

You can find the source code of this tutorial in my GitHub repository.

Thank you for following along!

Originally published at https://semaphoreci.com on August 21, 2024.

--

--

Semaphore

Supporting developers with insights and tutorials on delivering good software. · https://semaphoreci.com