1.15.6 Open Policy Agent Integration
The Open Policy Agent (OPA, pronounced “oh-pa”) is an open source, general-purpose policy engine that unifies policy enforcement across the stack.
OPA allows you to create arbitrary rules within Concourse without having to add a new feature to Concourse. You could even recreate Concourse's RBAC system using OPA.
More likely use-cases are to enforce rules your organization may have, such as not using certain container images or disallowing the use of privileged workloads. With OPA you can be as general or fine-grained as you want, enforcing these rules at the team or pipeline level.
The next few sections explain how to configure Concourse to talk to an OPA server and how to write OPA rules for Concourse.
Configuring Concourse
There are four configuration options you need to set on the concourse web
nodes to have them interact with OPA.
CONCOURSE_OPA_URL
: The OPA policy check endpoint. Should point to a specificpackage/rule
that contains all Concourse rules for your cluster.Example:
http://opa-endpoint.com/v1/data/concourse/decision
CONCOURSE_POLICY_CHECK_FILTER_HTTP_METHOD
: API http methods to go through policy check. You will need to make sure these match up with an API action in the next two configuration options.Example:
PUT,POST
CONCOURSE_POLICY_CHECK_FILTER_ACTION
: Actions in this list will go through policy check.Example:
ListWorkers,ListContainers
CONCOURSE_POLICY_CHECK_FILTER_ACTION_SKIP
: Actions in this list will not go through policy checkExample:
PausePipeline,UnpausePipeline
For the last three configuration options you can refer to the this list of routes for a list of API actions and their respective HTTP method. There are also some Special Actions not directly in the API.
Writing OPA Rules
On the OPA server you'll need to create a package and policy for Concourse. This should match up with the endpoint provided to Concourse. The OPA documentation has a good guide explaining how to generally write OPA rules and setup an OPA server.
For any actions that Concourse has been configured to filter it will send a JSON request to the OPA server with the following details. Top-level data directly under the input
key will be present for most actions. The information under the data
key will differ based on the action being checked.
This sample JSON payload is what OPA is sent when a user sets a pipeline. The data
key contains the pipeline in JSON format.
{
"input": {
"service": "concourse",
"cluster_name": "dev",
"cluster_version": "7.4.0",
"http_method": "PUT",
"action": "SaveConfig",
"user": "test",
"team": "main",
"pipeline": "check-pipeline",
"data": {
"jobs": [
{
"name": "test",
"plan": [
{
"get": "tiny"
},
{
"config": {
"image_resource": {
"source": {
"repository": "busybox"
},
"type": "registry-image"
},
"platform": "linux",
"run": {
"args": [
"-exc",
"echo hello"
],
"path": "sh"
}
},
"task": "a-task" }
] } ] } } }
An OPA rule can respond to Concourse with three fields:
allowed
(required): Boolean type. Setting toFalse
will deny the action unless theblock
field isFalse
.block
(optional): Boolean type. If set toFalse
andallowed
isTrue
this creates a soft-policy enforcement. The action will be allowed and thereasons
will still be printed to the web UI like a warning message.Not setting
block
is the same as setting"block": true
.reasons
(optional): List of string type. If an action is denied based on theallowed
field then the reason(s) will be displayed in the UI.
Here is an example OPA policy. By default it will allow whatever action it has been sent. It will deny the action if one or more of the three deny rules are true.
concourse.rego
package concourse
default decision = {"allowed": true}
decision = {"allowed": false, "reasons": reasons} {
count(deny) > 0
reasons := deny
}
deny["cannot use docker-image types"] {
input.action == "UseImage"
input.data.image_type == "docker-image"
}
deny["cannot run privileged tasks"] {
input.action == "SaveConfig"
input.data.jobs[_].plan[_].privileged
}
deny["cannot use privileged resource types"] {
input.action == "SaveConfig"
input.data.resource_types[_].privileged
}
Special Actions
Most of the actions you can filter for come directly from the list of API actions. There are currently two special actions you can also filter on.
UseImage
Before Concourse starts a container you can check what image it is going to use to create the container. Depending on the image_type
the image_source
field may contain other fields. The JSON payload for this action will look similar to the following example:
{
"input": {
"service": "concourse",
"cluster_name": "dev",
"cluster_version": "7.4.0",
"action": "UseImage",
"team": "main",
"pipeline": "simple",
"data": {
"image_type": "registry-image",
"privileged": true,
"image_source": {
"repository": "alpine",
"tag": "latest"
}
}
}
}
SetPipeline
This action occurs whenever a set_pipeline
step is run. The JSON payload for this action will contain the pipeline config in JSON format under the data
key:
{
"input": {
"service": "concourse",
"cluster_name": "dev",
"cluster_version": "7.4.0",
"action": "SetPipeline",
"team": "main",
"pipeline": "simple",
"data": {
"jobs": [
{
"name": "test",
"plan": [
{
"get": "tiny"
},
{
"config": {
"image_resource": {
"source": {
"repository": "busybox"
},
"type": "registry-image"
},
"platform": "linux",
"run": {
"args": [
"-exc",
"echo hello"
],
"path": "sh"
}
},
"task": "a-task" }
] } ] } } }