No More Hardcoding: Environment Variables Explained Simply.


Environment Variables is a topic that is very much essential in the real world where in many people might easily grasp the concept of what these are but applying them is a hell of a task and watching your program fail to recognize them or things might work in your local machine and are not working on your hosting platform or your colleagues machine it sucks and is so frustrating it just can't be put into words(explicitly speaking of configuration or key value issues across devices). So let's get straight into the topic and see how and what these are in simple analogical ways and how can we initiate them.
But first What are Variables?
Variables are like the alphabets of code these can be combined and manipulated to create meaningful instructions for your computer, we can store values and perform various things with them and for a deeper insight check out this blog where I have already mentioned them.
Now, What is an environment?
The environment in the coding context refers to the operating system, libraries, settings, and even external factors like user preferences come under the tag "environment" In a simpler manner we can also say that environment means the space that determines how your code interacts with the system and adapts to different situations present.
Now what are environment variables?
Think of environment variables as special knobs and dials within the environment which can be adjusted to your program's behavior without rewriting any code the value of these environment variables is not hardcoded in your program they just stay hidden inside your space and can be applied whenever needed without exposing their contents these work by being available to your program/application under the layers waiting to be called and the value of these variables can come from a range of sources — text files, third-party secret managers, calling scripts, etc.
For better understanding think of them as "secret switches" a dimmer switch to adjust the brightness of your program. Instead of rewiring the lamp (your code), you tweak the switch (an environment variable) and your program adapts accordingly. It's like giving your code remote control for its features, letting you adjust settings and toggle options, and also these help in improving security, simplifying configuration, and enabling dynamic behavior of the code for you(local) and across its existence(global).
Different Types of Environment Variables
There are 3 types of environment variables
System Environment Variables
These environment variables are built into your operating system which means these work the same for all the processes running on your machine and mostly these are not something we need to be messing around cause these create the env that your operating system works on
Ex:- PATH - This you might have probably seen in your program files be it Windows or Linux the "PATH" directory, tells your OS and apps where to find tools and files or a specific Python library that it needs and as mentioned if we try messing with them we might face issues pointing apps to a file's location which could lead to a lot of confusion if you don't know what you are doing
User Environment Variables
As for the difference between the System and the User environment variables the system env variables work for the entire system but if we have created users like one for you and maybe one for your family this changes things up now we will have "user environment variables" these are specific to a user separating one from an another or you can say customized variables that might differ from the system not essentially but we can tweak them to our ways. Basically, this lets you create settings that only apply to your user account or where you want to switch things, not affecting others on the same machine. It's like having your secret control panel! For example - you could set a custom path to your preferred Python libraries or Java SDK, ensuring your projects always use the right tools or maybe you wanted to test different configurations for an app, you can create temporary user variables without messing with system-wide settings.
Runtime/process Environment variables
System variables apply to all processes, and user variables apply to your account, but runtime variables are a little bit different from them these environment variables only affect the current process and its children(functions or process you are building). It's like having a conversation within a single entity, hidden from the rest and these can be defined and be used secretly if wanted also these runtime/process environment variables are what we use in coding for API key masking and stuff which I will address further in the blog.
Persistence and Temporary
System and user variables typically stick around until you change them(persistent). Still, runtime variables can be fleeting or persistent which means Temporary. This type of environment variables are like a secret handshake for a single mission or we can say a session if you wanna say it that way, they vanish when the process ends, leaving no trace behind. But for secrets(values) that need to endure throughout a project, you can configure runtime variables to exist within a specific project environment ensuring consistent behavior across different sessions.
The use case for these temporary and persistent environment variables is completely based on the requirement/use case
For, example in the initial build stage we might be testing our structure and we may employ a temporary env variable but mostly we see these in the testing phase of the code we can input various diff inputs for the same variable as the value of the variable is temporary but the variable itself is hard coded so it's like an x in your math problem and usually the persistent env variable is when used when we push our code to prod or might be pushing to the git these are generally used to hide key's or maybe any sensitive info like pre-input data that we don't want anyone outside the org should see the type of thing.
PS: Here I am going to speak of these Environment Variables initialization and specifics using Python lang. only there are many ways that different languages use them or initiate them some of their links will be at the bottom in the reference section so please check them out if you wanna see the concept specific to your language.
Development Phase Defining
If we are using Python it has an in-build module that can be imported into code to provide this functionality and that can be called into the code using the "os.environ" tag wherever we want to call it.
import os
print(os.environ['Environment_variable_1'])
print(os.environ['Environment_variable_2'])
# This prints out the Environment_variable_[1,2] if we have already assigned
# any value to them if.
We can also set environment variables just for a current session(temp) both inside the code and also in the PowerShell or bash this refers to setting a temporary environment variable. As mentioned if we close the session we lose the value assigned to it in case of the hard-coded(inside code) thing this is usually not recommended even for the testing phase as it exposes let that be an API key as in the example or any input we don't want others to see this is bad security practice is often seen in the dev-phase but is to be avoided.
os.environ["MY_API_KEY"] = "your_api_key_here"
set $Env:MY_API_KEY = "your_api_key_here"
Now let's see how we can make it a persistent thing
for that we need to create a file usually a ".env" and it holds the keys & values using the same import os function the values are called to replace the text as these are stored in a key-value manner.
ENV_VAR_1= # Your value here(prolly an api key maybe)
ENV_VAR_2= # Your value here(maybe something of the following )
## This a list of what kinds of things that are usually stored in using this method
# Database Connection Information
# Authentication Secrets
# API credentials
# Application Settings - Debug mode,Logging level
# SMTP and Email Configuration
# File Paths and URLs
# Feature Flags - Flags to enable or disable certain features in the application
# Cache and Session Configuration - generally used for Redis configs
# Rate Limiting and Throttling Settings - basiclly policies.
The above list specifies what type of values are usually stored as values in this environment variable structure and if you are wondering why this method makes the variable persistent you might think that's cause we are creating a "file" that will stay with us inside the root directory of our file and send values when demanded. Just to make things even simpler think of this file creation and storing values in the key-value way as an additional set to the temporary value assigned thing we have already discussed above the only difference is that to call the value of the key into the runtime we need to install a package by the name dotenv in python #
# cmd to install the package -->pip install python-dotenv
# this is how the actual code for the persistant env variable looks like
import os
import requests
# Accessing variables from a .env file (using dotenv)
from dotenv import load_dotenv
load_dotenv() # Load the .env file
# Retrieve secret keys and configuration
secret_key = os.getenv("SECRET_KEY")
api_base_url = os.getenv("API_BASE_URL")
#### Some advanced examples of how we use them in actual code
# Dynamically adjust behavior based on environment
if os.getenv("DEBUG"): # Checks if the "DEBUG" environment variable is set (to any value).
print("Running in debug mode...") # #If it's set, it indicates debug mode and prints message
enable_debug_features = True # Enables debugging features within the code.
else:
print("Running in production mode...")
enable_debug_features = False # If not it is assumed to be in production mode
# Send a request using an API key
api_key = os.getenv("API_KEY") # Creates an HTTP header for authorization using BTS
headers = {"Authorization": f"Bearer {api_key}"} # Creates an HTTP header for authorization using BTS
response = requests.get(f"{api_base_url}/data", headers=headers)
# Sends a GET request to the specified API endpoint, including the authorization header for authentication
# Handle user preferences - You can think of it as user specific preloaded environment specific to that particular user
locale = os.getenv("USER_LOCALE", "en_US") # Set a default if not specified
print(f"Welcome to the app! Your preferred locale is: {locale}")
# Customize logging based on environment
if os.getenv("LOG_LEVEL") == "DEBUG": # Activates debug logging when "LOG_LEVEL" is set to "DEBUG
logging.basicConfig(level=logging.DEBUG) # Configures the logging module for detailed debug output
This pretty much sums up the initialization and most use cases of the environment variables in the development and some test phases.
One thing we need to remember is we need to exclude the .env file when we push our code to any hosting or any space apart from your local machine or your organizational cluster we need to make sure to add the .env file to the .gitignore file if we are pushing to git or just delete the file when it is about to be uploaded to a space and use secret key managers.
Production Phase Defining
In the production phase, all the hosting providers provide a space for you to enter your specific value for the already hard-coded key(placeholder), where we need to refer to the hardcoded name and assign our specific value to it. Let's take AWS secret key manager and Docker as the hosting examples. Docker uses a YAML file to store and call these env variables but YAML files(which are used to initialize a docker image) can expose secrets, it's important to note that Docker Compose doesn't directly expose them in its public image registry but these risks primarily arise when we are sharing or storing YAML files in unsecured locations as previously mentioned this air gap is what that can expose the values that we want to be secret but to give you an example I'm presenting this example for the docker and this docker is usually used for configure storing keys and other but not the API keys the API key management scenario for big projects has a the major issue would be having to keep track a lot of keys and this countered by using the AWS secrets manager and there are many tools also for this they are called secrets management tools--> Ansible or Hashicorp vault, Doppler these organize and keeps the keys secure but that's a whole another topic but for now let's focus on AWS secrets manager for the time being and see how we can assign and call the environment values from the manager to the run in a function/our code.
# Docker composes step by step so while it recognises the code in the starting
# steps itself it waits till the runtime to apply which means it just looks
# for syntax at first and after the "environment" statement only the runtime starts.
version: "3.8"
services:
myapp:
build: .
environment:
- DATABASE_URL=postgres://user:password@host:port/database
- API_KEY=your-api-key
{
"containerDefinitions": [
{
"name": "my-container",
"image": "my-image",
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:region:aws_account_id:secret:secret_name"
}
]
}
]
}
// This is an example of defining the name of what our key be and what the is
// value of it be it can be called both into the production phase and dev phase
// if we have connected our dev space to the AWS CLI .
Set-AWSCredential -AccessKey YOUR_ACCESS_KEY -SecretKey YOUR_SECRET_KEY -StoreAs default
Write-AWSSecretStoreSecret -Name my-secret-name -SecretString '{"password": "your_database_password"}'
Get-AWSSecretValue -SecretId my-secret-name
These are the cmds that configure, set, and retrieve(to be viewed on cmd/PowerShell itself this is not recommended but just to check for ourselves we can do this) these values are sent through or retrieved from the cmd line to the AWS secrets manager and if we are using a Python code this is what happens when we want to call them into our code directly at run time.
$secretValue = Get-AWSSecretValue -SecretId my-secret-name
$env:SECRET_VALUE = $secretValue.SecretString
# this imports the key value pair into the runtime and all we need to
# do now is to use the import function as mentioned and use the same "key/name"
# for the assignment of the value.
import os
secret_value = os.environ.get('SECRET_VALUE')
The above examples are only possible in Python if we have a single key-value pair stored in the file cause generally we can call each value specifying the exact key name but when dealing with multiple related values that form a structured dataset, JSON excels at organizing them into a hierarchical structure this makes it easier to access, manage, and maintain the data within your code and also the storing multiple secrets as a single JSON string, encrypting them together provides for enhanced security so most of the secrete managers opt to the way and the below code is how we can call them into our code and use them.
import json
secret_json_string = os.environ.get('SECRET_VALUE')
secret_data = json.loads(secret_json_string)
password = secret_data['password']
api_key = secret_data['api_key']
database_url = secret_data['database_url']
As mentioned in the JSON file, all values are stored in a single sting as follows.
// Secret value stored as JSON:
{
"password": "mysecretpassword",
"api_key": "your_api_key",
"database_url": "postgres://user:password@host:port/database"
}
Probable Security risks and their mitigation
Command injection: An attacker can inject malicious code into an environment variable which can be used in a shell command executed by your application. This could lead to unauthorized file deletion, data exfiltration, or even system takeover.
Path manipulation: By injecting malicious paths into environment variables referencing libraries or even executables attackers can trick your application into loading and executing unauthorized code by replacing the original value.
Configuration hijacking: Sensitive configuration values stored in environment variables can be manipulated by attackers, altering application behavior or exposing confidential information.
Protecting your code from these threats requires a multi-layered approach:
Input validation: Always validate and sanitize the data coming from external sources before using it in environment variables.
Escape hatch characters: Utilize appropriate escape characters to neutralize potential code injection attempts.
Least privilege: Grant applications the minimum set of permissions necessary to function, limiting the potential damage caused by an injected variable.
Using trusted Secret Key managers: AWS, Ansible Vault, etc...
this in-depth overview of what Environment Variables are and how can be set and used in your code through Python could be of help.
Here is a list of in-depth explanations on this for other popular languages.
Node.js using Doppler
ReactCode reviews
Conclusion
Hope this in-depth overview of what Environment Variables are and how can be set and used in your code through Python could be of help.
Here is a list of in-depth explanations on this for other popular languages.
Node.js using Doppler
React
For Windows, Linux, and Mac OS
Subscribe to my newsletter
Read articles from Harshith Thalamala directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
