Encryption, signing, and certificate checks right inside Bash
Anyone managing backups, configuration files, and API secrets from the shell eventually runs into OpenSSL and GPG. This article shows how to use both tools safely and practically in Bash scripts, from symmetric encryption through GPG signing to automated certificate checks without any interactive password prompts.
Table of Contents
- 1. Why OpenSSL and GPG belong in shell scripts
- 2. Symmetric encryption with OpenSSL
- 3. Asymmetric encryption with OpenSSL RSA
- 4. GPG basics: keyrings and keyservers in scripts
- 5. Signing files and verifying signatures with GPG
- 6. Monitoring certificate validity and expiry with OpenSSL
- 7. Secure password handling without plaintext in the process list
- 8. Secrets in shell scripts: environment variables vs. files
- 9. OpenSSL vs. GPG side by side
- 10. Summary
- 11. FAQ
1. Why OpenSSL and GPG belong in shell scripts
Backup scripts that push data unencrypted into an S3 bucket, deployment scripts that pass passwords as plaintext through environment variables, monitoring scripts that only notice an expired TLS certificate once things break: these are real problems in production environments that a few lines of OpenSSL and GPG in shell scripts can solve. Both tools ship preinstalled on nearly every Linux server, have stable CLIs, and can run entirely without interactive input. That is exactly what makes them the first choice for security automation in the shell.
The difference between the two comes down to the use case: OpenSSL in shell scripts is the right choice for TLS certificates, symmetric file encryption with AES, and fast cryptographic hash operations. GPG in shell scripts handles asymmetric encryption with public keys, digital signatures for artifacts and release bundles, and managing a keyring for multiple recipients. Knowing both tools and applying the right one for the job covers the full spectrum of cryptographic shell operations, from simple password encryption to a signed package release.
One important rule before the first code examples: passwords and key material must never be passed as a command line argument. The process list (ps aux) is readable by every user on a multi user system. Both tools offer alternatives: file based password handling, environment variables through dedicated options, or direct pipe input. These techniques are applied consistently in every section that follows.
2. Symmetric encryption with OpenSSL
Symmetric encryption with OpenSSL in shell scripts typically uses AES-256-GCM or AES-256-CBC. AES-256-GCM is the more modern choice: it provides both confidentiality and integrity (authenticated encryption). For backup scripts and encrypting configuration files, that is the recommended algorithm. The command openssl enc -aes-256-gcm handles password derivation, salting, and the actual encryption in a single step. With the option -pass file:/dev/stdin or -pass env:VARNAME, the password never appears in the process list.
For key derivation from a password, always specify -pbkdf2 -iter 600000. Without this option, OpenSSL falls back to the outdated EVP_BytesToKey function, which is considerably weaker against brute force attacks. PBKDF2 with 600,000 iterations matches current NIST recommendations for password based key derivation. If you want to reuse the derived key across multiple operations, derive it once with openssl kdf and store it securely, which avoids repeated derivation inside loops.
#!/usr/bin/env bash
# backup-encrypt.sh: Encrypt backups with AES-256-GCM, no password in process list
set -euo pipefail
BACKUP_DIR="/var/backup"
ENCRYPTED_DIR="/var/backup/encrypted"
PASS_FILE="/etc/backup/.backup_passphrase" # mode 600, owned by root
encrypt_file() {
local src="$1"
local dest="${ENCRYPTED_DIR}/$(basename "$src").enc"
# -pass file: keeps password out of process list; -pbkdf2 uses modern KDF
openssl enc -aes-256-gcm \
-in "$src" \
-out "$dest" \
-pass "file:${PASS_FILE}" \
-pbkdf2 -iter 600000 \
-salt
echo "[OK] Encrypted: $(basename "$src")"
}
decrypt_file() {
local src="$1"
local dest="${BACKUP_DIR}/$(basename "${src%.enc"}")"
openssl enc -aes-256-gcm -d \
-in "$src" \
-out "$dest" \
-pass "file:${PASS_FILE}" \
-pbkdf2 -iter 600000
echo "[OK] Decrypted: $(basename "$src")"
}
mkdir -p "$ENCRYPTED_DIR"
# Encrypt all .sql.gz files created in the last 24 hours
while IFS= read -r -d '' f; do
encrypt_file "$f"
done < <(find "$BACKUP_DIR" -maxdepth 1 -name "*.sql.gz" -mtime -1 -print0)
3. Asymmetric encryption with OpenSSL RSA
For scenarios where several parties need to encrypt data that only one specific party should be able to decrypt, asymmetric encryption with RSA or elliptic curve cryptography is the right approach. OpenSSL in shell scripts offers openssl pkeyutl for this, the more modern interface compared to the older openssl rsautl. RSA can only encrypt small amounts of data directly. That is why the standard pattern in shell scripts is hybrid encryption: encrypt a random symmetric key with the public RSA key, then encrypt the actual data with that session key using AES.
Key generation for production use: openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 creates an RSA 4096 key. For new projects, elliptic curve keys (P-256 or X25519) are the better choice: smaller key size, equal or higher security, faster operations. Important: private keys get file permissions of 600, are never checked into a repository, and for non interactive use should be protected with a passphrase supplied through a secret manager.
#!/usr/bin/env bash
# hybrid-encrypt.sh: Hybrid encryption, RSA key wrap plus AES-256-GCM data encryption
set -euo pipefail
PUBLIC_KEY="/etc/deploy/recipient.pub.pem"
PRIVATE_KEY="/etc/deploy/recipient.priv.pem"
hybrid_encrypt() {
local plaintext="$1"
local outdir="$2"
# Generate a random 32-byte session key
local session_key
session_key="$(openssl rand -hex 32)"
# Encrypt session key with recipient's public RSA key
printf '%s' "$session_key" | \
openssl pkeyutl -encrypt -pubin -inkey "$PUBLIC_KEY" \
-pkeyopt rsa_padding_mode:oaep \
-pkeyopt rsa_oaep_md:sha256 \
> "${outdir}/session.key.enc"
# Encrypt the actual data with the session key (AES-256-GCM)
openssl enc -aes-256-gcm \
-in "$plaintext" \
-out "${outdir}/data.enc" \
-pass "pass:${session_key}" \
-pbkdf2 -iter 1
# Unset session key from memory
unset session_key
echo "[OK] Hybrid-encrypted to ${outdir}/"
}
hybrid_decrypt() {
local indir="$1"
local outfile="$2"
# Recover session key using private key
local session_key
session_key="$(openssl pkeyutl -decrypt -inkey "$PRIVATE_KEY" \
-pkeyopt rsa_padding_mode:oaep \
-pkeyopt rsa_oaep_md:sha256 \
< "${indir}/session.key.enc")"
openssl enc -aes-256-gcm -d \
-in "${indir}/data.enc" \
-out "$outfile" \
-pass "pass:${session_key}" \
-pbkdf2 -iter 1
unset session_key
echo "[OK] Decrypted to ${outfile}"
}
4. GPG basics: keyrings and keyservers in scripts
Using GPG in non interactive shell scripts requires a few specific configuration steps that are not needed in interactive use. The most important flag for shell scripts is --batch combined with --no-tty: without these options, GPG tries to interact with the terminal, which causes cron jobs and CI pipelines to hang. For automated signing operations that have no user sitting at a terminal, the key must be available in the agent without a passphrase, or the GPG agent must be configured to pull the passphrase from a pinentry file.
Using a dedicated GPG home directory (--homedir /etc/deploy/gnupg) with permissions of 700 for scripts running as a system service is an important security measure. This keeps keys out of a regular user's home directory and lets them be locked down granularly through the operating system's access controls. When importing public keys for recipients, --import-options import-minimal is recommended so no unnecessary signatures and user data get imported that would slow down verification.
5. Signing files and verifying signatures with GPG
Digitally signing release artifacts with GPG in shell scripts is a core part of secure release processes. A detached signature (gpg --detach-sign) is preferable to an attached signature because it leaves the original file untouched and verification is possible even without a GPG installation, as long as the public key is known. The signature file gets the extension .sig and is distributed alongside the artifact. For machine verification, gpg --verify --status-fd 2 writes structured output to stderr that a shell script can parse reliably.
Encrypting for multiple recipients, a common scenario in teams, is easier with GPG than with a plain RSA implementation. gpg --encrypt --recipient alice@example.com --recipient bob@example.com produces a package that both recipients can decrypt with their own key. The underlying mechanism is also hybrid encryption: GPG generates a session key internally, encrypts it separately for each recipient, and encrypts the data once with that session key. Combining signing and encryption (--sign --encrypt) in a single step is recommended for scenarios that require both confidentiality and authenticity.
#!/usr/bin/env bash
# gpg-release.sh: Sign release artifacts and verify signatures
set -euo pipefail
GPG_HOMEDIR="/etc/deploy/gnupg"
SIGNING_KEY_ID="0xABCD1234EFGH5678" # Use key fingerprint in production
RELEASE_DIR="/var/releases"
sign_artifact() {
local file="$1"
# --batch and --no-tty prevent interactive prompts in CI
gpg --homedir "$GPG_HOMEDIR" \
--batch --no-tty \
--local-user "$SIGNING_KEY_ID" \
--detach-sign \
--armor \
--output "${file}.sig" \
"$file"
echo "[OK] Signed: $(basename "$file") → $(basename "${file}.sig")"
}
verify_artifact() {
local file="$1"
local sigfile="${file}.sig"
[[ -f "$sigfile" ]] || { echo "[ERROR] Signature file missing: $sigfile" >&2; return 1; }
# --status-fd 2 writes machine-readable status to stderr
local gpg_status
gpg_status="$(gpg --homedir "$GPG_HOMEDIR" \
--batch --no-tty \
--status-fd 1 \
--verify "$sigfile" "$file" 2>/dev/null)"
if echo "$gpg_status" | grep -q "GOODSIG"; then
echo "[OK] Signature valid: $(basename "$file")"
return 0
else
echo "[ERROR] Signature INVALID: $(basename "$file")" >&2
return 1
fi
}
# Sign all .tar.gz files in release directory
while IFS= read -r -d '' f; do
sign_artifact "$f"
done < <(find "$RELEASE_DIR" -maxdepth 1 -name "*.tar.gz" -print0)
# Verify all signatures
while IFS= read -r -d '' f; do
verify_artifact "$f"
done < <(find "$RELEASE_DIR" -maxdepth 1 -name "*.tar.gz" -print0)
6. Monitoring certificate validity and expiry with OpenSSL
TLS certificates that expire unnoticed are one of the most common causes of unplanned outages. With OpenSSL in shell scripts, expiry dates can be checked automatically and alerts triggered early enough. The command openssl x509 -noout -enddate outputs a certificate's expiry date; openssl s_client can fetch the certificate from a running HTTPS server directly, without needing to download a file. For scripts that run regularly via cron, combining both commands is ideal: fetch the certificate, parse the expiry date, compare it against a threshold, and send an alert if needed.
Parsing the expiry date requires a detour through date, because OpenSSL outputs the date in the format notAfter=May 9 12:00:00 2027 GMT. Using date -d "$(openssl x509 -noout -enddate -in cert.pem | cut -d= -f2)" +%s gives you a Unix timestamp you can compare against the current time. Important: on macOS, date expects a different format than on Linux. For portable scripts, either platform detection or using Python for date conversion is the pragmatic solution, or you can consistently rely on openssl x509 -checkend N, which checks directly whether a certificate expires within the next N seconds without any date parsing.
#!/usr/bin/env bash
# cert-monitor.sh: Check TLS certificate expiry for a list of domains
set -euo pipefail
WARN_DAYS=30
CRITICAL_DAYS=7
DOMAINS=(
"mironsoft.de"
"api.mironsoft.de"
"shop.mironsoft.de"
)
check_cert_expiry() {
local domain="$1"
local port="${2:-443}"
# Retrieve certificate from live server; timeout after 5s
local cert
cert="$(echo | timeout 5 openssl s_client \
-servername "$domain" \
-connect "${domain}:${port}" \
2>/dev/null | openssl x509 2>/dev/null)"
[[ -z "$cert" ]] && { echo "[ERROR] Could not retrieve cert for ${domain}" >&2; return 1; }
# openssl -checkend N returns 0 if cert is valid for N more seconds
local warn_secs=$(( WARN_DAYS * 86400 ))
local crit_secs=$(( CRITICAL_DAYS * 86400 ))
if ! echo "$cert" | openssl x509 -noout -checkend "$crit_secs"; then
echo "[CRITICAL] ${domain}: certificate expires in less than ${CRITICAL_DAYS} days!"
return 2
elif ! echo "$cert" | openssl x509 -noout -checkend "$warn_secs"; then
local enddate
enddate="$(echo "$cert" | openssl x509 -noout -enddate | cut -d= -f2)"
echo "[WARN] ${domain}: certificate expires on ${enddate}"
return 1
else
local enddate
enddate="$(echo "$cert" | openssl x509 -noout -enddate | cut -d= -f2)"
echo "[OK] ${domain}: valid until ${enddate}"
return 0
fi
}
exit_code=0
for domain in "${DOMAINS[@]}"; do
check_cert_expiry "$domain" || exit_code=$?
done
exit $exit_code
7. Secure password handling without plaintext in the process list
The biggest security problem when using OpenSSL and GPG in shell scripts is carelessly passing passwords as a command line argument. A call like openssl enc -aes-256-cbc -pass pass:mypassword makes the password visible to every user on the system through ps aux, even if only briefly, for as long as the process runs. That is a real risk on multi user systems, and especially in Kubernetes pods or shared CI environments. The correct approach is always either -pass file:/path/to/passfile, -pass env:VARNAME, or passing it via stdin with -pass fd:0.
For GPG, the equivalent pattern is --passphrase-file /path/to/passfile or --passphrase-fd 0. Password files must have permissions of 600, be owned by root or the service account, and ideally live on a tmpfs filesystem that is never written to disk. An alternative pattern for briefly needed passwords is using a named pipe: mkfifo /tmp/passfifo; echo "passphrase" > /tmp/passfifo & gpg --passphrase-fd 3 3, so the password is never stored in a file at all. For the highest security environments, a secret manager such as HashiCorp Vault or AWS Secrets Manager is the right choice, integrated with the shell through their respective CLIs.
8. Secrets in shell scripts: environment variables vs. files
Whether secrets should be passed as environment variables or as files has no simple answer in shell scripting. Environment variables are readable in /proc/PID/environ, on Linux only by the process owner and root, but in some container environments that exposure can be broader. Files with permissions of 600 are protected by the Unix filesystem but leave persistent traces on disk. The pragmatic solution in modern deployments: fetch secrets from a secret manager, hold them briefly in an environment variable, write them to a temporary file (on tmpfs) for cryptographic operations, then delete them immediately afterward.
Clearing sensitive variables from the shell environment is done with unset VARIABLE_NAME. That does not prevent child processes from inheriting the variable if it was already exported, which is why sensitive variables should be exported as late as possible and cleared as early as possible. For OpenSSL and GPG in shell scripts running in Docker containers or Kubernetes pods, the recommended architecture is: mount the secret as a file into a tmpfs volume via a Kubernetes Secret or Vault Agent, reference the file directly inside the cryptographic operation, and make sure the tmpfs volume never ends up in image layers or container snapshots.
9. OpenSSL vs. GPG side by side
Both tools overlap in some areas but have clearly distinct strengths. Choosing between OpenSSL and GPG in shell scripts depends on the concrete use case; the table below shows the most important differences.
| Criterion | OpenSSL | GPG | Recommendation |
|---|---|---|---|
| Symmetric file encryption | AES-256-GCM, PBKDF2 | AES-256, passphrase | OpenSSL for scripts |
| Multiple recipients | Manual hybrid impl. | --recipient native | GPG |
| Digital signature | openssl dgst -sign | --detach-sign, web of trust | GPG for releases |
| TLS certificate checks | openssl s_client, x509 | Not designed for this | OpenSSL |
| Non interactive operation | Native, no agent needed | --batch --no-tty required | OpenSSL is simpler |
| Keyring / team use | Manage manually | Keyserver, web of trust | GPG |
In practice this is not an either-or decision: deployment scripts can use OpenSSL for backup encryption and TLS checks while using GPG for verifying downloaded packages and signing release artifacts at the same time. What matters is that the chosen method is applied consistently, with no shortcuts on password handling.
Mironsoft
Shell security, cryptography integration, and secrets management
Is the cryptography in your shell scripts done right?
We audit existing backup and deployment scripts for insecure password handling, weak algorithms, and missing certificate checks, and replace fragile solutions with robust OpenSSL and GPG integration.
Security audit
Review shell scripts for insecure password handling and weak algorithms
Backup encryption
Build AES-256-GCM backup pipelines with secure key management
Certificate monitoring
Set up automated TLS monitoring with OpenSSL and alert integration
10. Summary
Putting OpenSSL and GPG in shell scripts to practical use requires consistently following a handful of ground rules: never pass passwords as a command line argument, always set --batch and --no-tty for non interactive GPG operation, use AES-256-GCM with PBKDF2 for symmetric encryption, and use openssl x509 -checkend for portable certificate checks. Hybrid encryption combines the strengths of both worlds: RSA or GPG keys for key distribution, AES for the bulk of the actual data.
The most important takeaway for teams: cryptography code in shell scripts is not a one time setup, it needs to be reviewed regularly. Algorithms age, keys expire, new attack vectors appear. Consistent monitoring of certificate expiry and regular rotation of encryption keys are just as much a part of secure shell automation as getting the initial implementation right.
OpenSSL and GPG in Shell Scripts: The Essentials at a Glance
Password security
Never -pass pass:word, always -pass file:, -pass env:, or -pass fd:0. Passwords in the process list are readable on multi user systems.
Choosing algorithms
AES-256-GCM with PBKDF2 (600k iterations) for symmetric encryption. RSA-OAEP-SHA256 or X25519 for asymmetric. Avoid outdated DES/3DES.
GPG non interactive
--batch --no-tty for CI and cron. Dedicated --homedir with mode 700 for system services. --status-fd 1 for machine readable output.
Certificate monitoring
openssl x509 -checkend N for portable expiry checks. openssl s_client for live servers. Send an alert 30 days before expiry.