Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ssh multiplexing? #20

Open
coolaj86 opened this issue Mar 12, 2023 · 10 comments
Open

ssh multiplexing? #20

coolaj86 opened this issue Mar 12, 2023 · 10 comments
Labels
documentation Improvements or additions to documentation question Further information is requested

Comments

@coolaj86
Copy link

Can this be used to handle ssh over TLS with SNI routing?

@mohammed90
Copy link
Collaborator

To better understand, do you mean the ssh connection is wrapped in TLS to hide the SSH bytes (like what's described here)? Or to serve 2 services on the same port and respond based on the peeked bytes (as in #17)?

If it's the latter, then you can do that by combining the layer4 app with the SSH app, as Matt said on that other issue. If it's the former, then not in its current shape, but I'm intrigued and will consider it a feature request.

@mholt
Copy link
Collaborator

mholt commented Mar 12, 2023

You could probably do the former with layer4 too. Just terminate TLS and proxy the bytes to the ssh server.

@mohammed90
Copy link
Collaborator

mohammed90 commented Mar 12, 2023

What do you know, I stand corrected! I took @mholt's clue and combining this module, the layer4 app, and socat. I have successful results. Here are the logs:

{"level":"info","ts":1678661484.4180212,"msg":"using provided configuration","config_file":"/Users/mohammed/projects/caddyserver/caddy-ssh/XDG_DATA_HOME/caddy/shell-tls.json","config_adapter":""}
{"level":"info","ts":1678661484.4228532,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
{"level":"info","ts":1678661484.4248362,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc00057efc0"}
{"level":"warn","ts":1678661484.5783281,"logger":"tls","msg":"stapling OCSP","error":"no OCSP stapling for [localhost]: no OCSP server specified in certificate","identifiers":["localhost"]}
{"level":"debug","ts":1678661484.578481,"logger":"tls.cache","msg":"added certificate to cache","subjects":["localhost"],"expiration":1678703896,"managed":true,"issuer_key":"local","hash":"c66af9471a244ea6a6266a51aa6a35f7d913405c50ae75ec6e45bc68d199acda","cache_size":1,"cache_capacity":10000}
{"level":"debug","ts":1678661484.578608,"logger":"events","msg":"event","name":"cached_managed_cert","id":"f6c3a667-1bbd-4ac5-9824-987e33803642","origin":"tls","data":{"sans":["localhost"]}}
{"level":"debug","ts":1678661484.578933,"logger":"layer4","msg":"listening","address":"tcp/127.0.0.1:8443"}
{"level":"info","ts":1678661484.5807302,"msg":"serving initial configuration"}
{"level":"info","ts":1678661484.594663,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"debug","ts":1678661485.447563,"logger":"events","msg":"event","name":"tls_get_certificate","id":"a02f2148-04c3-46aa-9f1b-10cbb6c509ee","origin":"tls","data":{"client_hello":{"CipherSuites":[4866,4867,4865,49196,49200,163,159,52393,52392,52394,49327,49325,49315,49311,49245,49249,49239,49235,49195,49199,162,158,49326,49324,49314,49310,49244,49248,49238,49234,49188,49192,107,106,49267,49271,196,195,49187,49191,103,64,49266,49270,190,189,49162,49172,57,56,136,135,49161,49171,51,50,69,68,157,49313,49309,49233,156,49312,49308,49232,61,192,60,186,53,132,47,65,255],"ServerName":"localhost","SupportedCurves":[23],"SupportedPoints":"AAEC","SignatureSchemes":[1027,1283,1539,2055,2056,2057,2058,2059,2052,2053,2054,1025,1281,1537,771,769,770,1026,1282,1538],"SupportedProtos":null,"SupportedVersions":[772,771,770,769],"Conn":{"Conn":{},"Context":{"Context":{"Context":0}},"Logger":{}}}}}
{"level":"debug","ts":1678661485.449942,"logger":"tls.handshake","msg":"choosing certificate","identifier":"localhost","num_choices":1}
{"level":"debug","ts":1678661485.4534678,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"localhost","subjects":["localhost"],"managed":true,"issuer_key":"local","hash":"c66af9471a244ea6a6266a51aa6a35f7d913405c50ae75ec6e45bc68d199acda"}
{"level":"debug","ts":1678661485.4534988,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"127.0.0.1","remote_port":"56660","subjects":["localhost"],"managed":true,"expiration":1678703896,"hash":"c66af9471a244ea6a6266a51aa6a35f7d913405c50ae75ec6e45bc68d199acda"}
{"level":"debug","ts":1678661485.5190191,"logger":"layer4.handlers.tls","msg":"terminated TLS","remote":"127.0.0.1:56660","server_name":"localhost"}
{"level":"debug","ts":1678661485.599809,"logger":"layer4.handlers.proxy","msg":"dial upstream","remote":"127.0.0.1:56660","upstream":"localhost:2012"}
{"level":"info","ts":1678661487.213503,"logger":"ssh.authentication.flows.public_key","msg":"authentication start","providers_count":1,"remote_address":"[::1]:56661","username":"mohammed","key_type":"ssh-ed25519"}
{"level":"debug","ts":1678661487.213547,"logger":"ssh.authentication.providers.public_key.os","msg":"authenticating user","username":"mohammed"}
{"level":"info","ts":1678661487.215381,"logger":"ssh.authentication.flows.public_key","msg":"authentication successful","provider":"os","user_id":"501","username":"mohammed","key_type":"ssh-ed25519"}
{"level":"info","ts":1678661487.222975,"logger":"ssh.ask.pty.allow","msg":"asking for permission","session_id":"f98b808a69889c891f56f2a78d93db24d2f7e7081c005c38f672dca99ee312d7","local_address":"[::1]:2012","client_version":"SSH-2.0-OpenSSH_9.0","user":"mohammed","terminal":"xterm-256color"}
{"level":"info","ts":1678661487.223532,"logger":"ssh.actors.shell","msg":"start pty session","term":"xterm-256color","session_id":"f98b808a69889c891f56f2a78d93db24d2f7e7081c005c38f672dca99ee312d7","remote_ip":"[::1]:56661","user":"mohammed","command":"","force_command":"zsh","force_pty":false,"window_height":47,"window_width":193}
{"level":"info","ts":1678661487.25058,"logger":"ssh.actors.shell","msg":"found user","session_id":"f98b808a69889c891f56f2a78d93db24d2f7e7081c005c38f672dca99ee312d7","user":{"username":"mohammed","password":"*","uid":501,"gid":20,"info":"Mohammed Al Sahaf","home_dir":"/Users/mohammed","shell":"/bin/zsh"}}
{"level":"info","ts":1678661487.2527208,"logger":"ssh.actors.shell","msg":"update window size","session_id":"f98b808a69889c891f56f2a78d93db24d2f7e7081c005c38f672dca99ee312d7","new_height":47,"new_width":193}

Here's the config I used (of course it's adapted to some local changes that I haven't pushed yet):

{
	"logging": {
		"logs": {
			"default": {
				"level": "DEBUG"
			}
		}
	},
	"apps": {
		"layer4": {
			"servers": {
				"example": {
					"listen": [
						"127.0.0.1:8443"
					],
					"routes": [
						{
							"handle": [
								{
									"handler": "tls"
								},
								{
									"handler": "proxy",
									"upstreams": [
										{"dial": ["localhost:2012"]}
									]
								}
							]
						}
					]
				}
			}
		},
		"tls": {
			"certificates": {
				"automate": [
					"localhost"
				]
			},
			"automation": {
				"policies": [
					{
						"issuers": [
							{
								"module": "internal"
							}
						]
					}
				]
			}
		},
		"ssh": {
			"grace_period": "2s",
			"servers": {
				"srv0": {
					"address": "tcp/0.0.0.0:2000-2012",
					"pty": {
						"pty": "allow"
					},
					"configs": [
						{
							"config": {
								"loader": "provided",
								"no_client_auth": false,
								"authentication": {
									"public_key": {
										"providers": {
											"os": {}
										}
									}
								}
							}
						}
					],
					"actors": [
						{
							"act": {
								"action": "shell",
								"force_command": "zsh"
							}
						}
					]
				}
			}
		}
	}
}

I installed socat and added this to my ~/.ssh/config:

Host tlshost
	ProxyCommand /usr/local/bin/socat - OPENSSL:localhost:8443,verify=0
	ForwardAgent no
	IdentityFile ~/.ssh/id_ed25519
	UserKnownHostsFile /dev/null

@coolaj86
Copy link
Author

I'm looking for a combo of both.

scenario is this:

  • example.com:443 will ALWAYS receive TLS
    • If the TLS contains HTTP, reverse proxy to that host on 80
    • If the TLS contains SSH, then connect to that host on 22
  • example.net is the same, but a completely different IP address

Acceptable "halfway" solution: SNI-based, but no peeking

  • ssh.example.com always goes to 22
  • example.com always goes to 80
  • ssh.example.net and example.net forward to a different host, in the same manner

I think caddy l4 is a solution (that's what I was actually expecting to find, but search landed me here). Telebit is also a solution, but caddy handles load balancing too, which Telebit does not.

I can't use something like sshttp, slt, sclient, stunnel, or sshl because I need in-the-fly config API. Or maybe one of those has the kind of graceful restart that would work, but... that's a lot of docs / trial and error to go through to find out.

That all said... the warnings of "experimental" and "read the code, don't rely on the docs" on Caddy l4 do scare me a little bit. However, I see that what I'm talking about seems to be mentioned in the README as a use case so... is there a ready-made example for either of those?

@coolaj86
Copy link
Author

Oh, more comments have come in that I didn't see yet. Reading now...

@coolaj86
Copy link
Author

FYI: I'd recommend OpenSSL s_client or Telebit sclient over socat - just due to the sheer complexity of the docs of socat vs the simplicity of openssl s_client and sclient (which I literally created exactly for this use case, but on the individual scale, and to work on Windows clients).

@coolaj86
Copy link
Author

Thanks for that sample config. That makes sense at a glance. I'll try it out tonight.

@coolaj86
Copy link
Author

coolaj86 commented Mar 14, 2023

This doesn't really have to do with caddy-ssh (now kadeessh as of a few hours ago?) at this point, but since this is where search has led me and where the conversation has evolved here, I want to provide the solution here as well.

Full Instructions

  1. caddy online build is currently broken due to go bug, must use xcaddy
  2. must build caddy with caddy-l4 and its plugins
  3. sample config for terminating tls on a full port capture (no sni, no multiplexing)

1. Install xcaddy

Download xcaddy, make it executable, and put it in your PATH (pathman can help if you're unfamiliar with that):

pushd /tmp/
curl -o ./xcaddy_0.3.2_linux_amd64.tar.gz \
    -L https://github.com/caddyserver/xcaddy/releases/download/v0.3.2/xcaddy_0.3.2_linux_amd64.tar.gz
tar xvf ./xcaddy_0.3.2_linux_amd64.tar.gz
chmod a+x caddy
mv ./caddy ~/bin/caddy
popd

2. Build with layer4

Needs layer4, l4tls, l4ssh, l4proxy. Using duckdns as an example

#!/bin/sh

#export XCADDY_SETCAP=1
export XCADDY_SUDO=0
export XCADDY_SKIP_CLEANUP=1

xcaddy build \
    --with github.com/mholt/caddy-l4/layer4 \
    --with github.com/mholt/caddy-l4/modules/l4tls \
    --with github.com/mholt/caddy-l4/modules/l4ssh \
    --with github.com/mholt/caddy-l4/modules/l4subroute \
    --with github.com/mholt/caddy-l4/modules/l4proxy \
    --with github.com/caddy-dns/duckdns

2b. How to run

Assuming an .env with the DUCKDNS_API_TOKEN:

caddy run --envfile ./.env --config ./config.json

3. Test 1 TLS to SSH

  • my-sshtls is an arbitrary name
  • 192.168.0.4 is the IP of my server
  • 0.0.0.0:22443 means that the server is listening for TLS-SSH on any network interface on port 22443 (because it consumes the entire port - no multiplexing yet)
  • localhost:22 is for the network connection, not indicative of hostname or SNI, etc
{
  "logging": {
    "logs": {
      "default": {
        "level": "DEBUG"
      }
    }
  },
  "apps": {
    "layer4": {
      "servers": {
        "my-sshtls": {
          "listen": ["0.0.0.0:22443"],
          "routes": [
            {
              "handle": [
                {
                  "handler": "tls"
                },
                {
                  "handler": "proxy",
                  "upstreams": [{ "dial": ["localhost:22"] }]
                }
              ]
            }
          ]
        }
      }
    },
    "tls": {
      "certificates": {
        "automate": ["localhost", "192.168.0.4"]
      },
      "automation": {
        "policies": [
          {
            "issuers": [
              {
                "module": "internal"
              }
            ]
          }
        ]
      }
    }
  }
}

4. SNI Routing to SSH

I got TLS SNI matching working too!

  • ssh-example.duckdns.org terminates and goes to port 22
  • web-example.duckdns.org terminates and goes to port 80
  • 192.168.0.4 is my reverse proxy running caddy
  • using DuckDNS to demonstrate proper SNI
    (many tools don't work with SNI in insecure mode)
{
  "logging": {
    "logs": {
      "default": {
        "level": "DEBUG"
      }
    }
  },
  "apps": {
    "layer4": {
      "servers": {
        "my-sshtls": {
          "listen": ["0.0.0.0:22443"],
          "routes": [
            {
              "match": [
                {
                  "tls": {}
                }
              ],
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "match": [
                        {
                          "tls": {
                            "sni": ["ssh-example.duckdns.org"]
                          }
                        }
                      ],
                      "handle": [
                        {
                          "handler": "tls"
                        },
                        {
                          "handler": "proxy",
                          "upstreams": [{ "dial": ["localhost:22"] }]
                        }
                      ]
                    },
                    {
                      "match": [
                        {
                          "tls": {
                            "sni": ["web-example.duckdns.org"]
                          }
                        }
                      ],
                      "handle": [
                        {
                          "handler": "tls"
                        },
                        {
                          "handler": "proxy",
                          "upstreams": [{ "dial": ["localhost:3000"] }]
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          ]
        }
      }
    },
    "tls": {
      "certificates": {
        "automate": [
          "localhost",
          "192.168.0.4",
          "ssh-example.duckdns.org",
          "web-example.duckdns.org"
        ]
      },
      "automation": {
        "policies": [
          {
            "subjects": ["localhost", "192.168.0.4"],
            "issuers": [
              {
                "module": "internal"
              }
            ]
          },
          {
            "subjects": ["ssh-example.duckdns.org", "web-example.duckdns.org"],
            "issuers": [
              {
                "challenges": {
                  "dns": {
                    "provider": {
                      "api_token": "{env.DUCKDNS_API_TOKEN}",
                      "name": "duckdns"
                    }
                  }
                },
                "module": "acme"
              },
              {
                "challenges": {
                  "dns": {
                    "provider": {
                      "api_token": "{env.DUCKDNS_API_TOKEN}",
                      "name": "duckdns"
                    }
                  }
                },
                "module": "zerossl"
              }
            ]
          }
        ]
      }
    }
  }
}

5. Multiplexing HTTPS and SSH

  • example-a.duckdns.org
    • TLS is terminated
    • ssh goes to localhost on port 22
    • http goes to localhost http on port 3000
      • host header match to prevent SNI/Host mismatch attack
  • the same is true for exampl-b.duckdns.org

This feels a little too verbose. I wonder if there isn't a simpler way... ?

{
  "logging": {
    "logs": {
      "default": {
        "level": "DEBUG"
      }
    }
  },
  "apps": {
    "layer4": {
      "servers": {
        "my-sshttp": {
          "listen": ["0.0.0.0:443"],
          "routes": [
            {
              "match": [
                {
                  "tls": {}
                }
              ],
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "match": [
                        {
                          "tls": {
                            "sni": ["example-a.duckdns.org"]
                          }
                        }
                      ],
                      "handle": [
                        {
                          "handler": "tls",
                          "connection_policies": [
                            {
                              "alpn": ["http/1.1"]
                            }
                          ]
                        },
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "match": [{ "ssh": {} }],
                              "handle": [
                                {
                                  "handler": "proxy",
                                  "upstreams": [{ "dial": ["localhost:22"] }]
                                }
                              ]
                            },
                            {
                              "match": [
                                {
                                  "http": [
                                    { "host": ["example-a.duckdns.org"] }
                                  ]
                                }
                              ],
                              "handle": [
                                {
                                  "handler": "proxy",
                                  "upstreams": [{ "dial": ["localhost:3000"] }]
                                }
                              ]
                            }
                          ]
                        }
                      ]
                    },
                    {
                      "match": [
                        {
                          "tls": {
                            "sni": ["example-b.duckdns.org"]
                          }
                        }
                      ],
                      "handle": [
                        {
                          "handler": "tls",
                          "connection_policies": [
                            {
                              "alpn": ["http/1.1"]
                            }
                          ]
                        },
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "match": [{ "ssh": {} }],
                              "handle": [
                                {
                                  "handler": "proxy",
                                  "upstreams": [{ "dial": ["10.0.0.222:22"] }]
                                }
                              ]
                            },
                            {
                              "match": [
                                {
                                  "http": [
                                    { "host": ["example-b.duckdns.org"] }
                                  ]
                                }
                              ],
                              "handle": [
                                {
                                  "handler": "proxy",
                                  "upstreams": [{ "dial": ["10.0.0.222:3000"] }]
                                }
                              ]
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          ]
        }
      }
    },
    "tls": {
      "certificates": {
        "automate": [
          "localhost",
          "192.168.0.4",
          "example-a.duckdns.org",
          "example-b.duckdns.org"
        ]
      },
      "automation": {
        "policies": [
          {
            "subjects": ["localhost", "192.168.0.4"],
            "issuers": [
              {
                "module": "internal"
              }
            ]
          },
          {
            "subjects": ["example-a.duckdns.org", "example-b.duckdns.org"],
            "issuers": [
              {
                "challenges": {
                  "dns": {
                    "provider": {
                      "api_token": "{env.DUCKDNS_API_TOKEN}",
                      "name": "duckdns"
                    }
                  }
                },
                "module": "acme"
              },
              {
                "challenges": {
                  "dns": {
                    "provider": {
                      "api_token": "{env.DUCKDNS_API_TOKEN}",
                      "name": "duckdns"
                    }
                  }
                },
                "module": "zerossl"
              }
            ]
          }
        ]
      }
    }
  }
}

@gedw99
Copy link

gedw99 commented May 29, 2023

I guess this is still a WIP ? Would like to try this out but it’s not merged yet into this repo ?

@mohammed90
Copy link
Collaborator

I guess this is still a WIP ? Would like to try this out but it’s not merged yet into this repo ?

There isn't any necessary work here nor are there any necessary PRs to merge. You can use TLS-terminated SSH sessions as-is using a combination of caddy-l4 and kadeessh. It's a matter of configuration, which I could include as an example on the README or the wiki. There's a working config sample by me above. I kept it open to remind myself to include the example config of this use-case somewhere handy for users.

Anything specific you're missing?

@mohammed90 mohammed90 added documentation Improvements or additions to documentation question Further information is requested labels May 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants