Repository: kserve/kserve

Issue:
Authentication setup interfering with kfserving
/kind bug

How authentication is supposed to interact with kfserving?

I followed this article to setup istio/dex authentication here:
https://journal.arrikto.com/kubeflow-authentication-with-istio-dex-5eafdfac4782

And it has been working great. However, we are trying to get kfserving to work and now the envoy filter below is interfering with it.

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: authn-filter
spec:
  workloadLabels:
    istio: ingressgateway
  filters:
  - filterConfig:
      httpService:
        serverUri:
          uri: http://authservice.istio-system.svc.cluster.local
          cluster: outbound|8080||authservice.istio-system.svc.cluster.local
        authorizationRequest: 
          allowedHeaders:
            patterns:
            - exact: "cookie"
    filterName: envoy.ext_authz
    filterType: HTTP
    listenerMatch:
      portNumber: 443
      listenerType: GATEWAY

We are trying to simply make serving api calls pass this filter all together, so I'm trying to see how I can scope down when this filter applies.

For example the call below (making a backend prediction api call) shouldn't go through this filter:
curl -v -H "Host: sklearn-iris.foo.example.com" http://localhost:8080/v1/models/sklearn-iris:predict -d @https://raw.githubusercontent.com/kubeflow/kfserving/master/docs/samples/sklearn/iris-input.json

Ideally we want a mechanism to say if the host header is "sklearn-iris.foo.example.com", then pass the authentication filter...

Looks to me kfserving is using the same kubeflow-gateway and thus the filter will apply no matter what, which seems wrong.

So just in general, how is this kfserving configuration supposed to be compatible with the recommended UI authentication setup/filter indicated above?



Discussion:
Issue-Label Bot is automatically applying the labels:

| Label  | Probability |
| ------------- | ------------- |
| kind/question | 0.67 |

