Expose and Secure a Workload with a JWT Using SAP Cloud Identity Services ​
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 using the Client Credentials grant.
Prerequisites ​
- You have an SAP BTP, Kyma runtime instance with the Istio and API Gateway modules added. The Istio and API Gateway modules are added to your Kyma cluster by default.
- You have an SAP Cloud Identity Services tenant. See Initial Setup.
Context ​
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.
Configure a TLS Gateway for Your Custom Domain ​
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.
bashkubectl create ns test kubectl label namespace test istio-injection=enabled --overwriteExport 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.bashPARENT_DOMAIN="my-own-domain.example.com" SUBDOMAIN="tls.${PARENT_DOMAIN}" GATEWAY_DOMAIN="*.${SUBDOMAIN}" WORKLOAD_DOMAIN="httpbin.${SUBDOMAIN}"Placeholder Example domain name Description PARENT_DOMAIN my-own-domain.example.comThe domain name available in the public DNS zone. SUBDOMAIN tls.my-own-domain.example.comA subdomain created under the parent domain, specifically for the TLS 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.tls.my-own-domain.example.com,test.httpbin.tls.my-own-domain.example.com) without creating separate Gateway rules for each one.WORKLOAD_DOMAIN httpbin.tls.my-own-domain.example.comThe 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. See an example Secret for the AWS Route 53 DNS provider. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are base-64 encoded credentials.
bashapiVersion: v1 kind: Secret metadata: name: aws-credentials namespace: test type: Opaque data: AWS_ACCESS_KEY_ID: ... AWS_SECRET_ACCESS_KEY: ... # Optionally, specify the region #AWS_REGION: {YOUR_SECRET_ACCESS_KEY} # Optionally, specify the token #AWS_SESSION_TOKEN: ...To verify that the Secret is created, run:
bashkubectl get secret -n test {SECRET_NAME}Create a DNSProvider resource that references the Secret with your DNS provider's credentials.
See an example Secret for AWS Route 53 DNS provider:
yamlapiVersion: dns.gardener.cloud/v1alpha1 kind: DNSProvider metadata: name: aws namespace: test spec: type: aws-route53 secretRef: name: aws-credentials domains: include: - "${PARENT_DOMAIN}"To verify that the DNSProvider is created, run:
bashkubectl 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 ip field (for example, on GCP) or in the ingress Gateway's hostname field (for example, on AWS).bashLOAD_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}') fiCreate a DNSEntry resource.
bashcat <<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 created, run:
bashkubectl get DNSEntry -n test dns-entryCreate 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 secretName field. For more information, see Manage certificates with Gardener for public domain.
bashcat <<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 created, run:
bashkubectl get secret -n istio-system custom-tls-secretCreate a TLS Gateway.
bashcat <<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 created, run:
bashkubectl get gateway -n test custom-tls-gateway
Create and Configure OpenID Connect Application ​
You need an identity provider to issue JWTs. Creating an OpenID Connect application allows SAP Cloud Identity Services to act as your issuer and manage authentication for your workloads.
Sign in to the administration console for SAP Cloud Identity Services. See Access Admin Console.
Create an OpenID Connect Application.
- Go to Application Resources > Application.
- Choose Create, provide the application name, and select the OpenID Connect radio button. For more configuration options, see Create OpenID Connect Application.
- Choose +Create.
Configure OpenID Connect Application for the Client Credentials flow.
- In the Trust > Single Sign-On section of your created application, choose OpenID Connect Configuration.
- Provide the name.
- In the Grant types section, check Client Credentials. For more configuration options, see Configure OpenID Connect Application for Client Credentials Flow.
- Choose Save.
Configure a secret for API authentication.
- In the Application API section of your created application, choose Client Authentication.
- In the Secrets section, choose Add.
- Choose the OpenID API access and provide other configuration as needed. For more configuration options, see Configure Secrets for API Authentication.
- Choose Save. Your client ID and secret appear in a pop-up window. Save the secret, as you will not be able to retrieve it from the system later.
Get a JWT ​
Export the following values as environment variables:
The name of your Cloud Identity Services instance in the URL of the administration console. For example, if your URL is
https://abc123.trial-accounts.ondemand.com/admin/, the name of the instance isabc123.trial-accounts.ondemand.com.bashCLOUD_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.
bashexport ENCODED_CREDENTIALS=$(echo -n "$CLIENT_ID:$CLIENT_SECRET" | base64)Get token_endpoint, jwks_uri, and issuer from your OpenID application, and save these values as environment variables:
bashTOKEN_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: $ISSUERGet the JWT access token:
bashACCESS_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
Configure JWT Authentication in Kyma ​
To configure JWT authentication, expose your workload using APIRule custom resource (CR). Configure jwt as the access strategy:
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
namespace: test
port: 8000To test the connection, first, do not provide the JWT.
bashcurl -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.
bashcurl -ik -X GET https://${WORKLOAD_DOMAIN}/headers --header "Authorization:Bearer $ACCESS_TOKEN"You get the
200 OKresponse code.