diff --git a/packages/buendia-backup/data/usr/bin/buendia-backup b/packages/buendia-backup/data/usr/bin/buendia-backup index cd7597fe..37822862 100755 --- a/packages/buendia-backup/data/usr/bin/buendia-backup +++ b/packages/buendia-backup/data/usr/bin/buendia-backup @@ -137,7 +137,9 @@ echo "Saving Buendia configuration..." # about any which might be missing. config_paths=$(ls -d /usr/share/buendia/{counts,distilled,openmrs,profiles,site} || true) tar cfz "$new_dir/buendia.tar.gz" --exclude '*.omod' $config_paths -ls -l "$new_dir/buendia.tar.gz" +encrypt_file "$new_dir/buendia.tar.gz" +rm "$new_dir/buendia.tar.gz" +ls -l "$new_dir/buendia.tar.gz.enc" # ---- Back up the MySQL database. @@ -148,7 +150,9 @@ if ! buendia-mysql-dump openmrs "$new_dir/openmrs.zip" >"$tmp/out" 2>&1; then echo "buendia-mysql-dump failed!" exit 1 fi -ls -l "$new_dir/openmrs.zip" +encrypt_file "$new_dir/openmrs.zip" +rm "$new_dir/openmrs.zip" +ls -l "$new_dir/openmrs.zip.enc" # ---- Back up the package list and non-base packages. @@ -216,6 +220,17 @@ sync mv "$new_dir" "$final_dir" sync +# If we're backing up to a block device, make sure that the system key is +# copied there, now that the most recent backup has been committed. +# +# This may overwrite the key used to create earlier backups, which is why +# we waited until this point to copy the key. +if [ -n "$mnt_dir" ]; then + echo "Backing up system key to external storage." + cp /usr/share/buendia/system.key "$mnt_dir" + sync +fi + # ---- Sweep all the packages in completed backups into a common directory. echo diff --git a/packages/buendia-backup/data/usr/bin/buendia-restore b/packages/buendia-backup/data/usr/bin/buendia-restore index 400d46a1..46ff52db 100755 --- a/packages/buendia-backup/data/usr/bin/buendia-restore +++ b/packages/buendia-backup/data/usr/bin/buendia-restore @@ -137,11 +137,27 @@ if [ $progress_state -le $PROGRESS_SETTINGS_NEXT ]; then buendia-pkgserver-index-debs /usr/share/buendia/packages $suite fi + if [ -n "$mnt_dir" ]; then + echo + if [ ! -r "$mnt_dir/system.key" ]; then + echo "ERROR: Cannot find system key on external storage!" + exit 1 + fi + echo "Restoring system key from external storage..." + cp "$mnt_dir/system.key" /usr/share/buendia/system.key + chown root:mysql /usr/share/buendia/system.key + chmod 640 /usr/share/buendia/system.key + fi + # Restore backed up site settings echo echo "Restoring settings..." mv /usr/share/buendia/site /usr/share/buendia/site.$$ - if tar -xzf $root/buendia.tar.gz -C / usr/share/buendia/site; then + # Copy the file off the backup device to ensure we have enough space to + # decrypt it + cp "$root/buendia.tar.gz.enc" "$tmp" + decrypt_file "$tmp/buendia.tar.gz" + if tar -xzf $tmp/buendia.tar.gz -C / usr/share/buendia/site; then rm -rf /usr/share/buendia/site.$$ else rm -rf /usr/share/buendia/site @@ -171,8 +187,12 @@ if [ $progress_state -le $PROGRESS_MYSQL_NEXT ]; then # ---- Restore the MySQL database. echo echo "Restoring MySQL database..." + # Copy the file off the backup device to ensure we have enough space to + # decrypt it + cp "$root/openmrs.zip.enc" "$tmp" + decrypt_file "$tmp/openmrs.zip" service tomcat7 stop - if ! buendia-mysql-load -f openmrs "$root/openmrs.zip" >"$tmp/out" 2>&1; then + if ! buendia-mysql-load -f openmrs "$tmp/openmrs.zip" >"$tmp/out" 2>&1; then if [ -e $tmp/out ]; then cat "$tmp/out" fi diff --git a/packages/buendia-backup/data/usr/share/buendia/tests/40-backup-to-local b/packages/buendia-backup/data/usr/share/buendia/tests/40-backup-to-local index 920f6368..d00c2c96 100755 --- a/packages/buendia-backup/data/usr/share/buendia/tests/40-backup-to-local +++ b/packages/buendia-backup/data/usr/share/buendia/tests/40-backup-to-local @@ -2,20 +2,36 @@ BUENDIA_TEST_BACKUP_TARGET=/var/backups/buendia/backup.$(date +%Y-%m-%d) # When the backup cron job runs test_10_run_backup_cron () { - rm -rf $BUENDIA_TEST_BACK_TARGET + rm -rf $BUENDIA_TEST_BACKUP_TARGET execute_cron_right_now backup } -# Then it stores a tarball of site configs locally -test_20_cron_saved_site_config () { - tar tfz $BUENDIA_TEST_BACKUP_TARGET/buendia.tar.gz +# Then it encrypts the backup +test_20_no_unencrypted_backup_files () { + ! [ -f $BUENDIA_TEST_BACKUP_TARGET/buendia.tar.gz \ + -o -f $BUENDIA_TEST_BACKUP_TARGET/openmrs.zip ] +} + +# And it stores a tarball of site configs locally +test_30_cron_saved_site_config () { + cp $BUENDIA_TEST_BACKUP_TARGET/buendia.tar.gz.enc . + decrypt_file buendia.tar.gz + tar tfz buendia.tar.gz } # And it stores a database dump locally -test_20_cron_saved_database_dump () { - unzip -t $BUENDIA_TEST_BACKUP_TARGET/openmrs.zip +test_30_cron_saved_database_dump () { + cp $BUENDIA_TEST_BACKUP_TARGET/openmrs.zip.enc . + decrypt_file openmrs.zip + unzip -t openmrs.zip } -test_20_cron_saved_package_list () { +# And it stores a list of installed Buendia packages +test_40_cron_saved_package_list () { [ -s $BUENDIA_TEST_BACKUP_TARGET/buendia.list ] } + +# But it doesn't back up the system key locally +test_50_no_local_system_key_backup () { + [ ! -f $BUENDIA_TEST_BACKUP_TARGET/system.key ] +} diff --git a/packages/buendia-backup/data/usr/share/buendia/tests/50-backup-locked-safe b/packages/buendia-backup/data/usr/share/buendia/tests/50-backup-locked-safe index 50e128af..77759123 100755 --- a/packages/buendia-backup/data/usr/share/buendia/tests/50-backup-locked-safe +++ b/packages/buendia-backup/data/usr/share/buendia/tests/50-backup-locked-safe @@ -1,7 +1,11 @@ test_10_prevent_simultaneous_backup () { mount_loopback - buendia-backup /dev/loop0 & + buendia-backup /dev/loop0 | tee backup.log & sleep 0.1 + if grep skip backup.log; then + echo "Backup was skipped; test can't be evaluated" + return 0 + fi if buendia-backup /dev/loop0; then echo "Simultaneous backup should be prevented" return 1 diff --git a/packages/buendia-backup/data/usr/share/buendia/tests/50-backup-to-external-safe b/packages/buendia-backup/data/usr/share/buendia/tests/50-backup-to-external-safe index 539120a6..4ffb5390 100755 --- a/packages/buendia-backup/data/usr/share/buendia/tests/50-backup-to-external-safe +++ b/packages/buendia-backup/data/usr/share/buendia/tests/50-backup-to-external-safe @@ -16,15 +16,24 @@ test_10_run_backup_cron () { # Then it stores a tarball of site configs on the external device test_20_cron_saved_site_config () { - tar tfz $BUENDIA_TEST_BACKUP_TARGET/buendia.tar.gz + cp $BUENDIA_TEST_BACKUP_TARGET/buendia.tar.gz.enc . + decrypt_file buendia.tar.gz + tar tfz buendia.tar.gz } # And it stores a database dump on the external device test_20_cron_saved_database_dump () { - unzip -t $BUENDIA_TEST_BACKUP_TARGET/openmrs.zip + cp $BUENDIA_TEST_BACKUP_TARGET/openmrs.zip.enc . + decrypt_file openmrs.zip + unzip -t openmrs.zip } # And it stores a package listing on the external device test_20_cron_saved_package_list () { [ -s $BUENDIA_TEST_BACKUP_TARGET/buendia.list ] } + +# And it stores a copy of the system key +test_30_cron_saved_system_key () { + [ "$(cat /usr/share/buendia/system.key)" = "$(cat loop/system.key)" ] +} diff --git a/packages/buendia-backup/data/usr/share/buendia/tests/60-backup-and-restore b/packages/buendia-backup/data/usr/share/buendia/tests/60-backup-and-restore index 8df9dc05..b0228666 100755 --- a/packages/buendia-backup/data/usr/share/buendia/tests/60-backup-and-restore +++ b/packages/buendia-backup/data/usr/share/buendia/tests/60-backup-and-restore @@ -31,6 +31,11 @@ test_50_confirm_changed_patient_list () { } test_60_restore_from_backup () { + # Replace the current system key, to ensure that the restore process + # correctly pulls in the one we just backed up. + openssl rand -hex 128 > /usr/share/buendia/system.key + + # Now try restoring! execute_cron_right_now backup } diff --git a/packages/buendia-mysql/control/control.template b/packages/buendia-mysql/control/control.template index ba7b43e7..a1d074a7 100644 --- a/packages/buendia-mysql/control/control.template +++ b/packages/buendia-mysql/control/control.template @@ -2,6 +2,6 @@ Package: ${PACKAGE_NAME} Version: ${PACKAGE_VERSION} Architecture: all Pre-Depends: buendia-utils -Depends: buendia-monitoring, cron-daemon, mysql-server, sysvinit-utils, unzip, zip +Depends: buendia-monitoring, cron-daemon, mariadb-server-10.1 (>= 10.1.4), sysvinit-utils, unzip, zip Description: MySQL server configured for Buendia Maintainer: projectbuendia.org diff --git a/packages/buendia-mysql/control/postinst b/packages/buendia-mysql/control/postinst index c58b4b58..da6348e9 100755 --- a/packages/buendia-mysql/control/postinst +++ b/packages/buendia-mysql/control/postinst @@ -14,6 +14,28 @@ set -e; . /usr/share/buendia/utils.sh case $1 in configure) + if [ ! -r /etc/mysql/keyfile.enc ]; then + # Key #1 is required by MariaDB for encrypting system data. It will be used + # for other purposes as well, if no other keys are defined. + # https://mariadb.com/kb/en/encryption-key-management/#using-multiple-encryption-keys + ( echo -n '1;'; openssl rand -hex 32 ) > /etc/mysql/keyfile + + # Encrypt the keyfile using the Buendia system key and remove the plaintext version + openssl enc -aes-256-cbc -md sha1 -in /etc/mysql/keyfile -out /etc/mysql/keyfile.enc \ + -pass file:/usr/share/buendia/system.key + rm /etc/mysql/keyfile + fi + + # Ensure that MariaDB can read the encrypted key. + chown root:mysql /etc/mysql/keyfile.enc + chmod 640 /etc/mysql/keyfile.enc + + # Ensure that MariaDB can read the system key. + chown root:mysql /usr/share/buendia/system.key + chmod 640 /usr/share/buendia/system.key + + service mysql restart + buendia-reconfigure mysql service cron start ;; diff --git a/packages/buendia-mysql/control/preinst b/packages/buendia-mysql/control/preinst index 9d9e8bd5..d3ab65d6 100755 --- a/packages/buendia-mysql/control/preinst +++ b/packages/buendia-mysql/control/preinst @@ -13,6 +13,9 @@ set -e; . /usr/share/buendia/utils.sh case $1 in - install|upgrade) service_if_exists cron stop ;; + install|upgrade) + service_if_exists cron stop + service_if_exists mysql stop + ;; *) exit 1 esac diff --git a/packages/buendia-mysql/data/etc/mysql/conf.d/buendia-mysql b/packages/buendia-mysql/data/etc/mysql/conf.d/buendia-mysql.cnf similarity index 100% rename from packages/buendia-mysql/data/etc/mysql/conf.d/buendia-mysql rename to packages/buendia-mysql/data/etc/mysql/conf.d/buendia-mysql.cnf diff --git a/packages/buendia-mysql/data/etc/mysql/mariadb.conf.d/60-encryption.cnf b/packages/buendia-mysql/data/etc/mysql/mariadb.conf.d/60-encryption.cnf new file mode 100644 index 00000000..652a4e52 --- /dev/null +++ b/packages/buendia-mysql/data/etc/mysql/mariadb.conf.d/60-encryption.cnf @@ -0,0 +1,19 @@ +[mariadb] +plugin_load_add = file_key_management + +# The table encryption secret gets *itself* encrypted at rest. +# See https://mariadb.com/kb/en/file-key-management-encryption-plugin/#creating-the-key-file +file_key_management_filename = /etc/mysql/keyfile.enc +file_key_management_filekey = FILE:/usr/share/buendia/system.key + +# AES_CBC is used by default. AES_CTR is preferred, but not supported in older MariaDB builds. +# https://mariadb.com/kb/en/file-key-management-encryption-plugin/#choosing-an-encryption-algorithm +# file_key_management_encryption_algorithm = AES_CTR + +# InnoDB/XtraDB Encryption +# https://mariadb.com/kb/en/innodb-encryption-overview/#basic-configuration +# The 'FORCE' option requires all InnoDB tables to be encrypted. +innodb_encrypt_tables = FORCE +innodb_encrypt_log = ON +innodb_encryption_threads = 4 +innodb_encryption_rotate_key_age = 1 diff --git a/packages/buendia-mysql/data/usr/share/buendia/tests/30-mysql-encryption-safe b/packages/buendia-mysql/data/usr/share/buendia/tests/30-mysql-encryption-safe new file mode 100755 index 00000000..f39f76bf --- /dev/null +++ b/packages/buendia-mysql/data/usr/share/buendia/tests/30-mysql-encryption-safe @@ -0,0 +1,14 @@ +test_10_openmrs_tables_are_encrypted () { + # MariaDB starts encrypting tables on a background thread when encryption + # is enabled. As a result, there's no obvious way to know when the job is + # done. This test makes the assumption that if at least one of the OpenMRS + # tables is encrypted, then the server background thread is doing its work + # and all OpenMRS tables will be encrypted eventually. + + sudo mysql -s openmrs >table_count <<End + SELECT COUNT(*) FROM information_schema.INNODB_TABLESPACES_ENCRYPTION + WHERE name LIKE "openmrs/%" + AND encryption_scheme = 1; +End + [ "$(cat table_count)" -gt 0 ] +} diff --git a/packages/buendia-utils/control/control.template b/packages/buendia-utils/control/control.template index 9c249c5c..9658fef3 100644 --- a/packages/buendia-utils/control/control.template +++ b/packages/buendia-utils/control/control.template @@ -1,6 +1,6 @@ Package: ${PACKAGE_NAME} Version: ${PACKAGE_VERSION} Architecture: all -Depends: coreutils, cron-daemon, curl, perl +Depends: coreutils, cron-daemon, curl, perl, openssl Description: Utility scripts for Buendia Maintainer: projectbuendia.org diff --git a/packages/buendia-utils/control/postinst b/packages/buendia-utils/control/postinst index 502f5600..019d0ed3 100755 --- a/packages/buendia-utils/control/postinst +++ b/packages/buendia-utils/control/postinst @@ -14,6 +14,11 @@ set -e; . /usr/share/buendia/utils.sh case $1 in configure) + if [ ! -f /usr/share/buendia/system.key ]; then + openssl rand -hex 128 > /usr/share/buendia/system.key + chmod 600 /usr/share/buendia/system.key + fi + if [ -d /usr/share/buendia/systemd ]; then cp /usr/share/buendia/systemd/* /lib/systemd/system systemctl enable reboot-check.timer diff --git a/packages/buendia-utils/data/usr/share/buendia/utils.sh b/packages/buendia-utils/data/usr/share/buendia/utils.sh index b9f70a97..197a058c 100644 --- a/packages/buendia-utils/data/usr/share/buendia/utils.sh +++ b/packages/buendia-utils/data/usr/share/buendia/utils.sh @@ -52,5 +52,18 @@ function external_file_systems() { done } +# Encrypt a file using the system key, given the input file name. +function encrypt_file() { + openssl enc -aes-256-cbc -md sha256 -salt -in $1 -out $1.enc \ + -pass file:/usr/share/buendia/system.key +} + +# Decrypt a file using the system key, given the expected output filename. The +# input filename is assumed to be the output filename with '.enc' appended. +function decrypt_file() { + openssl enc -d -aes-256-cbc -md sha256 -in $1.enc -out $1 \ + -pass file:/usr/share/buendia/system.key +} + # A handy shortcut, just for typing convenience. usb=usr/share/buendia diff --git a/packages/tests/check-filenames b/packages/tests/check-filenames index e172ed77..71da5694 100755 --- a/packages/tests/check-filenames +++ b/packages/tests/check-filenames @@ -20,6 +20,8 @@ for dir in $(find data -name '*.d'); do case $dir:$PACKAGE_NAME in data/etc/apt/sources.list.d:*) name=buendia*.list ;; data/etc/udev/rules.d:*) continue ;; + data/etc/mysql/mariadb.conf.d:*) continue ;; + data/etc/mysql/conf.d:*) name=${PACKAGE_NAME}.cnf ;; data/usr/share/buendia/*:buendia-site-*) name=site ;; data/usr/share/buendia/config.d:*) name="??-${PACKAGE_NAME#buendia-}" ;; data/usr/share/buendia/*:*) name=${PACKAGE_NAME#buendia-} ;;