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-pty 
The 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.local
The 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="".
None of this is very useful if the oracle user can simply log into the host using his password and edit this file. Use the chattr command to lock the file for editing as follows: (NOTE: Only the root user can do this)
[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.local
As 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
exit
The 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
Any other command will be rejected.

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

Popular posts from this blog

Automatically mount NVME volumes in AWS EC2 on Windows with Cloudformation and Powershell Userdata

Extending the AD Schema on Samba4 - Part 2

Python + inotify = Pyinotify [ how to watch folders for file activity ]