Using Istio Gateway as Entry Point in SAP BTP Kyma Runtime

When you deploy a CAP application to SAP BTP Kyma Runtime, sooner or later you have to answer a deceptively simple question: what is the entry point for my application? On Cloud Foundry the answer was almost always the Application Router (AppRouter). On Kyma, you also have Istio and its Gateway sitting at the edge of the cluster. This leads many developers to ask: can I replace the AppRouter with an Istio Gateway?

In this post we will explain what Istio is, what the Gateway does, compare it with the AppRouter, and then propose an architecture that uses both — the Istio Ingress Gateway as the network entry point, and the AppRouter for login and sessions — with the CAP application acting as a protected API.

Series — SAP BTP Kyma Runtime:

  1. Preparing Your Environment For SAP BTP Development on Kyma Runtime
  2. Deploying Your First CAP Application on SAP BTP Kyma Runtime
  3. Creating BTP Service Instances on SAP BTP Kyma Runtime with the Service Operator
  4. Binding BTP Service Instances to your CAP Application with Kyma Runtime
  5. The Chart Templates Behind Your CAP Kyma Deployment
  6. Creating roles for your CAP Application on SAP BTP Kyma Runtime
  7. Using Istio Gateway as Entry Point in SAP BTP Kyma Runtime (this post)

What is Istio?

Istio is a service mesh — an infrastructure layer that sits between your workloads and transparently manages the network traffic that flows between them. In Kyma, Istio comes preinstalled and is a core part of the runtime.

The way it works is by injecting a lightweight Envoy proxy as a sidecar container into every Pod (this is why, in the previous posts, we always labelled our namespace with istio-injection=enabled). All inbound and outbound traffic for the Pod goes through this proxy. Because every request is intercepted, Istio can provide, without changing your application code:

  • Traffic management — routing, load balancing, retries, timeouts, circuit breaking and canary deployments.
  • Security — mutual TLS (mTLS) between services, and JWT validation at the edge.
  • Observability — metrics, distributed tracing and access logs for every call.

The important word above is validation. Istio can check whether a request carries a valid token, but it cannot acquire one on behalf of a user. Keep this in mind — it is the crux of the whole AppRouter discussion.

What is the Istio Gateway?

Within Istio, the Gateway is the resource that configures the Envoy proxy running at the edge of the mesh — the Istio Ingress Gateway. A Gateway describes, at the network level (L4–L6), how traffic enters the mesh: which hosts and ports to listen on, and which TLS configuration to use.

By itself, a Gateway does not route anything. Routing is the job of a companion resource:

  • A Gateway opens the door: "listen for HTTPS traffic on host *.my-domain.kyma.ondemand.com".
  • A VirtualService decides where the traffic goes: "for this host and path, forward to service X inside the mesh".

In Kyma you rarely write these two resources by hand. Instead, you use the higher-level APIRule custom resource, which generates the VirtualService (and optionally the authentication policies) for you. If you have read The Chart Templates Behind Your CAP Kyma Deployment, this is exactly what the expose block of the web-application subchart produces.

So the real edge combination is Gateway + VirtualService (APIRule), and together they give you:

  • TLS termination at the edge
  • Host and path based routing
  • Load balancing across your Pod replicas (native to the mesh)
  • Traffic policies: retries, timeouts, circuit breaking, canary
  • JWT validation through the jwt access strategy

What the Application Router does

The AppRouter (@sap/approuter) is not infrastructure — it is an L7 application, a Node.js reverse proxy purpose-built for the SAP BTP programming model. We covered it in depth in Understanding SAP Application Router and used it in Creating roles for your CAP Application. Its responsibilities go far beyond routing:

  • OAuth2 login flow orchestration — it redirects the user to XSUAA/IAS, handles the callback and exchanges the authorization code for tokens. This is the piece Istio fundamentally does not do.
  • Session management — it keeps the user logged in via cookies, so the browser does not re-authenticate on every request.
  • Token forwarding and refresh — it injects the JWT into backend calls and refreshes it when needed.
  • Serving static UI content (Fiori/UI5 apps) and CSRF protection.
  • Destination-based routing via xs-app.json and the Destination service.
  • Central logout.

In short, the AppRouter is the OAuth2 client (the relying party). That role has no equivalent in Istio.

Istio Gateway vs. Application Router

Here is a side-by-side comparison of the two, so the boundary between them is clear:

