API key and OPA

You can combine the API key and OPA extauth modules to perform multi-step authentication for incoming requests.

When a client sends an API key to authenticate with another service in the cluster, the gateway proxy can extract and validate the API key by using the API key extauth module. In addition, if the API key contains additional metadata, such as the user ID or email address, you can specify these fields in the headersFromMetadataEntry section of your extauth policy. This way, Gloo Gateway can extract these metadata fields from the API key and add them as headers to the request. The headers and the API key are then forwarded to the OPA module where additional validation checks can be performed.

In this guide, you can try out different API key and OPA configurations, such as:

To learn more about each module that is being used as part of this guide, see the API key and OPA guides.

If you import or export resources across workspaces, your policies might not apply. For more information, see Import and export policies.

Before you begin

This guide assumes that you use the same names for components like clusters, workspaces, and namespaces as in the getting started, and that your Kubernetes context is set to the cluster you store your Gloo config in (typically the management cluster). If you have different names, make sure to update the sample configuration files in this guide.

Follow the getting started instructions to:

  1. Set up Gloo Gateway in a single cluster.

  2. Deploy sample apps.

  3. Configure an HTTP listener on your gateway and set up basic routing for the sample apps.

  4. Make sure that the external auth service is installed and running. If not, install the external auth service in your single or multicluster environment. kubectl get pods -A -l app=ext-auth-service

  5. Make sure that you have the following CLI tools, or something comparable:

    • base64 to encode strings.

Configure API key and OPA external auth

  1. Create an extauth server that you use to enforce the extauth policies in this guide.

    kubectl apply -f - <<EOF
    apiVersion: admin.gloo.solo.io/v2
    kind: ExtAuthServer
    metadata:
      name: ext-auth-server
      namespace: bookinfo
    spec:
      destinationServer:
        port:
          number: 8083
        ref:
          cluster: $CLUSTER_NAME
          name: ext-auth-service
          namespace: gloo-mesh-addons
    EOF
    
  2. Create a Kubernetes secret that stores your API key and additional metadata, such as the user ID and email address.

    kubectl -n bookinfo create secret generic user-glooy --type extauth.solo.io/apikey --from-literal=user-id=user-id-glooy --from-literal=user-email=glooy@solo.io --from-literal=user-name=glooy --from-literal=api-key=N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy
    
  3. Label the secret so that you can reference this secret in your extauth policy more easily.

    kubectl -n bookinfo label secret user-glooy extauth=apikey
    
  4. Create a configmap for an OPA rule that validates the user's email address that you added earlier. The following examples verifies that the email address in the x-user-email header ends with solo.io. Note that the OPA rules refers to the x-user-email header and not the actual user-email field in the Kubernetes secret. The mapping of the user-email field to the x-user-email request header is done when you create the extauth policy in the next step.

    cat << EOF | kubectl apply -f -
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: allow-api-key-from-trusted-email-domain
      namespace: bookinfo
      labels:
        team: infrastructure
    data:
      policy.rego: |
        package test
    
        default allow = false
        allow {
          endswith(input.state["x-user-email"], "@solo.io")
        }
    EOF
    
  5. Create an extauth policy that references both the secret that contains the API key and the configmap with your OPA rule. In this example, you configure Gloo Gateway to extract the user-email field from the API key and add it as the x-user-email header to the request so that it can be passed on to the OPA module for further validation.

    kubectl apply -f - <<EOF
    apiVersion: security.policy.gloo.solo.io/v2
    kind: ExtAuthPolicy
    metadata:
      name: ratings-apikey
      namespace: bookinfo
    spec:
      applyToRoutes:
      - route:
          labels:
            route: ratings
      config:
        server:
          name: ext-auth-server
          namespace: bookinfo
          cluster: $CLUSTER_NAME
        glooAuth:
          configs:
          - name: APIKey
            apiKeyAuth:
              headerName: api-key
              headersFromMetadataEntry:
                x-user-email:
                  name: user-email
                  required: true
              k8sSecretApikeyStorage:
                labelSelector:
                  extauth: apikey
          - name: opa
            opaAuth:
              modules:
              - name: allow-api-key-from-trusted-email-domain
                namespace: bookinfo
              query: "data.test.allow == true"
    EOF
    
  6. Send a request to the ratings app and pass the API key that you added to the Kubernetes secret.

    curl -vik --resolve www.example.com:80:${INGRESS_GW_IP} http://www.example.com:80/ratings/1 -i -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    
    curl -vik --resolve www.example.com:443:${INGRESS_GW_IP} https://www.example.com:443/ratings/1 -i -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    

    Example output:

    < HTTP/2 200 
    HTTP/2 200 
    < content-type: application/json
    content-type: application/json
    < date: Tue, 28 Mar 2023 20:16:40 GMT
    date: Tue, 28 Mar 2023 20:16:40 GMT
    < x-envoy-upstream-service-time: 5
    x-envoy-upstream-service-time: 5
    < server: istio-envoy
    server: istio-envoy
    
    * Connection #0 to host www.example.com left intact
    {"id":1,"ratings":{"Reviewer1":5,"Reviewer2":4}}%   
    
  7. Create another configmap and add an OPA rule that references metadata that does not exist in the API key.

    kubectl apply -f - <<EOF
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: allow-api-key-from-certain-cost-centers
      namespace: bookinfo
      labels:
        team: infrastructure
    data:
      policy.rego: |
        package test
    
        default allow = false
        allow {
          startswith(input.state["x-user-cost-center"], "733")
        }
    EOF
    
  8. Update the extauth policy to reference the new configmap.

    kubectl apply -f - <<EOF
    apiVersion: security.policy.gloo.solo.io/v2
    kind: ExtAuthPolicy
    metadata:
      name: ratings-apikey
      namespace: bookinfo
    spec:
      applyToRoutes:
      - route:
          labels:
            route: ratings
      config:
        server:
          name: ext-auth-server
          namespace: bookinfo
          cluster: $CLUSTER_NAME
        glooAuth:
          configs:
          - name: APIKey
            apiKeyAuth:
              headerName: api-key
              headersFromMetadataEntry:
                x-user-email:
                  name: user-email
                  required: true
              k8sSecretApikeyStorage:
                labelSelector:
                  extauth: apikey
          - name: opa
            opaAuth:
              modules:
              - name: allow-api-key-from-certain-cost-centers
                namespace: bookinfo
              query: "data.test.allow == true"
    EOF
    
  9. Send another request to the ratings service. This time, the request is denied, because the new x-user-cost-center metadata field could not be extracted from the API key to be processed by the OPA module.

    curl -vik --resolve www.example.com:80:${INGRESS_GW_IP} http://www.example.com:80/ratings/1 -i -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    
    curl -vik --resolve www.example.com:443:${INGRESS_GW_IP} https://www.example.com:443/ratings/1 -i -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    

    Example output:

    * Connection state changed (MAX_CONCURRENT_STREAMS == 2147483647)!
    < HTTP/2 403 
    HTTP/2 403 
    < date: Tue, 28 Mar 2023 20:51:24 GMT
    date: Tue, 28 Mar 2023 20:51:24 GMT
    < server: istio-envoy
    server: istio-envoy
    
  10. Add the x-user-cost-center field to the API key.

    kubectl -n bookinfo get secret user-glooy -o json | jq --arg cost_center "$(echo -n 73355 | base64)" '.data["user-cost-center"]=$cost_center' | kubectl apply -f -
    
  11. Update the extauth policy to add the x-user-cost-center to the headersFromMetadataEntry section so that the field can be extracted from the API key and passed to the OPA extauth module as a header.

    kubectl apply -f - <<EOF
    apiVersion: security.policy.gloo.solo.io/v2
    kind: ExtAuthPolicy
    metadata:
      name: ratings-apikey
      namespace: bookinfo
    spec:
     applyToRoutes:
     - route:
         labels:
           route: ratings
     config:
       server:
         name: ext-auth-server
         namespace: bookinfo
         cluster: $CLUSTER_NAME
       glooAuth:
         configs:
         - name: APIKey
           apiKeyAuth:
             headerName: api-key
             headersFromMetadataEntry:
               x-user-email:
                 name: user-email
                 required: true
               x-user-cost-center:
                 name: user-cost-center
                 required: true
             k8sSecretApikeyStorage:
               labelSelector:
                 extauth: apikey
         - name: opa
           opaAuth:
             modules:
             - name: allow-api-key-from-certain-cost-centers
               namespace: bookinfo
             query: "data.test.allow == true"
    EOF
    
  12. Send another request to the ratings service. This time, the request succeeds as the x-user-cost-center can be extracted and forwarded to the OPA extauth module for further validation.

    curl -vik --resolve www.example.com:80:${INGRESS_GW_IP} http://www.example.com:80/ratings/1 -i -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    
    curl -vik --resolve www.example.com:443:${INGRESS_GW_IP} https://www.example.com:443/ratings/1 -i -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    

    Example output:

    * Connection state changed (MAX_CONCURRENT_STREAMS == 2147483647)!
    < HTTP/2 200 
    HTTP/2 200 
    < content-type: application/json
    content-type: application/json
    < date: Tue, 28 Mar 2023 20:54:17 GMT
    date: Tue, 28 Mar 2023 20:54:17 GMT
    < x-envoy-upstream-service-time: 3
    x-envoy-upstream-service-time: 3
    < server: istio-envoy
    server: istio-envoy
    
  13. Create another configmap and add another OPA rule to verify the API key itself and deny requests that use a specific API key.

    kubectl apply -f - <<EOF
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: allow-api-key-from-allowlist
      namespace: bookinfo
      labels:
        team: infrastructure
    data:
      policy.rego: |
        package test
    
        default allow = false
        is_key_allowed = true {
          allowed_keys := { "N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy", "974b3a3f-0aa9-4a94-bfe7-3fd42942d5e3" }
          input.state["api_key_value"] == allowed_keys[_]
        }
        allow {
          # deny any keys listed in the allowed_keys array.
          not is_key_allowed
        }
    EOF
    
  14. Update the extauth policy to use the new OPA rule.

    kubectl apply -f - <<EOF
    apiVersion: security.policy.gloo.solo.io/v2
    kind: ExtAuthPolicy
    metadata:
      name: ratings-apikey
      namespace: bookinfo
    spec:
     applyToRoutes:
     - route:
         labels:
           route: ratings
     config:
       server:
         name: ext-auth-server
         namespace: bookinfo
         cluster: $CLUSTER_NAME
       glooAuth:
         configs:
         - name: APIKey
           apiKeyAuth:
             k8sSecretApikeyStorage:
               labelSelector:
                 extauth: apikey
         - name: opa
           opaAuth:
             modules:
             - name: allow-api-key-from-allowlist
               namespace: bookinfo
             query: "data.test.allow == true"
    EOF
    
  15. Send another request to the ratings app. This time, the request is denied, because the API key that is used in the curl request is part of the API keys that are not allowed to be forwarded to the ratings app.

    ```shell
    curl -vik --resolve www.example.com:80:${INGRESS_GW_IP} http://www.example.com:80/ratings/1 -i -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    ```
    
    ```shell
    curl -vik --resolve www.example.com:443:${INGRESS_GW_IP} https://www.example.com:443/ratings/1 -i -H "api-key: N2YwMDIxZTEtNGUzNS1jNzgzLTRkYjAtYjE2YzRkZGVmNjcy"
    ```
    
    Example output:

    * Connection state changed (MAX_CONCURRENT_STREAMS == 2147483647)!
    < HTTP/2 403 
    HTTP/2 403 
    < date: Tue, 28 Mar 2023 20:54:45 GMT
    date: Tue, 28 Mar 2023 20:54:45 GMT
    < server: istio-envoy
    server: istio-envoy
    

Cleanup

You can optionally remove the resources that you set up as part of this guide.
kubectl delete extauthpolicy ratings-apikey -n bookinfo
kubectl delete configmap allow-api-key-from-allowlist -n bookinfo
kubectl delete configmap allow-api-key-from-certain-cost-centers -n bookinfo
kubectl delete configmap allow-api-key-from-trusted-email-domain -n bookinfo
kubectl delete secret user-glooy -n bookinfo  

Known limitations

When you have multiple Kubernetes secrets that share the same label, and you use labels to reference the Kubernetes secret, the extauth policy passes the API key and metadata information from the first Kubernetes secret that is found to the OPA module.