Using OpenSSL and GPG in Shell Scripts
AI generated
Bash · OpenSSL · GPG · Security · DevOps
Using OpenSSL and GPG in Shell Scripts
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.

18 min read OpenSSL · GPG · Signing · Password Handling · Certificates Bash 4.x · 5.x · Linux · macOS

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.

11. FAQ: OpenSSL and GPG in Shell Scripts

1Why no passwords as a command line argument?
The process list (ps aux) is readable by every user. -pass pass:word exposes the password for the whole lifetime of the process. Use -pass file:, -pass env:, or stdin instead.
2AES-256-CBC vs. AES-256-GCM?
GCM is authenticated encryption, providing confidentiality and integrity. CBC only provides confidentiality; tampering goes unnoticed. Always choose GCM for new scripts.
3Why -pbkdf2 with openssl enc?
Without pbkdf2, EVP_BytesToKey is used, which is outdated and weak against brute forcing. PBKDF2 with 600,000 iterations matches NIST recommendations for 2026.
4GPG in a cron job without interactive input?
gpg --batch --no-tty --passphrase-file /path/to/file. The passphrase file must have permissions of 600. Alternatively, use a GPG agent with a preloaded key.
5Checking a TLS certificate for upcoming expiry?
openssl x509 -noout -checkend N: exit code 1 if it expires in less than N seconds. For 30 days: N=2592000. For live servers, fetch it first with openssl s_client.
6OpenSSL or GPG, when to use which?
OpenSSL: TLS certificates, symmetric encryption, hashes. GPG: asymmetric encryption with multiple recipients, release signatures, team keyring management.
7What is hybrid encryption?
RSA encrypts a random session key; AES encrypts the actual data. Combines the strengths of both approaches. GPG handles this internally and automatically.
8GPG signing in a CI pipeline?
gpg --batch --no-tty --local-user KEY_ID --detach-sign --armor. Store the private key securely as a CI secret. Remove it from the keyring right after signing.
9Keeping sensitive variables from being inherited by child processes?
unset VARNAME right after use. Do not export the variable. For external commands: env -i VAR=value command, which inherits only explicitly given variables.
10Parsing openssl s_client reliably in scripts?
echo | timeout 5 openssl s_client -connect host:443 prevents hanging. Pipe the output through openssl x509 2>/dev/null. Separate connection errors from certificate errors.