Authorization Policy denies a request, but claim exists, request.auth.claims[realm_access_roles] with given value

Goal: Use keycloak to authenticate and (somehow)authorize for ingressgateway exposed services.

So I am using oauth2-proxy as ext_authz provider. In terms of authentication this is fine, but for authorization it doesnt have access control like for these hosts+paths allow users with these roles, etc.
So I still want to use istio’s claim based access control.

So I have got the pipeline setup this way.

  1. First an authpolicy with custom action “oauth2-proxy” that uses keycloak for token and login, and forwards all relaent headers to upstream.
  2. Another Deny(/allow) action authpolicy that dictates whether or not to allow based on the jwt token claim, that oauth2-proxy is putting.

I thought technically this should work because Custom action comes before deny/allow.
But it not working. Need HELP!!

The request(httpbin) gets denied by the second auth policy, even though i verify that, the token part of “Authorization: Bearer $JWT_TOKEN” this header contains the value in claim request.auth.claims[realm_access_roles], see below for jwt token and authpolicies.
For this I removed the second authpolicy and the setup works. The request is properly redirected to keycloak & authenticated and I can see the header also in httpbin.

questions:

  • Do i need a RequestAuthentication in this case? because a valid JWT token is coming as an Authorization: Bearer header anyway
  • Did I get something wrong in my setup?
  • Is there any other way I can have better access control with oauth2-proxy’s builtin settings which use the keycloak roles and match url, something like that (I know this is more an oauth2-proxy question)
  • Is there any other way with istio to achieve the feature like this in oauth2-proxy where; if there is no(valid) auth header redirect to keycloak login and get token put in header, and redirect back to previous url, then authorize based on claims/roles??.
  • Is there any other way of achieving what i want?

Thank a lot.
Any input is much appreciated.


(click) Here are my two authpolices:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: sample-httpbin-authn-policy
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: ingressgateway-internal
  action: CUSTOM
  provider:
    name: oauth2-proxy
  rules:
  - to:
    - operation:
        hosts: ["api-internal.v3box1.mosip.net","temp-gate.v3box1"]
        paths: ["/httpbin","/httpbin/*"]
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: sample-httpbin-authz-policy
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: ingressgateway-internal
  action: DENY
  rules:
  - to:
    - operation:
        hosts: ["api-internal.v3box1.mosip.net","temp-gate.v3box1"]
        paths: ["/httpbin","/httpbin/*"]
    when:
    - key: request.auth.claims[realm_access_roles]
      notValues: ["kibana_access"]

(click) And when i remove the second AuthPolicy, here is the output that i get on httpbin (redacted):
{
  "args": {}, 
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 
    "Accept-Encoding": "gzip, deflate, br", 
    "Accept-Language": "en-GB,en;q=0.9", 
    "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2VS05d2w5NmZpLXpIRFNuUDNTVTUzd0lhVVRVVnljUTl2WnZVMmlmNDlVIn0.eyJleHAiOjE2Mzc3NzYzNTksImlhdCI6MTYzNzc3NjA1OSwiYXV0aF90aW1lIjoxNjM3Nzc2MDU5LCJqdGkiOiI4Njk4YzFkMS1iNjc5LTRmMDctYmIxOS1kOTY3MTllMDlmNWMiLCJpc3MiOiJodHRwczovL2lhbS52M2JveDEubW9zaXAubmV0L2F1dGgvcmVhbG1zL2lzdGlvIiwiYXVkIjoiaXN0aW8tYXV0aC1jbGllbnQiLCJzdWIiOiI1OTQ4ZGQyOS1hZjFiLTQxM2MtODQyYS03N2U4YWNhNGJjMDIiLCJ0eXAiOiJJRCIsImF6cCI6ImlzdGlvLWF1dGgtY2xpZW50Iiwibm9uY2UiOiJmakMwRUo4TkdjRUxGdk1LaTFkbWRjdXFpRll1YXlNSTc5OXdwQ1k4NzNJIiwic2Vzc2lvbl9zdGF0ZSI6IjVlMjMyZDdiLWNiOGMtNDQxYi1iNTA4LWE5ZjRjZjhjYzEwOSIsImF0X2hhc2giOiI3a3RZQmlsR1pHbDVfQjRDcG54LTR3IiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicmVhbG1fYWNjZXNzX3JvbGVzIjpbImtpYmFuYV9hY2Nlc3MiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1pc3RpbyJdLCJuYW1lIjoiTGFsaXRoIEtvdGEiLCJncm91cHMiOltdLCJpc19raWJhbmFfYWNjZXNzIjoidHJ1ZSIsInByZWZlcnJlZF91c2VybmFtZSI6ImxhbGl0aCIsImdpdmVuX25hbWUiOiJMYWxpdGgiLCJmYW1pbHlfbmFtZSI6IktvdGEiLCJlbWFpbCI6ImxhbGl0aEBtb3NpcC5pbyJ9.dKyMO_ubVRh_Dcv1T0dJEG0WtxMSgjt3nfM67OCzgTTT0jILYZDKCXh-x7TyGWV6QJ0ibK-JOsUAsHEemLvlhDt_HvtXAIm4kC1DhAFwj8O6f2jfm9bpocmOdC4lKUFV3YDdUxcX8QMzyJTfHLYkPtVjwvomDB6DVDL5PhIqhJI5sVXPNo14bkKVLb-SBQoEI9QtfRQmuFqoOk3UZBOdfX-ROVyTn2mMKRUa4SVMhGbPvcPViScscVpDYTl6tjxwnto7VwHRa1-oOmGJhrmpZ9jzqZnMWKG9D68ikcn1oA1qd_pqOeIBZRTe6-NjhBaRr2kY-6brN9k7SYeYuZNarg", 
    "Cookie": <big cookie>
    <lots of headers>
    "X-Auth-Request-Access-Token": <bearer token>
    <lots of other headers>
  }
  <others>
}
(click) here is the decoded jwt token(redacted). Note the string "kibana_access" is present in the list in claim "realm_access_roles".
{
  <some_fields>
  "iss": "https://<domain>/auth/realms/istio",
  "aud": "istio-auth-client",
  "sub": "<key>",
  "typ": "ID",
  "realm_access_roles": [
    "kibana_access",
    "offline_access",
    "uma_authorization",
    "default-roles-istio"
  ],
  "name": "Lalith Kota",
  "groups": [],
  <some_other_metadata>
}