Please mark this comment with :thumbsup: or :thumbsdown: to give our bot feedback! 
Links: [app homepage](https://github.com/marketplace/issue-label-bot), [dashboard](https://github.com/marketplace/issue-label-botdata/kubeflow/kfserving) and [code](https://github.com/hamelsmu/MLapp) for this bot.
Issue Label Bot is not confident enough to auto-label this issue.
                See [dashboard](https://github.com/marketplace/issue-label-botdata/kubeflow/kfserving) for more details.
                
Issue-Label Bot is automatically applying the labels:

| Label  | Probability |
| ------------- | ------------- |
| area/front-end | 0.52 |

Please mark this comment with :thumbsup: or :thumbsdown: to give our bot feedback! 
Links: [app homepage](https://github.com/marketplace/issue-label-bot), [dashboard](https://github.com/marketplace/issue-label-botdata/kubeflow/kfserving) and [code](https://github.com/hamelsmu/MLapp) for this bot.
Issue-Label Bot is automatically applying the labels:

| Label  | Probability |
| ------------- | ------------- |
| area/front-end | 0.52 |

Please mark this comment with :thumbsup: or :thumbsdown: to give our bot feedback! 
Links: [app homepage](https://github.com/marketplace/issue-label-bot), [dashboard](https://github.com/marketplace/issue-label-botdata/kubeflow/kfserving) and [code](https://github.com/hamelsmu/MLapp) for this bot.
@yanniszark  @krishnadurai Any thoughts on this?
@yuzisun any update on this one? I'm falling in the same error!
Issue-Label Bot is automatically applying the labels:

| Label  | Probability |
| ------------- | ------------- |
| area/engprod | 0.50 |

Please mark this comment with :thumbsup: or :thumbsdown: to give our bot feedback! 
Links: [app homepage](https://github.com/marketplace/issue-label-bot), [dashboard](https://label-bot-prod.mlbot.net/data/kubeflow/kfserving) and [code](https://github.com/hamelsmu/MLapp) for this bot.
Is there an  update or recommendation on this? I was wondering if there are sample envoyfilter that overrides authentication for kfserving - sudhir
Hi, is there any update on this one?
What I found as a workaround or probably the right way is -
Have separate ingress-gateways and loadbalancer/IP addresses with each own entry point. One for main kubeflow pipelines /training and another ingress-gateway for kfserving with no authentication. So we do not have to be constrained using the same envoyfilter for both. 

That is what I did and made it work

Sudhir
> What I found as a workaround or probably the right way is -
> Have separate ingress-gateways and loadbalancer/IP addresses with each own entry point. One for main kubeflow pipelines /training and another ingress-gateway for kfserving with no authentication. So we do not have to be constrained using the same envoyfilter for both.
> 
> That is what I did and made it work
> 
> Sudhir

@nallagangus can you share how you configured the separate ingress?
Any update on this ?

Resolving patch:
diff --git a/docs/samples/istio-dex/dex_auth.py b/docs/samples/istio-dex/dex_auth.py
new file mode 100644
--- /dev/null
+++ b/docs/samples/istio-dex/dex_auth.py
@@ -0,0 +1,129 @@
+import re
+from urllib.parse import urlsplit
+import requests
+
+
+def get_istio_auth_session(url: str, username: str, password: str) -> dict:
+    """
+    Determine if the specified URL is secured by Dex and try to obtain a session cookie.
+    WARNING: only Dex `staticPasswords` and `LDAP` authentication are currently supported
+             (we default to using `staticPasswords` if both are enabled)
+
+    :param url: Kubeflow server URL, including protocol
+    :param username: Dex `staticPasswords` or `LDAP` username
+    :param password: Dex `staticPasswords` or `LDAP` password
+    :return: auth session information
+    """
+    # define the default return object
+    auth_session = {
+        "endpoint_url": url,  # KF endpoint URL
+        "redirect_url": None,  # KF redirect URL, if applicable
+        "dex_login_url": None,  # Dex login URL (for POST of credentials)
+        "is_secured": None,  # True if KF endpoint is secured
+        "session_cookie": None,  # Resulting session cookies in the form "key1=value1; key2=value2"
+    }
+
+    # use a persistent session (for cookies)
+    with requests.Session() as s:
+        ################
+        # Determine if Endpoint is Secured
+        ################
+        resp = s.get(url, allow_redirects=True)
+        if resp.status_code != 200:
+            raise RuntimeError(
+                f"HTTP status code '{resp.status_code}' for GET against: {url}"
+            )
+
+        auth_session["redirect_url"] = resp.url
+
+        # if we were NOT redirected, then the endpoint is UNSECURED
+        if len(resp.history) == 0:
+            auth_session["is_secured"] = False
+            return auth_session
+        else:
+            auth_session["is_secured"] = True
+
+        ################
+        # Get Dex Login URL
+        ################
+        redirect_url_obj = urlsplit(auth_session["redirect_url"])
+
+        # if we are at `/auth?=xxxx` path, we need to select an auth type
+        if re.search(r"/auth$", redirect_url_obj.path):
+            #######
+            # TIP: choose the default auth type by including ONE of the following
+            #######
+
+            # OPTION 1: set "staticPasswords" as default auth type
+            redirect_url_obj = redirect_url_obj._replace(
+                path=re.sub(r"/auth$", "/auth/local", redirect_url_obj.path)
+            )
+            # OPTION 2: set "ldap" as default auth type
+            # redirect_url_obj = redirect_url_obj._replace(
+            #     path=re.sub(r"/auth$", "/auth/ldap", redirect_url_obj.path)
+            # )
+
+        # if we are at `/auth/xxxx/login` path, then no further action is needed (we can use it for login POST)
+        if re.search(r"/auth/.*/login$", redirect_url_obj.path):
+            auth_session["dex_login_url"] = redirect_url_obj.geturl()
+
+        # else, we need to be redirected to the actual login page
+        else:
+            # this GET should redirect us to the `/auth/xxxx/login` path
+            resp = s.get(redirect_url_obj.geturl(), allow_redirects=True)
+            if resp.status_code != 200:
+                raise RuntimeError(
+                    f"HTTP status code '{resp.status_code}' for GET against: {redirect_url_obj.geturl()}"
+                )
+
+            # set the login url
+            auth_session["dex_login_url"] = resp.url
+
+        ################
+        # Attempt Dex Login
+        ################
+        resp = s.post(
+            auth_session["dex_login_url"],
+            data={"login": username, "password": password},
+            allow_redirects=True,
+        )
+        if len(resp.history) == 0:
+            raise RuntimeError(
+                f"Login credentials were probably invalid - "
+                f"No redirect after POST to: {auth_session['dex_login_url']}"
+            )
+
+        # store the session cookies in a "key1=value1; key2=value2" string
+        auth_session["session_cookie"] = "; ".join(
+            [f"{c.name}={c.value}" for c in s.cookies]
+        )
+        auth_session["authservice_session"] = s.cookies.get("authservice_session")
+
+    return auth_session
+
+
+if __name__ == "__main__":
+    KUBEFLOW_ENDPOINT = "http://localhost:8080"     # Cluster IP and port
+    KUBEFLOW_USERNAME = "user@example.com"
+    KUBEFLOW_PASSWORD = "12341234"
+    MODEL_NAME = "sklearn-iris"
+    SERVICE_HOSTNAME = "sklearn-iris.kubeflow-user-example-com.example.com"
+    PREDICT_ENDPOINT = f"{KUBEFLOW_ENDPOINT}/v1/models/{MODEL_NAME}:predict"
+    iris_input = {"instances": [[6.8, 2.8, 4.8, 1.4], [6.0, 3.4, 4.5, 1.6]]}
+
+    _auth_session = get_istio_auth_session(
+        url=KUBEFLOW_ENDPOINT, username=KUBEFLOW_USERNAME, password=KUBEFLOW_PASSWORD
+    )
+
+    cookies = {"authservice_session": _auth_session['authservice_session']}
+    jar = requests.cookies.cookiejar_from_dict(cookies)
+
+    res = requests.post(
+        url=PREDICT_ENDPOINT,
+        headers={"Host": SERVICE_HOSTNAME, "Content-Type": "application/json"},
+        cookies=jar,
+        json=iris_input,
+        timeout=200,
+    )
+    print("Status Code: ", res.status_code)
+    print("Response: ", res.json())


Test patch: