This example deployment provides the following external behaviors for users:
- The authorization server is exposed at
https://login.democluster.example
- The authorization server's admin UI is exposed at
https://admin.democluster.example
- An API is exposed from a cluster at
https://api.democluster.example/orders
- A console app is used to perform a login and get a user level access token
- The console app sends the user level access token to an API to access order information
Deploy the system on a computer running Linux, macOS or Windows (with Git bash).
First ensure that you have these tools installed:
- A Docker Engine, preferably configured to use 16GB of RAM
- KIND
- kubectl
- Helm
- jq
- Open Policy Agent
Create a cluster and run a load balancer to enable the API to be exposed on an external IP address.
On Windows, use a Run as administrator
shell in order to run the load balancer:
./1-create-cluster.sh
Then run another shell to create an ingress.
On macOS accept the prompt to allow the load balancer to accept connections.
Note the external IP address that the script outputs:
./2-deploy-api-gateway.sh
Update your hosts file with the external IP address, similar to the following:
172.18.0.5 api.democluster.example login.democluster.example admin.democluster.example
Deploy the authorization server with some preconfigured clients and users.
If required, the deployment gets a community edition license file for the Curity Identity Server.
See the License README for details.
./3-deploy-authorization-server.sh
- Login to the Admin UI at
https://admin.democluster.example/admin
with credentialsadmin / Password1
- Locate OpenId Connect metadata at
https://login.democluster.example/oauth/v2/oauth-anonymous/.well-known/openid-configuration
To avoid browser SSL trust warnings you should trust the following development root certificate.
For example, on macOS use Keychain Access to add it to the system keystore.
../resources/apigateway/external-certs/democluster.ca.pem
Deploy an HTTP server, the policy retrieval point, that hosts the policy bundle for OPA. The endpoint for the policy-bundle is not exposed from the cluster:
./4-deploy-policy-retrieval-point.sh
The internal URL to the policy retrieval point and policy-bundle is http://policy-retrieval-point-svc/bundle.tar.gz
. This is the URL where OPA gets the bundle from.
Deploy the example API from chapter 5, running OPA as a sidecar.
./5-deploy-api-with-opa.sh
- Locate the API endpoint at
https://api.democluster.example/orders
. - If you run into external connectivity problems, see the Connectivity README document.
The API queries OPA for the authorization decision. OPA runs as a sidecar on the same pod as the API and points to the policy retrieval point to get its policy. The API can communicate with the OPA over its local interface. See an excerpt from the deployment file (chapter-05-secure-api-development/deployment/kubernetes/deployment.yaml
).
...
spec:
serviceAccountName: zerotrustapi
containers:
- name: zerotrustapi
env:
- name: 'POLICY_ENDPOINT'
value: 'http://127.0.0.1:8181/v1/data/orders/allow' # Endpoint at OPA for the policy
...
...
initContainers:
- name: policyengine
image: openpolicyagent/opa:latest
restartPolicy: Always
args:
- "run"
- "--ignore=.*"
- "--server"
- "--addr"
- "127.0.0.1:8181" # Listen on local interface
- "--set"
- "decision_logs.console=true" # Enable decision logging
- "--set"
- "bundles.cli1.persist=false" # Do not persist bundle on local file system
- "http://policy-retrieval-point-svc/bundle.tar.gz" # URL to download policy bundle from
Run a simple console app client that invokes the system browser:
./6-run-oauth-client.sh
Log in with a username and password using the following test credential:
- dana / Password1
The console app gets an access token and makes two secured API requests.
The first API request is authorized, whereas the second is for an unauthorized order ID.
The API rejects the second request because the policy does not allow access, so the client reports an error.
Console client is authenticating a user via the system browser ...
Authentication successful: access token received
Console client is calling API to get a list of authorized orders ...
[
{
"id": "20881",
"customerId": "2099",
"amountUsd": 900,
"region": "USA",
"date": "2024-06-11",
"status": "completed"
},
{
"id": "20885",
"customerId": "2099",
"amountUsd": 3000,
"region": "USA",
"date": "2024-06-14",
"status": "completed"
}
]
Console client is calling API to get an order by ID ...
Problem encountered: status: 404, code: not_found, message: Resource not found for user
Alternatively, you can re-run the client and login with this the following test credential.
Both API requests are then authorized, and responses include different authorized data.
- kim / Password1
The authorization rules are based on these data relationships:
- Dana's user account has
customer_id=2099
androle=customer
. - Kims's user account has a
role=admin
andregion=USA
. - The client requests an order with an ID of 20882.
- The order 20882 resource has
customer_id=3044
andregion=USA
. - Dana is not authorized to access the order since it is for another customer.
- Kim is authorized to access the order since she has access to all USA orders.
- If you edit the client code to request the order 20881 resource, both Dana and Kim are granted access, since Dana owns this USA order.
Instead of hardcoding authorization rules in the API, the API queries OPA for a decision. For that, it makes a POST request to the policy endpoint and adds details to its query:
- the access token so that OPA can retrieve the user claims and perform claims-based authorization
- the type of requested access in form of the requested action and type of resource that the action targets
- optionally, details about the resource, i.e. the customerId or region of an order if applicable
To query the authorization policy, you can remote to the orders container:
ORDERS_POD=$(kubectl -n applications get pod -o name | grep 'zerotrustapi')
kubectl -n applications exec -it $ORDERS_POD -c zerotrustapi -- bash
Then use curl to send a request to the policy engine in the same way as the API code:
INPUT='{
"input": {
"accessToken": "eyJraWQiOiItMzczNDk1MzE2IiwieDV0IjoiXzNHeFpDS0ZzcXQyeEJSZkpxSTJ4TXppWl8wIiwiYWxnIjoiRVMyNTYifQ.eyJqdGkiOiI5ZTcxNDIyNy1mYzc3LTQ2NDctYWY5NC05ZDBkYTYwZjU1MDciLCJkZWxlZ2F0aW9uSWQiOiJlMjE3ZmNjYS1iZDQ2LTQ1ZjgtYjRkZi0yODliZDdiZTE5ZDEiLCJleHAiOjE3MTUxNjM1MTUsIm5iZiI6MTcxNTE2MjYxNSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSByZXRhaWwvb3JkZXJzIiwiaXNzIjoiaHR0cDovL2xvZ2luLmV4YW1wbGVjbHVzdGVyLmNvbS9vYXV0aC92Mi9vYXV0aC1hbm9ueW1vdXMiLCJzdWIiOiJib2IiLCJhdWQiOlsiZGVtby1jb25zb2xlLWNsaWVudCIsImFwaS5leGFtcGxlLmNvbSJdLCJpYXQiOjE3MTUxNjI2MTUsInB1cnBvc2UiOiJhY2Nlc3NfdG9rZW4iLCJsZXZlbF9vZl9hc3N1cmFuY2UiOjIsInJvbGVzIjpbImN1c3RvbWVyIl0sImN1c3RvbWVyX2lkIjoiMjA5OSIsInJlZ2lvbiI6IlVTQSJ9.pENFaz8TMMWoV9I1c-MtKfuf9lhIi2veSUQSSJcps6GiW7v8hq1IJqFY1kEp-DQDDrD64LL-_3igSA2eJYQEwg",
"action": "list",
"type": "order"
}
}'
curl -X POST http://127.0.0.1:8181/v1/data/orders/allow \
-H 'content-type: application/json' \
-d "$INPUT"
The following is a response to the query is this user allowed to list orders?
where the access token in accessToken
contains the details about the user, action
is list
, and type
is order
. The policy allows the request with a condition. The user is allowed to list orders but only for their own customer ID:
{
"decision_id": "c1496ca4-a05b-4751-b8d6-ab934a283e99" ,
"result" : {
"allowed": true,
"condition": {
"customerId" : "2099"
}
}
}
It is up to the API to enforce this decision. As allowed
can have conditions, the API also has to check the conditions in the result when enforcing the policy:
if (result.allowed && result.condition.customerId) {
return await this.repository.getOrdersForCustomer(result.condition.customerId);
}
To view API logs you can run these commands:
ORDERS_POD=$(kubectl -n applications get pod -o name | grep 'zerotrustapi')
kubectl -n applications logs -f $ORDERS_POD -c zerotrustapi
To view OPA logs you can run the following commands:
POLICY_POD=$(kubectl -n applications get pod -o name | grep 'zerotrustapi')
kubectl -n applications logs -f $POLICY_POD -c policyengine
In this deployment, we enabled OPA decision logs, which serve as audit logs for the API authorization.
The output is customized using masking to include some token claims and to avoid logging the JWT access token:
{
"bundles": {
"policyRetrievalPoint": {}
},
"decision_id": "f5549793-fc36-4b2f-814e-b304b731d5b6",
"erased": [
"/input/accessToken"
],
"input": {
"action": "list",
"claims": {
"customer_id": "2099",
"level_of_assurance": 2,
"region": "USA",
"roles": [
"customer"
],
"scope": "openid profile retail/orders"
},
"type": "order"
},
"labels": {
"id": "2b514d01-c1fe-4c38-b8ae-d360aa1d3d69",
"version": "0.69.0"
},
"level": "info",
"masked": [
"/input/claims"
],
"metrics": {
"counter_server_query_cache_hit": 0,
"timer_rego_external_resolve_ns": 1172,
"timer_rego_input_parse_ns": 63366,
"timer_rego_query_compile_ns": 83142,
"timer_rego_query_eval_ns": 620485,
"timer_rego_query_parse_ns": 60773,
"timer_server_handler_ns": 907231
},
"msg": "Decision Log",
"path": "orders/allow",
"req_id": 62,
"requested_by": "127.0.0.1:34396",
"result": {
"allowed": true,
"condition": {
"customerId": "2099"
}
},
"time": "2024-10-15T10:53:10Z",
"timestamp": "2024-10-15T10:53:10.255805984Z",
"type": "openpolicyagent.org/decision_logs"
}
You could log ship the audit logs to a central log aggregation system.
You could then run queries to analyze particular types of authorization decisions.
Logging output includes any errors that the policy engine reports.
During development you can troubleshoot behavior if you write OPA print statements, to include debug information in logs.
When you have finished testing, you can run this command to free resources:
./7-delete-cluster.sh
See the Rego Cheat Sheet for further information on Rego syntax.