Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Code Block
languagetext
titleGO OPA
package main

import (
        "context"
        "encoding/json"
        "fmt"
        "github.com/open-policy-agent/opa/rego"
        "io/ioutil"
        "net/http"
        "net/url"
        "os"
)

type Jwttoken struct {
        Access_token       string
        Expires_in         int
        Refresh_expires_in int
        Refresh_token      string
        Token_type         string
        Not_before_policy  int
        Session_state      string
        Scope              string
}

var token Jwttoken

var opaPolicy string = `
package authz

import future.keywords.in

default allow = false

jwks := jwks_request("http://keycloak:8080/auth/realms/opa/protocol/openid-connect/certs").body
filtered_jwks := [ key |
      some key in jwks.keys
      key.use == "sig"
    ]
token_cert := json.marshal({"keys": filtered_jwks})

token = { "isValid": isValid, "header": header, "payload": payload } {
        [isValid, header, payload] := io.jwt.decode_verify(input, { "cert": token_cert, "aud": "account", "iss": "http://keycloak:808
0/auth/realms/opa"})
}

allow {
    is_token_valid
}

is_token_valid {
        token.isValid
        now := time.now_ns() / 1000000000
        token.payload.iat <= now
        now < token.payload.exp
        token.payload.clientRole = "[opa-client-role]"
}

jwks_request(url) = http.send({
      "url": url,
      "method": "GET",
      "force_cache": true,
      "force_json_decode": true,
      "force_cache_duration_seconds": 3600 # Cache response for an hour
})
`

func getToken() string {
        clientSecret := "63wkv0RUXkp01pbqtNTSwghhTxeMW55I"
        clientId := "opacli"
        realmName := "opa"
        keycloakHost := "keycloak"
        keycloakPort := "8080"
        keycloakUrl := "http://" + keycloakHost + ":" + keycloakPort + "/auth/realms/" + realmName + "/protocol/openid-connect/token"        resp, err := http.PostForm(keycloakUrl,
                url.Values{"client_secret": {clientSecret}, "grant_type": {"client_credentials"}, "client_id": {clientId}})
        if err != nil {
                fmt.Println(err)
                panic("Something wrong with the credentials or url ")
        }
        defer resp.Body.Close()
        body, err := ioutil.ReadAll(resp.Body)
        json.Unmarshal([]byte(body), &token)
        return token.Access_token
}

func traceOpa(input string) {
        ctx := context.TODO()

        test := rego.New(
                rego.Query("x = data.authz.allow"),
                rego.Trace(true),
                rego.Module("example.rego", opaPolicy),
                rego.Input(input),
        )

        test.Eval(ctx)
        rego.PrintTraceWithLocation(os.Stdout, test)
}

func evaluateOpa(input string) {
        ctx := context.TODO()

        query, err := rego.New(
                rego.Query("x = data.authz.allow"),
                rego.Module("example.rego", opaPolicy),
        ).PrepareForEval(ctx)
        if err != nil {
                // Handle error.
                fmt.Println(err.Error())
        }

        results, err := query.Eval(ctx, rego.EvalInput(input))
        // Inspect results.
        if err != nil {
                // Handle evaluation error.
                fmt.Println("Error: " + err.Error())
        } else if len(results) == 0 {
                // Handle undefined result.
                fmt.Println("Results are empty")
        } else {
                // Handle result/decision.
                fmt.Printf("Results = %+v\n", results) //=> [{Expressions:[true] Bindings:map[x:true]}]
        }
  }

func main() {
        tokenStr := getToken()
        traceOpa(tokenStr)
        evaluateOpa(tokenStr)

}                                                                                                    


OPA bundles and dynamic composition

Method 1

We can combined OPA bundles with dynamic composition to provide different policies for differect services.

...

Code Block
languagetext
titleopa_test.sh
#!/bin/bash
INGRESS_HOST=$(minikube ip)
INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}')
TESTS=0
PASSED=0
FAILED=0
TEST_TS=$(date +%F-%T)
TOKEN=""
ACCESS_TOKEN=""
REFRESH_TOKEN=""

