On Mon, Dec 21, 2020 at 04:35:14PM -0500, Bruce Momjian wrote: > On Sun, Dec 20, 2020 at 05:59:09PM -0500, Stephen Frost wrote: > OK, here it the updated patch. The major change, as Stephen pointed > out, is that the cluser_key_command (was cluster_passphrase_command) now > returns a hex-string representing the 32-byte KEK, rather than having > the server hash whatever the command used to return. This avoids > double-hashing in cases where you are _not_ using a passphrase, but are > computing a random 32-byte value in the script. This does mean the > script has to run sha256 for passphrases, but it seems like a win. > Updated script are attached too.
Attached is the script patch. It is also at: https://github.com/postgres/postgres/compare/master...bmomjian:cfe-sh.diff I think it still needs docs but those will have to be done after the code doc patch is added. -- Bruce Momjian <br...@momjian.us> https://momjian.us EnterpriseDB https://enterprisedb.com The usefulness of a cup is in its emptiness, Bruce Lee
diff --git a/src/backend/utils/auth_key_cmds/Makefile b/src/backend/utils/auth_key_cmds/Makefile new file mode 100644 index ...e0bee97 *** a/src/backend/utils/auth_key_cmds/Makefile --- b/src/backend/utils/auth_key_cmds/Makefile *************** *** 0 **** --- 1,41 ---- + #------------------------------------------------------------------------- + # + # Makefile for src/backend/utils/auth_key_cmds + # + # src/backend/utils/auth_key_cmds/Makefile + # + #------------------------------------------------------------------------- + + PGFILEDESC = "auth_key_cmds - authentication key commands" + PGAPPICON = win32 + + subdir = src/backend/utils/auth_key_cmds + top_builddir = ../../../.. + include $(top_builddir)/src/Makefile.global + + SCRIPTS = ckey_aws.sh.sample \ + ckey_direct.sh.sample \ + ckey_passphrase.sh.sample \ + ckey_piv_nopin.sh.sample \ + ckey_piv_pin.sh.sample \ + ssl_paasphrase.sh.sample + + SCRIPTDIR=auth_key_cmds + + install: all installdirs + $(INSTALL_DATA) $(SCRIPTS) '$(DESTDIR)$(datadir)/$(SCRIPTDIR)' + + installdirs: + $(MKDIR_P) '$(DESTDIR)$(datadir)' '$(DESTDIR)$(datadir)/$(SCRIPTDIR)' + + uninstall: + @set -e; \ + set $(SCRIPT) ; \ + while [ "$$#" -gt 0 ] ; \ + do \ + script=$$1; shift; \ + rm -f '$(DESTDIR)$(datadir)/$(SCRIPTDIR)/'$${lang}.stop ; \ + done + + clean distclean maintainer-clean: clean-lib + rm -f $(OBJS) $(SQLSCRIPT) diff --git a/src/backend/utils/auth_key_cmds/ckey_aws.sh.sample b/src/backend/utils/auth_key_cmds/ckey_aws.sh.sample new file mode 100755 index ...0341621 *** a/src/backend/utils/auth_key_cmds/ckey_aws.sh.sample --- b/src/backend/utils/auth_key_cmds/ckey_aws.sh.sample *************** *** 0 **** --- 1,50 ---- + #!/bin/sh + + # This uses the AWS Secrets Manager using the AWS CLI and OpenSSL. + + [ "$#" -ne 1 ] && echo "cluster_key_command usage: $0 \"%d\"" 1>&2 && exit 1 + # No need for %R or -R since we are not prompting + + DIR="$1" + [ ! -e "$DIR" ] && echo "$DIR does not exist" 1>&2 && exit 1 + [ ! -d "$DIR" ] && echo "$DIR is not a directory" 1>&2 && exit 1 + + # File containing the id of the AWS secret + AWS_ID_FILE="$DIR/aws-secret.id" + + + # ---------------------------------------------------------------------- + + + # Create an AWS Secrets Manager secret? + if [ ! -e "$AWS_ID_FILE" ] + then # The 'postgres' operating system user must have permission to + # access the AWS CLI + + # The epoch-time/directory/hostname combination is unique + HASH=$(echo -n "$(date '+%s')$DIR$(hostname)" | sha1sum | cut -d' ' -f1) + AWS_SECRET_ID="Postgres-cluster-key-$HASH" + + # Use stdin to avoid passing the secret on the command line + openssl rand -hex 32 | + aws secretsmanager create-secret \ + --name "$AWS_SECRET_ID" \ + --description 'Used for Postgres cluster file encryption' \ + --secret-string 'file:///dev/stdin' \ + --output text > /dev/null + if [ "$?" -ne 0 ] + then echo 'cluster key generation failed' 1>&2 + exit 1 + fi + + echo "$AWS_SECRET_ID" > "$AWS_ID_FILE" + fi + + if ! aws secretsmanager get-secret-value \ + --secret-id "$(cat "$AWS_ID_FILE")" \ + --output text + then echo 'cluster key retrieval failed' 1>&2 + exit 1 + fi | awk -F'\t' 'NR == 1 {print $4}' + + exit 0 diff --git a/src/backend/utils/auth_key_cmds/ckey_direct.sh.sample b/src/backend/utils/auth_key_cmds/ckey_direct.sh.sample new file mode 100755 index ...4a56cc3 *** a/src/backend/utils/auth_key_cmds/ckey_direct.sh.sample --- b/src/backend/utils/auth_key_cmds/ckey_direct.sh.sample *************** *** 0 **** --- 1,37 ---- + #!/bin/sh + + # This uses a key supplied by the user + # If OpenSSL is installed, you can generate a pseudo-random key by running: + # openssl rand -hex 32 + # To get a true random key, run: + # wget -q -O - 'https://www.random.org/cgi-bin/randbyte?nbytes=32&format=h' | tr -d ' \n'; echo + + [ "$#" -lt 1 ] && echo "cluster_key_command usage: $0 %R [\"%P\"]" 1>&2 && exit 1 + # Supports environment variable PROMPT + + FD="$1" + [ ! -t "$FD" ] && echo "file descriptor $FD does not refer to a terminal" 1>&2 && exit 1 + + [ "$2" ] && PROMPT="$2" + + + # ---------------------------------------------------------------------- + + [ ! "$PROMPT" ] && PROMPT='Enter cluster key as 64 hexadecimal characters: ' + + stty -echo <&"$FD" + + echo 1>&"$FD" + echo -n "$PROMPT" 1>&"$FD" + read KEY <&"$FD" + + stty echo <&"$FD" + + if [ "$(expr "$KEY" : '[0-9a-fA-F]*$')" -ne 64 ] + then echo 'invalid; must be 64 hexadecimal characters' 1>&2 + exit 1 + fi + + echo "$KEY" + + exit 0 diff --git a/src/backend/utils/auth_key_cmds/ckey_passphrase.sh.sample b/src/backend/utils/auth_key_cmds/ckey_passphrase.sh.sample new file mode 100755 index ...8640c48 *** a/src/backend/utils/auth_key_cmds/ckey_passphrase.sh.sample --- b/src/backend/utils/auth_key_cmds/ckey_passphrase.sh.sample *************** *** 0 **** --- 1,33 ---- + #!/bin/sh + + # This uses a passphrase supplied by the user. + + [ "$#" -lt 1 ] && echo "cluster_key_command usage: $0 %R [\"%P\"]" 1>&2 && exit 1 + + FD="$1" + [ ! -t "$FD" ] && echo "file descriptor $FD does not refer to a terminal" 1>&2 && exit 1 + # Supports environment variable PROMPT + + [ "$2" ] && PROMPT="$2" + + + # ---------------------------------------------------------------------- + + [ ! "$PROMPT" ] && PROMPT='Enter cluster passphrase: ' + + stty -echo <&"$FD" + + echo 1>&"$FD" + echo -n "$PROMPT" 1>&"$FD" + read PASS <&"$FD" + + stty echo <&"$FD" + + if [ ! "$PASS" ] + then echo 'invalid: empty passphrase' 1>&2 + exit 1 + fi + + echo "$PASS" | sha256sum | cut -d' ' -f1 + + exit 0 diff --git a/src/backend/utils/auth_key_cmds/ckey_piv_nopin.sh.sample b/src/backend/utils/auth_key_cmds/ckey_piv_nopin.sh.sample new file mode 100755 index ...ac7dc94 *** a/src/backend/utils/auth_key_cmds/ckey_piv_nopin.sh.sample --- b/src/backend/utils/auth_key_cmds/ckey_piv_nopin.sh.sample *************** *** 0 **** --- 1,63 ---- + #!/bin/sh + + # This uses the public/private keys on a PIV device, like a CAC or Yubikey. + # It uses a PIN stored in a file. + # It uses OpenSSL with PKCS11 enabled via OpenSC. + + [ "$#" -ne 1 ] && echo "cluster_key_command usage: $0 \"%d\"" 1>&2 && exit 1 + # Supports environment variable PIV_PIN_FILE + # No need for %R or -R since we are not prompting for a PIN + + DIR="$1" + [ ! -e "$DIR" ] && echo "$DIR does not exist" 1>&2 && exit 1 + [ ! -d "$DIR" ] && echo "$DIR is not a directory" 1>&2 && exit 1 + + # Set these here or pass in as environment variables. + # File that stores the PIN to unlock the PIV + #PIV_PIN_FILE='' + # PIV slot 3 is the "Key Management" slot, so we use '0:3' + PIV_SLOT='0:3' + + # File containing the cluster key encrypted with the PIV_SLOT's public key + KEY_FILE="$DIR/pivpass.key" + + + # ---------------------------------------------------------------------- + + [ ! "$PIV_PIN_FILE" ] && echo 'PIV_PIN_FILE undefined' 1>&2 && exit 1 + [ ! -e "$PIV_PIN_FILE" ] && echo "$PIV_PIN_FILE does not exist" 1>&2 && exit 1 + [ -d "$PIV_PIN_FILE" ] && echo "$PIV_PIN_FILE is a directory" 1>&2 && exit 1 + + [ ! "$KEY_FILE" ] && echo 'KEY_FILE undefined' 1>&2 && exit 1 + [ -d "$KEY_FILE" ] && echo "$KEY_FILE is a directory" 1>&2 && exit 1 + + # Create a cluster key encrypted with the PIV_SLOT's public key? + if [ ! -e "$KEY_FILE" ] + then # The 'postgres' operating system user must have permission to + # access the PIV device. + + openssl rand -hex 32 | + if ! openssl rsautl -engine pkcs11 -keyform engine -encrypt \ + -inkey "$PIV_SLOT" -passin file:"$PIV_PIN_FILE" -out "$KEY_FILE" + then echo 'cluster key generation failed' 1>&2 + exit 1 + fi + + # Warn the user to save the cluster key in a safe place + cat 1>&2 <<END + + WARNING: The PIV device can be locked and require a reset if too many PIN + attempts fail. It is recommended to run this command manually and save + the cluster key in a secure location for possible recovery. + END + + fi + + # Decrypt the cluster key encrypted with the PIV_SLOT's public key + if ! openssl rsautl -engine pkcs11 -keyform engine -decrypt \ + -inkey "$PIV_SLOT" -passin file:"$PIV_PIN_FILE" -in "$KEY_FILE" + then echo 'cluster key decryption failed' 1>&2 + exit 1 + fi + + exit 0 diff --git a/src/backend/utils/auth_key_cmds/ckey_piv_pin.sh.sample b/src/backend/utils/auth_key_cmds/ckey_piv_pin.sh.sample new file mode 100755 index ...d283ad8 *** a/src/backend/utils/auth_key_cmds/ckey_piv_pin.sh.sample --- b/src/backend/utils/auth_key_cmds/ckey_piv_pin.sh.sample *************** *** 0 **** --- 1,76 ---- + #!/bin/sh + + # This uses the public/private keys on a PIV device, like a CAC or Yubikey. + # It requires a user-entered PIN. + # It uses OpenSSL with PKCS11 enabled via OpenSC. + + [ "$#" -lt 2 ] && echo "cluster_key_command usage: $0 \"%d\" %R [\"%P\"]" 1>&2 && exit 1 + # Supports environment variable PROMPT + + DIR="$1" + [ ! -e "$DIR" ] && echo "$DIR does not exist" 1>&2 && exit 1 + [ ! -d "$DIR" ] && echo "$DIR is not a directory" 1>&2 && exit 1 + + FD="$2" + [ ! -t "$FD" ] && echo "file descriptor $FD does not refer to a terminal" 1>&2 && exit 1 + + [ "$3" ] && PROMPT="$3" + + # PIV slot 3 is the "Key Management" slot, so we use '0:3' + PIV_SLOT='0:3' + + # File containing the cluster key encrypted with the PIV_SLOT's public key + KEY_FILE="$DIR/pivpass.key" + + + # ---------------------------------------------------------------------- + + [ ! "$PROMPT" ] && PROMPT='Enter PIV PIN: ' + + stty -echo <&"$FD" + + # Create a cluster key encrypted with the PIV_SLOT's public key? + if [ ! -e "$KEY_FILE" ] + then echo 1>&"$FD" + echo -n "$PROMPT" 1>&"$FD" + + # The 'postgres' operating system user must have permission to + # access the PIV device. + + openssl rand -hex 32 | + # 'engine "pkcs11" set.' message confuses prompting + if ! openssl rsautl -engine pkcs11 -keyform engine -encrypt \ + -inkey "$PIV_SLOT" -passin fd:"$FD" -out "$KEY_FILE" 2>&1 + then stty echo <&"$FD" + echo 'cluster key generation failed' 1>&2 + exit 1 + fi | grep -v 'engine "pkcs11" set\.' + + echo 1>&"$FD" + + # Warn the user to save the cluster key in a safe place + cat 1>&"$FD" <<END + + WARNING: The PIV can be locked and require a reset if too many PIN + attempts fail. It is recommended to run this command manually and save + the cluster key in a secure location for possible recovery. + END + + fi + + echo 1>&"$FD" + echo -n "$PROMPT" 1>&"$FD" + + # Decrypt the cluster key encrypted with the PIV_SLOT's public key + if ! openssl rsautl -engine pkcs11 -keyform engine -decrypt \ + -inkey "$PIV_SLOT" -passin fd:"$FD" -in "$KEY_FILE" 2>&1 + then stty echo <&"$FD" + echo 'cluster key retrieval failed' 1>&2 + exit 1 + fi | grep -v 'engine "pkcs11" set\.' + + echo 1>&"$FD" + + stty echo <&"$FD" + + exit 0 diff --git a/src/backend/utils/auth_key_cmds/ssl_paasphrase.sh.sample b/src/backend/utils/auth_key_cmds/ssl_paasphrase.sh.sample new file mode 100755 index ...7d8f422 *** a/src/backend/utils/auth_key_cmds/ssl_paasphrase.sh.sample --- b/src/backend/utils/auth_key_cmds/ssl_paasphrase.sh.sample *************** *** 0 **** --- 1,33 ---- + #!/bin/sh + + # This uses a passphrase supplied by the user. + + [ "$#" -ne 2 ] && echo "cluster_key_command usage: $0 %R [\"%P\"]" 1>&2 && exit 1 + + FD="$1" + [ ! -t "$FD" ] && echo "file descriptor $FD does not refer to a terminal" 1>&2 && exit 1 + # Supports environment variable PROMPT + + [ "$2" ] && PROMPT="$2" + + + # ---------------------------------------------------------------------- + + [ ! "$PROMPT" ] && PROMPT='Enter cluster passphrase: ' + + stty -echo <&"$FD" + + echo 1>&"$FD" + echo -n "$PROMPT" 1>&"$FD" + read PASS <&"$FD" + + stty echo <&"$FD" + + if [ ! "$PASS" ] + then echo 'invalid: empty passphrase' 1>&2 + exit 1 + fi + + echo "$PASS" | sha256sum | cut -d' ' -f1 + + exit 0