Navigation überspringen
Diese Seite ist leider nicht in deutscher Sprache verfügbar. Nachfolgend finden Sie die englische Version.

SSH File Access through Gateway

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

HostIP (DMZ)IP (ext.)
gateway.remote.net10.0.0.1240.0.0.1
target.remote.net10.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 gatewaygateway.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.

ScriptPurpose
gatewayconnect to gateway (ssh)
targetdto. to target
tunnel80.shdigs SSH-tunnel to target/port 80 for accessing HTTP
tunnel443.shdto. for port 443
tunnel22.shdto for port 22 (needed for direct scp)
scpwrapper for scp; calls OpenSSH scp, gateway.scp.sh or target.scp.sh
gateway.scp.shbuilds scp command with correct IP and port for gateway
target.scp.shdto. for target
gateway-reverse.shdigs reverse tunnel for ssh/scp from gateway to localhost
target-reverse.shdto. 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.

Author: Ulf Stegemann <ulf@zeitform.de>

Date: 2010-02-25 16:24:34

HTML generated by org-mode 6.34trans in emacs 23

Valid XHTML 1.0 strict! Valid CSS! Org-Mode Gehostet von zeitform Internet Dienste. [FSF Associate Member]