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-backup/data/usr/bin/buendia-db-is-empty b/packages/buendia-db/data/usr/bin/buendia-db-is-empty similarity index 100% rename from packages/buendia-backup/data/usr/bin/buendia-db-is-empty rename to packages/buendia-db/data/usr/bin/buendia-db-is-empty 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 < 1.0), buendia-db +Description: Data theft prevention monitor for Buendia server. +Maintainer: projectbuendia.org diff --git a/packages/buendia-security-monitor/data/lib/systemd/system/buendia-security-monitor.service b/packages/buendia-security-monitor/data/lib/systemd/system/buendia-security-monitor.service new file mode 100644 index 00000000..7ce9b4ce --- /dev/null +++ b/packages/buendia-security-monitor/data/lib/systemd/system/buendia-security-monitor.service @@ -0,0 +1,11 @@ +[Unit] +Description=Buendia security monitor +Wants=buendia-reconfigure.service +After=buendia-reconfigure.service + +[Service] +Type=simple +ExecStart=/usr/bin/buendia-log buendia-security-monitor + +[Install] +WantedBy=multi-user.target diff --git a/packages/buendia-security-monitor/data/usr/bin/buendia-security-monitor b/packages/buendia-security-monitor/data/usr/bin/buendia-security-monitor new file mode 100644 index 00000000..a836aed9 --- /dev/null +++ b/packages/buendia-security-monitor/data/usr/bin/buendia-security-monitor @@ -0,0 +1,80 @@ +#!/bin/bash +# Copyright 2020 The Project Buendia Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at: http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distrib- +# uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. See the License for +# specific language governing permissions and limitations under the License. + +# buendia-security-monitor is designed to remove the Buendia system keys in the +# event that a Buendia system with user records that was configured to talk to +# a particular wifi network cannot connect to that network within +# $SECURITY_MONITOR_TIMEOUT_MINS minutes of booting. + +set -e; . /usr/share/buendia/utils.sh + +if [ "$(id -u)" != 0 ]; then + echo "ERROR: This script must be run as root!" + exit 1 +fi + +if [ -z "$SECURITY_MONITOR_TIMEOUT_MINS" ]; then + echo "skipping: no security monitor timeout set" + exit 0 +fi + +if [ -z "$NETWORKING_WIFI_INTERFACE" ]; then + echo "skipping: no wifi interface configured" + exit 0 +fi + +# According to man proc(5): /proc/uptime contains two numbers (values in +# seconds): the uptime of the system (including time spent in suspend) and the +# amount of time spent in the idle process. +current_uptime_secs=$(cut -d" " -f1 /proc/uptime) +monitor_timeout_secs=$(( $SECURITY_MONITOR_TIMEOUT_MINS * 60 )) +if [ $current_uptime_secs -le $monitor_timeout_secs ]; then + echo "deferring: system has been up for ${current_uptime_secs}s; check will be at ${monitor_timeout_secs}s" + sleep $(( $monitor_timeout_secs - $current_uptime_secs )) +fi + +if [ ! -f /etc/buendia-db-init-installed ] || buendia-db-is-empty; then + echo "skipping: Buendia database is empty" + exit 0 +fi + +# This file should be created by buendia-wifi-watchdog +if [ -r /run/buendia-networking.state ]; then + echo "skipping: configured wifi network is active" + exit 0 +fi + +echo "ERROR: system has been up for ${SECURITY_MONITOR_TIMEOUT_MINS}m without a wireless network connection!" +echo "Stopping database server." +systemctl mysql stop || true + +echo "Marking local database as uninitialized." +rm -f /etc/buendia-db-init-installed + +echo "Deleting local system key." +rm -f /usr/share/buendia/system.key + +echo "Checking external devices for system keys." +tmpdir=$(mktemp -d) +trap "rmdir $tmpdir || true" EXIT +for device in external_file_systems; do + if mount $device $tmpdir; then + trap "umount $tmpdir || true; rmdir $tmpdir" EXIT + if [ -f $tmpdir/system.key ]; then + echo "Deleting system key from $device." + rm -f $tmpdir/system.key + fi + umount $tmpdir || true + fi +done + +echo "$0 finished." diff --git a/packages/buendia-security-monitor/data/usr/share/buendia/config.d/10-security-monitor b/packages/buendia-security-monitor/data/usr/share/buendia/config.d/10-security-monitor new file mode 100644 index 00000000..f1270144 --- /dev/null +++ b/packages/buendia-security-monitor/data/usr/share/buendia/config.d/10-security-monitor @@ -0,0 +1 @@ +SECURITY_MONITOR_TIMEOUT_MINS=10 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-} ;;