From d7602afa2ef6d8b2018103dccd89e75b4985ac06 Mon Sep 17 00:00:00 2001 From: Bruce Momjian Date: Sat, 26 Dec 2020 01:19:09 -0500 Subject: [PATCH] Add scripts for retrieving the cluster file encryption key Scripts are passphrase, direct, AWS, and two Yubikey ones. Backpatch-through: master --- src/backend/Makefile | 12 ++++ src/backend/crypto/ckey_aws.sh.sample | 50 +++++++++++++ src/backend/crypto/ckey_direct.sh.sample | 37 ++++++++++ src/backend/crypto/ckey_passphrase.sh.sample | 33 +++++++++ src/backend/crypto/ckey_piv_nopin.sh.sample | 63 ++++++++++++++++ src/backend/crypto/ckey_piv_pin.sh.sample | 76 ++++++++++++++++++++ src/backend/crypto/ssl_passphrase.sh.sample | 33 +++++++++ 7 files changed, 304 insertions(+) create mode 100755 src/backend/crypto/ckey_aws.sh.sample create mode 100755 src/backend/crypto/ckey_direct.sh.sample create mode 100755 src/backend/crypto/ckey_passphrase.sh.sample create mode 100755 src/backend/crypto/ckey_piv_nopin.sh.sample create mode 100755 src/backend/crypto/ckey_piv_pin.sh.sample create mode 100755 src/backend/crypto/ssl_passphrase.sh.sample diff --git a/src/backend/Makefile b/src/backend/Makefile index 4ace3020382..7e22423edcc 100644 --- a/src/backend/Makefile +++ b/src/backend/Makefile @@ -54,6 +54,15 @@ ifeq ($(with_systemd),yes) LIBS += -lsystemd endif +CRYPTO_SCRIPTDIR=auth_commands +CRYPTO_SCRIPTS = \ + ckey_aws.sh.sample \ + ckey_direct.sh.sample \ + ckey_passphrase.sh.sample \ + ckey_piv_nopin.sh.sample \ + ckey_piv_pin.sh.sample \ + ssl_passphrase.sh.sample + ########################################################################## all: submake-libpgport submake-catalog-headers submake-utils-headers postgres $(POSTGRES_IMP) @@ -212,6 +221,7 @@ endif $(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample' $(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample' $(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample' + $(INSTALL_DATA) $(addprefix 'crypto/', $(CRYPTO_SCRIPTS)) '$(DESTDIR)$(datadir)/$(CRYPTO_SCRIPTDIR)' ifeq ($(with_llvm), yes) install-bin: install-postgres-bitcode @@ -237,6 +247,7 @@ endif installdirs: $(MKDIR_P) '$(DESTDIR)$(bindir)' '$(DESTDIR)$(datadir)' + $(MKDIR_P) '$(DESTDIR)$(datadir)' '$(DESTDIR)$(datadir)/$(CRYPTO_SCRIPTDIR)' ifeq ($(PORTNAME), cygwin) ifeq ($(MAKE_DLL), true) $(MKDIR_P) '$(DESTDIR)$(libdir)' @@ -257,6 +268,7 @@ endif uninstall: rm -f '$(DESTDIR)$(bindir)/postgres$(X)' '$(DESTDIR)$(bindir)/postmaster' + rm -f $(addprefix '$(DESTDIR)$(datadir)/$(CRYPTO_SCRIPTDIR)'/, $(CRYPTO_SCRIPTS)) ifeq ($(MAKE_EXPORTS), true) rm -f '$(DESTDIR)$(pkglibdir)/$(POSTGRES_IMP)' rm -f '$(DESTDIR)$(pgxsdir)/$(MKLDEXPORT_DIR)/mkldexport.sh' diff --git a/src/backend/crypto/ckey_aws.sh.sample b/src/backend/crypto/ckey_aws.sh.sample new file mode 100755 index 00000000000..0341621c2e5 --- /dev/null +++ b/src/backend/crypto/ckey_aws.sh.sample @@ -0,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/crypto/ckey_direct.sh.sample b/src/backend/crypto/ckey_direct.sh.sample new file mode 100755 index 00000000000..1c41d53acc1 --- /dev/null +++ b/src/backend/crypto/ckey_direct.sh.sample @@ -0,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/crypto/ckey_passphrase.sh.sample b/src/backend/crypto/ckey_passphrase.sh.sample new file mode 100755 index 00000000000..1098e99e549 --- /dev/null +++ b/src/backend/crypto/ckey_passphrase.sh.sample @@ -0,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/crypto/ckey_piv_nopin.sh.sample b/src/backend/crypto/ckey_piv_nopin.sh.sample new file mode 100755 index 00000000000..ac7dc941ee6 --- /dev/null +++ b/src/backend/crypto/ckey_piv_nopin.sh.sample @@ -0,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 <&2 + exit 1 +fi + +exit 0 diff --git a/src/backend/crypto/ckey_piv_pin.sh.sample b/src/backend/crypto/ckey_piv_pin.sh.sample new file mode 100755 index 00000000000..e6310087ffd --- /dev/null +++ b/src/backend/crypto/ckey_piv_pin.sh.sample @@ -0,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" <&"$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/crypto/ssl_passphrase.sh.sample b/src/backend/crypto/ssl_passphrase.sh.sample new file mode 100755 index 00000000000..6859f1bbb58 --- /dev/null +++ b/src/backend/crypto/ssl_passphrase.sh.sample @@ -0,0 +1,33 @@ +#!/bin/sh + +# This uses a passphrase supplied by the user. + +[ "$#" -lt 1 ] && echo "ssl_passphrase_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" + +exit 0