For IT and DevOps professionals, securing API credentials is crucial. While production systems often pull Cloudinary API credentials from secret managers, hands-on tasks like setup or maintenance present some challenges:
- Importing credentials into the shell session. This leaves the API secret in the shell history.
- Using a
.env
file. This risks storing unencrypted credentials on the hard drive.
So, how can we handle API credentials securely during hands-on work?
In this article, we’ll directly pull credentials from the secrets manager into a shell session, minimizing exposure and aligning with cybersecurity best practices. Our sample code is designed for AWS Secrets Manager users with bash
or zsh
shells and the aws
CLI. However, this approach can be adapted for other platforms.
Let’s dive into a safer way to manage our API secrets during hands-on tasks.
In our solution, we’ll utilize a shell script designed to be sourced into your active shell session.
The distinction of sourcing is essential. By sourcing, the script operates within the current shell environment, allowing direct modification and introduction of environment variables.
This method ensures Cloudinary API credentials are integrated into your session without leaving traces in shell history or persisting on the disk. It’s a technique that prioritizes both convenience and security, ensuring sensitive data remains transient and confined to the active session.
We’ll store the environment variable definitions as new-line separated shell variable definitions with an AWS Secrets Manager.
Don’t use quotes around the variable values.
```bash
CLOUDINARY_ENV_DISPLAY_NAME=SAMPLE ENVIRONMENT
CLOUDINARY_URL=cloudinary://***************:**********************@sample-environment
```
Code language: Bash (bash)
Step 1: Store your credentials with AWS Secrets Manager.
Once authenticated against the AWS account with sufficient permissions to retrieve the secret value, you’ll get this:
```sh
> aws secretsmanager get-secret-value \
--secret-id "cld-credentials/sample-environment" \
--region "us-east-2" \
--query SecretString \
--output text
CLOUDINARY_ENV_DISPLAY_NAME=SAMPLE ENVIRONMENT
CLOUDINARY_URL=cloudinary://***************:**********************@sample-environment
```#
Code language: Bash (bash)
From here you’ll need to process the result line by line and export each variable definition:
# --------------------------------------------------------------------------
# Assumes input to be string in the format "<VAR_NAME>=<VAR_VALUE>"
# Exports the variable definition into the current shell session to make it
# available to any child process started from the shell.
# --------------------------------------------------------------------------
export_variable_from_string() {
local str="$1"
# Extract the variable name
local var_name="${str%%=*}" # Everything before the first '='
# Extract the value
local var_value="${str#*${var_name}=}" # Everything after the first '='
export "${var_name}=${var_value}"
if [[ $? -ne 0 ]]
then
echo "One of the variables failed to be imported." >&2
echo "It is still recommended to terminate the shell session to dispose of other imported variables." >&2
return 1
fi
}
# --------------------------------------------------------------------------
# Assumes input to be a multi-line string (secret value retrieved from
# secrets manager).
# Exports variable definition from each line into the shell session.
# --------------------------------------------------------------------------
load_secrets_from_string() {
local secret_text_value="$1"
local IFS=$'\n' #Read line-by-line
while IFS= read -r var_definition; do
export_variable_from_string "$var_definition"
done <<< "$secret_text_value"
}
Code language: Bash (bash)
#!/usr/bin/env sh
#
# ❗️make sure to paste the above shell functions from the article
#
ARG_SECRET_ID="$1"
ARG_SECRET_REGION="$2"
# Ensure required parameters are set
if [ -z "${ARG_SECRET_ID}" ] || [ -z "${ARG_SECRET_REGION}" ]; then
echo "Error: Missing required parameters."
return 1
fi
#
# MAIN
#
SECRET_VALUE=$( \
aws secretsmanager get-secret-value \
--secret-id "${ARG_SECRET_ID}" \
--region "${ARG_SECRET_REGION}" \
--query SecretString \
--output text \
)
load_secrets_from_string "$SECRET_VALUE"
Code language: Bash (bash)
Let’s assume you’ve saved the script as a load-cred.sh
file. Then using it may look like this.
Using the load credentials script:
If you plan to use this solution repeatedly with other team members, you may find the following improvement tips to be useful.
# --------------------------------------------------------------------------
# Uses values of previously set variables to update shell prompt (updates PS1
# environment variable)
#
# - Appends environment display name (value of the CLOUDINARY_ENV_DISPLAY_NAME variable)
# + Green background for "Sandbox" environments ($CLOUDINARY_ENV_TYPE == 'sandbox')
# + Red background otherwise (Production credentials assumed)
# - Appends "Cloud with lightning" emoji to the shell prompt
# --------------------------------------------------------------------------
update_shell_prompt() {
local cloud_name="$CLOUDINARY_ENV_DISPLAY_NAME"
if [ -z "$cloud_name" ]; then
cloud_name="Cred. label not set"
fi
local fg_color='37' # white
local bg_color='41' # red
if [[ "$CLOUDINARY_ENV_TYPE" == *'sandbox'* ]]
then
bg_color='42' # green
fi
local color_seq=$'\e['"${fg_color};${bg_color}m"
local color_reset_seq=$'\e[0m'
PS1="${PS1:-} ${color_seq}${cloud_name}${color_reset_seq} 🌩️ "
export PS1
}
# --------------------------------------------------------------------------
# Informs script user of the changes made to the shell
# --------------------------------------------------------------------------
inform_user() {
local color_seq=$'\e[30;43m'
local color_reset_seq=$'\e[0m'
echo ""
echo "❗️${color_seq}Important${color_reset_seq} Credentials loaded from Secrets Manager."$'\n' \
"Exit the shell when done to dispose of secrets in environment variables."$'\n' \
"Child processes started from this shell will inherit environment variables."
echo ""
}
Code language: Bash (bash)
# --------------------------------------------------------------------------
# Unsets (removes) interim variables from the shell session to avoid
# unnecessary copies of the secret info and secret value.
# Use before exiting the script
# --------------------------------------------------------------------------
unset_interim_variables() {
unset ARG_SECRET_ID
unset ARG_SECRET_REGION
unset SECRET_VALUE
}
Code language: Bash (bash)
# --------------------------------------------------------------------------
# Use at the beginning of the script to ensure the script is explicitly
# sourced by the user
# --------------------------------------------------------------------------
inform_must_be_sourced() {
echo "The script must be sourced. Use 'source <script> --help' for more information." >&2
}
if [[ -n "$BASH_VERSION" ]]; then
if [[ "$0" == "$BASH_SOURCE" ]]; then
# Evaluates to true when script is being directly executed in bash
inform_must_be_sourced
exit 1
fi
elif [[ -n "$ZSH_VERSION" ]]; then
if [[ ! "$ZSH_EVAL_CONTEXT" == *"toplevel:file"* ]]; then
# Evaluates to true when script is being directly executed in zsh
inform_must_be_sourced
exit 1
fi
else
echo "Only bash and zsh shells are supported."
exit 1
fi
Code language: Bash (bash)
inform_already_loaded() {
echo "Credentials already loaded into this shell session. Exit the shell to dispose of them." >&2
}
if [ -n "$__CLD_CREDS_LOADED" ]; then
inform_already_loaded
return 1
fi
# ... After credentials have been loaded into the session
__CLD_CREDS_LOADED=1
# ...
Code language: Bash (bash)
print_script_usage() {
echo "Usage: source cld-cred-from-aws-secret.sh --secret-id <AWS_SECRET_ID> --region <AWS_REGION>"
echo ""
echo "Description: Retrieves credentials from AWS Secrets Manager and imports them as environment variables" \
"into the current shell session."
echo ""
echo "Options:"
echo " --secret-id The ID of the AWS secret."
echo " --region The AWS region to retrieve the secret from."
}
## Print use when no arguments were provided
if [ "$#" -eq 0 ]; then
print_script_usage
return 0
fi
# Parse commandline arguments
ARG_SECRET_ID=""
ARG_SECRET_REGION=""
while [ "$#" -gt 0 ]; do
case "$1" in
--secret-id)
ARG_SECRET_ID="$2"
shift 2
;;
--region)
ARG_SECRET_REGION="$2"
shift 2
;;
--help)
print_script_usage
return 0
;;
*)
echo "Unknown option: $1" >&2
return 1
;;
esac
done
# Check if required parameters are set
if [ -z "$ARG_SECRET_ID" ] || [ -z "$ARG_SECRET_REGION" ]; then
echo "Error: Missing required parameters."
print_script_usage
return 1
fi
Code language: Bash (bash)
- Add aliases for repeated use.
- Adding aliases to your shell profile will let you leverage shell auto-completion and speed up access to the credentials for repeated use.
- Use the terminal multiplexer.
- For long-running batch scripts, especially when running on a remote VM over SSH, using the terminal multiplexer, such as
screen
ortmux
, can prevent losing progress when, for example, the SSH connection times out.
- For long-running batch scripts, especially when running on a remote VM over SSH, using the terminal multiplexer, such as
This is how the final results may look:
If you found this article helpful and want to discuss it in more detail, head over to Cloudinary Community forum and its associated Discord.