This procedure explains how to expose a workload on a custom domain and secure it with JSON Web Tokens (JWTs) issued by SAP Cloud Identity Services - Identity Authentication using the Client Credentials grant.
- You have the Istio and API Gateway modules in your cluster. See Adding and Deleting a Kyma Module.
- You have an SAP Cloud Identity Services tenant. See Initial Setup.
Use this procedure to secure your workload with a short-lived JWT. To get the JWT, you must first register an OpenID Connect (OIDC) application in SAP Cloud Identity Services and enable the Client Credentials grant. This generates a client ID (public identifier) and a client secret (confidential credential). A calling system sends these credentials to the OIDC token endpoint over TLS, receiving a signed JWT.
When the client calls your exposed API, it includes the token in the Authorization header using the Bearer scheme. The API Gateway module validates the token based on the configuration you include in the APIRule custom resource (CR). If the validation fails, the Gateway returns `HTTP/2 403 RBAC: access denied` without forwarding the request to the backend Service.
If the validation is successful, the request proceeds to the Service behind the Gateway. At that point, you can implement optional, deeper authorization (examining scopes, audience, or custom claims) inside your application code.
Follow these steps:
- Configure a TLS Gateway for Your Custom Domain.
- Create and Configure an OpenID Connect Application.
- Get a JWT.
- Configure JWT Authentication in Kyma.
To configure the flow in Kyma, you must first provide credentials for a supported DNS provider so Gardener can create and verify the necessary DNS records for your custom wildcard domain. After this, Let’s Encrypt issues a trusted TLS certificate. The issued certificate is stored in a Kubernetes Secret referenced by an Istio Gateway, which terminates HTTPS at the cluster edge, so all downstream traffic enters encrypted.
-
Create a namespace with enabled Istio sidecar proxy injection.
kubectl create ns test kubectl label namespace test istio-injection=enabled --overwrite -
Export the following domain names as environment variables. Replace
my-own-domain.example.comwith the name of your domain. You can adjust these values as needed.PARENT_DOMAIN="my-own-domain.example.com" SUBDOMAIN="tls.${PARENT_DOMAIN}" GATEWAY_DOMAIN="*.${SUBDOMAIN}" WORKLOAD_DOMAIN="httpbin.${SUBDOMAIN}"Placeholder
Example domain name
Description
PARENT_DOMAINmy-own-domain.example.comThe domain name available in your public DNS zone.
SUBDOMAINtls.my-own-domain.example.comA subdomain created under the parent domain, specifically for the mTLS Gateway.
GATEWAY_DOMAIN*.tls.my-own-domain.example.comA wildcard domain covering all possible subdomains under the TLS subdomain. When configuring the Gateway, this allows you to expose workloads on multiple hosts (for example,
httpbin.mtls.my-own-domain.example.com,test.httpbin.mtls.my-own-domain.example.com) without creating separate Gateway rules for each one.WORKLOAD_DOMAINhttpbin.tls.my-own-domain.example.coThe specific domain assigned to your workload.
-
Create a Secret containing credentials for your DNS cloud service provider.
The information you provide to the data field differs depending on the DNS provider you're using. The DNS provider must be supported by Gardener. To learn how to configure the Secret for a specific provider, follow External DNS Management Guidelines and the External DNS Management examples.
See an example Secret for the AWS Route 53 DNS provider.
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYare base-64 encoded credentials.apiVersion: v1 kind: Secret metadata: annotations: dns.gardener.cloud/class: garden name: aws-credentials namespace: test type: Opaque data: AWS_ACCESS_KEY_ID: <your AWS access key> AWS_SECRET_ACCESS_KEY: <your AWS secret access key> # Optionally, specify the region #AWS_REGION: <your AWS region> # Optionally, specify the token #AWS_SESSION_TOKEN: <your AWS session token> -
Create a DNSProvider resource that references the Secret with your DNS provider's credentials.
See an example Secret for the AWS Route 53 DNS provider:
apiVersion: dns.gardener.cloud/v1alpha1 kind: DNSProvider metadata: name: aws namespace: test spec: type: aws-route53 secretRef: name: aws-credentials domains: include: - "${PARENT_DOMAIN}"For DNSProvider configuration for other providers, see the External DNS Management examples.
To verify that the DNSProvider is ready, run:
kubectl get DNSProvider -n test {DNSPROVIDER_NAME} -
Get the external access point of the
istio-ingressgatewayservice.The external access point is either stored in the ingress Gateway's
ipfield (for example, on GCP) or in the ingress Gateway'shostnamefield (for example, on AWS).LOAD_BALANCER_ADDRESS=$(kubectl get services --namespace istio-system istio-ingressgateway --output jsonpath='{.status.loadBalancer.ingress[0].ip}') if [[ -z $LOAD_BALANCER_ADDRESS ]]; then LOAD_BALANCER_ADDRESS=$(kubectl get services --namespace istio-system istio-ingressgateway --output jsonpath='{.status.loadBalancer.ingress[0].hostname}') fi echo "The load balancer address is ${LOAD_BALANCER_ADDRESS}" -
Create a DNSEntry resource.
cat <<EOF | kubectl apply -f - apiVersion: dns.gardener.cloud/v1alpha1 kind: DNSEntry metadata: name: dns-entry namespace: test annotations: dns.gardener.cloud/class: garden spec: dnsName: "${GATEWAY_DOMAIN}" ttl: 600 targets: - "${LOAD_BALANCER_ADDRESS}" EOFTo verify that the DNSEntry is ready, run:
kubectl get DNSEntry -n test dns-entry -
Create the server's certificate.
You use a Certificate CR to request and manage Let's Encrypt certificates from your Kyma cluster. When you create a Certificate CR, one of Gardener's operators detects it and creates an ACME request to Let's Encrypt requesting certificate for the specified domain names. The issued certificate is stored in an automatically created Kubernetes Secret, which name you specify in the Certificate's
secretNamefield. For more information, see Manage certificates with Gardener for public domain.cat <<EOF | kubectl apply -f - apiVersion: cert.gardener.cloud/v1alpha1 kind: Certificate metadata: name: domain-certificate namespace: "istio-system" spec: secretName: custom-tls-secret commonName: "${GATEWAY_DOMAIN}" issuerRef: name: garden EOFTo verify that the Secret with Gateway certificates is ready, run:
kubectl get secret -n istio-system custom-tls-secret -
Create a TLS Gateway.
cat <<EOF | kubectl apply -f - apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: custom-tls-gateway namespace: test spec: selector: app: istio-ingressgateway istio: ingressgateway servers: - port: number: 443 name: tls protocol: HTTPS tls: mode: SIMPLE credentialName: custom-tls-secret hosts: - "${GATEWAY_DOMAIN}" EOFTo verify that the TLS Gateway is ready, run:
kubectl get gateway -n test custom-tls-gateway
You need an identity provider to issue JWTs. Create an OpenID Connect application to use SAP Cloud Identity Services as your JWT issuer that manages authentication for your workloads.
-
Sign in to the administration console for SAP Cloud Identity Services. See Access Admin Console.
-
Configure OpenID Connect Application for Client Credentials Flow.
-
Export the following values as environment variables:
CLOUD_IDENTITY_SERVICES_INSTANCE="my-example-tenant.accounts.ondemand.com" CLIENT_ID="{YOUR-CLIENT-ID}" CLIENT_SECRET="{YOUR-CLIENT-SECRET}" -
Export base 64 encoded client ID and client secret.
export ENCODED_CREDENTIALS=$(echo -n "$CLIENT_ID:$CLIENT_SECRET" | base64) -
Get
token_endpoint,jwks_uri, andissuerfrom your OpenID application, and save these values as environment variables:TOKEN_ENDPOINT=$(curl -s https://$CLOUD_IDENTITY_SERVICES_INSTANCE/.well-known/openid-configuration | jq -r '.token_endpoint') echo token_endpoint: $TOKEN_ENDPOINT JWKS_URI=$(curl -s https://$CLOUD_IDENTITY_SERVICES_INSTANCE/.well-known/openid-configuration | jq -r '.jwks_uri') echo jwks_uri: $JWKS_URI ISSUER=$(curl -s https://$CLOUD_IDENTITY_SERVICES_INSTANCE/.well-known/openid-configuration | jq -r '.issuer') echo issuer: $ISSUER -
Get the JWT access token:
ACCESS_TOKEN=$(curl -s -X POST "$TOKEN_ENDPOINT" \ -d "grant_type=client_credentials" \ -d "client_id=$CLIENT_ID" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Authorization: Basic $ENCODED_CREDENTIALS" | jq -r '.access_token') echo $ACCESS_TOKEN
To configure JWT authentication, expose your workload using APIRule custom resource (CR) and configure jwt as the access strategy. You can either use kubectl or Kyma dashboard.
-
Use Kyma dashboard.
-
Go to Discovery and Network → API Rules and choose Create.
-
Provide all the required configuration details.
-
For each path you want to secure with a JWT, add a rule with the following configuration.
Option
Description
Path
Specify the request path.
Methods
Check the allowed access methods.
Access Strategy
JWT
JWT → Authentications
In the JWT section, add an authentication with your issuer and JSON Web Key Set URIs.
See the following example of a sample HTTPBin Deployment exposed by an APIRule with JWT authentication:
apiVersion: v1 kind: ServiceAccount metadata: name: httpbin namespace: test --- apiVersion: v1 kind: Service metadata: name: httpbin namespace: test labels: app: httpbin service: httpbin spec: ports: - name: http port: 8000 targetPort: 80 selector: app: httpbin --- apiVersion: apps/v1 kind: Deployment metadata: name: httpbin namespace: test spec: replicas: 1 selector: matchLabels: app: httpbin version: v1 template: metadata: labels: app: httpbin version: v1 spec: serviceAccountName: httpbin containers: - image: docker.io/kennethreitz/httpbin imagePullPolicy: IfNotPresent name: httpbin ports: - containerPort: 80apiVersion: gateway.kyma-project.io/v2 kind: APIRule metadata: name: httpbin-tls namespace: test spec: gateway: test/custom-tls-gateway hosts: - "${WORKLOAD_DOMAIN}" rules: - jwt: authentications: - issuer: ${ISSUER} jwksUri: ${JWKS_URI} methods: - GET path: /* service: name: httpbin port: 8000
-
-
Use kubectl.
To expose and secure your Service, create the APIRule CR. For each path you want to secure with a JWT, add a rule with the
jwtfield and specify theissuerandjwksUri.rules: ... - jwt: authentications: - issuer: ${ISSUER} jwksUri: ${JWKS_URI} ...See the following example of a sample HTTPBin Deployment exposed by an APIRule with JWT authentication:
apiVersion: v1 kind: ServiceAccount metadata: name: httpbin namespace: test --- apiVersion: v1 kind: Service metadata: name: httpbin namespace: test labels: app: httpbin service: httpbin spec: ports: - name: http port: 8000 targetPort: 80 selector: app: httpbin --- apiVersion: apps/v1 kind: Deployment metadata: name: httpbin namespace: test spec: replicas: 1 selector: matchLabels: app: httpbin version: v1 template: metadata: labels: app: httpbin version: v1 spec: serviceAccountName: httpbin containers: - image: docker.io/kennethreitz/httpbin imagePullPolicy: IfNotPresent name: httpbin ports: - containerPort: 80apiVersion: gateway.kyma-project.io/v2 kind: APIRule metadata: name: httpbin-tls namespace: test spec: gateway: test/custom-tls-gateway hosts: - "${WORKLOAD_DOMAIN}" rules: - jwt: authentications: - issuer: ${ISSUER} jwksUri: ${JWKS_URI} methods: - GET path: /* service: name: httpbin port: 8000
You have exposed the workload with APIRule custom resource.
-
First, test the connection without the JWT.
curl -ik -X GET https://${WORKLOAD_DOMAIN}/headersYou get the error
HTTP/2 403 RBAC: access denied. -
Now, access the secured workload using the correct JWT.
curl -ik -X GET https://${WORKLOAD_DOMAIN}/headers --header "Authorization:Bearer $ACCESS_TOKEN"You get the
200 OKresponse code.