Restrict Commands executed via pre-shared SSH keys
Introduction
Using pre-shared SSH keys is a great way to make logging into remote hosts quick and easy. No pesky passwords to remember. The downside is that if your organization relies on passwords for access control, the pre-shared key will negate any password control you have. For example: If your organization rolls a password for a system account, and the new password is not shared with all the original people, then some people who should not have access any more will continue to have access via their pre-shared key.Also, anyone with access to a user's account on a client host, could access the system account via the pre-shared key on the remote host. This is not ideal.
Sometimes, it is neccessary to allow certain commands only to be executed over SSH from specified client hosts without a password. Especially when thinking about automated tasks.
In my examples, I will demonstrate how we can create a simple remote procedure call type scenario using pre-shared keys and some built in linux security tools that lock things down.
These examples will use the "oracle" user which has been set up on both the client and remote hosts identically.
[oracle@centos-dev-1 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
The Steps
Create the public private key on the private host
Use ssh-keygen to create a public/private key pair without a password.[oracle@centos-dev-1 ~]$ ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/home/oracle/.ssh/id_rsa): [ENTER] Created directory '/home/oracle/.ssh'. Enter passphrase (empty for no passphrase): [ENTER] Enter same passphrase again: [ENTER] Your identification has been saved in /home/oracle/.ssh/id_rsa. Your public key has been saved in /home/oracle/.ssh/id_rsa.pub. The key fingerprint is: 64:cf:72:de:f7:85:af:fe:36:87:ce:8c:6e:31:8d:ed oracle@centos-dev-1.vbox.local The key's randomart image is: +--[ RSA 2048]----+ | | | | | o | | o o | | S + + | | + .+ o. | | . .+o..| | .=E++| | ooo*+*| +-----------------+ [oracle@centos-dev-1 ~]$ ls -l .ssh/ total 8 -rw-------. 1 oracle oinstall 1675 Dec 20 10:33 id_rsa -rw-r--r--. 1 oracle oinstall 412 Dec 20 10:33 id_rsa.pub
Share the public key to the remote host
You can use the ssh-copy-id tool or you could manually do it. I will use the ssh-copy-id tool to share the key.[oracle@centos-dev-1 ~]$ ssh-copy-id -i /home/oracle/.ssh/id_rsa.pub oracle@centos-dev-2 The authenticity of host 'centos-dev-2 (192.168.56.102)' can't be established. RSA key fingerprint is df:87:48:48:0a:15:fa:ca:e4:a7:41:8e:9e:af:19:b5. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added 'centos-dev-2,192.168.56.102' (RSA) to the list of known hosts. oracle@centos-dev-2's password: Now try logging into the machine, with "ssh 'oracle@centos-dev-2'", and check in: .ssh/authorized_keys to make sure we haven't added extra keys that you weren't expecting. [oracle@centos-dev-1 ~]$ ssh oracle@centos-dev-2 Last login: Thu Dec 19 13:38:03 2013 from centos-dev-1.vbox.local [oracle@centos-dev-2 ~]$ hostname centos-dev-2.vbox.local [oracle@centos-dev-2 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [oracle@centos-dev-2 ~]$ exit logout Connection to centos-dev-2 closed.At this point, we have successfully enabled transparent ssh logins using a pre-shared public key from centos-dev-1 to centos-dev-2 as the "oracle" user.
The result was a file created on the remote host called /home/oracle/.ssh/authorized_keys which looks like this:
[oracle@centos-dev-2 ~]$ hostname centos-dev-2.vbox.local [oracle@centos-dev-2 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [oracle@centos-dev-2 ~]$ cd /home/oracle/.ssh/ [oracle@centos-dev-2 .ssh]$ ls -l total 4 -rw-------. 1 oracle oinstall 412 Dec 20 10:35 authorized_keys [oracle@centos-dev-2 .ssh]$ cat authorized_keys ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAqdZI3PbacEoZ4uIk1hIc9Tl2pmLZtv0Y1/ZowqNOLRRQV84ziiNXoUXa8y+x9DxMzIPxKHdyYy0k0HFl06oIjfP4l3P3GY9L0MM/oYxYNr0UaJz46hDy7p3cKVjmeY/Xn3PsepNY3kAdp4zdNb1Hfs9G/PiB70LTfseJZjqwBJ6edYAt7YztWuO9OgrmDoW7QAdsPTlQGF9iW0gUqH/1UA5hXtGePRq2My9ahXmXOu1hy3H1wFygCBZaxP7A3qohgvVIT76DQ1mxhgLDiQCVkTzxVhHgMmVi4A7C0vpr2G5WxpLblVHWpjnXn/eAdQBQLnFtj/pza1U5K3Dy/GDxMQ== oracle@centos-dev-1.vbox.local
Restrict the public key on the remote host
Here we edit the /home/oracle/.ssh/authorized_keys file that was created in the previous step to include some restrictions for the key. We prepend the following to the key:from="centos-dev-1.vbox.local",command="/usr/local/bin/remote-commands.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-ptyThe key now looks like this:
from="centos-dev-1.vbox.local",command="/usr/local/bin/remote-commands.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAqdZI3PbacEoZ4uIk1hIc9Tl2pmLZtv0Y1/ZowqNOLRRQV84ziiNXoUXa8y+x9DxMzIPxKHdyYy0k0HFl06oIjfP4l3P3GY9L0MM/oYxYNr0UaJz46hDy7p3cKVjmeY/Xn3PsepNY3kAdp4zdNb1Hfs9G/PiB70LTfseJZjqwBJ6edYAt7YztWuO9OgrmDoW7QAdsPTlQGF9iW0gUqH/1UA5hXtGePRq2My9ahXmXOu1hy3H1wFygCBZaxP7A3qohgvVIT76DQ1mxhgLDiQCVkTzxVhHgMmVi4A7C0vpr2G5WxpLblVHWpjnXn/eAdQBQLnFtj/pza1U5K3Dy/GDxMQ== oracle@centos-dev-1.vbox.localThe extra options are broken down as follows:
- from="centos-dev-1" Only allow this key to be used when connections are made from centos-dev-1 host.
- command="/usr/local/bin/remote-commands.sh" This is a script that will be executed by the owner of this key (oracle) whenever the key is used to establish an ssh connection. We will write the script soon.
- no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty These options add extra security and prevent the key being used for anything other than running the command specified in command="".
[root@centos-dev-2 ~]# hostname centos-dev-2.vbox.local [root@centos-dev-2 ~]# id uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [root@centos-dev-2 ~]# lsattr /home/oracle/.ssh/authorized_keys -------------e- /home/oracle/.ssh/authorized_keys [root@centos-dev-2 ~]# chattr +i /home/oracle/.ssh/authorized_keys [root@centos-dev-2 ~]# lsattr /home/oracle/.ssh/authorized_keys ----i--------e- /home/oracle/.ssh/authorized_keys [root@centos-dev-2 ~]# rm /home/oracle/.ssh/authorized_keys rm: remove regular file `/home/oracle/.ssh/authorized_keys'? y rm: cannot remove `/home/oracle/.ssh/authorized_keys': Operation not permitted [root@centos-dev-2 ~]# ls -l /home/oracle/.ssh/authorized_keys -rw-------. 1 oracle oinstall 551 Dec 20 10:41 /home/oracle/.ssh/authorized_keys [root@centos-dev-2 ~]# su - oracle [oracle@centos-dev-2 ~]$ cat .ssh/authorized_keys from="centos-dev-1.vbox.local",command="/usr/local/bin/remote-commands.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAqdZI3PbacEoZ4uIk1hIc9Tl2pmLZtv0Y1/ZowqNOLRRQV84ziiNXoUXa8y+x9DxMzIPxKHdyYy0k0HFl06oIjfP4l3P3GY9L0MM/oYxYNr0UaJz46hDy7p3cKVjmeY/Xn3PsepNY3kAdp4zdNb1Hfs9G/PiB70LTfseJZjqwBJ6edYAt7YztWuO9OgrmDoW7QAdsPTlQGF9iW0gUqH/1UA5hXtGePRq2My9ahXmXOu1hy3H1wFygCBZaxP7A3qohgvVIT76DQ1mxhgLDiQCVkTzxVhHgMmVi4A7C0vpr2G5WxpLblVHWpjnXn/eAdQBQLnFtj/pza1U5K3Dy/GDxMQ== oracle@centos-dev-1.vbox.localAs you can see, once this attribute is set on the file, not even the root user can edit or delete it. The root user will first need to remove the "immutable" attribute with "chattr -i /home/oracle/.ssh/authorized_keys".
The oracle user can still read the file which is a requirement of the SSH daemon.
Write a command parser on the remote host
This script will parse the command sent by the ssh client and execute based on some simple tests.[oracle@centos-dev-2 ~]$ ls -l /usr/local/bin/remote-commands.sh -rwxr-xr-x. 1 root root 315 Dec 19 13:30 /usr/local/bin/remote-commands.sh [oracle@centos-dev-2 ~]$ cat /usr/local/bin/remote-commands.sh #!/bin/sh # if [ -z "${SSH_ORIGINAL_COMMAND}" ]; then echo "This script can only be executed via SSH remote calls" exit fi case "$SSH_ORIGINAL_COMMAND" in "get hosts") cat /etc/hosts ;; "get resolv.conf") cat /etc/resolv.conf ;; "get env") env ;; *) echo "Bad Command" ;; esac exitThe script is owned by root:root but all users have read and execute permissions on the script.
That's it! We are ready to begin our tests now.
Test
The script we write allows clients to execute 3 commands:- get hosts
- get resolv.conf
- get env
Using the key to establish an ssh connection without passing a command will be rejected. So logins are now disabled.
Executing the script from the local host will be rejected too. This script should only be called by remote hosts.
Test "get hosts"
[oracle@centos-dev-1 ~]$ hostname centos-dev-1.vbox.local [oracle@centos-dev-1 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [oracle@centos-dev-1 ~]$ ssh centos-dev-2 get hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
Test "get resolv.conf"
[oracle@centos-dev-1 ~]$ hostname centos-dev-1.vbox.local [oracle@centos-dev-1 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [oracle@centos-dev-1 ~]$ ssh centos-dev-2 resolv.conf ; generated by /sbin/dhclient-script search vbox.local nameserver 192.168.56.1
Test "get env"
My fake oracle environment is nothing like what a real one would look like. This example is given just to prove that the remote script does execute with all of the expected environment variables.[oracle@centos-dev-1 ~]$ hostname centos-dev-1.vbox.local [oracle@centos-dev-1 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [oracle@centos-dev-1 ~]$ ssh centos-dev-2 get env SELINUX_ROLE_REQUESTED= SHELL=/bin/bash SSH_CLIENT=192.168.56.101 46697 22 SELINUX_USE_CURRENT_RANGE= USER=oracle PATH=/usr/local/bin:/bin:/usr/bin MAIL=/var/mail/oracle PWD=/home/oracle LANG=en_US.UTF-8 SELINUX_LEVEL_REQUESTED= HOME=/home/oracle SHLVL=2 SSH_ORIGINAL_COMMAND=get env LOGNAME=oracle CVS_RSH=ssh SSH_CONNECTION=192.168.56.101 46697 192.168.56.102 22 LESSOPEN=|/usr/bin/lesspipe.sh %s G_BROKEN_FILENAMES=1 _=/bin/env
Test some bad commands
[oracle@centos-dev-1 ~]$ hostname centos-dev-1.vbox.local [oracle@centos-dev-1 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [oracle@centos-dev-1 ~]$ ssh centos-dev-2 userdel oracle Bad Command [oracle@centos-dev-1 ~]$ ssh centos-dev-2 "su -c \"userdel oracle\"" Bad Command [oracle@centos-dev-1 ~]$ ssh centos-dev-2 rm -fr /* Bad Command
Test logging in without a password
[oracle@centos-dev-1 ~]$ hostname centos-dev-1.vbox.local [oracle@centos-dev-1 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [oracle@centos-dev-1 ~]$ ssh centos-dev-2 PTY allocation request failed on channel 0 This script can only be executed via SSH remote calls Connection to centos-dev-2 closed.
Using SSH to force the requirement for a password
Because a shared key exists, ssh will automatically try to use it for authentication. We now need to explicitly declare that no public private key should be used for authentication.[oracle@centos-dev-1 ~]$ hostname centos-dev-1.vbox.local [oracle@centos-dev-1 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [oracle@centos-dev-1 ~]$ ssh -o PubkeyAuthentication=no centos-dev-2 oracle@centos-dev-2's password: Last login: Fri Dec 20 10:35:22 2013 from centos-dev-1.vbox.local [oracle@centos-dev-2 ~]$ hostname centos-dev-2.vbox.local [oracle@centos-dev-2 ~]$ id uid=500(oracle) gid=501(oinstall) groups=501(oinstall) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 [oracle@centos-dev-2 ~]$
Comments