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:
- Preparing Your Environment For SAP BTP Development on Kyma Runtime
- Deploying Your First CAP Application on SAP BTP Kyma Runtime
- Creating BTP Service Instances on SAP BTP Kyma Runtime with the Service Operator
- Binding BTP Service Instances to your CAP Application with Kyma Runtime
- The Chart Templates Behind Your CAP Kyma Deployment
- Creating roles for your CAP Application on SAP BTP Kyma Runtime
- 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
Gatewayopens the door: "listen for HTTPS traffic on host*.my-domain.kyma.ondemand.com". - A
VirtualServicedecides where the traffic goes: "for this host and path, forward to serviceXinside 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
jwtaccess 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.jsonand 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
Bearertoken. Istio then validates that token at the edge (via the APIRulejwtaccess 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.
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:
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 kymaAdd the roles and authorization
We protect the CatalogService with the same reader/admin model from the roles post. In srv/cat-service.cds:
@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:
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:
cds add approuterThis 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:
{
"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-apidynamically from the bound Destination service instead. In practice the@sap/approutervalidates every entry in itsdestinationsenvironment variable and requires aurlfor each one — it will not start with a name-only, "look it up at runtime" entry (it fails withMissing required property: url). The Destination service shines for calling external / remote systems; for routing to a sibling workload in the same cluster, the chart-generatedbackendDestinationsis 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):
# /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:
# /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.0A few things worth highlighting about this configuration:
- The
exposeblock is where the Istio Gateway is configured. Settingexpose.enabled: trueon theapprouterandgateway: kyma-system/kyma-gatewaytells theweb-applicationchart to create theAPIRule/VirtualServicethat attaches this workload to the shared Kyma Istio Ingress Gateway. - The CAP service (
srv) hasexpose.enabled: false, so it is not reachable from the internet — only from inside the namespace, andnetworkSecurity.allowNamespaceInternal: truelets the AppRouter reach it. Because it is internal, the chart routes to it overhttpat its*.svc.cluster.localaddress. - The
approuterroute isnoAuth: trueat 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-apiis what makes the router boot and route. The chart renders it into the AppRouter'sdestinationsenvironment variable as.svc.cluster.local:8080","forwardAuthToken":true}. WithforwardAuthToken: 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:
kubectl create namespace my-namespace
kubectl label namespace my-namespace istio-injection=enabled
cds up -2 k8s --namespace my-namespaceOnce the deployment completes, you should see both workloads running (plus the completed HANA deployer job):
$ 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 3mThe 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:
https://my-cap-app-approuter-my-namespace.<your-kyma-subdomain>.kyma.ondemand.com/odata/v4/catalog/BooksIf 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.
Related links
- Creating roles for your CAP Application on SAP BTP Kyma Runtime
- Binding BTP Service Instances to your CAP Application with Kyma Runtime
- The Chart Templates Behind Your CAP Kyma Deployment
- Understanding SAP Application Router
- Kubernetes for Developers - Services
- Istio documentation — Gateways
- SAP BTP - Istio Gateway