Istio Gateway (+ VirtualService/APIRule) Application Router
Layer L4–L7 infrastructure (Envoy) L7 application (Node.js)
Routing ✅ Host/path routing ✅ Route via xs-app.json
Load balancing ✅ Native to the mesh ⚠️ Delegates to platform
TLS termination ✅ At the edge ⚠️ Usually behind the gateway
JWT validation ✅ RequestAuthentication + AuthorizationPolicy
OAuth2 login flow (redirect, code exchange) ❌ Cannot do this ✅ Its main job
Session / cookie management
Token acquisition & refresh
Serving static UI (Fiori/UI5)
Destination service integration
CSRF protection, central logout
Traffic management (retries, canary, circuit breaking) ✅ Rich
Runs as Shared cluster infrastructure A Pod you deploy and pay for

So, can I use the Istio Gateway instead of the AppRouter?

It depends on your clients and your authentication model:

  • Yes, you can drop the AppRouter if your clients are machine-to-machine or API consumers (other services, native apps, an external SPA) that already obtain their own JWT from XSUAA/IAS and send it as a Bearer token. Istio then validates that token at the edge (via the APIRule jwt access strategy) and forwards it. This is a clean, lightweight setup with no proxy Pod to run.
  • No, you still need the AppRouter (or an equivalent) if you serve a browser-based UI that requires interactive user login. Someone must perform the OAuth2 authorization-code flow — the redirect, the session cookie, the token exchange — and Istio does none of that.

The proposed architecture: Istio Gateway and AppRouter

Because the two components solve different problems, the most robust setup for a user-facing CAP application on Kyma uses both, layered. Istio owns the network edge; the AppRouter owns the user-authentication and session concern; and the CAP application is a protected API that only trusts a validated XSUAA token.

Copy
                    Internet
                       │
                       ▼
        ┌──────────────────────────────┐
        │   Istio Ingress Gateway       │  ← TLS termination, edge routing,
        │   (Gateway + APIRule)         │    load balancing
        └──────────────┬────────────────┘
                       │ (only the AppRouter is exposed)
                       ▼
              ┌──────────────────┐
              │    AppRouter      │
              │ (web-application) │
              │                   │
              │ • OAuth2 login    │──▶ XSUAA (login / tokens)
              │ • Sessions        │
              │ • Token forward   │
              └────────┬──────────┘
                       │ routes "srv-api" (in-cluster URL)
                       ▼
              ┌──────────────────┐
              │   CAP srv (API)   │  ← internal only (ClusterIP),
              │ (web-application) │    not exposed to the internet
              │                   │
              │ • Business logic  │
              │ • Validates JWT   │
              └──────────────────┘

The user always enters through the Istio Ingress Gateway, which only exposes the AppRouter. The router runs the OAuth2 flow against XSUAA, establishes a session, and forwards the authenticated request to the CAP application — reached by its internal in-cluster service URL — with the token in the Authorization header. The CAP service then validates the token and enforces the roles — exactly the roles we created in the previous post.

Building the CAP application

As in the previous posts of the series, let's start from a fresh CAP application bound to HANA, XSUAA and Destination. If you have followed along, this will feel familiar:

Copy
cds init my-cap-app
cd my-cap-app
cds add nodejs
npm install
cds add tiny-sample
cds add hana xsuaa destination
cds add kyma

Add the roles and authorization

We protect the CatalogService with the same reader/admin model from the roles post. In srv/cat-service.cds:

Copy
@odata
annotate CatalogService with @(restrict: [
    { grant: ['READ'], to: 'reader' },
    { grant: ['READ', 'WRITE'], to: 'admin' }
]);
service CatalogService {
  entity Books {
    key ID     : Integer;
        title  : String;
        author : String;
  }
}

And declare the matching scopes, role templates and role collections in the XSUAA parameters, inside chart/values.yaml:

Copy
xsuaa:
  serviceOfferingName: xsuaa
  servicePlanName: application
  parameters:
    tenant-mode: dedicated
    oauth2-configuration:
      redirect-uris:
        - https://*.{{ tpl .Values.global.domain . }}/**
    xsappname: my-cap-app-{{ .Release.Namespace }}
    scopes:
      - name: "$XSAPPNAME.reader"
        description: Reader scope for my-cap-app in kyma
      - name: "$XSAPPNAME.admin"
        description: Admin scope for my-cap-app in kyma
    role-templates:
      - name: reader-role
        description: Reader role for my-cap-app in kyma
        scope-references:
          - "$XSAPPNAME.reader"
      - name: admin-role
        description: Admin role for my-cap-app in kyma
        scope-references:
          - "$XSAPPNAME.admin"
    role-collections:
      - name: reader-role-collection
        description: Reader role collection for my-cap-app in kyma
        role-template-references:
          - "$XSAPPNAME.reader-role"
      - name: admin-role-collection
        description: Admin role collection for my-cap-app in kyma
        role-template-references:
          - "$XSAPPNAME.admin-role"