And for the oauth2-proxy setup;

(click) Here is my istio configmap (redacted)
data:
  mesh: |-
    defaultConfig:
      proxyMetadata:
        ISTIO_META_IDLE_TIMEOUT: 0s
      holdApplicationUntilProxyStarts: true
      discoveryAddress: istiod.istio-system.svc:15012
      gatewayTopology:
        numTrustedProxies: 2
      proxyMetadata: {}
      tracing:
        zipkin:
          address: zipkin.istio-system:9411
    enablePrometheusMerge: true
    pathNormalization:
      normalization: MERGE_SLASHES
    rootNamespace: istio-system
    trustDomain: cluster.local
    extensionProviders:
    - name: oauth2-proxy
      envoyExtAuthzHttp:
        service: oauth2-proxy.oauth2-proxy.svc.cluster.local
        port: 80
        includeRequestHeadersInCheck: ["authorization", "cookie"]
        includeAdditionalHeadersInCheck:
          X-Auth-Request-Redirect: "https://%REQ(:authority)%%REQ(:path)%"
        headersToUpstreamOnAllow: ["x-forwarded-access-token", "authorization", "path", "x-auth-request-user", "x-auth-request-email", "x-auth-request-access-token"]
        headersToDownstreamOnDeny: ["content-type", "set-cookie"]
(click) and here is my oauth2-proxy configuration (redacted):
provider = "keycloak-oidc"
oidc_issuer_url = "https://<domain>/auth/realms/istio"
email_domains = ["*"]
upstreams = ["static://200"]
redirect_url = "https://<domaint>/oauth2/callback"
insecure_oidc_allow_unverified_email = true
reverse_proxy = true
pass_access_token = true
pass_authorization_header = true
silence_ping_logging = true
set_authorization_header = true
set_xauthrequest = true
skip_provider_button = true
skip_auth_strip_headers = true
ssl_insecure_skip_verify = true
whitelist_domains = ["<base domain>"]
cookie_domains = ["<base domain>"]

1 Like

