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';
const authenticator = new ContainerAuthenticator({
iamProfileName: process.env.TRUSTED_PROFILE_NAME
});
const requestOptions = {
method: 'GET',
};
await authenticator.authenticate(requestOptions);
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.
authenticator = ContainerAuthenticator(iam_profile_name=os.getenv('TRUSTED_PROFILE_NAME'))
request = {
'method': 'GET',
'url': 'https://s3.direct.eu-es.cloud-object-storage.appdomain.cloud/my-bucket',
'headers': {}
}
authenticator.authenticate(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.
Authenticator authenticator = new ContainerAuthenticator.Builder()
.iamProfileName(System.getenv("TRUSTED_PROFILE_NAME"))
.build();
Request.Builder requestBuilder = new Request.Builder()
.get()
.url("https://s3.direct." + cosRegion + ".cloud-object-storage.appdomain.cloud/" + cosBucket);
authenticator.authenticate(requestBuilder);
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() {
authenticator := core.NewContainerAuthenticatorBuilder().SetIAMProfileName("trustedProfileName")
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)
}
if err = authenticator.Authenticate(request); err != nil {
log.Panicf("Failed to authenticate request: %v", err)
}
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.