Prevent data decryption on pulling backup server

Hi,

please excuse me if such scenario was already discussed.

I was wondering how to ensure that syncoid, running in pull mode at backup-server, doesn’t have any chance to decrypt data from its target backup-target, even if backup-server is compromised (root account is accessible to malicious user)?

backup-target has encrypted dataset.
backup-server is pulling snapshots of that encrypted dataset from backup-target

I tested that by running syncoid without --sendoptions=w at backup-server , would result in decrypted data snapshot from backup-target at backup-server.

Thanks

Running the command as:

# Assume pull from backup-target --> backup-server
syncoid --sendoptions="w" user@backup-target/pool backup-server/pool

The -w send option is documented here: zfs-send.8 — OpenZFS documentation

For encrypted datasets, send data exactly as it exists on disk. This allows backups to be taken even if encryption keys are not currently loaded. The backup may then be received on an untrusted machine since that machine will not have the encryption keys to read the protected data or alter it without being detected. Upon being received, the dataset will have the same encryption keys as it did on the send side…

You can unmount the datasets and unload all of your encryption keys on both machines (zfs unmount -u pool) and send fresh snapshots. It’ll work, but nothing is mounted and no keys are loaded.

Hi @SirGeorge ,

Thank you for your input, but backup-target is constantly using the (target) dataset, so unmounting or unloading the keys is not an option.

Basically what I would like to achieve is to prevent backup-server to run syncoid without --sendoptions=w. Is it possible somehow?

I thought about limiting allowed commands on backup-target via ~/.ssh/authorized-keys file, e.g:

zfsbackup@backup-target:~$ cat ~/.ssh/authorized_keys
restrict,command="zfs send -w ..." ssh-ed25519 abcd1234 zfsstorage@backup-server

as syncoid is just a wrapper (?) for zfs send/receive/... I guess there is a chance to enforce -w like this? I would appreciate some opinions on this idea and my issue in general.

Thanks!

This might be possible using delegated permissions, and disallowing the load-key permission.

https://manpages.ubuntu.com/manpages/mantic/man8/zfs-allow.8.html

Tagging in @allan to see if he’s got any direct experience with this particular request…

This feature does not currently exist, but I did discuss the need for it a few weeks ago at the OpenZFS Developer Summit.

It is on my wish list. If someones company is interested in sponsoring this feature to get it done, definitely reach out: ZFS Custom Feature Development - Klara Systems

3 Likes

Here is the wrapper script I have been using for ~ 4 years. It’s messy and needs updating when datasets are added, but it works for me:

#!/usr/bin/ruby

# Log function so that we can record what commands were requested by Syncoid
def log(msg)
  File.write('/var/log/workstation-pull.log', "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] #{msg}\n", mode: 'a')
end

# Function to check whether a command is explicitly allowed by this script
def allowed?(cmd)
  pool = "(ssd|bpool)"
  dataset = "(eu1|root|boot|home)"
  path_element_pattern = "/(\\w|-)+\\.?(\\w|-)*"
  dataset_pattern_string  = "#{pool}/#{dataset}(#{path_element_pattern})*"
  snapshot_name = "(base_install|split_boot_pool|autosnap_20\\d\\d-\\d\\d-\\d\\d_\\d\\d:\\d\\d:\\d\\d_(frequently|hourly|daily|monthly|yearly))"
  snapshot_pattern_string = "#{dataset_pattern_string}(@|'@')#{snapshot_name}"
  allowed_commands = [
    "exit",
    "echo -n",
    "command -v lzop",
    "command -v mbuffer",
    Regexp.new("^zpool get -o value -H feature@extensible_dataset '#{pool}'$"),
    Regexp.new("^zpool get -o value -H feature@extensible_dataset '#{pool}'$"),
    Regexp.new("^zfs list -o name,origin -t filesystem,volume -Hr '#{pool}/#{dataset}'$"),
    Regexp.new("^zfs get -H syncoid:sync '#{dataset_pattern_string}'$"),
    Regexp.new("^zfs get -Hpd 1 -t snapshot guid,creation '#{dataset_pattern_string}'$"),
    Regexp.new("^zfs get -Hpd 1 -t bookmark guid,creation '#{dataset_pattern_string}'$"),
    Regexp.new("^zfs send -w -nv?P (-I '#{snapshot_pattern_string}' )?'#{snapshot_pattern_string}'$"),
    Regexp.new("^ zfs send -w  (-I '#{snapshot_pattern_string}' )?'#{snapshot_pattern_string}' \\| lzop  \\| mbuffer  -q -s 128k -m 16M( 2>/dev/null)?$"),
    Regexp.new("^zfs send -nv?P -t [0-9a-f\-]+$"),
    Regexp.new("^ zfs send -w  -t [0-9a-f\\-]+ \\| lzop  \\| mbuffer  -q -s 128k -m 16M 2>/dev/null$"),
  ]
  return allowed_commands.any? do |pattern|
    pattern.is_a?(String) ? pattern == cmd : pattern =~ cmd
  end
end 

# Get the command that was requested via SSH from an environment variable
cmd = ENV['SSH_ORIGINAL_COMMAND']


if allowed? cmd
  log "authorised command was : #{cmd}"
  # print result of command to SSH client via STDOUT 
  # bash -c because Ruby.
  print `bash -c "#{cmd}"` 
else
  log "ILLEGAL COMMAND: #{cmd}"
  # return failure to SSH client
  exit 1 
end