Cloud Platform as a Service

Cloud Platform as a Service

Join us to learn more from a community of collaborative experts and IBM Cloud product users to share advice and best practices with peers and stay up to date regarding product enhancements, regional user group meetings, webinars, how-to blogs, and other helpful materials.

 View Only

Accessing IBM Cloud services from Code Engine using a trusted profile – Part 1: accessing a service that supports IAM authentication

By Sascha Schwarze posted 27 days ago

  
Written by

Sascha Schwarze (schwarzs@de.ibm.com)
Senior Software Engineer @ IBM

Enrico Regge (reggeenr@de.ibm.com)
Software Architect @ IBM

Introduction

IBM Cloud Code Engine is a fully managed, serverless platform that runs your containerized workloads, including web apps, REST APIs, microservices, event-driven functions or batch jobs. In most of the cases, the workload you run needs access to other services, for example a Cloud Object Storage bucket, or a database. To authenticate and authorize at those destination services, using API keys is a common solution. Those API keys may have been created through different means, for example as part of the creation of a service binding, or by manually creating a service ID with an API, or by using a user’s personal API key. API keys impose the risk that in case they are leaked or stolen, they can be used by others to gain access to your resources.

By using trusted profiles instead for the authentication and authorization, we eliminate that risk.

How do trusted profiles work

Let’s have a look at how trusted profiles work conceptually.

Trusted profiles grant IBM Cloud entities access to resources in your IBM Cloud account. An IBM Cloud entity here can be a so-called compute resource. So far, you were able to specify compute resources to be a virtual service running in your virtual private cloud, or a service account in your IBM Cloud Kubernetes or OpenShift cluster. New is that you can specify your components running in Code Engine as a compute resource in a trusted profile. The access you define through reusable access groups, or access policies.

When your workload runs, IBM Cloud services make a so-called compute resource token available through some means. This token identifies your workload. It can be used by your code together with the name or identifier of the trusted profile to create an access token, similar as if you use an API key to create the same. With the access token, your workload can call the destination service.

But wait a moment, there is still this compute resource token. What if through whatever security issue, this one is stolen? Would not that be as bad as if your API key is stolen? The answer for this is No, for two reasons:

Other than with an API key, a compute resource token alone is not enough to retrieve an access token. The attacker that stole your token must also know the name or identifier of a trusted profile where the compute resource is assigned to create a useful access token.

What if the attacker managed to get both, the compute resource token and the trusted profile identity? Even then, things are less bad because compute resource tokens have a short validity of one hour and can only be used to create non-refreshable access tokens with a validity of one hour. This time may be enough for an attacker to do harm, but eventually, the compute resource token that was stolen becomes useless.

For continuous access to your resources, an attacker must therefore have a way to continuously steal your compute resource token and know the trusted profile identity.

Let’s try it out

To try out using a trusted profile with a Code Engine compute resource, we will build a small job which is listing the content of a Cloud Object Storage bucket.

Setting up the Cloud Object Storage (COS) bucket

For this exercise, you can use any COS bucket that you already have. If you never used COS, then here is a one-sentence introduction: Cloud Object Storage is a managed data service where you can store data in files. With the following commands, you will setup your first COS instance and a bucket in the Madrid region. The COS bucket uses a random suffix as bucket names must be unique across all customers. Make sure you replace those random characters with your own.

REGION=eu-es
RESOURCE_GROUP=Default
COS_INSTANCE_NAME=my-first-cos
COS_BUCKET=my-first-bucket-a2khdu

ibmcloud resource service-instance-create ${COS_INSTANCE_NAME} cloud-object-storage standard global -g ${RESOURCE_GROUP} -d premium-global-deployment-iam
COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --crn 2>/dev/null | grep ':cloud-object-storage:')
ibmcloud cos config crn --crn ${COS_INSTANCE_ID} --force
ibmcloud cos bucket-create --bucket ${COS_BUCKET} --class smart --ibm-service-instance-id ${COS_INSTANCE_ID} --region ${REGION}

To have content in the bucket, let’s store a sample text file: 

