Multiline ENV secrets in Circle CI

TL;DR — Save base64-encoded multiline secret and put decoded version of it into $BASH_ENV

Виктор Русакович
4 min readApr 28, 2020

Secrets — are a common thing in CI/CD systems. You don’t want to save, let’s say, Google Cloud Console password into your public GitHub repository. Instead, in your code you’ll rather use some secret variable, that will be substituted with a real password during the build.

Photo by Shane Avery on Unsplash

In CircleCI you’ll go to Project Settings — Environment Variables — Add Variable:

Some systems provide multiline private keys. And CircleCI has a simple single-line input. I tried to replace line breaks in out key with “\n” char…

…but my script could not authorize:

(node:215) UnhandledPromiseRejectionWarning: Error: error:0909006C:PEM routines:get_name:no start line 
at Sign.sign (internal/crypto/sig.js:112:29)
at Object.sign (/home/circleci/project/node_modules/jwa/index.js:152:45)
at Object.jwsSign [as sign] (/home/circleci/project/node_modules/jws/lib/sign-stream.js:32:24)
at GoogleToken.requestToken (/home/circleci/project/node_modules/gtoken/build/src/index.js:196:31)
at GoogleToken.getTokenAsync (/home/circleci/project/node_modules/gtoken/build/src/index.js:135:21)
at GoogleToken.getToken (/home/circleci/project/node_modules/gtoken/build/src/index.js:77:21)
at JWT.refreshTokenNoCache (/home/circleci/project/node_modules/google-auth-library/build/src/auth/jwtclient.js:156:36)
at JWT.refreshToken (/home/circleci/project/node_modules/google-auth-library/build/src/auth/oauth2client.js:142:25)
at JWT.authorizeAsync (/home/circleci/project/node_modules/google-auth-library/build/src/auth/jwtclient.js:139:35)
at JWT.authorize (/home/circleci/project/node_modules/google-auth-library/build/src/auth/jwtclient.js:135:25)

(node:215) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)

(node:215) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

FAIL test/test.spec.js

Something is wrong with CircleCI when it processes line breaks in a single line input (though it was fine within GitHub Actions).

Solution

The first thing we have to do is to encode our “unsafe” multiline secret into a single line that will have no line breaks. The easiest thing is to use Base64 encoding. To encode your string you can use various online tools, or, what is preferred, use in-built in Unix bash base64:

$ echo "Hello, world!" | base64 -w 0
SGVsbG8sIHdvcmxkIQo=

Now, let’s save this secret to CircleCI:

Saving a single line secret works totally flawlessly and your code will be able to read secret though ENV. For example, in Node.js script run in a pipeline, you can do like this:

console.log(process.ENV.SOME_APY_KEY_BASE64)
"SGVsbG8sIHdvcmxkISBIZWxsb3cgTWluc2shCg=="

Now you have two options:

  1. Decode base64 string inside your program
  2. Decode base64 string inside CircleCI pipeline and create a proper ENV variable

I’m sure you’ll choose #2, though the solution #1 is usually pretty easy in most programming languages. For example, in Node.js it might look like this:

🔐Decoding base64 secret in CircleCI pipeline

Not much to say here — just call a command that will add a new variable to $BASH_ENV. Open your .circleci/config.yml and add a new step prior to steps where you access your secret:

version: 2.1
orbs:
node: circleci/node@1.1.6
jobs:
build-and-test:
executor:
name: node/default
steps:
- checkout
- node/with-cache:
steps:
- run:
name: "Setup custom ENV variable"
command: echo 'export SOME_API_KEY="$(echo $SOME_API_KEY_BASE64 | base64 -d)"' >> $BASH_ENV
- run: npm install
- run: npm test

The juice 🥤is this command (shortened variable names to fit into one line):

echo 'export KEY="$(echo $KEY_BASE64 | base64 -d)"' >> $BASH_ENV

I found it convenient to add a _BASE64 suffix to secrets that need to be decoded. Check official CIrcleCI documentation to see how to add multiple variables in a single command. Feeling lazy following the link? Ok, here is the example (converting K1_BASE64 and K2_BASE64 secrets into K1 and K2 ENV variables):

- run:
name: "Setup custom ENV variables"
command: |
echo 'export K1="$(echo $K1_BASE64 | base64 -d)"' >> $BASH_ENV
echo 'export K2="$(echo $K2_BASE64 | base64 -d)"' >> $BASH_ENV

That’s it!

Photo by Jackson Simmer on Unsplash

More links about this issue:

  1. https://support.circleci.com/hc/en-us/articles/360046094254-Using-Multiple-Line-newline-Environment-Variables-in-CircleCI
  2. https://discuss.circleci.com/t/multi-line-environment-variables/13148
  3. https://circleci.com/docs/2.0/env-vars/#setting-an-environment-variable-in-a-shell-command

NB

This can not work for you if:

  1. your build is run on Windows
  2. you have some custom bash

--

--