Add the Application Router

Now add the AppRouter, which becomes the login entry point behind the Istio Gateway:

Copy
cds add approuter

This creates the app/router folder. Edit its xs-app.json so all routes require XSUAA authentication and are proxied to the CAP service through a destination named srv-api:

Copy
{
  "authenticationMethod": "route",
  "routes": [
    {
      "source": "^/(.*)$",
      "target": "$1",
      "destination": "srv-api",
      "csrfProtection": true,
      "authenticationType": "xsuaa"
    }
  ]
}

The destination field here does not name a Kubernetes service directly — it names a backend destination that the AppRouter knows about through its destinations environment variable. The cds-dk Helm chart generates that variable for us from a backendDestinations block in values.yaml: we declare a destination called srv-api pointing at the srv service, and the chart resolves its in-cluster URL at deploy time. This keeps xs-app.json free of any hard-coded host — the mapping from the logical name srv-api to the real address lives in the chart.

A note on the SAP BTP Destination service. It is tempting to try to make the AppRouter resolve srv-api dynamically from the bound Destination service instead. In practice the @sap/approuter validates every entry in its destinations environment variable and requires a url for each one — it will not start with a name-only, "look it up at runtime" entry (it fails with Missing required property: url). The Destination service shines for calling external / remote systems; for routing to a sibling workload in the same cluster, the chart-generated backendDestinations is the correct and simplest mechanism, and it is what we use here.

Configuring the entry point with YAML

With both workloads in place, the chart wires everything together. First, the Chart.yaml gains a second web-application dependency for the AppRouter (the CAP service is srv, the router is approuter):

Copy
# /chart/Chart.yaml
apiVersion: v2
name: my-cap-app
description: A simple CAP project.
type: application
version: 1.0.0
appVersion: 1.0.0
annotations:
  app.kubernetes.io/managed-by: cds-dk/helm
dependencies:
  - name: web-application
    alias: srv
    version: ">0.0.0"
  - name: web-application       # The Application Router
    alias: approuter
    version: ">0.0.0"
  - name: service-instance
    alias: hana
    version: ">0.0.0"
  - name: content-deployment
    alias: hana-deployer
    version: ">0.0.0"
  - name: service-instance
    alias: xsuaa
    version: ">0.0.0"
  - name: service-instance
    alias: destination
    version: ">0.0.0"

Then the values.yaml configures who is exposed to the Istio Gateway and who is internal. The AppRouter is the one exposed through the Istio Ingress Gateway (expose.enabled: true, pointing at the Kyma gateway), while the CAP service is kept internal (expose.enabled: false) so it can only be reached through the router, and only with a valid token.

The routing between the two is declared with a backendDestinations block. It defines the srv-api destination that xs-app.json refers to, and the chart turns it into the AppRouter's destinations environment variable at deploy time — computing the in-cluster URL of the srv workload for us:

Copy
# /chart/values.yaml
global:
  domain: <your-kyma-subdomain>.kyma.ondemand.com
  imagePullSecret:
    name: docker-registry
  image:
    registry: <your-container-registry-url>
    tag: latest

srv: # The CAP application — internal API, reached only through the AppRouter
  bindings:
    auth:
      serviceInstanceName: xsuaa
    db:
      serviceInstanceName: hana
    destination:
      serviceInstanceName: destination
  image:
    repository: my-cap-app-srv
  resources:
    limits:
      ephemeral-storage: 1G
      memory: 500M
    requests:
      ephemeral-storage: 1G
      cpu: 500m
      memory: 500M
  health:
    liveness:
      path: /health
    readiness:
      path: /health
  expose:
    enabled: false            # Internal only — reached through the AppRouter, not the internet
  networkSecurity:
    allowNamespaceInternal: true