Ok. I did manage to achieve what i wanted but in a slightly different way. So I will mark this reply as solution.

  • One: To use the rules: when: key: request.auth.claims[realm_access_roles], we need a RequestAuthentication. And I wasnt able to successfully create one, it always failed with the rbac access denied error or with infinite redirects to keycloak
  • Two: I thought I will simply use a header instead of checking the claims themself. If I could write these realm roles to a header and then check for values of that header with istio authpolicy. “oauth2-proxy” already provides functionality where it can write the ‘groups’ claim to a header. And the ‘groups’ claim also contains the roles assigned. I ended up using AlphaConfiguration of oauth2-proxy, but that is not necessary.
  • Three: Now that I can see the roles in a header, x-auth-request-groups, I wanted to use authpolicy as below (similar to the previous claim checking, replaced with header checking).
    rules:
      when:
      - key: request.headers[x-auth-request-groups]
        notValues: ["*role:httpbin_access*"]
    
    But this time I cannot directly give notValues: ["role:httpbin_access"] coz istio treats this header as string, not as a list (Not sure why). So we have to use that vague string star matching thing, which also doesnt work. (role:httpbin_access* works, *role:httpbin_access also works, *role:httpbin_access* doesnt work. And there is no regex also yet)
  • Four: So I ended up using an Envoyfilter with contains_match on the header, in place of the second authpolicy. The rest of the structure is same as OP.
  • Final Structure: So for each particular url like abc.xyz.com/httpbin, I have one CUSTOM action AuthzPolicy to oauth2-proxy, which redirects to keycloak.xyz.com for login, then I have one Envoyfilter that denies the request if the appropriate role is not present in the groups header, x-auth-request-groups set by oauth2-proxy. I plan on having multiple of these for different urls with different roles.

Here is my final setup (as of now):

My AuthzPolicy and EnvoyFilter
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: sample-httpbin-authn-policy
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: ingressgateway-internal
  action: CUSTOM
  provider:
    name: oauth2-proxy
  rules:
  - to:
    - operation:
        hosts: ["myhost.xyz.com"]
        paths: ["/httpbin*"]
---
# # The following AuthorizationPolicy functionality is not supported by istio yet.
# # Hence an envoyfilter is written manually
# # In future once support is added replace the subsequent envoyfilter with this authorizationPolicy (or similar one).
#
# apiVersion: security.istio.io/v1beta1
# kind: AuthorizationPolicy
# metadata:
#   name: sample-httpbin-authz-policy
#   namespace: istio-system
# spec:
#   selector:
#     matchLabels:
#       istio: ingressgateway-internal
#   action: DENY
#   rules:
#   - to:
#     - operation:
#         hosts: ["myhost.xyz.com"]
#         paths: ["/httpbin*"]
#     when:
#     - key: request.headers[x-auth-request-groups]
#       notValues: ["*role:httpbin_access*"]
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: sample-httpbin-authz-filter
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway-internal
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: GATEWAY
      listener:
        portNumber: 8080
        filterChain:
          filter:
            name: "envoy.filters.network.http_connection_manager"
    patch:
      operation: ADD
      filterClass: AUTHZ
      value:
        name: envoy.filters.http.rbac.mydeny
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
          rules:
            action: DENY
            policies:
              "httpbinPolicy":
                permissions:
                - and_rules:
                    rules:
                    - or_rules:
                        rules:
                        - header:
                            name: ":authority"
                            exact_match: myhost.xyz.com
                    - or_rules:
                        rules:
                        - url_path:
                            path:
                              prefix: "/httpbin"
                principals:
                - and_ids:
                    ids:
                    - not_id:
                        or_ids:
                          ids:
                          - header:
                              name: x-auth-request-groups
                              contains_match: 'role:httpbin_access'
My Oauth2-proxy Setup
apiVersion: v1
kind: ConfigMap
metadata:
  name: oauth2-proxy-confmap