function get_token
{
    local prefix="${1}"
    url="http://${KEYCLOAK_HOST}:${KEYCLOAK_PORT}/auth/realms"
         TOKEN=$(curl -s -X POST $url/opa/protocol/openid-connect/token -H \
                 "Content-Type: application/x-www-form-urlencoded" -d client_secret=63wkv0RUXkp01pbqtNTSwghhTxeMW55I \
                 -d 'grant_type=client_credentials' -d client_id=opacli)
        ACCESS_TOKEN=$(echo $TOKEN | jq -r '.access_token')
}

function run_test
{
    local prefix="${1}" type=${2} msg="${3}" data=${4}
    TESTS=$((TESTS+1))
    echo "Test ${TESTS}: Testing $type /${prefix}"
    get_token $prefix
    url=$INGRESS_HOST:$INGRESS_PORT"/"$prefix
    result=$(curl -s -X ${type} -H "Content-type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" $url)
    echo $result
    if [ "$result" != "$msg" ]; then
            echo "FAIL"
            FAILED=$((FAILED+1))
    else
            echo "PASS"
            PASSED=$((PASSED+1))
    fi
    echo ""
}


run_test "rapp-opa-provider" "GET" "Hello OPA World!" ""

echo
echo "-----------------------------------------------------------------------"
echo "Number of Tests: $TESTS, Tests Passed: $PASSED, Tests Failed: $FAILED"
echo "Date: $TEST_TS"
echo "-----------------------------------------------------------------------"

Method 2

We can also organize the policies in the following way.

...

You should see a dashboard similar to the following:


OPA Profiling and Bench Marking

Policy Performance

Below is a sample file we can use for profiling/benchmarking

Code Block
languagetext
titlerbactest.rego
package rbactest

import data.policy.common.request
import data.policy.services

input =  {
    "attributes": {
      "destination": {
        "address": {
          "socketAddress": {
            "address": "172.17.0.15",
            "portValue": 9000
          }
        },
        "principal": "spiffe://cluster.local/ns/istio-nonrtric/sa/default"
      },
      "metadataContext": {},
      "request": {
        "http": {
          "headers": {
            ":authority": "192.168.49.2:31000",
            ":method": "GET",
            ":path": "/rapp-opa-provider",
            ":scheme": "http",
            "accept": "*/*",
            "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDamJ4a2FONjRVcUdYNThWU2R3WjBxQTdWRmN1TGdEQWhnUWJTVG55UE9JIn0.eyJleHAiOjE2NTY0MDYwMTYsImlhdCI6MTY1NjQwNTcxNiwianRpIjoiOWE0MzAxODMtZjYzZS00MzY1LTg4NTMtYjc0ZTY2MmFhYTUwIiwiaXNzIjoiaHR0cDovL2tleWNsb2FrOjgwODAvYXV0aC9yZWFsbXMvb3BhIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjU3NGMxODQzLTZiMWEtNGMwZC04M2U0LTljMDkwYjM5ZjIyZiIsInR5cCI6IkJlYXJlciIsImF6cCI6Im9wYWNsaSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtb3BhIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsib3BhY2xpIjp7InJvbGVzIjpbIm9wYS1jbGllbnQtcm9sZSJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiY2xpZW50SWQiOiJvcGFjbGkiLCJjbGllbnRIb3N0IjoiMTI3LjAuMC42IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJjbGllbnRSb2xlIjoiW29wYS1jbGllbnQtcm9sZV0iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtb3BhY2xpIiwiY2xpZW50QWRkcmVzcyI6IjEyNy4wLjAuNiJ9.k_NtngXgWyTPB2z8IArnxqvx3iYP18xc-1fWQoZ0Az2BOK3kfWrdSmIngn_ilwLXrTSdX-n_Tx_o2NLlwQ13RMRXN5zoJaCnU2iSXvel7hjYA_PSDZbv3MW1boqOZmqzuTA5ugRXuVAGQ42k0PIPNWqSm6JujUGfVzB_mrc43I3jXNuHuqwx6miat4NmJo2MCxM_Y1s39ixxREfQIovqFz1ky69IKfz8QcxyFhSsCmydjk4T6HufC3_SJO0XaBKWAoJpdcgdom1kYcIeoGxWWn3lX5E0kQ3eL4TH0F6IfCtjLZwFlzlhtDJItD6ddglJpEsc6rcuLw-06_VyjeqzSg",
            "content-type": "application/json",
            "user-agent": "curl/7.68.0",
          },
          "host": "192.168.49.2:31000",
          "id": "15472195549358141958",
          "method": "GET",
          "path": "/rapp-opa-provider",
          "protocol": "HTTP/1.1",
          "scheme": "http"
        },
        "time": "2022-06-28T07:07:47.099076Z"
      },
      "source": {
        "address": {
          "socketAddress": {
            "address": "172.17.0.4",
            "portValue": 54862
          }
        },
        "principal": "spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"
      }
    },
    "parsed_body": null,
    "parsed_path": [
      "rapp-opa-provider"
    ],
    "parsed_query": {},
    "truncated_body": false,
    "version": {
      "encoding": "protojson",
      "ext_authz": "v3"
    }
  }


