Working with several workstations and shared dot-files with lots of credentials and configurations doesn’t feel very secure nowadays. There have been libraries that steal your credentials and environment variables during the installation process. No bueno. 1 2 3 4

I came up with something to make me and my day-to-day work feel safer. It is a file with some bash functions to encrypt and decrypt .rc and open shells with those files.

I use several YubiKeys for it depending on what I am working on, that way, everything is contained and “properly” separated.

The special feature of the YubiKey I use is that the GPG key are stored inside the stick itself. Not in my computer. So anybody wanting to use it will require physical access to it and the passphrase.

If you want to know how to configure the YubiKey, there is no better guide than this one

What is a secure shell?

We can say a “secure shell” is a ephemeral shell with encrypted variables.

How do I use a secure shell?

In the .zshrc file, instead of having dangerous secrets right there for the taking. There is only a way to load secure shells with those secrets in a secure shell.

# .zshrc

# load secure shell functions
source "$HOME/secureshell.sh"

# work settings
alias edit_workterm='encrypt_rc "$HOME/workrc.gpg"'
alias workterm='secure_shell "$HOME/workrc.gpg"'

# oss setting
alias edit_ossterm='encrypt_rc "$HOME/oss.gpg"'
alias ossterm='secure_shell "$HOME/oss.gpg"'

The code of the implementation is simple, it has two functions:

  1. encrypt_rc to decrypt the .rc, edit, and encrypt files
  2. secure_shell to decrypt the .rc, and open an ephemeral zsh instance with it

The file is at $HOME/secureshell.sh

#!/bin/bash
# secureshell.sh

function encrypt_rc {
  TARGET_FILE="$1"

  # Ensure our function gets called using a gpg file
  if [ -z "$TARGET_FILE" ]; then
    echo "ASSERT: MISSING TARGET_FILE";
    echo "usage: ";
    echo "  encrypt_rc ./target_file.gpg ";
    return 1
  else
    # Create a temp file to store our unencrypted .rc file
    TMP_FILE=$(mktemp /tmp/log.XXXXXXXXXXXXXXXXXXXXXXXX)

    # Ensure file deletion after we finish, whatever signal is raised
    trap 'rm -f $TMP_FILE' 0 1 2 3 13 15

    echo "Decrypting '$TARGET_FILE' touching the YubiKey and passphrase may be required."
    touch "$TARGET_FILE"
    if gpg --decrypt "$TARGET_FILE" > "$TMP_FILE"; then
      echo "Failed to decrypt with gpg"
      return 0
    else
      vi "$TMP_FILE"
      echo 'Encrypting file. Touching the YubiKey and passphrase may be required...'

      # Here you specify the recipients, I have several YubiKeys
      # for different porpuses.

      gpg \
        --recipient [email protected] \
        --recipient [email protected] \
        --armor --encrypt < "$TMP_FILE" > "$TARGET_FILE"
    fi
  fi
}

function secure_shell {
  SECURE_FILE="$1"

  if [ -z "$SECURE_FILE" ]; then
    echo "ASSERT: MISSING SECURE_FILE";
    echo "usage:";
    echo "  secure_shell secure_file.gpg";
    return 1
  else
    # Create a temp file to store our unencrypted file
    TMP_FILE=$(mktemp /tmp/log.XXXXXXXXXXXXXXXXXXXXXXXX)

    # this trap makes the parent shell print a line and just in case delete the
    # temporary file after it gets closed
    trap "echo '<<< Secure shell closed >>>' && rm -f ${TMP_FILE}" 0 1 2 3 13 15

    echo "Decrypting '$SECURE_FILE', touching the YubiKey and passphrase may be required."

    # The first thing our TMP_FILE.rc will do is to delete itself.
    { echo "#!/bin/zsh"
      echo "rm -f '$TMP_FILE'"
      echo 'echo "This shell will be closed after 5 minutes of inactivity."'
    } >> "$TMP_FILE"

    # Then we print the contents of the encrypted .rc file to our TMP_FILE
    if gpg --quiet --decrypt "$SECURE_FILE" >> "$TMP_FILE"; then
      TMP_SAFE_SHELL=$(basename "$SECURE_FILE")
      # Set an inactivity timeout
      { echo 'TMOUT=300'
        echo 'readonly TMOUT'
        echo 'export TMOUT'
        echo "export SAFE_SHELL=\"${TMP_SAFE_SHELL}\""
        # I have an if in the .zsh-theme that changes the colors
        # in case of SAFE_SHELL and prints the name of the safe file
        echo 'readonly SAFE_SHELL'
        echo 'zsh -i -s'
      } >> "$TMP_FILE"
      echo '<<< Opening secure shell >>>'
      zsh -c "source $TMP_FILE"
    else
      echo "! Error decrypting $SECURE_FILE"
      return 1
    fi
  fi
}

Finally, I added a theme variation to the .zshrc to identify we are in a secure shell:

The full .zshrc looks like this:

# full .zshrc

# load secure shell scripts (the previous code)
source "$HOME/secureshell.sh"

# work settings
alias edit_workterm='encrypt_rc "$HOME/workrc.gpg"'
alias workterm='secure_shell "$HOME/workrc.gpg"'

# oss setting
alias edit_ossterm='encrypt_rc "$HOME/oss.gpg"'
alias ossterm='secure_shell "$HOME/oss.gpg"'

# load zsh theme
source "$HOME/.zsh-theme"
if [ -n "$SAFE_SHELL" ]; then
  # in safe shell mode, show a Lock  and the name of the safe shell file
  PROMPT="%{$fg[red]%} $SAFE_SHELL %{$reset_color%}${_current_dir}%{$fg[$CARETCOLOR]%}❭ %{$resetcolor%}"
fi