How to Hide Secrets in Unix Command Line Parameters to Prevent Exposure in ps -ef Output
In Unix and Linux systems, command-line parameters are a common way to pass arguments to scripts and programs. However, this convenience comes with a critical security risk: secrets like passwords, API keys, or tokens passed as command-line arguments are visible to anyone with access to the system via tools like ps -ef. This exposure can lead to unauthorized access, data breaches, or misuse of sensitive credentials.
Whether you’re a system administrator automating backups, a developer testing APIs, or a DevOps engineer deploying services, understanding how to protect secrets from being leaked in process lists is essential. In this blog, we’ll demystify why command-line parameters are risky, explore practical methods to hide secrets, and share best practices to keep your sensitive data secure.
Table of Contents#
- The Problem: Why Command Line Parameters Expose Secrets
- Why Command Line Arguments Are Insecure: Beyond
ps -ef - Methods to Hide Secrets: Secure Alternatives
- Best Practices for Secure Secret Handling
- Conclusion
- References
The Problem: Why Command Line Parameters Expose Secrets#
How ps -ef Reveals Process Details#
The ps -ef command lists running processes on a Unix system, including their command-line arguments. This is because Unix stores process metadata, including the command used to launch the process and its arguments, in the /proc filesystem (specifically, /proc/<pid>/cmdline). When you run ps -ef, it reads this file to display process details.
By default, /proc/<pid>/cmdline is world-readable (or readable by all users with access to the system), meaning any user can run ps -ef and see the command-line arguments of all processes (unless restricted by system policies like procfs permissions).
Example: Exposed Secrets in ps Output#
Suppose you run a script to back up a database, passing the database password as a command-line argument:
# Risky: Secret in command-line argument
./backup_script.sh --db-user admin --db-pass "MySuperSecret123!"Now, another user on the system runs ps -ef | grep backup_script.sh:
user 1234 5678 0 10:00 pts/0 00:00:00 ./backup_script.sh --db-user admin --db-pass MySuperSecret123!
The password MySuperSecret123! is plainly visible. This is a critical vulnerability—an attacker with access to the system can steal the secret in seconds.
Why Command Line Arguments Are Insecure: Beyond ps -ef#
Even if you avoid ps -ef, command-line arguments pose additional risks:
Persistence in Logs and Shell History#
Command-line arguments are often logged by system tools (e.g., auditd, syslog) or stored in shell history (e.g., ~/.bash_history). For example, if you run the risky backup command above, bash will save it to ~/.bash_history by default, leaving the secret exposed even after the process exits.
Inheritance by Child Processes#
When a process spawns child processes, its command-line arguments may be inherited or leaked to the children. Tools like strace or ltrace can also intercept arguments during process execution, increasing exposure.
Methods to Hide Secrets: Secure Alternatives#
Let’s explore proven techniques to pass secrets without exposing them in command-line arguments.
3.1 Environment Variables#
Environment variables store data that processes can access at runtime, but they are not included in the command-line arguments visible via ps -ef. Instead, they live in the process’s environment block, stored in /proc/<pid>/environ.
How It Works#
Environment variables are set before launching a process (e.g., via export in bash) and accessed by the process using $VARIABLE_NAME. Since they’re not part of cmdline, ps -ef won’t display them.
Example: Using Environment Variables#
-
Set the secret as an environment variable (avoid storing it in shell history by prefixing the command with a space, if
HISTCONTROL=ignorespaceis enabled):# Set the secret (space at start prevents logging to bash_history) export DB_PASS="MySuperSecret123!" -
Use the variable in your script (e.g.,
backup_script.sh):# In backup_script.sh DB_USER="admin" mysql -u "$DB_USER" -p"$DB_PASS" -e "BACKUP DATABASE mydb;" -
Verify with
ps -ef:ps -ef | grep backup_script.sh # Output: user 1234 5678 0 10:00 pts/0 00:00:00 /bin/bash ./backup_script.shThe secret
MySuperSecret123!is not visible.
Caveats: /proc/<pid>/environ and Permissions#
While environment variables hide secrets from ps -ef, they are stored in /proc/<pid>/environ. By default, this file is readable only by the process owner and root (thanks to procfs permissions: r-------- for the owner). Thus, non-root users cannot access another user’s environment variables. For most use cases, this is secure enough.
3.2 Reading Secrets from Files#
Storing secrets in a file with restrictive permissions (e.g., chmod 600) and reading them into your script avoids exposing the secret in command-line arguments.
How It Works#
The file path may appear in ps -ef (e.g., cat /path/to/secret.txt), but the content of the file (the secret) remains hidden. With chmod 600, only the owner can read the file, preventing unauthorized access.
Example: Reading from a File#
-
Create a secure secret file (restrict permissions to owner-only):
# Create the file echo "MySuperSecret123!" > /home/user/.secrets/db_pass.txt # Restrict permissions (read/write for owner only) chmod 600 /home/user/.secrets/db_pass.txt -
Read the secret in your script:
# In backup_script.sh DB_USER="admin" DB_PASS=$(cat /home/user/.secrets/db_pass.txt) # Read secret from file mysql -u "$DB_USER" -p"$DB_PASS" -e "BACKUP DATABASE mydb;" -
Check
ps -ef:ps -ef | grep backup_script.sh # Output: user 1234 5678 0 10:00 pts/0 00:00:00 /bin/bash ./backup_script.shThe command
cat /home/user/.secrets/db_pass.txtmay appear inpsbriefly (during the$(...)subshell), but this is rare and the secret itself is never exposed.
3.3 Passing Secrets via Standard Input (stdin)#
Many tools (e.g., mysql, curl, ssh) accept input via standard input (stdin). Using stdin to pass secrets avoids command-line arguments entirely.
Here-Documents (<<) and Here-Strings (<<<)#
Here-documents (<<) and here-strings (<<<) let you pass data to a command via stdin. For example, mysql can read a password from stdin using the -p flag with no argument.
Example: Using stdin with mysql#
Instead of passing the password via -p"secret", use a here-string to send it via stdin:
# Secure: Pass secret via stdin
echo "MySuperSecret123!" | mysql -u admin -p -e "BACKUP DATABASE mydb;"- The
-pflag with no argument tellsmysqlto read the password from stdin. ps -efwill showmysql -u admin -p -e BACKUP DATABASE mydb;—no secret exposed.
Example: Using curl with stdin#
For APIs requiring authentication, use curl with --data @- to read the secret from stdin:
# Pass API key via stdin to curl
echo '{"api_key": "MySuperSecret123!"}' | curl -X POST -H "Content-Type: application/json" -d @- https://api.example.com3.4 Interactive Input with read Command#
For user-driven scripts, use the read command to prompt for secrets interactively. The -s flag suppresses input echoing, so the secret isn’t visible on the terminal.
Example: Secure Script Prompt#
#!/bin/bash
# interactive_backup.sh
read -s -p "Enter database password: " DB_PASS # -s hides input
echo # Newline after input
DB_USER="admin"
mysql -u "$DB_USER" -p"$DB_PASS" -e "BACKUP DATABASE mydb;"- When run, the script prompts the user to type the password, which isn’t displayed.
- Limitation: Not suitable for automated workflows (e.g., cron jobs), as it requires human input.
3.5 Keyring and Secret Management Tools#
For advanced use cases, leverage dedicated secret managers like pass, gnome-keyring, or vault to store and retrieve secrets securely.
Example: Using pass (Password Store)#
pass is a command-line password manager that encrypts secrets with GPG.
-
Install
passand initialize a password store:sudo apt install pass # Debian/Ubuntu pass init your_gpg_key_id # Replace with your GPG key ID -
Store the secret:
pass insert myapp/db_pass # Prompts for the secret (not logged) -
Retrieve the secret in a script:
# In backup_script.sh DB_PASS=$(pass show myapp/db_pass) mysql -u admin -p"$DB_PASS" -e "BACKUP DATABASE mydb;"ps -efwill showpass show myapp/db_pass, but the secret itself remains encrypted until retrieved.
Best Practices for Secure Secret Handling#
- Restrict File Permissions: Always set secret files to
chmod 600(read/write for owner only) to prevent access by other users. - Avoid Shell History: Prefix commands with a space (if
HISTCONTROL=ignorespace) or usehistory -d $(history | tail -n1 | awk '{print $1}')to delete sensitive commands from history. - Limit Environment Variable Exposure: Never export secrets in shared shells (e.g.,
screen,tmux). Use tools likedirenvto manage per-project environment variables securely. - Use Least Privilege: Ensure the user running the script has minimal permissions (e.g., a dedicated service account with no sudo access).
- Rotate Secrets: Regularly rotate secrets to minimize damage if they’re accidentally exposed.
Conclusion#
Exposing secrets in Unix command-line parameters is a critical but avoidable risk. By using environment variables, secure files, stdin, interactive prompts, or secret managers, you can keep sensitive data hidden from ps -ef and other exposure vectors. Always prioritize least privilege, restrict access to secrets, and stay vigilant about logging and inheritance risks. With these practices, you’ll significantly reduce the chance of secret leaks in your Unix workflows.
References#
- proc(5) - Linux man page (details on
/proc/pid/cmdlineand/proc/pid/environ). - ps(1) - Linux man page (process listing).
- OWASP Secrets Management Cheat Sheet.
- pass: The Standard Unix Password Manager.
- MySQL Documentation: Secure Password Handling.