default allow = false


allow = true {
 services[request.policy].ingress.allow
}


opa eval --data rbactest.rego --profile --count=100 --format=pretty 'data.rbactest.allow'
false
+--------------------------------+---------+---------+--------------+-------------------+------------------------+
| METRIC | MIN | MAX | MEAN | 90% | 99% |
+--------------------------------+---------+---------+--------------+-------------------+------------------------+
| timer_rego_external_resolve_ns | 300 | 2400 | 461 | 600 | 2382.9999999999914 |
| timer_rego_load_files_ns | 3069300 | 6160300 | 4.024052e+06 | 4.82397e+06 | 6.152662999999996e+06 |
| timer_rego_module_compile_ns | 690400 | 2288700 | 983743 | 1.42366e+06 | 2.2863819999999986e+06 |
| timer_rego_module_parse_ns | 439300 | 1834400 | 613517 | 882270.0000000001 | 1.832610999999999e+06 |
| timer_rego_query_compile_ns | 49500 | 190100 | 68390 | 93410 | 189761.99999999983 |
| timer_rego_query_eval_ns | 25600 | 423300 | 40197 | 42630.00000000001 | 421415.99999999907 |
| timer_rego_query_parse_ns | 44700 | 674500 | 70035 | 81690 | 671625.9999999986 |
+--------------------------------+---------+---------+--------------+-------------------+------------------------+
+--------+--------+----------+---------+-----------+----------+----------+---------------------+
| MIN | MAX | MEAN | 90% | 99% | NUM EVAL | NUM REDO | LOCATION |
+--------+--------+----------+---------+-----------+----------+----------+---------------------+
| 14.1µs | 404µs | 25.251µs | 28.82µs | 402.438µs | 1 | 1 | data.rbactest.allow |
| 9.8µs | 27.8µs | 12.843µs | 16.48µs | 27.756µs | 1 | 1 | rbactest.rego:62 |
+--------+--------+----------+---------+-----------+----------+----------+---------------------+


opa bench --data rbactest.rego 'data.rbactest.allow'
+-------------------------------------------------+------------+
| samples | 22605 |
| ns/op | 47760 |
| B/op | 6269 |
| allocs/op | 112 |
| histogram_timer_rego_external_resolve_ns_75% | 400 |
| histogram_timer_rego_external_resolve_ns_90% | 500 |
| histogram_timer_rego_external_resolve_ns_95% | 500 |
| histogram_timer_rego_external_resolve_ns_99% | 871 |
| histogram_timer_rego_external_resolve_ns_99.9% | 29394 |
| histogram_timer_rego_external_resolve_ns_99.99% | 29800 |
| histogram_timer_rego_external_resolve_ns_count | 22605 |
| histogram_timer_rego_external_resolve_ns_max | 29800 |
| histogram_timer_rego_external_resolve_ns_mean | 434 |
| histogram_timer_rego_external_resolve_ns_median | 400 |
| histogram_timer_rego_external_resolve_ns_min | 200 |
| histogram_timer_rego_external_resolve_ns_stddev | 1045 |
| histogram_timer_rego_query_eval_ns_75% | 31100 |
| histogram_timer_rego_query_eval_ns_90% | 37210 |
| histogram_timer_rego_query_eval_ns_95% | 47160 |
| histogram_timer_rego_query_eval_ns_99% | 91606 |
| histogram_timer_rego_query_eval_ns_99.9% | 630561 |
| histogram_timer_rego_query_eval_ns_99.99% | 631300 |
| histogram_timer_rego_query_eval_ns_count | 22605 |
| histogram_timer_rego_query_eval_ns_max | 631300 |
| histogram_timer_rego_query_eval_ns_mean | 29182 |
| histogram_timer_rego_query_eval_ns_median | 25300 |
| histogram_timer_rego_query_eval_ns_min | 15200 |
| histogram_timer_rego_query_eval_ns_stddev | 32411 |
+-------------------------------------------------+------------+


