Every few months I find myself needing a fresh, strong password for a new server, a database user, or some API key I’m about to rotate. Instead of relying on a browser extension or an online generator (which I never fully trust with something as sensitive as a password), I built my own password generator directly in Bash. It runs locally, it’s fast, and I can tweak it however I like. In this article I’ll walk you through exactly how I built it, from the simplest one-liner to a fully-featured script with options, validation, and security safeguards.
Why Build a Password Generator in Bash?
Bash is already installed on almost every Linux server and macOS machine I touch, so there’s no dependency to install and no third-party tool to trust. A password generator written in Bash is:
- Portable across almost any Unix-like system
- Easy to audit line by line (you can literally read every character of logic)
- Fast to run as part of provisioning scripts, CI pipelines, or onboarding automation
- Customizable to match whatever password policy I’m dealing with that day
The Simplest Version: A One-Liner
Before building anything elaborate, it helps to understand the core idea. The simplest password generator in Bash looks like this:
< /dev/urandom tr -dc 'A-Za-z0-9' | head -c 16; echo
Here’s what’s happening internally:
/dev/urandomis a special file in Linux that produces a stream of pseudo-random bytes on demand. It’s the same source of randomness the kernel uses internally for cryptographic operations.tr -dc 'A-Za-z0-9'filters that stream, deleting (-d) any byte that is not (-c, meaning complement) in the set of uppercase letters, lowercase letters, and digits.head -c 16takes exactly 16 characters from what’s left.echojust adds a trailing newline so the output looks clean in the terminal.
This one-liner works, but it has no options, no way to include symbols, and no error handling. Let’s build something more robust.
Step-by-Step: Building a Full Script
I like to structure my scripts so they accept flags, similar to a real CLI tool. Here’s the full script I actually use:
#!/usr/bin/env bash
set -euo pipefail
# Default configuration
LENGTH=16
USE_SYMBOLS=false
COUNT=1
usage() {
echo "Usage: $0 [-l length] [-s] [-n count]"
echo " -l Password length (default: 16)"
echo " -s Include symbols"
echo " -n Number of passwords to generate (default: 1)"
exit 1
}
while getopts "l:sn:h" opt; do
case "$opt" in
l) LENGTH="$OPTARG" ;;
s) USE_SYMBOLS=true ;;
n) COUNT="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
if ! [[ "$LENGTH" =~ ^[0-9]+$ ]] || [ "$LENGTH" -lt 8 ]; then
echo "Error: length must be a number >= 8" >&2
exit 1
fi
CHARSET='A-Za-z0-9'
if [ "$USE_SYMBOLS" = true ]; then
CHARSET='A-Za-z0-9!@#$%^&*()_+-='
fi
generate_password() {
tr -dc "$CHARSET" < /dev/urandom | head -c "$LENGTH"
echo
}
for ((i = 0; i < COUNT; i++)); do
generate_password
done
Save this as pwgen.sh, make it executable, and run it:
chmod +x pwgen.sh
./pwgen.sh -l 20 -s -n 3
Example output:
xT9!mQ2z@Lp8#Vr6_Kd1
qW3z*Nf7$Ht2^Bc9=Ry5
Zm4@Xk8!Gs1#Pn6_Vq3+
Explaining the Script Internally
set -euo pipefailis something I add to almost every serious script I write.-eexits immediately if a command fails,-utreats unset variables as an error, and-o pipefailmakes a pipeline fail if any command inside it fails, not just the last one.getoptsis Bash’s built-in option parser. The string"l:sn:h"tells it that-land-nexpect an argument (indicated by the colon), while-sand-hare simple flags.- The
[[ "$LENGTH" =~ ^[0-9]+$ ]]check uses a regex match to make sure the user didn’t pass something like-l abc. tr -dc "$CHARSET"is reused from the one-liner, but now the character set is dynamic depending on whether symbols were requested.- The
forloop with((i = 0; i < COUNT; i++))is Bash’s C-style arithmetic loop, letting me generate multiple passwords in one call.
Real-World Use Cases
I use variations of this script for:
- Server provisioning: generating a random root password when spinning up a new VM, then piping it directly into
passwdor a cloud-init template. - Database user creation: generating credentials for a new MySQL or PostgreSQL user as part of a setup script.
- CI/CD secrets: creating temporary tokens for test environments that get discarded after the pipeline run.
- Bulk account creation: combined with
-n, generating dozens of unique passwords for a batch of new user accounts.
A common pattern I use is writing the generated password straight into a secrets manager instead of a file:
PASSWORD=$(./pwgen.sh -l 24 -s)
vault kv put secret/myapp/db-password value="$PASSWORD"
Automation Example: Auto-Rotating a Service Account Password
Here’s a small automation snippet I run on a schedule via cron to rotate a service account password monthly:
#!/usr/bin/env bash
set -euo pipefail
NEW_PASS=$(tr -dc 'A-Za-z0-9!@#$%^&*' < /dev/urandom | head -c 24)
echo "Rotating password for svc_account..."
echo "svc_account:${NEW_PASS}" | chpasswd
echo "${NEW_PASS}" > /root/.svc_account_pass
chmod 600 /root/.svc_account_pass
Scheduled with:
0 3 1 * * /usr/local/bin/rotate_password.sh
Security Considerations
This is the part I take most seriously, because a password generator is only as good as its randomness and its handling of the output.
- Always use
/dev/urandom, never$RANDOM. Bash’s built-in$RANDOMvariable is a simple pseudo-random number generator seeded from the process ID and time; it is predictable and unsuitable for anything security-related./dev/urandompulls from the kernel’s cryptographically secure entropy pool. - Never log generated passwords. I avoid
echo-ing passwords into shell history or CI logs. I mask output in CI systems and clear my.bash_historyif I run this interactively for something sensitive. - Set strict file permissions. If a password is written to disk, I immediately run
chmod 600on that file so only the owner can read it. - Avoid predictable character sets. If your charset always starts with the same letters or excludes ambiguous characters inconsistently, you can accidentally reduce entropy. I keep my sets broad and consistent.
- Consider
openssl randas an alternative.openssl rand -base64 24is another cryptographically sound option that ships on most systems and can be a nice complement to thetr/urandomapproach.
Optimization Tips
- Piping
/dev/urandomthroughtrcan occasionally be slower than expected on constrained systems becausetrhas to filter out a lot of unwanted bytes. If speed matters,openssl rand -base64is often faster since it doesn’t need character-by-character filtering. - For generating many passwords at once, avoid calling
trandheadin a loop for every single password; instead, pull a larger block of random data once and slice it in memory where possible.
Troubleshooting
- “tr: Illegal byte sequence” errors: this usually happens when your locale settings misinterpret raw binary data. Fix it by setting
LC_ALL=Cbefore running the pipeline:LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16 - Script hangs with no output: this typically means
head -cnever got enough matching bytes, which shouldn’t happen with/dev/urandombut can occur if you accidentally point it at/dev/randomon a low-entropy system instead. - Passwords look weaker than expected: double-check your
CHARSETvariable — a stray character class in thetrset can silently narrow the pool of characters.
Common Mistakes to Avoid
- Using
$RANDOMfor anything security-sensitive. - Forgetting to quote variables like
"$LENGTH", which can cause word-splitting bugs. - Hardcoding password length below 12 characters for anything facing the internet.
- Writing generated passwords to world-readable files.
FAQs
Is /dev/urandom really secure enough for passwords? Yes. On modern Linux kernels, /dev/urandom is backed by a cryptographically secure pseudo-random number generator and is considered safe for generating passwords, keys, and tokens.
Can I generate passwords without symbols for systems that don’t allow them? Yes, just omit the -s flag in the script above, and it will default to alphanumeric-only output.
How long should my generated passwords be? I personally default to at least 16 characters for user accounts and 24+ for service accounts or API keys.
Can this script run on macOS? Yes, macOS ships with Bash (or you can install a newer version via Homebrew) and /dev/urandom behaves the same way.
Summary
Building a password generator in Bash turned out to be a great way to understand how randomness, character filtering, and shell scripting all fit together. Starting from a simple tr/urandom one-liner, I extended it into a flexible script with flags for length, symbols, and batch generation, while keeping security front and center by relying on /dev/urandom instead of Bash’s weaker built-in randomness.
References
- GNU Coreutils manual for
tr: https://www.gnu.org/software/coreutils/manual/html_node/tr-invocation.html - Bash Reference Manual: https://www.gnu.org/software/bash/manual/bash.html
- Linux
random(4)man page (for/dev/urandomdetails): https://man7.org/linux/man-pages/man4/random.4.html - OpenSSL
randdocumentation: https://www.openssl.org/docs/man1.1.1/man1/rand.html