approuter: # The entry point exposed through the Istio Ingress Gateway
  bindings:
    auth:
      serviceInstanceName: xsuaa          # The router needs XSUAA to run the login flow
  image:
    repository: my-cap-app-approuter
  resources:
    limits:
      ephemeral-storage: 1G
      memory: 500M
    requests:
      ephemeral-storage: 1G
      cpu: 500m
      memory: 500M
  health:
    liveness:
      path: /
    readiness:
      path: /
  expose:
    enabled: true                       # Exposed via the Istio Gateway
    gateway: kyma-system/kyma-gateway   # The shared Kyma Istio Ingress Gateway
    rules:
      - path: /*
        methods: [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS]
        noAuth: true                    # The router itself handles the OAuth2 login
  envFrom:
    - configMapRef:
        name: "{{ .Release.Name }}-approuter-configmap"

# Generates the AppRouter's `destinations` env var. `service: srv` makes the chart
# compute the in-cluster URL of the srv workload and forward the auth token to it.
backendDestinations:
  srv-api:
    service: srv
    forwardAuthToken: true

xsuaa:
  serviceOfferingName: xsuaa
  servicePlanName: application
  parameters:
    tenant-mode: dedicated
    oauth2-configuration:
      redirect-uris:
        - https://*.{{ tpl .Values.global.domain . }}/**
    xsappname: my-cap-app-{{ .Release.Namespace }}
    # ...scopes, role-templates and role-collections from the section above

hana-deployer:
  image:
    repository: my-cap-app-hana-deployer
  bindings:
    hana:
      serviceInstanceName: hana
  resources:
    limits:
      cpu: 2000m
      memory: 1G
    requests:
      cpu: 1000m
      memory: 1G

hana:
  serviceOfferingName: hana
  servicePlanName: hdi-shared

destination:
  serviceOfferingName: destination
  servicePlanName: lite
  parameters:
    version: 1.0.0

A few things worth highlighting about this configuration:

  • The expose block is where the Istio Gateway is configured. Setting expose.enabled: true on the approuter and gateway: kyma-system/kyma-gateway tells the web-application chart to create the APIRule/VirtualService that attaches this workload to the shared Kyma Istio Ingress Gateway.
  • The CAP service (srv) has expose.enabled: false, so it is not reachable from the internet — only from inside the namespace, and networkSecurity.allowNamespaceInternal: true lets the AppRouter reach it. Because it is internal, the chart routes to it over http at its *.svc.cluster.local address.
  • The approuter route is noAuth: true at the Istio level on purpose: we do not want Istio to reject the initial browser request, because the AppRouter is precisely the component that will start the login flow. Istio here is doing edge routing and TLS, and the AppRouter is doing authentication.
  • backendDestinations.srv-api is what makes the router boot and route. The chart renders it into the AppRouter's destinations environment variable as .svc.cluster.local:8080","forwardAuthToken":true}. With forwardAuthToken: true, the user's XSUAA token is passed on to the CAP service, which then validates it and enforces the roles.

Deploy

Create and label the namespace so Istio injects its sidecars, then deploy:

Copy
kubectl create namespace my-namespace
kubectl label namespace my-namespace istio-injection=enabled
cds up -2 k8s --namespace my-namespace

Once the deployment completes, you should see both workloads running (plus the completed HANA deployer job):

Copy
$ kubectl get pods -n my-namespace
NAME                                    READY   STATUS      RESTARTS   AGE
my-cap-app-approuter-7477994d5c-4h4v8   2/2     Running     0          3m
my-cap-app-hana-deployer-0001-nrwl7     0/1     Completed   0          3m
my-cap-app-srv-55f8f65c47-4k4j2         2/2     Running     0          3m

The 2/2 on the running pods is the workload container plus its Istio sidecar. The entry point is the AppRouter URL exposed through the Istio Gateway. Accessing it triggers the XSUAA login, and after authenticating you are routed to the CAP catalog service — protected by the roles we defined:

Copy
https://my-cap-app-approuter-my-namespace.<your-kyma-subdomain>.kyma.ondemand.com/odata/v4/catalog/Books

If your user does not have the reader-role-collection assigned, you will get a 403 Forbidden; after assigning it in the BTP cockpit (see Creating roles for your CAP Application), you will see the list of books.

Conclusion

Istio and the AppRouter are complementary, not competing. The Istio Gateway is infrastructure — it terminates TLS, routes traffic and load-balances at the edge, and can validate JWTs. The AppRouter is an application — it acquires tokens by driving the OAuth2 login, manages sessions and serves the UI. For a pure API consumed by clients that bring their own token, Istio alone is enough. For a user-facing application that needs an interactive login, put the Istio Ingress Gateway at the front and the AppRouter right behind it, with the CAP application as a protected internal API.