How to Create a Bash Password Generator

How to Create a Bash Password Generator

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/urandom is 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 16 takes exactly 16 characters from what’s left.
  • echo just 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 pipefail is something I add to almost every serious script I write. -e exits immediately if a command fails, -u treats unset variables as an error, and -o pipefail makes a pipeline fail if any command inside it fails, not just the last one.
  • getopts is Bash’s built-in option parser. The string "l:sn:h" tells it that -l and -n expect an argument (indicated by the colon), while -s and -h are 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 for loop 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 passwd or 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 $RANDOM variable is a simple pseudo-random number generator seeded from the process ID and time; it is predictable and unsuitable for anything security-related. /dev/urandom pulls 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_history if I run this interactively for something sensitive.
  • Set strict file permissions. If a password is written to disk, I immediately run chmod 600 on 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 rand as an alternative. openssl rand -base64 24 is another cryptographically sound option that ships on most systems and can be a nice complement to the tr/urandom approach.

Optimization Tips

  • Piping /dev/urandom through tr can occasionally be slower than expected on constrained systems because tr has to filter out a lot of unwanted bytes. If speed matters, openssl rand -base64 is often faster since it doesn’t need character-by-character filtering.
  • For generating many passwords at once, avoid calling tr and head in 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=C before 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 -c never got enough matching bytes, which shouldn’t happen with /dev/urandom but can occur if you accidentally point it at /dev/random on a low-entropy system instead.
  • Passwords look weaker than expected: double-check your CHARSET variable — a stray character class in the tr set can silently narrow the pool of characters.

Common Mistakes to Avoid

  • Using $RANDOM for 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/urandom details): https://man7.org/linux/man-pages/man4/random.4.html
  • OpenSSL rand documentation: https://www.openssl.org/docs/man1.1.1/man1/rand.html
Total
2
Shares

Leave a Reply

Previous Post
How to Use Case Statements in Bash

How to Use Case Statements in Bash

Next Post
How to Automate Backup Tasks in Bash

How to Automate Backup Tasks in Bash

Related Posts