Security

Open Policy Agent

Security is one, if not the most important architecture characteristic. When we talk about security we usually talk in terms of authentication and authorization. Authentication refers to the ability of a user(or service) to access functionalities provided by other services. What are those functionalities are controlled by the authorization process. Two of the most used strategies are Role-Based Access Control (RBAC) and Attribute-Based Access Control(ABAC).

RBAC is based on assigning roles to users. Roles are composed of permissions. For example, if we talk about organizations we may have CreateOrganization permission, DeleteOrganization permission, UpdateOrganization permission, ViewOrganization etc. An admin role would have all this roles embedded while a super role could create and update organizations. Plain and simple.

What about if we want to control a fixed set of organizations? Like regional. A regional manager can work within his regions. In this case roles are not enough. We need fine grained control. ABAC is an generalization of RBAC in the sense that role is just another attribute. These attributes can be fixed or dynamic. When I say dynamic I refer to the fact that they may be computed on the fly. The attributes are bundled into policies, which are then assigned to services. A policy is usually(not mandatory) composed of a resource, action and environment values(this is the dynamic part).

Open Policy Agent(OPA) is a rule engine that makes the use of ABAC simple. We can define policies using Rego. Let’s see it action.

docker run -p 8181:8181 openpolicyagent/opa run --server --log-level debug

If everything is fine you should see

{"addrs":[":8181"],"diagnostic-addrs":[],"insecure_addr":"","level":"info","msg":"Initializing server.","time":"2020-07-27T12:46:40Z"}
{"headers":{"Content-Type":["application/json"],"User-Agent":["Open Policy Agent/0.20.5 (linux, amd64)"]},"level":"debug","method":"POST","msg":"Sending request.","time":"2020-07-27T12:46:40Z","url":"https://telemetry.openpolicyagent.org/v1/version"}
{"err":"Post https://telemetry.openpolicyagent.org/v1/version: context deadline exceeded","level":"debug","msg":"Unable to send OPA version report.","time":"2020-07-27T12:46:41Z"}

Now onto defining the policy. For this I would recommend to use the playground. There are examples including JWT tokens which in most cases this will part of the authorization.

package sergiuoltean.test

default allow = false

allow {
    group == "RegionalManager"
    input.action == "Read"
    input.resource = "Organization"
    data.east[input.organization]
}
group := g {
    g := payload.group
}

payload := p {
    token := input.input
	startswith(token, "Bearer ")
	t := substring(token, count("Bearer "), -1)
	[_, p, _] := io.jwt.decode(t)
}

This will allow access to all the users that are RegionalManager who are trying to read the organization and they are in the east organization list. The full example can be run in the playground.

 input.action == "Read"
 input.resource = "Organization"

The input field represents the the input that is send to be validated against the policy. It is a json.

 data.east[input.organization]

The data represent the place where we can keep data that has purpose in the authorization process. In most cases this needs to be syncronized from your own data stores. For example if your organization data is kept in a database a migration process must be put in place to copy it to OPA. For tactics on how to achieve this check out the documentation on external data.

Cool. Onto the integration with our services. OPA has also a REST API. Before going any further I would recommend to take a look at OPA document model in order to understand what data and input is.

The first thing we need to do is to load our policy into the server.

curl --location --request PUT 'localhost:8181/v1/policies/test' \
--header 'Content-Type: text/plain' \
--data-raw 'package sergiuoltean.test

default allow = false

allow {
    group == "RegionalManager"
    input.action == "Read"
    input.resource = "Organization"
    data.east[input.organization]
}
#-------------- Utilities --------------

group := g {
    g := payload.group
}

payload := p {
    token := input.input
        startswith(token, "Bearer ")
        t := substring(token, count("Bearer "), -1)
        [_, p, _] := io.jwt.decode(t)
}'

The server log should look like

{"client_addr":"172.17.0.1:36854","level":"info","msg":"Received request.","req_body":"package sergiuoltean.test\n\ndefault allow = false\n\nallow {\n    group == \"RegionalManager\"\n    input.action == \"Read\"\n    input.resource = \"Organization\"\n    data.east[input.organization]\n}\n#-------------- Utilities --------------\n\ngroup := g {\n    g := payload.group\n}\n\npayload := p {\n    token := input.input\n\tstartswith(token, \"Bearer \")\n\tt := substring(token, count(\"Bearer \"), -1)\n\t[_, p, _] := io.jwt.decode(t)\n}","req_id":5,"req_method":"PUT","req_params":{},"req_path":"/v1/policies/test","time":"2020-07-28T08:18:46Z"}
{"client_addr":"172.17.0.1:36854","level":"info","msg":"Sent response.","req_id":5,"req_method":"PUT","req_path":"/v1/policies/test","resp_body":"{}","resp_bytes":2,"resp_duration":0.166154,"resp_status":200,"time":"2020-07-28T08:18:46Z"}

We need to place our custom data inside the OPA server. Careful at the url /data/east. Basically allows us to call data.east inside the policy definition.

curl --location --request PUT 'localhost:8181/v1/data/east' \
--header 'Content-Type: application/json' \
--data-raw '{
    "Org1": {
        "location": "New York"
    },
    "Org2": {
        "location": "Boston"
    },
    "Org3": {
        "location": "Washington"
    }
}'

We verify that everything is in place

curl --location --request GET 'localhost:8181/v1/data'
{
    "result": {
        "east": {
            "Org1": {
                "location": "New York"
            },
            "Org2": {
                "location": "Boston"
            },
            "Org3": {
                "location": "Washington"
            }
        },
        "sergiuoltean": {
            "test": {
                "allow": false
            }
        }
    }
}

Next step is to run an input against the policy.

curl --location --request POST 'http://localhost:8181/v1/data/sergiuoltean/test' \
--header 'Content-Type: application/json' \
--data-raw '{
    "input": {
        "action": "Read",
        "input": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1OTU4NjE4MzksImV4cCI6MTYyNzM5NzgzOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjoiTWFuYWdlciIsImdyb3VwIjoiUmVnaW9uYWxNYW5hZ2VyIn0.75A_tZe6DNCGp7Y_58acnU7xMP7rcQ3Pk46O9owS5_4",
        "organization": "Org2",
        "resource": "Organization"
    }
}'

At the end of this call we will have our answer.

{
    "result": {
        "allow": true,
        "group": "RegionalManager",
        "payload": {
            "Email": "jrocket@example.com",
            "GivenName": "Johnny",
            "Role": "Manager",
            "Surname": "Rocket",
            "aud": "www.example.com",
            "exp": 1627397839,
            "group": "RegionalManager",
            "iat": 1595861839,
            "iss": "Online JWT Builder",
            "sub": "jrocket@example.com"
        }
    }
}

Everything seems in order. We are allowed to perform the action. Another important thing to observe is that the response is 200 if the query is successful. That being said if we are not authorized we still get a 200 response code, but the response body will contain “allow”: false.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.