echo Hello World >helloworld.txt
ibmcloud cos object-put --region ${REGION} --bucket ${COS_BUCKET} --key helloworld.txt --body helloworld.txt

Setting up a Code Engine project with a Job that lists the COS bucket

In Code Engine, we will create a project and configure a Job that uses a pre-compiled image which accesses COS and lists the files in a bucket. We will have a look at the implementation details later:

REGION=eu-es
RESOURCE_GROUP=Default
COS_BUCKET=my-first-bucket-a2khdu
CE_PROJECT_NAME=trusted-profiles-test
JOB_NAME=list-cos-files

ibmcloud target -r ${REGION} -g ${RESOURCE_GROUP}
ibmcloud ce project create --name ${CE_PROJECT_NAME}
ibmcloud ce job create --name ${JOB_NAME} --image icr.io/codeengine/trusted-profiles/go --env COS_REGION=${REGION} --env COS_BUCKET=${COS_BUCKET}

With that we have a Job which knows where the COS bucket is and what its name is. What the Job is missing, is the necessary authorization to access this bucket. That we’re going to configure next using a trusted profile.

Setting up a trusted profile that grants the Code Engine Job to access your COS bucket

In our next step, we are creating a trusted profile which is linked to the Code Engine Job:

REGION=eu-es
RESOURCE_GROUP=Default
COS_INSTANCE_NAME=my-first-cos
COS_BUCKET=my-first-bucket-a2khdu
CE_PROJECT_NAME=trusted-profiles-test
JOB_NAME=list-cos-files
TRUSTED_PROFILE_NAME=code-engine-cos-access

CE_PROJECT_CRN=$(ibmcloud resource service-instance ${CE_PROJECT_NAME} --location ${REGION} -g ${RESOURCE_GROUP} --crn 2>/dev/null | grep ':codeengine:')
COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --crn 2>/dev/null | grep ':cloud-object-storage:')

ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_NAME}
ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_NAME} --name ce-job-${JOB_NAME} --cr-type CE --link-crn ${CE_PROJECT_CRN} --link-component-type job --link-component-name ${JOB_NAME}
ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_NAME} --roles "Content Reader" --service-name cloud-object-storage --service-instance ${COS_INSTANCE_ID} --resource-type bucket --resource ${COS_BUCKET}

Configuring your Code Engine Job to use the trusted profile

Finally, we can configure the Code Engine Job to support trusted profiles. This causes Code Engine to make a file /var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token available while your container runs. It contains a compute resource token which is regularly refreshed before expiration. In addition, we add another environment variable to make the name of the trusted profile available. This will be used by the code logic to perform authentication.

TRUSTED_PROFILE_NAME=code-engine-cos-access
JOB_NAME=list-cos-files

ibmcloud ce job update --name ${JOB_NAME} --trusted-profiles-enabled=true --env TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_NAME}

Running it

To try things out, let’s run it:

JOB_NAME=list-cos-files

ibmcloud ce jobrun submit --job ${JOB_NAME} --name ${JOB_NAME}-run-1
ibmcloud ce jobrun logs --name ${JOB_NAME}-run-1 --follow

After some moments that Code Engine needs to pull the container image and to start the container, you should see that the Job logs the names of the files in your COS bucket.

Looking at the code behind it

Let’s have a look at the code behind the Job implementation: the examples are available in https://github.com/IBM/CodeEngine/tree/main/trusted-profiles and outline how you can use the IBM Cloud SDKs to help you with authenticating.

Node

The Node example in https://github.com/IBM/CodeEngine/tree/main/trusted-profiles/node uses the ibm-cloud-sdk-core package which provides the ContainerAuthenticator. You only provide the name of the trusted profile that you created. All communication with the IBM Cloud IAM endpoint as well as token refresh handling is done for you. Below example then authenticates an API call against Cloud Object Storage.

import { ContainerAuthenticator } from 'ibm-cloud-sdk-core';

// create an authenticator based on a trusted profile
const authenticator = new ContainerAuthenticator({
    iamProfileName: process.env.TRUSTED_PROFILE_NAME
});

// prepare the request to list the files in the bucket
const requestOptions = {
    method: 'GET',
};

