2

I have two servers, A and B. Login to B requires 2FA every time, so I use a ControlMaster to persist the authentication status.

However, I'm using a laptop, which sometimes sleeps and stops all TCP connections. This breaks the ControlMaster Socket. So I'm using another server as a jump server. I persist the authentication status on A and connect to B from A (ssh a -tt ssh b). I then write a RemoteCommand ssh -tt -q b to the ssh config for host a on my laptop, so I can just run ssh a.

However, the current setup doesn't work for VSCode's Remote SSH and rsync. How can I make this work?

Solutions I've tried:

  1. ProxyJump. It don't work because I want to presist the authentication statsu on the jump server. I don't want to only use it as a network gateway.
  2. Forwarding the 'control master socket` on A to my local laptop, then use it as socket to B. For some reason the ssh session on A isn't responding, so the ssh client on my laptop freezes.
Yuxuan Lu
  • 123

3 Answers3

1

A posteriori addendum

After posting this answer, I developed an alternative solution that overcomes the limitations of the solution given below ("drawbacks" section). I find the other solution superior, you may want to start with that, it's in this other answer.


Analysis

I then write a RemoteCommand ssh -tt -q b to the ssh config for host a on my laptop, so I can just run ssh a.

(For consistency I will use capital A and B even in code.)

This creates a chain consisting of the local ssh (that connects the local machine to A) and ssh -tt running on A (and connecting A to B).

Locally invoked ssh A, depending on what options and operands you (or anything, e.g. rsync) add, either allocates a tty on A or not; but the next ssh in the chain is always ssh -tt, so it always allocates a tty on B. ssh that allocates a tty cannot be used as (a part of) a transport pipe because it will break any protocol that sends binary data. See this question (and the links therein) for some insight: ssh with separate stdin, stdout, stderr AND tty.

In general in a link of sshs almost always we should either make each link allocate a tty, or make each link not allocate a tty.

A tool like rsync invokes ssh in a way that does not allocate a tty on the remote side; it needs an 8-bit clean channel. Adding ssh -tt to the chain breaks this. Or would break this, because…

In the first place a tool like rsync invokes ssh with argument(s) that constitute a remote command the tool needs. In your case this command should get to a shell on B, so it would need to be provided as additional argument(s) to the ssh inside your remote command. In fact the flow never gets to the remote command. In my tests ssh from OpenSSH 9.6 does not allow me to use the RemoteCommand option together with argument(s) building a remote command:

$ ssh -o RemoteCommand=foo stranger@nonexistent bar baz
Cannot execute a command-line and remote command.

Solution

In OpenSSH there is a way to supply a RemoteCommand-like command together with a command build from arguments.

This solution requires you to be able to log in to A via SSH by using a key, and to be able to edit your ~/.ssh/authorized_keys on A (in general: one of whatever files sshd on A is configured to use; but this answer assumes the defaults).

The solution assumes that ssh from A to B uses ControlMaster with ControlPersist (in SSH config of A). It seems you have already set this up.

Proceed as follows:

  1. Remove the RemoteCommand ssh -tt -q b from your local SSH config.

  2. Generate a new key pair on the local computer:

    # on local
    ssh-keygen -f ~/.ssh/special_AB -C 'Special key to connect to A and then to B.'
    
  3. Make A recognize the key:

    # on local
    ssh-copy-id -i ~/.ssh/special_AB A
    

    (If the default key allows you to log in to A and you get All keys were skipped, retry with -f exactly one time.)

  4. Force A to run specific code when the key is used. The full code we want A to run is somewhat complicated, therefore we will put it in a separate helper script (in a moment). Now let's just associate the key with the future script.

    To do this, log in to A (to an interactive shell), open ~/.ssh/authorized_keys in a text editor, locate the line that ends with Special key to connect to A and then to B. (most likely this will be the last line). To the beginning of this line add:

    command="exec ~/.ssh/helper_AB" 
    

    Note there is a whitespace after the last ". The resulting line shall look similarly to the following example:

    command="exec ~/.ssh/helper_AB" ssh-ed25519 AAAAC3…
    

    Save the file.

  5. Still on A, create our helper script:

    # on A
    cat >~/.ssh/helper_AB <<'EOF'
    #!/bin/sh
    set -- ssh
    [ -t 0 ] && set -- "$@" -t
    exec "$@" -q -- B "$SSH_ORIGINAL_COMMAND"
    EOF
    

    And make it executable:

    # on A
    chmod +x ~/.ssh/helper_AB
    
  6. Tell your local ssh how to reach B; put this at the beginning* of your local SSH config (~/.ssh/config):

    Host B
      Hostname A
      IdentitiesOnly yes
      IdentityFile ~/.ssh/special_AB
    Host *
    

    * Since the first obtained value for each parameter is used, more host-specific declarations should be given near the beginning of the file, and general defaults at the end (elaborated in this answer).

    Host * in the snippet is in case your original file did not start with Host/Match and there were some lines applied unconditionally. If so, we want them to still apply unconditionally, not under Host B.

Now local usage of ssh B will reach B via A. Requests for a tty (or for no tty) will propagate properly. Command(s) given in the local command line will be passed properly.


Drawbacks

The solution is not perfect, there are drawbacks:

  • Upon disconnecting, you may see Connection to A closed (not Connection to B …).
  • Forwarding (tunnels) will not involve B automatically; so local tools that use ssh B to create tunnel(s) (and that obviously expect the remote side to be B) may still fail. In particular ssh -A B will expose your local agent only to A.

