A Lazy Bash Script Alternative to KeePass for Storing Secrets

May 1st, 2016 Permalink

Every development project generates secrets: passwords, keys, and access tokens, as well as third party master credentials for any number of useful services ranging from hosting to log analysis. Even when using services and systems that enable each developer to have their own account and level of access, it is impossible to entirely eliminate a core set of secrets that must be stored securely and kept up to date. It is common practice even in larger organizations to place such core secrets into an encrypted, password-protected store that is checked in to a version control system. Since versioned repositories are one of the first things to be both offsite and regularly backed up in any endeavor, this approach solves several problems. Access to the encrypted store is then provided on a need to know basis.

The model of a single store with a single password isn't suitable for general storage of all secrets. It is important to be able to revoke access individually to any import systems, among other reasons. Nonetheless, the single store is convenient and effortless for secrets such as third party root credentials that are used only infrequently and by a small group of people. One useful tool is the open source KeePass application, which provides a GUI front end to an encrypted file. If, however, all you really care about is a set of text files, and don't need a GUI, then a simple bash script and some of the standard Linux tools will achieve the same goal.

Managing Secrets with a Bash Script

The script below is a bare-bones example, but works just fine in and of itself. Add it to the root of a Git repository as manage-secrets and make it executable. Then edit .gitignore to exclude the directory containing unencrypted secrets, and the backup of that directory that is made on decryption:

# Unencrypted secrets, not to be committed.
secrets/*
secrets-bak/*

To encrypt the current contents of secrets and write them to a secrets.tar.gz.enc archive file:

./manage-secrets --encrypt

When decrypting the archive file, the secrets directory, if it exists, will be moved to secrets-bak to help prevent accidental deletion of work in progress. To decrypt secrets.tar.gz.enc to the secrets directory:

./manage-secrets --decrypt

In both cases you will be prompted to provide a password. When updating secrets, decrypt the latest files, edit them as needed, encrypt to the archive, then commit and push the updated archive.

The manage-secrets Script

#!/bin/bash
#
# A script to manage an encrypted store of secrets.
#

# ----------------------------------------------------------------------------
# Usage and error handling.
# ----------------------------------------------------------------------------

set -o errexit
set -o nounset

# Exit on error.
function error() {
  echo 1>&2 "ERROR: $@"
  exit 1
}

function usage () {
  cat <<EOF

Usage: ${0#./} [OPTION]...

The following options are supported:

  --decrypt
    Decrypt ./secrets.tar.gz.enc to ./secrets

  --encrypt
    Encrypt files in ./secrets to ./secrets.tar.gz.enc

All options prompt for a password. Use the password provided to you.
EOF
}

# Send errors through the error function.
trap 'error ${LINENO}' ERR

# ----------------------------------------------------------------------------
# Variables.
# ----------------------------------------------------------------------------

# Get a normalized absolute path to the password directory.
DIR="$( cd "$( dirname "$0" )" && pwd)"
UNENCRYPTED_DIRNAME="secrets"
UNENCRYPTED_DIR="${DIR}/${UNENCRYPTED_DIRNAME}"
UNENCRYPTED_DIR_BAK="${UNENCRYPTED_DIR}-bak"
UNENCRYPTED_ARCHIVE="${DIR}/secrets.tar.gz"
ENCRYPTED_ARCHIVE="${UNENCRYPTED_ARCHIVE}.enc"

# ----------------------------------------------------------------------------
# Encrypt or decrypt.
# ----------------------------------------------------------------------------

if [ "$#" -ne 1 ]; then
  usage
  exit 1
fi

case "${1}" in
  # Decrypt from archive.
  --decrypt)

    openssl des3 -salt -d \
      -in "${ENCRYPTED_ARCHIVE}" \
      -out "${UNENCRYPTED_ARCHIVE}"

    if [ -d "${UNENCRYPTED_DIR}" ]; then
      rm -Rf "${UNENCRYPTED_DIR_BAK}"
      mv "${UNENCRYPTED_DIR}" "${UNENCRYPTED_DIR_BAK}"
    fi

    tar -C "${DIR}" -zvxf "${UNENCRYPTED_ARCHIVE}"
    rm -f "${UNENCRYPTED_ARCHIVE}"
    ;;

  # Encrypt to archive.
  --encrypt)
    rm -f "${UNENCRYPTED_ARCHIVE}"
    tar -C "${DIR}" -zcvf "${UNENCRYPTED_ARCHIVE}" "${UNENCRYPTED_DIRNAME}"

    openssl des3 -salt \
      -in "${UNENCRYPTED_ARCHIVE}" \
      -out "${ENCRYPTED_ARCHIVE}"

    rm -f "${UNENCRYPTED_ARCHIVE}"
    ;;

  # Utility options.
  -h|--help)
    usage
    exit 0
    ;;

  *)
    error "Unrecognized option ${1}"
    ;;
esac