SSH File Access through Gateway
Table of Contents
Overview
Accessing files on a remote host using SSH is usually not a big problem. Things change however, when the remote host is not directly accessible from the client but must be accessed through a gateway. Nevertheless, SSH can “hop” over such a gateway to reach the real target destination. This document discusses how to achieve this. Accessing the target host by SSH, with Emacs and even accessing other services on the target host should work as smooth as possible, preferably not so much different from accessing a directly reachable host.
Initial Situation and Objective
The initial situation involves three hosts:
A client – the machine (desktop, workstation, whatsoever) we will use, have direct access to and from which connections will be made. Host name and IP number of this client (or in fact any client) in not important for our purpose. We'll call this machine client.home.net or client from now on and its colour is cyan as both client and cyan start with c.
The
target host
– the machine we want to connect to. That server resides in a
demilitarized zone (DMZ) and is only accessible from inside that DMZ
(and possibly other networks) but not from the internet, especially
not from
client.home.net.
We'll give this machine a private IP of 10.0.0.2 and it has no globally
assigned IP. This machine will be called
target.remote.net
or target host or target from now on and its colour is tan.
The
gateway
– a machine that resides in the same DMZ as
target.remote.net
and can connect to that host via SSH. Furthermore that machine is
accessible from the outside. Consequently it has two IP addresses, one in
the DMZ which we will assume as 10.0.0.1 and one globally assigned
address which is 240.0.0.1.1 We'll call this machine
gateway.remote.net
or gateway from now on and its colour is green.
Here is a summary of the hosts we'd like to access from the client
| Host | IP (DMZ) | IP (ext.) |
|---|---|---|
| gateway.remote.net | 10.0.0.1 | 240.0.0.1 |
| target.remote.net | 10.0.0.2 | — |
This means that ssh, scp and the like to
target.remote.net
do not work out of the box. Our goal is now to make these commands work
and furthermore to make this machine accessible directly in Emacs (as
ordinary user as well as as root). Finally, we'd like to have a simple
method to access other services on
target.remote.net
from
client.home.net
by tunnelling them through SSH.
Step 1: Shell Access (ssh)
Accessing the Gateway
SSH access to
gateway.remote.net
should work out of the box. However, the hostname (gateway.remote.net)
is not known outside remote.net. To avoid re-typing IP numbers all the
time, we can define shell variables, aliases, /etc/hosts entries
etc.2 Personally, I prefer a little wrapper script that I put in
~/bin as gateway:3
#!/bin/bash __SSH=/usr/local/bin/ssh $__SSH 240.0.0.1 $*
This means we can use gateway to ssh to the
gateway. This document will use this shortcut in the further
sections.
Accessing the Target Host
The first goal to achieve is easy shell access to
target.remote.net.
Ideally ssh target should just work and
give us a shell on the
target host. To accomplish that, we'll need an additional SSH
public/private key pair, i.e. keys different from those we use to access
gateway.remote.net. So let's start with creating the
keys:
The public key is then transferred to our gateway like this:
Ha, there it is the again: the IP number of the gateway:. We'll address the problem to avoid typing the address in scp commands later on.
Now add the new public key to our authorized_keys file on the
gateway
(and while we are at it, push the public key to the
target host) with
scp:
Okay, above commands could be simplified but at least it's clear what's
going on. We now need to edit the authorized_keys file. What we need to
do is to add a forced command, i.e. a command that is automagically
called, whenever an SSH connection with the referring key is made. The
forced command is included in the authorized_keys file directly in front
of the key itself. So the key file should look like this:
ssh-rsa AAAA [...] command="/usr/local/bin/ssh 10.0.0.2 $SSH_ORIGINAL_COMMAND" ssh-rsa AAAA [...]
First line contains our usual SSH key to connect to the gateway. The second line contains the new key together with the forced command. What's done here is basically that your original SSH command will be passed through to the target host. If your original command is empty, you'll get an interactive shell.
As last step, simply add the public key just used to the authorized_keys
file on the target host:
You may note that we connect from the
gateway to the
target host with public-key-authentication but that the private
key is not available on the
gateway. To circumvent any problems, we do agent forwarding.
In our ~/.ssh/config we just add:
Accessing the Target Host more comfortably
SSH Wrapper Script
Now we can get an interactive shell on the
target host as well as commit commands there. To make this
more comfortable we should make another small script. Let's call is
target:
#!/bin/bash ## binaries we need __SSH=/usr/local/bin/ssh __EGREP=/bin/egrep ## ssh key to use __KEY=/home/me/.ssh/target ## ssh server to use __HOST=240.0.0.1 ## test if we should avoid tty alloc and escape chars if [ -n "`echo $* | $__EGREP '^((cat )?>|cat ).+'`" ]; then __OPT="-T -e none" fi ## do the ssh connection $__SSH $__OPT -i $__KEY $__HOST $* exit
After some config and test, this script basically performs an SSH
connection with the appropriate options. You may need to add more things
here, especially the user name used for the connect is not specified and
thus defaults to your local user name. The script checks if the command
passed to ssh starts with cat or > and adds some options then.
We'll discuss this later.
Adding Keys to Agent automatically
To add the new key to the agent on login, we can use pam_ssh. If you don't use pam_ssh, yet, have a look at http://pam-ssh.sourceforge.net/. If you still don't want to use pam_ssh afterwards you'll have to add your keys manually to the SSH agent.
By default, pam_ssh will only add identity, id_rsa and id_dsa and
not target … but this can be configured. What you need to do is to
change all calls to pam_ssh when used for authentication:
should give you an idea of which files to edit. For me the files to modify where
- /etc/pam.d/common-auth
- /etc/pam.d/other
Additional keys are added with the keyfile parameter, e.g. my line for
pam_ssh authentication in the relevant files looks like this:
auth sufficient /lib/security/pam_ssh.so keyfiles=id_rsa,id_rsa2,[...]
As this is a generic approach, I had to symlink target in my SSH dir
to id_rsa2. However, you can also put target directly in your pam
config files.
Step 1 Wrap-up
Right now, we are able to connect to both the gateway and the target host via SSH, thus getting interactive shells or committing non-interactive commands. SCP however is another story …
Step 2: File Transfer (scp)
File Transfer to/from the Gateway
You may remember, that we still had to enter a clumsy IP number when doing
an scp to
gateway.remote.net
in Accessing the Gateway. To avoid this, we will
use a wrapper script for scp when using it to connect to the
gateway
–
gateway.scp.sh:
#!/bin/bash __SCP=/usr/local/bin/scp __SED=/bin/sed __COM=`echo $* | $__SED 's!gateway\(\.remote\.net\)*:!240.0.0.1:!g'` $__SCP $__COM
When using this command, the host occurrence of gateway will be replaced
by the proper IP number. You can already use this script but it's more
comfortable to have a wrapper for the wrapper … something we'll address
soon.
File Transfer to/from Target Host
This is a bit more problematic since our forced command is an explicit
ssh. The command should look quite different when using scp and what's
worse, must include path information.4 To approach this we have two
solutions, both with advantages and disadvantages.
File Transfer with the ssh Command
First of all, we can use ssh to transfer files. Have a look at these
examples:
target "cat /etc/passwd" > my.passwd cat my.passwd | target "cat > ~/passwd.bak" target "tar cspv ~/tmp" > tmp.tar tar cspv tmp | target "cat > tmp.tar"
This should work for all files, even binary files. However, we should
make sure that we do not have any SSH escaping or TTY allocation but
that's something our wrapper script takes care of, which adds the options
if cat or re-direction (>) occurs (see SSH Wrapper Script).
This approach has the advantage, that it works out of the box, without
any further setup or preparation. The disadvantages are obvious as well:
As we use cat (or output redirection) to write the target files, we
cannot do recursive copies and loose all permissions, at least if we do
not use tar. Furthermore the syntax is different from other SCP
commands.
File Transfer with scp and Port Forwarding
To actually use the scp command we must dig a tunnel to the target host
(aka port forwarding). The actual command to achieve this looks like
this:
ssh -L 2222:10.0.0.2:22 240.0.0.1
Looks complicated? It's not really that hard to understand. What this
does is to create a connection to 240.0.0.1,
gateway.remote.net. This connection is available on
localhost of port 2222. Furthermore, on the
gateway, that connection is
forwarded to
target.remote.net
(10.200.0.2), port 22. This means that we can access the
target host
directly via localhost:2222.
That said, we pack that in a nice script: tunnel22.sh:
#!/bin/bash __SSH=/usr/local/bin/ssh __PORT=2222 __DEST=10.0.0.2 __GATE=240.0.0.1 $__SSH -L $__PORT:$__DEST:22 $__GATE
If you fire this, you will end up with an interactive session on the gateway and with the tunnel just described. The tunnel is open as long as the connection to the gateway is not interrupted.5
Now we need another terminal to commit commands like this:
scp -P 2222 -r ~/tmp localhost:~/ scp -P 2222 -r localhost:~/tmp .
And we put this again in a nice wrapper script (target.scp.sh):
#!/bin/bash __SCP=/usr/local/bin/scp __SED=/bin/sed __PORT=2222 __COM=`echo $* | $__SED 's!target\(\.remote\.net\)*:!localhost:!g'` $__SCP -P 2222 $__COM
This script will re-write the scp commands for us (so that we can use
target instead of localhost and omit the port number). The script
will simply fail if no tunnel has been opened but you can of course
expand the script to check that before the secure copy attempt … or you
could even let the script create the tunnel if it is not available.
To make all this even more comfortable we wrap gateway.scp.sh and
target.scp.sh by even another script, which we call – tata – scp
(do not override your original one, ~/bin/ is a good place for this
script). This script will usually call the original scp but if
connections to
gateway.remote.net and
target.remote.net should be made, it will call the appropriate
wrappers:
#!/bin/bash __SCP=/usr/local/bin/scp __SCP1=/home/me/bin/gateway.scp.sh __SCP2=/home/me/bin/target.scp.sh __EGREP=/bin/egrep ## test if we should use gateway hopping if [ -n "`echo $* | egrep 'gateway(\.remote\.net)?:'`" ]; then exec $__SCP1 $* elif [ -n "`echo $* | egrep 'target(\.remote\.net)?:'`" ]; then exec $__SCP2 $* else exec $__SCP $* fi
The advantage of this approach is clear: we can use ordinary SCP commands with ordinary host names. The disadvantage is that we have to dig the tunnel in a separate terminal first. Choose your poison.
Step 2 Wrap Up
Shell-wise we've done all we can do and it became quite comfortable. We
could either use cat (and tar) and ordinary ssh for file transfer or
we use our modified scp with a tunnel in an extra terminal.
Step 3: Emacs Integration
Accessing remote files via SSH in Emacs is usually done with the package Tramp, which is part of the Emacs distribution. I've tested all of the following with different GNU Emacs 23 versions and Tramp 2.1.x. Here's my configuration:
;; tramp (require 'tramp) (setq tramp-default-method "scpc") (setq tramp-default-host "localhost") ;; ... other tramp config (add-to-list 'tramp-default-proxies-alist '("\\`240\\.0\\.0\\.1\\'" "\\`root\\'" "/ssh:%h:")) (add-to-list 'tramp-default-proxies-alist '("\\`10\\.0\\.0\\.2\\'" nil "/ssh:240.0.0.1:")) (add-to-list 'tramp-default-proxies-alist '("\\`10\\.0\\.0\\.2\\'" "\\`root\\'" "/ssh:%h:")) (setenv "gateway" "240.0.0.1") (setenv "target" "10.0.0.2")
This allows you to open the following files (e.g. with find-file):
/scpc:$gateway:~/tmp/tmp.txt ; default user on gateway /su:$gateway:/etc/passwd ; root on gateway /ssh:$target:~/tmp/tmp.txt ; default user on target host ; default method 'scpc' does not work with multihops /su:$target:/etc/passwd ; root on target host
I think the configuration along with examples are pretty self-explaining.
The Tramp User Manual gives further information. As a hint: the setenv gives
you environment vars that can be expanded in the minibuffer (Tramp however,
does not recognise /$VAR: as proper file name but does so if we put the
method in front.).
File Name Abbreviations
Instead of using environment variables, we can also use abbrev-mode. This mode is usually not active in the minibuffer and you probably do not want to have your ordinary abbreviations in the minibuffer. The solution is to define a special abbrev-table and activate it for the minibuffer. Here's something to put in your Emacs startup file:
; minibuffer abbrev (define-abbrev-table 'my-minibuffer-abbrev-table '( ("agateway" "/scpc:240.0.0.1:") ("atarget" "/ssh:10.0.0.2:") ("rgateway" "/su:240.0.0.1:") ("rtarget" "/su:10.0.0.2:") )) (add-hook 'minibuffer-setup-hook '(lambda () (abbrev-mode 1) (setq local-abbrev-table my-minibuffer-abbrev-table))) (defadvice minibuffer-complete (before my-minibuffer-complete activate) (expand-abbrev))
When loaded, you can open files on the gateway and target host quite easily, e.g.:
C-x C-f atarget TAB
For further information on file name completion with Tramp see the Tramp User Manual FAQ.
Authentication
Authentication is handled by ssh-agent for connections as ordinary user.
However, to handle root access the root password is required. You can
handle this comfortably by using an ~/.authinfo or ~/.authinfo.gpg
file which is part of auth-source.el which is in turn part of Gnus. The
.gpg version is an encrypted file and therefore preferred, but you will
need the epa Emacs package and gpg-agent to use it effectively.
Each entry in .authinfo.gpg consists of a single line like this:
machine 240.0.0.1 port su login root password PASSWORDHERE machine 10.0.0.2 port su login root password PASSWORDHERE
Of course, PASSWORDHERE must be replaced with the appropriate
password :)
Reverse Access
This is rather seldom necessary and therefore only pointers are given here. Wrapping this all up in scripts is left as exercise for the user ;)
Okay, okay, forget about the above, some scripts will follow.
The scenarios is that you want to access your client from the gateway and/or the target host. Also our client is accessible via SSH from the outside, neither gateway nor target host allow outgoing SSH.
Tunnel to the Gateway
SSH from gateway to our client is easily set up since the gateway is accessible directly via SSH from the client. All we do is this:
Note the -R switch instead of the -L that we've already used. As
long as this tunnel is open, you can do a ssh -p 2223 localhost (or
likewise scp with the -P switch) from
gateway.remote.net
to
client.home.net. BTW: it may be a good idea to define
alternate names in /etc/hosts for localhost and use those aliases for
reverse access. SSH won't complain about changing host keys then.
Tunnel to the Target Host
To do all this on the target host, we need to hop further from gateway.remote.net … in exactly the same way as we hopped from our client to the gateway (but this time with the port we've just established):
This will leave you on
target.remote.net while extending our original
tunnel. You can now connect from the
target host by using
ssh -p 2223 localhost (or likewise scp with the -P switch) to
connect to the
client.
Wrap up
Of course, this can be scripted: Here are the two examples, one for SSH from gateway.remote.net, one for SSH from target.remote.net:
#!/bin/bash __SSH=/usr/local/bin/ssh __PORT=2234 __GATE=240.0.0.1 echo "" echo "Once established this tunnel allows you to ssh/scp from" echo "gateway to your local host (e.g. localhost) with:" echo " ssh -p 2234 localhost" echo " scp -P 2234 foo localhost:~/bar" echo " scp -P 2234 localhost:~/foo bar" echo "" $__SSH -t -R $__PORT:localhost:22 $__GATE
#!/bin/bash __SSH=/usr/local/bin/ssh __PORT=2233 __GATE=240.0.0.1 __HOST=target echo "" echo "Once established this tunnel allows you to ssh/scp from" echo "target to you local host (e.g. localhost) with:" echo " ssh -p 2233 localhost" echo " scp -P 2233 foo localhost:~/bar" echo " scp -P 2233 localhost:~/foo bar" echo "" $__SSH -t -R $__PORT:localhost:22 $__GATE "ssh -R $__PORT:localhost:$__PORT $__HOST"
Web Access or Tunneling other Protocols
We've looked at tunneling SSH so far. In fact, tunneling other protocols (as long as they are suitable for this) is done in much the same way. You should know enough right now to understand the following without lengthy explanations so I will keep is short.
A common scenario is to connect to the HTTP(S) service on the target host. I have scripts for both and the HTTPS script follows:
#!/bin/bash __SSH=/usr/local/bin/ssh __PORT=22443 __DEST=10.0.0.2 __GATE=240.0.0.1 __CMD='while [ 0 ] ; do echo -n "$HOST (tunneling HTTPS): "; date; sleep 30; done;' $__SSH -L $__PORT:$__DEST:443 $__GATE $__CMD
This script is essentially much like the one we've discussed in File Transfer with scp and Port Forwarding. This one executes a while loop on
the gateway to a) give us feedback on what's going on there and b) prevent
any auto-logout features from closing our connection. Start this script in
a terminal window and leave it somewhere while you need the tunnel. In your
browser you can now use the address https://localhost:22443 to access the
HTTPS service on the target host.
Scripts for HTTP or other services look similar.
List of Scripts
Here's a list of scripts I use together with their meaning. Of course, the scripts listed on this page are not exactly the ones I use, what's printed here has been generalised and simplified a lot to make it a bit easier to understand.
| Script | Purpose |
|---|---|
| gateway | connect to gateway (ssh) |
| target | dto. to target |
| tunnel80.sh | digs SSH-tunnel to target/port 80 for accessing HTTP |
| tunnel443.sh | dto. for port 443 |
| tunnel22.sh | dto for port 22 (needed for direct scp) |
| scp | wrapper for scp; calls OpenSSH scp, gateway.scp.sh or target.scp.sh |
| gateway.scp.sh | builds scp command with correct IP and port for gateway |
| target.scp.sh | dto. for target |
| gateway-reverse.sh | digs reverse tunnel for ssh/scp from gateway to localhost |
| target-reverse.sh | dto. for ssh/scp from target |
Footnotes:
1 This address is of course fictional.
2 Of course, we could also make a DNS entry, but as it's comfortable to have a wrapper script anyway, it's not necessary here.
3 As with all example scripts here, you'll have to adapt paths, IP numbers etc. before using them.
4 And to make the forced command approach impossible: we do not have
all the information we need. Imagine an scp .bashrc host:~/tmp. In this
case the receiver will have in $SSH_ORIGINAL_COMMAND a scp -t ~/tmp so
the information about .bashrc is literally lost.
5 We can also add a command to the end of the last line to avoid being kicked-out by some auto-logout feature. An example of this is shown in Web Access or Tunneling other Protocols.
Date: 2010-02-25 16:24:34
HTML generated by org-mode 6.34trans in emacs 23