Possibly more. In general any implication of the fact your local ssh connects to A, not to B, may manifest itself. To solve these problems you need "nested tubes" instead of the chain (read this answer to see what I mean); but this is exactly the "ProxyJump" solution you have tried, I understand why it doesn't fit your needs.

(See my other answer. I believe I solved the problem there.)


Explanation, divagation

The core of the solution comes from this fragment of man 8 sshd:

command="command"

Specifies that the command is executed whenever this key is used for authentication. The command supplied by the user (if any) is ignored. The command is run on a pty if the client requests a pty; otherwise it is run without a tty. If an 8-bit clean channel is required, one must not request a pty or should specify no-pty. A quote may be included in the command by quoting it with a backslash.

This option might be useful to restrict certain public keys to perform just a specific operation. […]

The command originally supplied by the client is available in the SSH_ORIGINAL_COMMAND environment variable. […]

It seems to me that conceptually this command= is similar to RemoteCommand, still only the former supports using and accessing whatever command the user supplied in the command line. The mentioned Cannot execute a command-line and remote command seems to be an arbitrary obstacle, our solution works by using commands from two sources. It would be nice if also in case of RemoteCommand it worked and SSH_ORIGINAL_COMMAND was in the environment; we could then build a solution similar to the above, but without needing to involve a key (i.e. the solution could work with any authentication method).

0

I suggest creating a tunnel between A and B. Here's a rough a idea that you can test, then feel free to build upon it if it works:

On server a, run the following command

$ ssh -L 444:b:444

(444 is the port, you can replace it by any number you want)

This command will connect you to b and create a tunnel between a and b. Let this terminal run in the background.

Now back on your local computer, you just need to run

ssh a -p 444

And it will take you straight to server b

Cydouzo
  • 367
0

Preliminary note

In this answer I present an alternative to the solution from my previous answer. Some insight into the problem is there, I won't repeat it here. This answer contains a standalone solution, independent from the other answer. I'm answering the question anew, so this answer addresses the situation described in the question. If you have implemented another solution, then you may need to revert to the configuration described in the question before applying this solution.

Again, to denote the remote hosts, for consistency I will use capital A and B even in code.


Solution

  1. Remove the RemoteCommand ssh -tt -q b from your local SSH config.

  2. Run an SSH server (sshd) as your regular user on B.

    For this you need to:

    1. Create a host key file for the server:

      # on B
      ssh-keygen -f ~/.ssh/my_special_host_key -C 'Special host key for my private sshd.'
      

      (If you cannot run ssh-keygen on B, generate a key pair elsewhere and move it to your ~/.ssh/ on B.)

    2. Create a configuration file for the server:

      # on B
      cat >~/.ssh/my_special_sshd_config <<'EOF'
      HostKey ~/.ssh/my_special_host_key
      ListenAddress 127.0.0.1:17522
      EOF
      

      (This creates a minimal config. Customize the config at will.)

    3. Make sure the following command keeps running as your regular user on B:

      /usr/sbin/sshd -f ~/.ssh/my_special_sshd_config
      

      (Depending on how you run it, you may or may not want -D.)

      It may be a systemd user service, a command run in tmux on B, something your already configured connection (from A to B) that "persists the authentication status on A" runs automatically, or even the following command run locally:

      # on local
      ssh -t A 'ssh -t B "/usr/sbin/sshd -D -f ~/.ssh/my_special_sshd_config"'
      

      Note that invoking this way from your local machine may cause B to terminate this sshd short after the local machine goes to sleep; or after some time; or(?) never. So after waking up the same local command may succeed or fail. I don't know enough about A and B to guide you here, I leave the implementation to you.

      Nowadays running sshd (from OpenSSH at least) as a regular user does not require UsePrivilegeSeparation no advised in old guides (example). Your private sshd on B will be able to act as your user only, it shall not allow other users to log in, even if they provide credentials otherwise valid on B. (But if you are not convinced, you can add AllowUsers yourusername to my_special_sshd_config.) In my tests the right password was rejected anyway, but key-based authentication worked. By default sshd uses ~/.ssh/authorized_keys (and ~/.ssh/authorized_keys2); there, register the key your local ssh uses, if not done already.

      Now you (or anyone on B) can reach your private sshd by invoking ssh -p 17522 127.0.0.1 on B, but only your key(s) will be accepted. Of course logging in from B to B makes little to no sense, so let's move to A.

  3. On A, reconfigure your already configured connection (from A to B) that "persists the authentication status on A", so it also forwards the port all the time. The right line for ~/.ssh/config is:

    LocalForward 127.0.0.1:17522 127.0.0.1:17522
    

    under Host B.

    When the forwarding works and your private sshd on B works, you (or anyone on A) can reach the said sshd by invoking ssh -p 17522 127.0.0.1 on A.

  4. Now from your local computer you can reach your private sshd on B by jumping via A:

    # on local
    ssh -J A -p 17522 127.0.0.1
    

    To automate, put this at the beginning of your local SSH config (~/.ssh/config):

    Host B
      ProxyJump A
      Hostname 127.0.0.1
      Port 17522
    Host *
    

Now local usage of ssh B will reach B via A and every functionality (including port forwarding) should work.

Note 17522 is an arbitrary port number. Change it if it's already in use on B or on A.