How to configure shared secrets in Kubernetes for IDM?

The purpose of this article is to provide information on a Kubernetes shared secret to store sensitive information like refresh tokens or client secrets.

Dennis Andrade
5 min readSep 24, 2020

Overview

Kubernetes secrets is an object that lets you store and manage sensitive information such as a password, a token, or a key. It’s a good practice not to store such information in plain text in a configuration file. Just placing the information into a Kubernetes secret does not automatically make it more secure because it’s stored in Base64 encoding, which is almost the same as plain text. However, it gives you more control over access and usage. This information can be used in a Pod or in an image.

When deploying IDM in Kubernetes you might want to save sensitive configuration information in a Kubernetes secret. Some connectors configuration like refresh tokens or client secrets can be stored in a secret.

In the example below we will use the Salesforce connector as an example on how to use Kubernetes secret to save the refresh token that the Salesforce connector needs to work properly.

Pre-requisites

  1. Forgeops environment configured. https://backstage.forgerock.com/docs/forgeops/7/index-forgeops.html
  2. Configure IDM with the Salesforce Connector. https://backstage.forgerock.com/docs/idm/7/connector-reference/chap-salesforce.html

Setting up the Salesforce refresh token as a Kubernetes Shared Secret

STEP 1: The first step in this process is to get the Salesforce’s refresh token. There are two methods of doing it:

Method 1:

  1. The refreshtoken value in the Salesforce provisioner file is encrypted so you won’t be able to use it straight from this file. We will need to run the following curl command to get the unencrypted value:

a. Get a session token for the amadmin user:

curl \ 
— request POST \
— insecure \
— header “Content-Type: application/json” \
— header “X-OpenAM-Username: amadmin” \
— header “X-OpenAM-Password: f3rtpxrtquumvk1nzxhoq03vkiu8c1mn” \
— header “Accept-API-Version: resource=2.0, protocol=1.0” \
‘https://my-namespace.iam.example.com/am/json/realms/root/authenticate’
{“tokenId”:”LurR-Vy2Cz_ONOmpaa3wxAMbdLE.*AAJTSQACMDEAAlNLABxscVBORlMwSUE0UERZblpoOXBpOG5TV1EyWXM9AAR0eXBlAANDVFMAAlMxAAA.*”,”successUrl”:”/am/console”,”realm”:”/”}

b. Get an authorization code. Use the session token you got from the previous step in the Cookie header parameter:

curl \ 
— dump-header — \
— insecure \
— request GET \
— Cookie “iPlanetDirectoryPro=LurR-Vy2Cz_ONOmpaa3wxAMbdLE.*AAJTSQACMDEAAlNLABxscVBORlMwSUE0UERZblpoOXBpOG5TV1EyWXM9AAR0eXBlAANDVFMAAlMxAAA.*” \
“https://my-namespace.iam.example.com/am/oauth2/realms/root/authorize?redirect_uri=https://my-namespace.iam.example.com/platform/appAuthHelperRedirect.html&client_id=idm-admin-ui&scope=openid&response_type=code&state=abc123"
HTTP/2 302
server: nginx/1.17.10
date: Wed, 26 Aug 2020 16:53:30 GMT
content-length: 0
location: https://my-namespace.iam.example.com/platform/appAuthHelperRedirect.html?code=_3EW8hwx2jT_gNwGpYoYPs-OqZg&iss=https://my-namespace.iam.example.com/am/oauth2&state=abc123&client_id=idm-admin-ui
set-cookie: route=1598460811.251.828.782970; Path=/am; Secure; HttpOnly
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
cache-control: no-store
pragma: no-cache
set-cookie: OAUTH_REQUEST_ATTRIBUTES=DELETED; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; HttpOnly

c. Exchange the authorization code for an access token. Use the authorization code you obtained from the previous step in the URL:

