SSH Multiplexing with Caddy
I love using Caddy. For a long time, I had been using NGINX but ever since moving from it to Caddy I can't get over the simplicity of configuring it and not having to setup the what Caddy considers "sensible defaults" for every host. Many people online who consider themselves programmers would probably also watch, The Primeagen. He has a coffee brand that he uses an SSH application to make the sales for his coffee.
I had wanted to make something similar; it seems possible to do so using an OpenSSH server and configuring it to point to your application. It did not end up being this easy. Initially, there are a couple problems using this solution.
- OpenSSH Server does not allow a client to login without an account, nor a password-less prompt. We want the user to
ssh domain.comand be able to see our terminal application. To make this possible we would need to use another SSH server that can let us login without a user/password prompt. - How do we route the ssh connection? We would need to have Caddy route the SSH connections, but this is not available without making a custom Caddy build to include mholts/caddy-l4.
Caddy Multiplexing
To solve the Caddy issue, we can just make a custom image for Caddy with a Dockerfile like below.
FROM caddy:builder AS builder
RUN xcaddy build \
--with github.com/mholt/caddy-l4/layer4 \
--with github.com/mholt/caddy-l4/modules/l4tls \
--with github.com/mholt/caddy-l4/modules/l4subroute \
--with github.com/mholt/caddy-l4/modules/l4http \
--with github.com/mholt/caddy-l4/modules/l4ssh \
--with github.com/mholt/caddy-l4/modules/l4proxy
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
Using this image, we can now use Caddy for SSH Multiplexing. I added the following to my Caddyfile for a simple config. This is proxying any SSH traffic on port 2222 to host.com:2222, Since I need the SSH port accessible on my host machine, I have proxied the port 2222. This would require port forwarding your router port 22 to 2222 to not need to specify the port number on the client.
{
layer4 {
:2222 {
@ssh ssh
route @ssh {
proxy host.com:2222
}
}
}
}After adding this (and port forwarding [22:2222]) we can now ssh directly to our domain; however there is a caveat to this approach due to my network setup. Using SSH to any of my domains will result in the same final destination. I can get around using one application here by using SSH over TLS, having a different port per application, or building another application to let us present our applications on the single port.
Our traffic now with this Caddy config and build should now look like this. We can proxy the SSH traffic as needed in Caddy just like sslh, but without the need for another proxy server.
SSH Server Replacement
The next problem we face is accessing our application via SSH. We can either roll out our own SSH server implementation or we can use a different SSH server other than OpenSSH Server. Looking online I found alternatives like WolfSSH and Dropbear that looked promising; however both would require authentication. Lolbear, a Dropbear fork that spoofs the authentication, looked promising. Sadly, this fork requires setting up your keys differently to access which I did not want the end user to need to do. Finally after some more searching, I found cli2ssh, a Go program that allows you to share your TUI applications over SSH with some setup to do so.
To set up cli2ssh into my application, we just need to create a Dockerfile to build our app, then install cli2ssh and run our app using it. The following is an example from my Rust TUI app's dockerfile.
...app build stage steps...
# Final stage
FROM golang:bookworm AS final
# Copy the compiled binary
COPY --from=build /app/server /app/
# install go 1.23 for cli2ssh and
# set env vars/path
ENV PATH="$PATH:/usr/local/go/bin"
ENV HOME="/root"
ENV GOPATH="$HOME/go"
ENV PATH="$GOPATH/bin:$PATH"
# Arg for your apps flags/launch commands
ARG LAUNCH_COMMANDS
ENV LAUNCH_COMMANDS=$LAUNCH_COMMANDS
# finally install cli2ssh
RUN go install github.com/PeronGH/cli2ssh/cmd/cli2ssh@latest
EXPOSE 2222
# Run the server pointing to the compiled binary
CMD cli2ssh -h 0.0.0.0 -c "/app/server $LAUNCH_COMMANDS"
Using this approach, I can utilize Caddy to route my SSH connections to the appropriate server using the Layer4 ssh matcher and having each application on its own ssh port. There are other options like TLS -> SSH but that requires the client to modify their SSH config to adhere. My goal with this is to make it as simple and easy as possible for a user to access my terminal application from SSH. Since SSH does not include an SNI (Server Name Indicator) in the request we wouldn't be able to route based on that.
If you would like to check out the final product, just ssh this domain!