Password-less SSH keys are useful for setting up long running services but require care to limit access and prevent exploits. There are quite a few web sites that describe how to set up restrictions on password-less SSH keys but none that mention how to do it if jump hosts are involved. This snippet shows how to get it working.
Assume that this is the command that should work using a restricted SSH key:
$ scp foobar datasink:/tmp
For convenience, define host entries in ~/.ssh/config, where bastion is the jump host and datasink is the end host:
Host datasink ProxyJump bastion IdentitiesOnly yes IdentityFile ~/.ssh/transferkey Host bastion IdentitiesOnly yes IdentityFile ~/.ssh/transferkey
Create a keypair specifically for the transfer:
$ ssh-keygen -t ed25519 -C 'transfer: restricted' -N '' -f ~/.ssh/transferkey # ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
Note: separate keys for bastion and datasink could be used. Since usage of the key(s) will be restricted, security is probably not measurably improved by using separate keys.
To allow but limit the use of transferkey for SSH tunneling to the datasink, add the following to bastion:.ssh/authorize_keys to restrict bastion to only being used as a jump host:
command="ssh -W datasink",restrict,port-forwarding,from="datasource" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
Note: this assumes that datasink resolves on the jump host. If that is not the case, the IP address of datasink should be used.
Attempts to use bastion interactively or non-interactively for anything other than jumping should fail:
$ ssh bastion PTY allocation request failed on channel 0 usage: ssh [-1246AaCfGgKkMNnqsTtVvXxYy] [-b bind_address] [-c cipher_spec] [-D [bind_address:]port] [-E log_file] [-e escape_char] [-F configfile] [-I pkcs11] [-i identity_file] [-J [user@]host[:port]] [-L address] [-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port] [-Q query_option] [-R address] [-S ctl_path] [-W host:port] [-w local_tun[:remote_tun]] [user@]hostname [command] Connection to bastion closed. $ ssh bastion touch foo usage: ssh [-1246AaCfGgKkMNnqsTtVvXxYy] [-b bind_address] [-c cipher_spec] [-D [bind_address:]port] [-E log_file] [-e escape_char] [-F configfile] [-I pkcs11] [-i identity_file] [-J [user@]host[:port]] [-L address] [-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port] [-Q query_option] [-R address] [-S ctl_path] [-W host:port] [-w local_tun[:remote_tun]] [user@]hostname [command]
The displayed usage message shows it failed (but isn’t ideal; there should be a more descriptive error message); neither interactive nor non-interactive commands succeeded. But it does allow the scp and rsync commands to work properly:
$ scp foobar datasink:/tmp foobar 100% 1755KB 13.4MB/s 00:00 Killed by signal 1. $ time rsync foobar datasink:/tmp Killed by signal 1.
Note: the “Killed by signal 1” message likely occurs when the transfer between datasource and datasink ends so the outer tunnel between datasource and bastion is killed. The message appears to be harmless as foobar validates properly using SHA256. However, the killed message may be mistaken for failure so it would be better if it wasn’t displayed. It isn’t obvious how to silence the message however.
The same procedure should work for more than one jump host. The final step in securing the password-less SSH key is to restrict scp or rsync on datasink. There are lots of web sites that show how.
This approach is certainly less insecure that having no restrictions at all on a password-less key but there may still be weaknesses or improvements. Suggestions welcome.
This is a repost of the snippet found at https://code.vt.edu/-/snippets/212.