data:
  oauth2_proxy.cfg: |
    email_domains = ["*"]
    redirect_url = "https://___KEYCLOAK_HOST___/oauth2/callback"
    silence_ping_logging = true
    skip_provider_button = true
    whitelist_domains = ["___ROOT_DOMAIN___"]
    cookie_domains = ["___ROOT_DOMAIN___"]
    cookie_name = "_oauth2_proxy____INSTALLATION_NAME___"
  alpha.cfg: |
    injectRequestHeaders:
    - name: X-Forwarded-Groups
      values:
      - claim: groups
    - name: X-Forwarded-User
      values:
      - claim: user
    - name: X-Forwarded-Email
      values:
      - claim: email
    - name: X-Forwarded-Preferred-Username
      values:
      - claim: preferred_username
    - name: Authorization
      values:
      - claim: id_token
        prefix: 'Bearer '
    injectResponseHeaders:
    - name: X-Auth-Access-Token
      values:
      - claim: access_token
    - name: X-Auth-Request-User
      values:
      - claim: user
    - name: X-Auth-Request-Email
      values:
      - claim: email
    - name: X-Auth-Request-Preferred-Username
      values:
      - claim: preferred_username
    - name: X-Auth-Request-Groups
      values:
      - claim: groups
    - name: Authorization
      values:
      - claim: id_token
        prefix: 'Bearer '
    providers:
    - clientID: "___ISTIO_CLIENT_ID___"
      clientSecret: "___ISTIO_CLIENT_SECRET___"
      id: keycloak-oidc-istio
      provider: keycloak-oidc
      oidcConfig:
        emailClaim: email
        groupsClaim: groups
        userIDClaim: email
        insecureAllowUnverifiedEmail: true
        insecureSkipNonce: true
        issuerURL: https://___KEYCLOAK_HOST___/auth/realms/istio
    server:
      BindAddress: 0.0.0.0:4180
    upstreamConfig:
      upstreams:
      - id: static_200
        path: /
        static: true
        staticCode: 200
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: oauth2-proxy
  labels:
    app: oauth2-proxy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: oauth2-proxy
  template:
    metadata:
      labels:
        app: oauth2-proxy
    spec:
      containers:
      - name: oauth2-proxy
        image: quay.io/oauth2-proxy/oauth2-proxy:v7.2.0
        imagePullPolicy: IfNotPresent
        args:
        - --config=/conf/oauth2-proxy/oauth2_proxy.cfg
        - --alpha-config=/conf/oauth2-proxy/conf/alpha.cfg
        env:
        - name: OAUTH2_PROXY_COOKIE_SECRET
          value: "__some_base64_encoded_string___"
        ports:
        - containerPort: 4180
          name: http
          protocol: TCP
        resources:
          limits: {}
          requests: {}
        livenessProbe:
          httpGet:
            path: /ping
            port: http
            scheme: HTTP[spoiler]
[details="Summary"]
This text will be blurred
[/details]
[/spoiler]
          initialDelaySeconds: 0
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 5
        readinessProbe:
          httpGet:
            path: /ping
            port: http
            scheme: HTTP
          initialDelaySeconds: 0
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 5
        volumeMounts:
        - name: main-configuration
          mountPath: /conf/oauth2-proxy
      volumes:
      - name: main-configuration
        configMap:
          name: oauth2-proxy-confmap
---
apiVersion: v1
kind: Service
metadata:
  name: oauth2-proxy
  labels:
    app: oauth2-proxy
spec:
  type: ClusterIP
  selector:
    app: oauth2-proxy
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: http
IstioOperator Configuration
spec:
  meshConfig:
    ...
    extensionProviders:
    - name: oauth2-proxy
      envoyExtAuthzHttp:
        service: oauth2-proxy.oauth2-proxy.svc.cluster.local
        port: 80
        headersToDownstreamOnDeny:
        - content-type
        - set-cookie
        headersToUpstreamOnAllow:
        - authorization
        - path
        - x-auth-access-token
        - x-auth-request-user
        - x-auth-request-email
        - x-auth-request-preferred-username
        - x-auth-request-groups
        includeAdditionalHeadersInCheck:
          X-Auth-Request-Redirect: https://%REQ(:authority)%%REQ(:path)%
        includeRequestHeadersInCheck:
        - authorization
        - cookie
    ...

Also, goes without saying, in keycloak, relevant realm, client, roles, role mappers etc are required. Refer oauth2-proxy wiki for that configuration.

Any suggestions?! Thanks a lot!!

Istio Version: 1.11.4
Kubernetes Version: 1.19.13-eks
oauth2-proxy version: 7.2.0

1 Like

Just a quick thought and maybe it doesn’t apply.

Are you sure that you can’t sends a request with crafted headers to skip Authorization based on the JWT?

I think there is a value to validate the claims in the AuthorizationPolicy but the application underneath can use headers for logic stuff.

@revl-ca Thanks for the reply.

  1. Actually that’s the first thing I have tried. I have tried forging the “X-Auth-Request-Groups” with false role on client (on chrome) but the header seems to be overwritten by istio / oauth2-proxy. Phew!! Are there any security concerns if i do it this way?
  2. Currently I am not doing Authorization based on jwt. To check for the role in jwttoken itself, I need a proper RequestAuthentication. And I can’t seem to get that right… (like Custom action AuthzPolicy(oauth2-proxy) → RequestAuthentication (jwt validation) → Deny AuthzPolicy (based on role from claim) … when I try to access httpbin … it redirects to keycloak for login properly but I cannot login… I am stuck in infinite login loop on keycloak)
  3. Ya I can pass off the headers/token to upstream/application to delegate access control to application itself … but I don’t want to do that. That defeats my purpose.
    Did i understand your question right?

Did you find any solution? is there any way to implement istio authorization based on user role coming from keycloak through Oauth2 ?