OPA Sidecar injection

First create a namespace for your apps and enable istio and opa 

kubectl create ns opa
kubectl label namespace opa opa-istio-injection="enabled"
kubectl label namespace opa istio-injection="enabled"

Create the opa injection obects using:

kubectl create -f opa_inject.yaml

Ensure your istio mesh config has been setup to include grcp local authorizer

kubectl edit configmap istio -n istio-system

Code Block
languagetext
titleextensionProviders
    extensionProviders:
    - envoyExtAuthzGrpc:
        port: "9191"
        service: local-opa-grpc.local
      name: opa-local



Update your rapp-provider authorization policy to use this provider:





Code Block
languagetext
titleAuthorizationPolicy
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: rapp-opa-provider-opa
  namespace: opa
spec:
  selector:
    matchLabels:
      app: rapp-opa-provider
  action: CUSTOM
  provider:
    name: "opa-local"
  rules:
  - to:
    - operation:
        paths: ["/rapp-opa-provider"]
        notPaths: ["/health"]



Run the opa_test.sh script above and you should see a message confirming your connection to the service.

Note: References to keycloak need to be updated to include the keycloak schema i.e keycloak.default

Basic Authentication

We can add basic authentication to our NGINX bubdle server by following these steps:

Create a password file using the following command:   sudo htpasswd -c .htpasswd <user>, you will be prompted to input the password.

This will produce a file called .htpasswd containing the username and encrypted password

e.g. admin:$apr1$tPQCjrVW$sokcSj4QVkncEDna0Fc2o/

Add the following configmap definitions to your nginx.yaml


Code Block
languagetext
titleconfigMaps
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-pwd-config
  namespace: default
data:
  .htpasswd: |
    admin:$apr1$tPQCjrVW$sokcSj4QVkncEDna0Fc2o/
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf-config
  namespace: default
data:
  default.conf: |
   server {
           server_name localhost;

           location ~ ^/bundles/(.*)$ {
           root /usr/share/nginx/html/bundles;
           try_files /$1 =404;
           auth_basic "Restricted";
           auth_basic_user_file /etc/nginx/conf.d/conf/.htpasswd;
           }
   }
---


Then update your volumes and volume mounts to include these files with your deployment

Code Block
languagetext
titleVolumes
        volumeMounts:
        - name: bundlesdir
          mountPath: /usr/share/nginx/html/bundles
          readOnly: true
        - name: nginx-conf
          mountPath: /etc/nginx/conf.d/default.conf
          subPath: default.conf
        - name: nginx-pwd
          mountPath: /etc/nginx/conf.d/conf/.htpasswd
          subPath: .htpasswd
      volumes:
      - name: bundlesdir
        hostPath:
          # Ensure the file directory is created.
           path: /var/opa/bundles
           type: DirectoryOrCreate
      - name: nginx-conf
        configMap:
          name: nginx-conf-config
          defaultMode: 0644
      - name: nginx-pwd
        configMap:
          name: nginx-pwd-config
          defaultMode: 0644

This will add basic authentication to your bundles directory.

Run echo -n <username>:<password> | base64 to encrpt your usename and password

e.g. echo -n admin:admin | base64
YWRtaW46YWRtaW4=

Update the opa-istio-config ConfigMap in the opa_inject.yaml file to include the encrypted string as a token in the cedentials section:


Code Block
languagetext
titleopa-istio-config
apiVersion: v1
kind: ConfigMap
metadata:
  name: opa-istio-config
  namespace: opa
data:
  config.yaml: |
    plugins:
      envoy_ext_authz_grpc:
        addr: :9191
        path: policy/ingress/allow
    decision_logs:
      console: true
    services:
      - name: bundle-server
        url: http://bundle-server.default
        credentials:
          bearer:
            token: YWRtaW46YWRtaW4=
            scheme: Basic

    bundles:
      authz:
        service: bundle-server
        resource: bundles/opa-bundle.tar.gz
        persist: true
        polling:
          min_delay_seconds: 10
          max_delay_seconds: 20
---

Your bundle is now protected with basic authentication.