// authenticate the request
await authenticator.authenticate(requestOptions);

// perform the request
const response = await fetch('https://s3.direct.eu-es.cloud-object-storage.appdomain.cloud/my-bucket', requestOptions);

...

Python

The Python example in https://github.com/IBM/CodeEngine/tree/main/trusted-profiles/python uses the ibm_cloud_sdk_core package which provides the ContainerAuthenticator. You only provide the name of the trusted profile that you created. All communication with the IBM Cloud IAM endpoint as well as token refresh handling is done for you. Below example then authenticates an API call against Cloud Object Storage.

# create an authenticator based on a trusted profile
authenticator = ContainerAuthenticator(iam_profile_name=os.getenv('TRUSTED_PROFILE_NAME'))

# prepare the request to list the files in the bucket
request = {
    'method': 'GET',
    'url': 'https://s3.direct.eu-es.cloud-object-storage.appdomain.cloud/my-bucket',
    'headers': {}
}

# authenticate the request
authenticator.authenticate(request)

# perform the request
response = requests.request(**request)

...

Java

The Java example in https://github.com/IBM/CodeEngine/tree/main/trusted-profiles/java uses the com.ibm.cloud.sdk-core dependency which provides the ContainerAuthenticator. You only provide the name of the trusted profile that you created. All communication with the IBM Cloud IAM endpoint as well as token refresh handling is done for you. Below example then authenticates an API call against Cloud Object Storage.

// create an authenticator based on a trusted profile
Authenticator authenticator = new ContainerAuthenticator.Builder()
    .iamProfileName(System.getenv("TRUSTED_PROFILE_NAME"))
    .build();

// prepare the request to list the files in the bucket
Request.Builder requestBuilder = new Request.Builder()
    .get()
    .url("https://s3.direct." + cosRegion + ".cloud-object-storage.appdomain.cloud/" + cosBucket);

// authenticate the request
authenticator.authenticate(requestBuilder);

// perform the request
OkHttpClient client = new OkHttpClient();
Response response = client.newCall(requestBuilder.build()).execute()

...

Go

The Go example in https://github.com/IBM/CodeEngine/tree/main/trusted-profiles/go uses the github.com/IBM/go-sdk-core/v5 module which provides the ContainerAuthenticator. You only provide the name of the trusted profile that you created. All communication with the IBM Cloud IAM endpoint as well as token refresh handling is done for you. Below example then authenticates an API call against Cloud Object Storage.

import (
    "log"
    "net/http"

    "github.com/IBM/go-sdk-core/v5/core"
)

func main() {
    // create an authenticator based on a trusted profile
    authenticator := core.NewContainerAuthenticatorBuilder().SetIAMProfileName("trustedProfileName")

    // prepare the request to list the files in the bucket
    request, err := http.NewRequest(http.MethodGet, "https://s3.direct.eu-es.cloud-object-storage.appdomain.cloud/my-bucket", nil)
    if err != nil {
        log.Panicf("Failed to create request: %v", err)
    }

    // authenticate the request
    if err = authenticator.Authenticate(request); err != nil {
        log.Panicf("Failed to authenticate request: %v", err)
    }

    // perform the request
    response, err := http.DefaultClient.Do(request)

    ...
}

What if my service does not fully support IAM-based authentication

We have now seen that our Code Engine workload was able to authenticate without any API key using a trusted profile to access Cloud Object Storage. This works the same for all other IBM Cloud services which support IAM authentication. But what about those services that are not fully or not at all based on IAM authentication, for example if you connect to an IBM Cloud database which has some database-vendor specific authentication, or a third-party service that runs completely outside of the IBM Cloud? For those services we recommend that you include IBM Cloud Secrets Manager in your solution. Secrets Manager allows you to bring your own key so that your credentials are securely encrypted when persisted – something which is not possible with Secrets in Code Engine. How the setup then exactly looks, we look at in part 2 of this series.

Summary

We have built an example which shows how to use a trusted profile to authenticate against another IBM Cloud service without any credential being stored in Code Engine. We used the IBM Cloud SDKs to make the authentication process in our codebase very simple.

0 comments
9 views

Permalink