When a script needs a password, API key, token, or private secret, the worst place to put it is directly in the source code. Hardcoded credentials are easy to leak through Git commits, backups, screenshots, logs, shell history, and copied files.
A secure setup on Linux usually follows one rule:
store the secret outside the script, and inject it only at runtime.
This article explains 10 of the safest common options to using credentials securely in Linux, when to use them, and how to implement them in Python.
Why hardcoding secrets is dangerous
Let's look at the danger of hardcoding secrets based on an example. This is what not to do:
DB_PASSWORD = "SuperSecretPassword123"Even in a private repo, that password can end up in:
- Git history
- pull requests
- code review tools
- editor autosaves
- backups
- chat screenshots
- logs if accidentally printed
- AI conversations if you expose your codebase to an LLM
Even if you later remove it, Git may still contain the old value in previous commits.
General security principles of handling credentials
Before looking at specific methods, these principles apply everywhere:
-
Keep secrets out of source code: Your script should only refer to a variable name like
DB_PASSWORD, not the actual password.
-
Restrict access: Only the user or service account that needs the secret should be able to read it.
-
Never pass secrets on the command line: Avoid commands like:
python3 app.py --password mysecret. Command-line arguments can be visible in process listings and logs.
-
Never log secrets: Do not print them for debugging, and be careful not to include them in exceptions.
-
Prefer tokens over passwords when possible: If a service supports API tokens, short-lived credentials, or IAM-based access, those are often better than static passwords.
-
Rotate secrets: Even well-protected secrets can leak eventually. Rotation limits the damage.
-
Use gitignore: If for example you use a
.envfile, add it togitignore
-
Load passwords from an environment variable or protected file (at minimum)
-
File permissions: set file permissions to
600
Which option should you use?
In this article we deal with 10 options for handling sensitive credentials securely in Linux. For using these options, the general guidelines help you pick the best option:
For local development
- Python keyring
-
.envfile kept out of Git - Environment variable set interactively
For a manual admin script
getpass- Python keyring
For a server running a Linux service
- Systemd credentials
- Systemd with a protected environment file
For containers
- Docker/Kubernetes secrets
- Or your platform’s secret manager
For serious production
- A dedicated secret manager
- Or.. at least
systemdcredentials / mounted secret files
Option 1: Using environment variables
Environment variables are one of the most common approaches on Linux and keep the secret out of your code.
Pros, cons & use cases of using environment variables
| Pro | Con | Best use case |
| Simple | Environment variables may be visible to privileged users or via debugging tools | local development |
| widely supported | They can leak into logs, crash dumps, or child processes if not handled carefully | CI/CD |
| easy for automation | They are often fine, but not the strongest option for high-security environments | containers |
| keeps secret out of the source code |
temporary script execution
|
Python example
import os
def main():
db_user = os.getenv("DB_USER", "appuser")
db_host = os.getenv("DB_HOST", "localhost")
db_password = os.getenv("DB_PASSWORD")
if not db_password:
raise RuntimeError("DB_PASSWORD is not set")
print(f"Connecting to {db_host} as {db_user}")
# connect_to_db(user=db_user, password=db_password, host=db_host)
if __name__ == "__main__":
main()The password is not stored in the script. It is expected from the environment at runtime.
Secure ways to set the variable
Good: prompt interactively
read -s -p "DB_PASSWORD: " DB_PASSWORD; echo
export DB_PASSWORD
python3 app.py
unset DB_PASSWORDThis avoids putting the password into shell history.
Riskier: inline assignment
DB_PASSWORD='mypassword' python3 app.pyThis is convenient, but not ideal on shared systems because the secret appears in the shell command you typed and may be stored in shell history.
Option 2: Protected file with strict permissions
Another good Linux-native option is to store secrets in a separate file that is readable only by the correct user.This is often better than scattering secrets across shell profiles.
Pros, cons & use cases of using protected files with strict permissions
| Pros | Cons | Use cases |
| Very Linux-friendly | The secret is still plaintext at rest | personal servers |
| simple to manage on servers | File permissions must be correct | simple automation |
| works well with backup and permission controls | Easy to accidentally copy or back up insecurely | system accounts |
| controlled VM environments |
Example secret file
For example, we could have a secret file at this location:
~/.config/myapp/secrets.envThe file could have the following contents:
DB_USER=appuser
DB_HOST=localhost
DB_PASSWORD=replace_meTo protect the file with strict permissions, you'd for example set permissions such as these:
mkdir -p ~/.config/myapp
chmod 700 ~/.config/myapp
chmod 600 ~/.config/myapp/secrets.envThis means:
- The directory is only accessible by your user
- The file is only readable/writable by your user
Python example that uses the secret file
You can either load the secret file from the shell before starting Python, or read it directly.
Method A: source it in the shell
set -a
source ~/.config/myapp/secrets.env
set +a
python3 app.py
Method B: read from the file in Python
from pathlib import Path
def load_secrets_file(path):
data = {}
for line in Path(path).read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
key, value = line.split("=", 1)
data[key.strip()] = value.strip()
return data
def main():
secrets = load_secrets_file(Path.home() / ".config/myapp/secrets.env")
db_password = secrets.get("DB_PASSWORD")
db_user = secrets.get("DB_USER", "appuser")
db_host = secrets.get("DB_HOST", "localhost")
if not db_password:
raise RuntimeError("DB_PASSWORD missing in secrets file")
print(f"Connecting to {db_host} as {db_user}")
# connect_to_db(user=db_user, password=db_password, host=db_host)
if __name__ == "__main__":
main()
Option 3: .env file for development only
A env file is a common developer convenience pattern. It is fine for development if handled carefully, but it should not be treated as a high-security production secret store.
Pros, cons & use cases .env for development only
| Pros | Cons | Use cases |
| Very convenient for development | Easy to commit by mistake | local development |
| common ecosystem pattern | Plaintext file | demos |
| Not the best production choice | non-production setups |
Example .env file
DB_USER=appuser
DB_HOST=localhost
DB_PASSWORD=replace_me
Python example with python-dotenv
Step 1
Install python-dotenv:
pip install python-dotenv
Step 2
Create a script implementing python-dotenv, for example:
import os
from dotenv import load_dotenv
load_dotenv()
def main():
db_user = os.getenv("DB_USER", "appuser")
db_host = os.getenv("DB_HOST", "localhost")
db_password = os.getenv("DB_PASSWORD")
if not db_password:
raise RuntimeError("DB_PASSWORD is not set")
print(f"Connecting to {db_host} as {db_user}")
# connect_to_db(user=db_user, password=db_password, host=db_host)
if __name__ == "__main__":
main()
Recommended pattern
Commit a template instead of an actual .env file containing secrets, for example a file called env.example with the following contents:
DB_USER=appuser
DB_HOST=localhost
DB_PASSWORD=Then each developer creates their own local env file based on the example file.
Option 4: Prompt at runtime with getpass
If a human runs the script manually, the safest simple option is often not to store the password at all.
Pros, cons & use cases of using prompt at runtime with getpass
| Pros | Cons | Use cases |
| Very simple | Not usable for automation | one-off scripts |
| Nothing stored at rest | Still lives in memory while the process runs | admin tools |
| Good for occasional admin scripts | local manual tasks |
Python example
from getpass import getpass
def main():
db_user = "appuser"
db_host = "localhost"
db_password = getpass("Enter DB password: ")
print(f"Connecting to {db_host} as {db_user}")
# connect_to_db(user=db_user, password=db_password, host=db_host)
if __name__ == "__main__":
main()The password:
- is not shown while typing
- is not stored in the script
- does not need to be saved on disk
Option 5: Linux desktop secret store via Python keyring
For developer laptops and workstations, a better solution is to use the OS credential store. On Linux, Python’s keyring library can use:
- Secret Service API
- GNOME Keyring
- KWallet
- other supported backends
Pros, cons & use cases of using a desktop secret store via Python keyring
| Pros | Cons | Use cases |
| Better than plaintext files | Can be harder to set up on headless servers | Local development on Linux desktop |
| Integrates with desktop security mechanisms | Backend support varies by environment | Personal workstation scripts |
| Easy for local user workflows | Developer tooling |
Using a Python keyring
Step 1
Install Python keyring:
pip install keyring
Step 2
Store a password once:
import keyring
from getpass import getpass
service_name = "myapp-db"
username = "appuser"
password = getpass("Enter DB password to store: ")
keyring.set_password(service_name, username, password)
print("Password stored securely.")
Step 3
Retrieve the password which is stored in the keyring in your app
import keyring
def main():
service_name = "myapp-db"
username = "appuser"
db_password = keyring.get_password(service_name, username)
if not db_password:
raise RuntimeError("No password found in keyring")
print("Password loaded from keyring")
# connect_to_db(user=username, password=db_password, host="localhost")
if __name__ == "__main__":
main()
Option 6: systemd service environment files
For production Linux services, systemd is often the right place to inject secrets. Instead of storing the password in the script, you put it in a protected file and let systemd provide it to the process.
Pros, cons & use cases of using a systemd service
| Pros | Cons | Use cases |
| Fits standard Linux service deployment | Secret still exists in plaintext on disk | VM-based production apps |
| Keeps secret outside code | Requires careful permissions and ops hygiene | background daemons |
| Good access control via filesystem permissions | internal services on Linux |
Example application with systemd service environment files
Use the same environment-based Python script from earlier.
Step 1
Create a secret file, for example /etc/myapp/myapp.env and give it the following contents:
DB_USER=appuser
DB_HOST=localhost
DB_PASSWORD=replace_me
Step 2
Adjust the permissions of the directory and files used in step 1, for example:
sudo mkdir -p /etc/myapp
sudo chown root:root /etc/myapp
sudo chmod 755 /etc/myapp
sudo chown root:myapp /etc/myapp/myapp.env
sudo chmod 640 /etc/myapp/myapp.env The file is owned by root, but readable by the myapp group.
Step 3
Next, create a service unit file, for example etc/systemd/system/myapp.service
[Unit]
Description=My Python App
After=network.target
[Service]
User=myapp
Group=myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
Step 4
Start the service you just created:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
Option 7: systemd credentials (more secure than plain env files)
If you want something stronger than a simple environment file under systemd(option 6), the credential mechanism is worth considering. It avoids some of the drawbacks of standard environment variables.
Instead of exposing secrets as normal environment variables, the secret is provided to the service through credential files at runtime. Why is this better?
- It avoids normal environment variable exposure
- Cleaner separation between app and secret source
- More appropriate for serious service deployments
Using systemd credentials is aimed at more security-conscious Linux service deployments and production services managed by systemd. That asides, most of the pros, cons and use cases of using a systemd service also apply here.
Service unit example
[Unit]
Description=My Secure Python App
After=network.target
[Service]
User=myapp
Group=myapp
LoadCredential=db_password:/etc/secretstore/db_password
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
Python example
from pathlib import Path
import os
def get_systemd_credential(name: str) -> str:
creds_dir = os.environ.get("CREDENTIALS_DIRECTORY")
if not creds_dir:
raise RuntimeError("CREDENTIALS_DIRECTORY not set")
return (Path(creds_dir) / name).read_text().strip()
def main():
db_password = get_systemd_credential("db_password")
print("Loaded password via systemd credentials")
# connect_to_db(user="appuser", password=db_password, host="localhost")
if __name__ == "__main__":
main()
Option 8: Docker secrets
If your app runs in containers, environment variables are common, but Docker secrets are safer for sensitive values.
Pros, cons & use cases of using Docker secrets
| Pros | Cons | Use cases |
| Better than baking secrets into images | Depends on deployment setup | Docker Swarm |
| Better than plain container env vars in many cases | Local development may still use env or env vars |
Containerized Linux services |
| Production container deployments |
Example Python code
from pathlib import Path
import os
def get_secret():
secret_file = os.getenv("DB_PASSWORD_FILE", "/run/secrets/db_password")
return Path(secret_file).read_text().strip()
def main():
db_password = get_secret()
print("Loaded password from Docker secret file")
# connect_to_db(user="appuser", password=db_password, host="db")
if __name__ == "__main__":
main() Instead of setting DB_PASSWORD directly, you mount a secret file into the container.
Option 9: Kubernetes secrets
Kubernetes Secrets are a standard way to provide secrets to workloads. They are often mounted as files or exposed as env vars. For security, mounted files are usually preferable to environment variables.
Python example
from pathlib import Path
def main():
db_password = Path("/var/run/secrets/db_password").read_text().strip()
print("Loaded password from mounted Kubernetes secret")
# connect_to_db(user="appuser", password=db_password, host="db")
if __name__ == "__main__":
main() Important note: Kubernetes Secrets are not automatically "high security" just because they are called secrets. Their protection depends on:
- etcd encryption
- RBAC
- pod security
- cluster configuration
Option 10: Cloud or external secret managers
For larger or production-grade systems, dedicated secret managers are often the best option, for example Google Secret Manager or HashiCorp Vault. These can offer:
- Access control
- Auditing
- Secret rotation
- Temporary credentials
- Central management
Dedicated secret managers are best for:
- Production infrastructure
- Multiple services
- Regulated environments
- Teams needing auditability and rotation
Generic Python pattern
def get_secret_from_manager():
# Placeholder for Vault / AWS / Azure / GCP SDK logic
return "retrieved_secret"
def main():
db_password = get_secret_from_manager()
print("Loaded password from external secret manager")
# connect_to_db(user="appuser", password=db_password, host="db")
if __name__ == "__main__":
main()
A combined Python example
This version supports multiple secure sources in a sensible order:
- environment variable
- systemd credential
- local secret file
- interactive prompt
import os
import sys
from pathlib import Path
from getpass import getpass
def get_password() -> str:
pw = os.getenv("DB_PASSWORD")
if pw:
return pw
creds_dir = os.getenv("CREDENTIALS_DIRECTORY")
if creds_dir:
cred_file = Path(creds_dir) / "db_password"
if cred_file.exists():
return cred_file.read_text().strip()
secret_file = Path.home() / ".config/myapp/db_password"
if secret_file.exists():
return secret_file.read_text().strip()
if sys.stdin.isatty():
return getpass("Enter DB password: ")
raise RuntimeError("No password source available")
def main():
db_user = os.getenv("DB_USER", "appuser")
db_host = os.getenv("DB_HOST", "localhost")
db_password = get_password()
print(f"Connecting to {db_host} as {db_user}")
# connect_to_db(user=db_user, password=db_password, host=db_host)
if __name__ == "__main__":
main() This keeps the password out of the script while allowing secure setups in different environments.
Common mistakes when handling secure credentials
-
Putting secrets in Git and relying on
gitignore:gitignoreonly helps before a file is committed. Once committed, the secret is in history.
-
Storing secrets in shell startup files: Putting production passwords in
bashrcorzshrcis often messy and overly broad. Those files load for many shell sessions and can be exposed in backups or support bundles.
-
Using world-readable files: A secret file with
644permissions is not a secret file. Use: chmod 600 secretfile
-
Printing configuration dictionaries: This can accidentally expose values like passwords and API keys.
- Reusing the same password everywhere: If one system leaks, all others are affected.