Use systemd & ssh magic, to dynamically punch through firewalls; with "scale to zero" because coolness.
I have a bunch of systems I need to talk to which are hidden behind multiple jumpboxes.
I want to use local clients to talk to APIs and systems behind those jumpboxes,
I don't want to interactively log in to any jumpbox and run stuff or leave state there[1].
I want my ssh connections to be relatively short lived and don't want to
manually manage them; they should be established and closed on demand.
In my case this is mostly to talk to things like
- BOSH directors
- opsmanagers
- credhubs
- TKGi APIs
- kube apiservers
- ...
via socks5, where those live inside private networks accessible only via one ore more bastion / jump hosts.
[1]: That should be forbidden anyway. No state and no interactive sessions on jumpboxes. Don't do it.
mkdir -p ~/.config/systemd/{autosocks,user}
cp -r units/* ~/.config/systemd/autosocks/
cd ~/.config/systemd/user
proxyhost='opsman.nonprod.acme'
proxyport=65500
proxyhost_esc="$(systemd-escape "$proxyhost")"
ln -s ../autosocks/autosocks-@.socket "autosocks-${proxyhost_esc}@${proxyport}.socket"
ln -s ../autosocks/autosocks-@.service "autosocks-${proxyhost_esc}@${proxyport}.service"
ln -s ../autosocks/autosocks-connection-@.service "autosocks-connection-${proxyhost_esc}@${proxyport}.service"
systemctl --user daemon-reload
systemctl --user start "autosocks-${proxyhost_esc}@${proxyport}.socket"I don't care about which or how many jumpboxes I need to use. I set it up once
in my ~/.ssh/config and forget about it.
Host *
ControlMaster auto
ControlPath ~/.ssh/cm-%C
ControlPersist no
Host *.prod.acme
ForwardAgent yes
ProxyJump bastion.prod.acme
Host *.sandbox.acme
ForwardAgent yes
ProxyJump bastion.sandbox.acme
This, and the fact that I use the ssh-agent, pull keys from vault and add them
to the ssh-agent with a limited lifetime (ssh-add -t ...) allows me to e.g.
do ssh opsman.prod.acme and it logs me.
Going forward, for each system you want to use to proxy through this needs to be true: You need to be able to ssh in with just providing the hostname. There must not be any other interaction (password prompt) or any other config needed; everything needs to be handled by the ssh config and the ssh-agent.
Now the fun part: Setting up systemd to dynamically manage your connection
-
in units you'll find 3 systemd unit templates. You need to copy those to your user's systemd directory (e.g.
~/.config/systemd/autosocks/). Those are just templates and won't do anything by themselves, but implement all the needed parts once they are instantiated.-
autosocks-connection-@.serviceThis implements the actual ssh connection, that will be started on-demand. Essentially, it will just call
ssh $hostnameand will use a specific port to bind ssh as a socks server on. -
autosocks-@.serviceThis will run
systemd-socket-proxydwhich will take get the socket from systemd once connections have been observed and will proxy to the the socks proxy created by ssh.It will also make sure to:
- Start a
autosocks-connection-@.serviceinstance on-demand and shut it down - Shut itself down, once ther is no traffic going through
- Start a
-
autosocks-@.socketthis will have
systemdlisten on a port and wait for connections. Once a connection is observed,systemdwill start aautosocks-@.serviceinstance and pass the socket to that.
-
-
Instantiate & start those units
systemdunits can be templated with an "instance" (the thing after the@) and a "prefix" (the thing between the last-and the@). We'll use that to configure the host we want to proxy through and the local port we want the socks service to listen on.With the hosts from the above ssh config we can do the following:
cd ~/.config/systemd/user/ # set up a proxy through opsman.sandbox.acme ln -s ../autosocks/autosocks-@.socket autosocks-opsman.sandbox.acme@65500.socket ln -s ../autosocks/autosocks-@.service autosocks-opsman.sandbox.acme@65500.service ln -s ../autosocks/autosocks-connection-@.service autosocks-connection-opsman.sandbox.acme@65500.service # enable the sandbox proxy systemd --user start autosocks-opsman.sandbox.acme@65500.socket # set up a proxy through opsman.prod.acme ln -s ../autosocks/autosocks-@.socket autosocks-opsman.prod.acme@65501.socket ln -s ../autosocks/autosocks-@.service autosocks-opsman.prod.acme@65501.service ln -s ../autosocks/autosocks-connection-@.service autosocks-connection-opsman.prod.acme@65501.service # enable the prod proxy systemd --user start autosocks-opsman.prod.acme@65501.socket
This creates a listening socket on
127.0.0.1:65500for sandbox and127.0.0.1:65501for prod.If you did a
netstat -lnpor similar, you'd see that on the ports 65500 and 65501systemdis listening on. But not much more.Note: If you want to connect to a host with a
-in its FQDN/name, you need to escape that-by\x2d. So the commands to link the units for a hostname likeopsman.my-bizunit.acmewould become something like:ln -s ../autosocks/autosocks-@.socket autosocks-opsman.my\\x2dbizunit.acme@65502.socket ln -s ../autosocks/autosocks-@.service autosocks-opsman.my\\x2dbizunit.acme@65502.service ln -s ../autosocks/autosocks-connection-@.service autosocks-connection-opsman.my\\x2dbizunit.acme@65502.service
Now let's use that thing!
# It takes a bit to start all the systems, but eventually this should return
# (given, the host you proxy through has internet access).
https_proxy=socks5://localhost:65500 curl https://google.com/
# This is faster, because the connection is already established
https_proxy=socks5://localhost:65500 curl https://google.com/And what you see now:
- There is a ssh process, connecting to
opsman.sandbox.acmewhich is listening as a socks server on127.255.255.254:65500 - There is a process
systemd-socket-proxydrunning now, which proxies everything on port127.0.0.1:65500to port127.255.255.254:65500
If you wait a bit and there is no further traffic going through 65500
- both
sshandsystemd-socket-proxydwill shut down automatically. - the socket listening on
127.0.0.1:65500will be moved back to the systemd process
- Once the
autosocks-@.socketinstance is running,systemdwill listen on that intance's port (e.g.127.0.0.1:65500) - If there is a connection to that port,
systemdstartssshto the unit's instance's host, running a socks proxy service on the same port but on127.255.255.254(e.g.127.255.255.254:65500)127.255.255.254is just another IP on the loopback interface. Any other local IP can be used; this is configured asAUTOSOCKS_IPin theautosocks-@.service&autosocks-connection-@.serviceunits- It's probably best to keep
127.255.255.254exclusively for autosocks to reduce the chance of port collisions
systemd-socket-proxydwill be started; it will receive the socket fromsystemdand use it to receive traffic on127.0.0.1:65500and proxy it through to127.255.255.254:65500
systemd-socket-proxydwill shutdown, if it's idle for a bit- once
systemd-socket-proxydshuts down, so will thesshconnection providing the socks server
- once
- if any of either the ssh connection or the activation socket will shutdown or
die, so will the
systemd-socket-proxyd. The system will however try to reestablish all the things on the next connection attempt to the instnace's port
- See all statuses:
systemctl --user status --all 'autosocks*' - Tail all logs:
journalctl --user --all -f -u 'autosocks*'
- ...
This is pretty much stolen from ankostis over at StackExchange. The only thing I did was to make it a bit easier to reuse by making use of the units' "instance" and "prefix".