Skip to content

Latest commit

 

History

History
642 lines (472 loc) · 18.6 KB

File metadata and controls

642 lines (472 loc) · 18.6 KB

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 - Identity Authentication using the Client Credentials grant.

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:

  1. Configure a TLS Gateway for Your Custom Domain.
  2. Create and Configure an OpenID Connect Application.
  3. Get a JWT.
  4. 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.

Procedure

  1. Create a namespace with enabled Istio sidecar proxy injection.

    kubectl create ns test
    kubectl label namespace test istio-injection=enabled --overwrite
    
  2. Export the following domain names as environment variables. Replace my-own-domain.example.com with 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_DOMAIN

    my-own-domain.example.com

    The domain name available in your public DNS zone.

    SUBDOMAIN

    tls.my-own-domain.example.com

    A subdomain created under the parent domain, specifically for the mTLS Gateway.

    GATEWAY_DOMAIN

    *.tls.my-own-domain.example.com

    A 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_DOMAIN

    httpbin.tls.my-own-domain.example.co

    The specific domain assigned to your workload.

  3. 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_ID and AWS_SECRET_ACCESS_KEY are 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>
    
  4. 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:

    Sample Code:

    kubectl get DNSProvider -n test {DNSPROVIDER_NAME}
    
  5. Get the external access point of the istio-ingressgateway service.

    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).

    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}"
    
  6. 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}"
    EOF
    

    To verify that the DNSEntry is ready, run:

    Sample Code:

    kubectl get DNSEntry -n test dns-entry
    
  7. 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 secretName field. 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
    EOF
    

    To verify that the Secret with Gateway certificates is ready, run:

    Sample Code:

    kubectl get secret -n istio-system custom-tls-secret
    
  8. 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}"
    EOF
    

    To verify that the TLS Gateway is ready, run:

    Sample Code:

    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.

Procedure

  1. Sign in to the administration console for SAP Cloud Identity Services. See Access Admin Console.

  2. Create OpenID Connect Application.

  3. Configure OpenID Connect Application for Client Credentials Flow.

  4. Configure Secrets for API Authentication.

Procedure

  1. 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}"
    
  2. Export base 64 encoded client ID and client secret.

    export ENCODED_CREDENTIALS=$(echo -n "$CLIENT_ID:$CLIENT_SECRET" | base64)
    
  3. Get token_endpoint, jwks_uri, and issuer from 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
    
  4. 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.

Procedure

  • Use Kyma dashboard.

    1. Go to Discovery and Network → API Rules and choose Create.

    2. Provide all the required configuration details.

    3. 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: 80
      
      apiVersion: 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 jwt field and specify the issuer and jwksUri.

    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: 80
    
    apiVersion: 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.

  1. First, test the connection without the JWT.

    curl -ik -X GET https://${WORKLOAD_DOMAIN}/headers
    

    You get the error HTTP/2 403 RBAC: access denied.

  2. 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 OK response code.