Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encrypt patient data at rest and in backup files. #251

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/buendia-backup/data/usr/bin/buendia-backup
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down
24 changes: 22 additions & 2 deletions packages/buendia-backup/data/usr/bin/buendia-restore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)" ]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion packages/buendia-mysql/control/control.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 22 additions & 0 deletions packages/buendia-mysql/control/postinst
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
Expand Down
5 changes: 4 additions & 1 deletion packages/buendia-mysql/control/preinst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 ]
}
2 changes: 1 addition & 1 deletion packages/buendia-utils/control/control.template
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions packages/buendia-utils/control/postinst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions packages/buendia-utils/data/usr/share/buendia/utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions packages/tests/check-filenames
Original file line number Diff line number Diff line change
Expand Up @@ -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-} ;;
Expand Down