The ngrok plugin is currently part of the HTTP listener wrapper, so routing connections outside of the caddy http app is near impossible. I’d stick with Kadeessh.
The configuration is close to what you want, but requires a bit of modification. I’ll explain this config, then I’ll show you the final modified version so you understand what you have to validate and change according to your needs.
Starting with srv0
, this is just an identifier for the server within this key. It does not represent the target server. It is just to distinguish this particular server configuration set. The name can be anything, e.g. tunnerl_config
or john_doe
. For example, if you need to listen on port 22 for shell and on port 2020 for tunneling, you’d have 2 keys inside server, where one listening only on port 22 for shell access, and the other listening only on port 2020 for forwarding. In such case, Kadeessh configuration would be:
{
"apps": {
"ssh": {
"grace_period": "2s",
"servers": {
"shell_server": {
"address": "tcp/0.0.0.0:22",
"pty": {
"pty": "allow"
},
"configs": [
{
"config": {
"loader": "provided",
"no_client_auth": false,
"authentication": {
"public_key": {
"providers": {
"os": {}
}
}
}
}
}
],
"actors": [
{
"act": {
"action": "shell"
}
}
]
},
"tunnel_server": {
"address": "tcp/0.0.0.0:2020",
"configs": [
{
"config": {
"loader": "provided",
"signer": {
"module": "fallback"
},
"authentication": {
"public_key": {
"providers": {
"os": {}
}
}
}
}
}
],
"localforward": {
"forward": "allow"
}
}
}
}
}
}
See, it doesn’t have to say srv0
or any particular format. It’s a name set by the user for their own reference. The address
field is a Network Address which Kadeessh will establish listeners for the server.
Then comes configs
. It’s plural because the configuration for the every incoming connection can be varied, for instance, you might want to ignore authentication for connections from local network (LAN) but enforce them for external ones (WAN). Objects inside the configs
array have 2 fields: match
, and config
. The match
part is what allows you change behavior per aspects of the connection (currently only IP addresses and not
).
Inside config
, we start with loader
to tell Kadeessh where to get the configuration from. This is future-proofing to modularize the source of the configuration. Don’t worry much about it, but know that the provided
word means the configuration is given within the same file itself. As for this part
"signer": {
"module": "fallback"
}
This tells Kadeessh to look for the server’s private/public keys in storage (which don’t exist if fresh), load them, and generate new keys if absent. RSA and ed25519 key are loaded and generated if absent, but ecdsa are only loaded but not generated. DSA keys are ignored.
Now we come to authentication
"authentication": {
"public_key": {
"providers": {
"os": {}
}
}
}
This tells Kadeessh to validate users based on their public/private key pair, using the OS (operating system) as the source of data. In the operating system, the keys of authorized users are placed under the user’s home directory, specifically in ~/.ssh/authorized_keys
. So Kadeessh will take the user name, check the file under its home directory, and check if the matching key exists inside the file or not.
Lastly, there’s the localforward
part
"localforward": {
"forward": "allow"
}
By default, Kadeessh will not allow tunneling of any sort. This part tells Kadeessh to allow tunneling requests if they come through.
The complete structure of Kadeessh configuration format is available on Caddy documentation website here
Now we come for the config you need. I understand you can only listen on the ports 80 and 443. If you’ll only accept ssh connections on them, then you can do something like this (subject to your validation and scrutiny), where we tell Kadeessh to listen on port 443 and only process tunneling requests based on the successful authentication per the user’s keys as they’re available in the OS:
{
"apps": {
"ssh": {
"grace_period": "2s",
"servers": {
"tunnel_server": {
"address": "tcp/0.0.0.0:443",
"configs": [
{
"config": {
"loader": "provided",
"signer": {
"module": "fallback"
},
"authentication": {
"public_key": {
"providers": {
"os": {}
}
}
}
}
}
],
"localforward": {
"forward": "allow"
}
}
}
}
}
}
If you want to serve both HTTP and SSH on the same ports, then this requires an additional Caddy module, i.e. layer4. You’ll need to combine Kadeessh, layer4, and http apps to come up with this configuration (disclaimer: I have not vetted this config thoroughly, but I assure you the capability exists within Caddy ecosystem):
{
"apps": {
"http": {
"http_port": 8080,
"https_port": 8443,
"servers": {
"srv0": {
"listen": [
"tcp/127.0.0.1:8443"
],
"routes": [
{
"match": [
{
"host": [
"proxy1.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "OK!",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"layer4": {
"multiplexer": {
"listen": ["tcp/0.0.0.0:443", "tcp/0.0.0.0:80"],
"routes": [
{
"match": [{
"ssh": {}
}],
"handle": [
{
"handler": "proxy",
"upstreams": [
{"dial": ["127.0.0.1:2020"]}
]
}
]
},
{
"match": [
{"http": []}
],
"handle": [
{
"handler": "subroute",
"routes":[
{
"match": [
{
"tls": {}
}
],
"handler": [
{
"handler": "proxy",
"upstreams": [
{"dial": ["127.0.0.1:8443"]}
]
}
]
},
{
"handler": [
{
"handler": "proxy",
"upstreams": [
{"dial": ["127.0.0.1:8080"]}
]
}
]
}
]
}
]
}
]
}
},
"ssh": {
"grace_period": "2s",
"servers": {
"tunnel_server": {
"address": "tcp/127.0.0.1:2020",
"configs": [
{
"config": {
"loader": "provided",
"signer": {
"module": "fallback"
},
"authentication": {
"public_key": {
"providers": {
"os": {}
}
}
}
}
}
],
"localforward": {
"forward": "allow"
}
}
}
}
}
}
The above config follows this logic in processing incoming connections:
- If the connection is an SSH connection, proxy it to the local SSH server.
- Otherwise, if it’s an HTTP connection, follow the below logic:
– If the connection is TLS connection, proxy it to the https/TLS port of the HTTP server (which is Caddy, just next door).
– Otherwise proxy the request to the plain HTTP port of the HTTP server.
This way, you can serve both SSH and HTTP traffic on the same ports, and the users of SSH traffic are authenticated by the public/private that already exist on the operating system before proxying them to the other internal server.
Finally, regarding PuTTY, I can’t help with its configuration because I don’t use it. However, I found the following links showing how to use PuTTY to for ssh tunnels: