A Lazy Bash Script Alternative to KeePass for Storing Secrets
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