curl — request POST \
— insecure \
— data “grant_type=authorization_code” \
— data “code=_3EW8hwx2jT_gNwGpYoYPs-OqZg” \
— data “client_id=idm-admin-ui” \
— data “redirect_uri=https://my-namespace.iam.example.com/platform/appAuthHelperRedirect.html" \
“https://my-namespace.iam.example.com/am/oauth2/realms/root/access_token"
{“access_token”:”s8GmKVbM-25tBF-huCOjMogGJ_I”,”scope”:”openid”,”id_token”:”eyJ0eXAiOiJKV1QiLCJraWQiOiJRaWppYStScThIZ2tEc3prUkxENjBRNmswKzg9IiwiYWxnIjoiUlMyNTYifQ.eyJhdF9oYXNoIjoia1ZOeE5ENDhMcjREMUpLM0NLWDU0QSIsInN1YiI6ImFtYWRtaW4iLCJhdWRpdFRyYWNraW5nSWQiOiI4OTA1YTk2Ny0xMWNhLTQwYjctODg2Ny0wZjM0ZDQxNTkxNTMtMTM5MyIsImlzcyI6Imh0dHBzOi8vbXktbmFtZXNwYWNlLmlhbS5leGFtcGxlLmNvbS9hbS9vYXV0aDIiLCJ0b2tlbk5hbWUiOiJpZF90b2tlbiIsImF1ZCI6ImlkbS1hZG1pbi11aSIsImNfaGFzaCI6IkFJYjJMUlI1dHpQdTREWFg2Z0pmbFEiLCJhY3IiOiIwIiwib3JnLmZvcmdlcm9jay5vcGVuaWRjb25uZWN0Lm9wcyI6InVSbjBkV0I0RldSbzVsZTVIdjNwYV9tY2w0ayIsInNfaGFzaCI6ImJLRTlVc3B3eUlQZzhMc1FIa0phaVEiLCJhenAiOiJpZG0tYWRtaW4tdWkiLCJhdXRoX3RpbWUiOjE1OTg0NjA3NzQsInJlYWxtIjoiLyIsImV4cCI6MTU5ODQ2NDQ0NSwidG9rZW5UeXBlIjoiSldUVG9rZW4iLCJpYXQiOjE1OTg0NjA4NDV9.Kue8x4_6ipETT0c3M5Ir0Jc31cgZ0DY1RW6TIBkHf3uhmi9gfBrVBpary38gP9fVc82lfW1iDuELvH1O4xqr3U3E_wXNFWvG88Uz0GJURje0tkhBFcoGrcIgLRBxK5mJJOGAp-cS8E3x1hvtS6h_M5-kkoVuA9eJZVxm60LsHRRHKG4rZYZsi6juc5oQNnjFu-UALXR2lUKcHvIo10X13eaQx0tqhSEGmU64r-D0R7NcaR18xq-VQlQb5YbIJ2Us3jaG744yJFcZtKr2BpIzMEN9b_8Ok1fomyIAjZI7Spmw857-k5KrK5ahW6JrzyhFSerSD6TM2P85VXnzU1mi1g”,”token_type”:”Bearer”,”expires_in”:239

d. Get the decrypted refresh token and client secret. Use the bearer token received from the previous step. Look for “clientSecret” and “refreshToken” in the output:

curl \ 
— insecure \
— request POST \
— header “Authorization: Bearer s8GmKVbM-25tBF-huCOjMogGJ_I” \
— header “Content-Type: application/json” \
— data ‘{ “type”: “text/javascript”, “source”: “openidm.decrypt(openidm.read(‘\’’config/provisioner.openicf/Salesforce’\’’)).configurationProperties.clientSecret”}’ \
“https://my-namespace.iam.example.com/openidm/script?_action=eval"
“C57F78F3126315B79D743B8B48C566EEFE9CF5720C0B3AB21C3B59FC59EDD637”curl \
— insecure \
— request POST \
— header “Authorization: Bearer s8GmKVbM-25tBF-huCOjMogGJ_I” \
— header “Content-Type: application/json” \
— data ‘{ “type”: “text/javascript”, “source”: “openidm.decrypt(openidm.read(‘\’’config/provisioner.openicf/Salesforce’\’’)).configurationProperties.refreshToken”}’ \
“https://my-namespace.iam.example.com/openidm/script?_action=eval"
“5Aep861NEBUy_AOr1dpbrk7RjocmvzSeYXdiNh68kTGvuwimdeJxCuDfGiJi80QuCR7OJNpNt_5rQLnqwB8_5bO”

The values we are looking for are the following:

“clientSecret”:”C57F78F3126315B79D743B8B48C566EEFE9CF5720C0B3AB21C3B59FC59EDD637"

“refreshToken”:”5Aep861NEBUy_AOr1dpbrk7RjocmvzSeYXdiNh68kTGvuwimdeJxCuDfGiJi80QuCR7OJNpNt_5rQLnqwB8_5bO”

Method 2:

  1. Point your browser to the following URL:
SALESFORCE_URL/services/oauth2/authorize?response_type=code&client_id=CONSUMER_KEY&redirect_uri=REDIRECT_URI&scope=id+api+refresh_token

Where:

  • SALESFORCE_URL is one of the following:
  1. A production URL (https://login.salesforce.com)
  2. A sandbox URL (https://test.salesforce.com)
  3. A custom Salesforce MyDomain URL, such as:
    https://ic-example-com--SUP1.cs21.my.salesforce.com
  • CONSUMER_KEY is the Consumer Key associated with the Connected App that you created within your Salesforce organization.
  • REDIRECT_URI is the IDM URI Salesforce should redirect to during authentication. It must match the Redirect URI specified within your Salesforce Connect App configuration, for example:
https://my-namespace.iam.example.com

2. You are redirected to Salesforce, and prompted to give this application access to your Salesforce account. When you have given consent, you should receive a response URL that looks similar to the following:

https://my-namespace.iam.example.com/?code=aPrxaPAl8OC6v.J1mHFhPG4qVuRhMdrPBFlwDDsvgz1fMAGkkoYnSVtUQDdGiFjXT3YF1PoH3g%3D%3D

The &code part of this URL is an authorization code that you need for the following step.

CautionThis authorization code expires after 10 minutes. If you do not complete the OAuth flow within that time, you will need to start this process again.

3. Copy the authorization code from the response URL and use it as the value of the code parameter in the following REST call. The consumer-key, redirect-uri, and SALESFORCE_URL must match what you used in the first step of this procedure:

curl \
— verbose \
— data “grant_type=authorization_code” \
— data “client_id=consumer-key” \
— data “client_secret=consumer-secret” \
— data “redirect_uri=https://my-namespace.iam.example.com/" \
— data “code=access-token-code” \
“SALESFORCE_URL/services/oauth2/token”
{
“access_token”:”00D5w000007NrEl!ARIAQDvvR1UcVLGzkZFuPAJzjvvrEf_DeSlVx15jA.uIvjWYVSVqd_YxEuZvhKd_WHE99qv41XQsXRtUnNdRLCiQzDs_LAqK”,
“refresh_token”:”5Aep861NEBUy_AOr1dpbrk7RjocmvzSeYXdiNh68kTGvuwimdeXQGC0ZUguXoWGteevbAZbJn_qfYShkx4JMbeK”,
“signature”:”jGB2bnpCnhq7LfOjNORrD4Yt7a0b9g2ZmUtiwZnPLw8=”,
“scope”:”refresh_token api id”,
“instance_url”:”https://forgerocklab-dev-ed.my.salesforce.com",
“id”:”https://login.salesforce.com/id/00D5w000007NrElEAK/0055w00000DbbsQAAR",
“token_type”:”Bearer”,
“Issued_at”:”1600276697381"
}

The output includes the refresh_token and the instance_url that you need to configure the connector.

STEP 2: Now that we have the decrypted clientSecret and refreshToken we need to encode it using base64:

  1. Client Secret:
% echo C57F78F3126315B79D743B8B48C566EEFE9CF5720C0B3AB21C3B59FC59EDD637 | base64QzU3Rjc4RjMxMjYzMTVCNzlENzQzQjhCNDhDNTY2RUVGRTlDRjU3MjBDMEIzQUIyMUMzQjU5RkM1OUVERDYzNwo=

2. Refresh Token:

% echo 5Aep861NEBUy_AOr1dpbrk7RjocmvzSeYXdiNh68kTGvuwimdeJxCuDfGiJi80QuCR7OJNpNt_5rQLnqwB8_5bO | base64NUFlcDg2MU5FQlV5X0FPcjFkcGJyazdSam9jbXZ6U2VZWGRpTmg2OGtUR3Z1d2ltZGVKeEN1RGZHaUppODBRdUNSN09KTnBOdF81clFMbnF3QjhfNWJPCg==

STEP 3: Create a new YAML file to hold those two base64 encoded values with the following content:

apiVersion: v1
data:
SF_REFRESH_TOKEN: NUFlcDg2MU5FQlV5X0FPcjFkcGJyazdSam9jbXZ6U2VZWGRpTmg2OGtUR3Z1d2ltZGVKeEN1RGZHaUppODBRdUNSN09KTnBOdF81clFMbnF3QjhfNWJPCg==
SF_CLIENT_SECRET: QzU3Rjc4RjMxMjYzMTVCNzlENzQzQjhCNDhDNTY2RUVGRTlDRjU3MjBDMEIzQUIyMUMzQjU5RkM1OUVERDYzNwo=
kind: Secret
metadata:
name: refreshtokensecret
type: Opaque

In my example I called it SF_Tokens.yaml and I placed it in the same directory as the customization.yaml file (<forgeops_home>/kustomize/overlay/7.0/all)

STEP 4: Edit the customization.yaml file in <forgeops_home>/kustomize/overlay/7.0/all:

  1. Add the new SF_Tokens.yaml file under “resources”
resources:
- ../../../base/kustomizeConfig
- ../../../base/forgeops-secrets
- ../../../base/7.0/ds/cts
- ../../../base/7.0/ds/idrepo
- ../../../base/am
- ../../../base/amster
- ../../../base/idm
- ../../../base/end-user-ui
- ../../../base/login-ui
- ../../../base/admin-ui
- ../../../base/7.0/ingress
- ../../../base/ldif-importer
- SF_Tokens.yaml

2. Add the following to the “patchesStrategicMerge” section

- |-
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: idm
spec:
template:
spec:
containers:
— envFrom:
— secretRef:
name: refreshtokensecret

My final customization.yaml file after all the changes looks like this:

namespace: my-namespace
commonLabels:
app.kubernetes.io/name: “forgerock”
resources:
- ../../../base/kustomizeConfig
- ../../../base/forgeops-secrets
- ../../../base/7.0/ds/cts
- ../../../base/7.0/ds/idrepo
- ../../../base/am
- ../../../base/amster
- ../../../base/idm
- ../../../base/end-user-ui
- ../../../base/login-ui
- ../../../base/admin-ui
- ../../../base/7.0/ingress
- ../../../base/ldif-importer
- SF_Tokens.yaml
patchesStrategicMerge:
- |-
apiVersion: v1
kind: ConfigMap
metadata:
name: platform-config
data:
FQDN: my-namespace.iam.example.com
- |-
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: idm
- |-
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: idm
spec:
template:
spec:
containers:
- name: openidm
envFrom:
- configMapRef:
name: idm
- secretRef:
name: idm-env-secrets
- secretRef:
name: refreshtokensecret

Note that the name “refreshtokensecret” matches the metadata name in the SF_Tokens.yaml file we’ve created in step 3.

STEP 5: Edit the Salesforce provisioner file to use the Kubernetes shared secrets value:

  1. Open the Salesforce provisioner file under <Forgeops home>/config/7.0/cdk/idm/conf. Your path might vary depending on your deployment. This is the path if you are using minikube
  2. Change the “clientsecret” and the “refreshtoken” values to the following:
“clientSecret” : “&{sf.client.secret}”,
“refreshToken” : “&{sf.refresh.token}”,

STEP 6: Navigate to the <Forgeops home>/bin directory and run ./config.sh init

Troubleshooting

Problem: After following this documentation and making the changes, the Salesforce connector stopped working and it’s not picking up the shared secrets information

Answer: Make sure IDM has set the environment variables for you.

  1. Go to the IDM console: kubectl exec -it idm-0 bash
  2. Run the following command: printenv | grep SF

You should see the 2 values that you have configured: SF_REFRESH_TOKEN and SF_CLIENT_SECRET

  1. If you don’t see those two values, run skaffold dev again to force the server reload
  2. If you still don’t see those variables, make sure you look at the logs for any errors or exceptions.

--

--