Introduction

Welcome to the Sandhole book. This is a work-in-progress guide on how to install, maintain, and use an instance of Sandhole.

About the project

Sandhole is an experimental reverse proxy that uses SSH's built-in reverse port forwarding functionality, in order to allow services to expose themselves to the Internet. This is especially useful as a way for servers behind NAT to expose themselves, but you may also want this for:

  • Quickly prototyping websites and TCP services, and sharing them with others.
  • Handling a multi-tenant network with several websites under the same domain.
  • Hosting a dual-stack HTTP+SSH service (via ProxyJump), such as Git instances.
  • And possibly more!

Quick start

In order to run Sandhole, you'll need:

  • A server with public addresses.
  • A domain pointing to said server (in this example, server.com).

Then, install the Sandhole binary in your server. Currently, the only way to do so is to compile it yourself.

If you're compiling from a separate workstation, grab the source files, build the binary, and copy it over:

git clone https://github.com/EpicEric/sandhole
cd sandhole
cargo build --release
scp target/release/sandhole you@server.com:

If you're compiling on the machine that's running Sandhole, you can install it directly. This will also add sandhole to your PATH:

cargo install --git https://github.com/EpicEric/sandhole
# -- OR --
git clone https://github.com/EpicEric/sandhole
cargo install --path sandhole

Once this is all done, you can start running Sandhole! Just make sure it points to your own domain:

sandhole --domain server.com

By default, this will expose ports 80 (for HTTP), 443 (for HTTPS), and 2222 (for SSH). If it all succeeds, you should see the following:

[2024-11-23T13:10:51Z INFO  sandhole] Key file not found. Creating...
[2024-11-23T13:10:51Z INFO  sandhole] sandhole is now running.

Now you're ready to dig sandholes like a real crab!

Configuration

This is a list of the most important default settings to be aware of. For a comprehensive list, refer to the CLI options.

Adding users and admins

In order to do anything useful with Sandhole, connections must be authenticated. The main way of doing this is by adding your users' public keys to the user keys directory.

By default, this will be ./deploy/user_keys/, but it can be configured with the --user-keys-directory option. Once you add a public key, Sandhole will automatically pick up on the change, and allow that user to create remote port forwardings.

Similarly, there is a ./deploy/admin_keys/ directory (set by --admin-keys-directory), for users who should also have access to the admin interface.

Default ports

By default, Sandhole runs on ports 80, 443, and 2222. This assumes that your actual SSH server is running on port 22, and that no other services are listening on the HTTP/HTTPS ports.

However, it might be more desirable to have Sandhole listen on port 22 instead. In order to keep your SSH server running on a different port, edit the port in /etc/ssh/sshd_config, then restart your SSH daemon.

Now you'll be able to run Sandhole on port 22:

sandhole --domain server.com --ssh-port 22

Allow binding on any subdomains/ports

Without extra configuration, Sandhole will not let users bind to requested subdomains and ports, and will always allocate a random one instead.

If you wish to change the default behavior, and allow users to provide their own subdomains/ports to bind to, add the options --allow-provided-subdomains and --allow-requested-ports, respectively.

Alternative authentication with password

In some scenarios, it makes more sense to authenticate users dynamically with a password, rather than adding and removing keys from a directory.

For such use cases, you can provide a URL to --password-authentication-url. This should be running a service which accepts a POST request with the following JSON body containing the user's credentials, and returns 2xx on successful authentication:

{
  "user": "...",
  "password": "..."
}

TLS support

Sandhole supports TLS signing out of the box, including ACME challenges via TLS-ALPN-01 for custom domains.

However, especially for your main domain, it's recommended that you set up dnsrobocert to handle the wildcard certification via DNS. Sandhole already matches this tool's output directly. Please see its documentation to set it up yourself.

Assuming that the output of dnsrobocert is ./letsencrypt, Sandhole can then read the certificates via:

sandhole --domain server.com --certificates-directory ./letsencrypt/live

ACME support

Adding ACME support is as simple as adding your contact e-mail address via --acme-contact-email you@your.email.com, but first, make sure that you agree to the Let's Encrypt Subscriber Agreement (available here). Sandhole will automatically manage the cache for your account and any certificates generated this way.

Admin interface

Sandhole comes with a barebones admin interface available through SSH. In order to access it, you must be a user with admin credentials.

To access it, simply run the command:

ssh -t server.com -p 2222 admin

where server.com is your hostname and 2222 is Sandhole's SSH port.

A terminal screenshot showing the "Sandhole admin" interface, displaying the HTTP services currently running.

Exposing your first service

Now that you have a Sandhole instance running, and you authorized your public key, you can expose a local service through Sandhole. Assuming that your local HTTP service is running on port 3000, and that Sandhole is listening on server.com:2222, all you have to do is run

ssh -R 80:localhost:3000 server.com -p 2222

Yep, that's it! Sandhole will log that HTTP is being served for you, and you can access the provided URL to see that your service is available to the public.

For HTTP and HTTPS services, Websockets work out of the box.

Requesting multiple tunnels

You can request tunnels for several services in a single SSH command.

ssh -R 80:localhost:3000 -R 80:localhost:4000 -R 22:localhost:5000 server.com -p 2222

Requesting a particular subdomain/port

After you allow binding on any subdomain/port, it's possible to configure which of these will be assigned to you.

For example, to bind under test.server.com, we could use either of these commands:

ssh -R test:80:localhost:3000 server.com -p 2222
# -- OR --
ssh -R test.server.com:80:localhost:3000 server.com -p 2222

And if we'd like to bind to a specific port, say 4321:

ssh -R 4321:localhost:3000 server.com -p 2222
# -- OR --
ssh -R localhost:4321:localhost:3000 server.com -p 2222

Automatic reconnection

If you'd like to have persistent tunnels, use a tool like autossh with the -M 0 option to automatically reconnect when disconnected. Note that you'll be assigned a new subdomain/port if the option above is not enabled, depending on the server configuration.

Advanced uses

Local forwarding and aliasing

In addition to remote port forwarding, Sandhole also supports local port forwarding. This allows you to create SSH-based tunnels to connect to a service.

Given a remote service running as

ssh -R my.tunnel:3000:localhost:2000 server.com -p 2222

Note that the server won't listen on port 3000; instead, you can establish a local forward to the port from your machine:

ssh -L 4000:my.tunnel:3000

Then you can access localhost:4000, and all traffic will be redirected to port 2000 on the remote service. It's almost like a VPN!

If you'd like to restrict which users can access your service, you can provide the allowed fingerprints as a comma-separated list at the end of the command, like so:

ssh -R my.tunnel:3000:localhost:2000 server.com -p 2222 allowed-fingerprints=SHA256:GehKyA21BBK6eJCouziacUmqYDNl8BPMGG0CTtLSrbQ

Custom domains

You can also use your custom domains with Sandhole. For this, you'll need your SSH key's fingerprint and control over your domain's DNS.

For the former, you can run ssh-keygen -lf /path/to/private/key and take note of the second field - it will look something like:

SHA256:bwf4FDtNeZzFv8xHBzHJwRpDRxssCll8w2tCHFC9n1o

Then, add the following entries to your DNS (assuming that your domain is my.domain.net):

TypeDomainData
CNAMEmy.domain.netserver.com
TXT_sandhole.my.domain.netSHA256:bwf4FDtNeZzFv8xHBzHJwRpDRxssCll8w2tCHFC9n1o

This instructs your DNS to redirect requests to Sandhole, and tells Sandhole to authorize your SSH key for the given domain, respectively.

If you need to use multiple keys for the same domain, simply add a TXT record for each one.

Command-line interface options

Sandhole exposes several options, which you can see by running sandhole --help.

Expose HTTP/SSH/TCP services through SSH port forwarding.

Usage: sandhole [OPTIONS] --domain <DOMAIN>

Options:
      --domain <DOMAIN>
          The root domain of the application

      --domain-redirect <DOMAIN_REDIRECT>
          Where to redirect requests to the root domain

          [default: https://github.com/EpicEric/sandhole]

      --user-keys-directory <USER_KEYS_DIRECTORY>
          Directory containing public keys of authorized users. Each file must
          contain at least one key

          [default: ./deploy/user_keys/]

      --admin-keys-directory <ADMIN_KEYS_DIRECTORY>
          Directory containing public keys of admin users. Each file must
          contain at least one key

          [default: ./deploy/admin_keys/]

      --certificates-directory <CERTIFICATES_DIRECTORY>
          Directory containing SSL certificates and keys. Each sub-directory
          inside of this one must contain a certificate chain in a
          `fullchain.pem` file and its private key in a `privkey.pem` file

          [default: ./deploy/certificates/]

      --acme-cache-directory <ACME_CACHE_DIRECTORY>
          Directory to use as a cache for Let's Encrypt's account and
          certificates. This will automatically be created for you.

          Note that this setting ignores the --disable-directory-creation flag.

          [default: ./deploy/acme_cache]

      --private-key-file <PRIVATE_KEY_FILE>
          File path to the server's secret key. If missing, it will be created
          for you

          [default: ./deploy/server_keys/ssh]

      --disable-directory-creation
          If set, disables automatic creation of the directories expected by the
          application. This may result in application errors if the directories
          are missing

      --listen-address <LISTEN_ADDRESS>
          Address to listen for all client connections

          [default: ::]

      --ssh-port <SSH_PORT>
          Port to listen for SSH connections

          [default: 2222]

      --http-port <HTTP_PORT>
          Port to listen for HTTP connections

          [default: 80]

      --https-port <HTTPS_PORT>
          Port to listen for HTTPS connections

          [default: 443]

      --force-https
          Always redirect HTTP requests to HTTPS

      --acme-contact-email <ACME_CONTACT_EMAIL>
          Contact e-mail to use with Let's Encrypt. If set, enables ACME for
          HTTPS certificates.

          By providing your e-mail, you agree to Let's Encrypt Subscriber
          Agreement

      --acme-use-staging
          Controls whether to use the staging directory for Let's Encrypt
          certificates (default is production). Only set this option for testing

      --password-authentication-url <PASSWORD_AUTHENTICATION_URL>
          If set, defines a URL against which password authentication requests
          will be validated. This is done by sending the following JSON payload:

          `{"user": "...", "password": "..."}`

          Any 2xx response indicates that the credentials are authorized.

      --bind-hostnames <BIND_HOSTNAMES>
          Policy on whether to allow binding specific hostnames.

          Beware that this can lead to domain takeovers if misused!

          [default: txt]

          Possible values:
          - all:   Allow any hostnames unconditionally, including the main
                   domain
          - cname: Allow any hostnames with a CNAME record pointing to the main
                   domain
          - txt:   Allow any hostnames with a TXT record containing a
                   fingerprint, including the main domain
          - none:  Don't allow user-provided hostnames, enforce subdomains

      --load-balancing <LOAD_BALANCING>
          Strategy for load-balancing when multiple services request the same
          hostname/port.

          By default, traffic towards matching hostnames/ports will be
          load-balanced.

          [default: allow]

          Possible values:
          - allow:   Load-balance with all available handlers
          - replace: When adding a new handler, replace the existing one
          - deny:    Deny the new handler if there's an existing one

      --txt-record-prefix <TXT_RECORD_PREFIX>
          Prefix for TXT DNS records containing key fingerprints, for
          authorization to bind under a specific domain.

          In other words, valid records will be of the form:
          `TXT prefix.custom-domain SHA256:...`

          [default: _sandhole]

      --allow-provided-subdomains
          Allow user-provided subdomains. By default, subdomains are always
          random

      --allow-requested-ports
          Allow user-requested ports. By default, ports are always random

      --random-subdomain-seed <RANDOM_SUBDOMAIN_SEED>
          Which value to seed with when generating random subdomains, for
          determinism. This allows binding to the same random address until
          Sandhole is restarted.

          Beware that this can lead to collisions if misused!

          If unset, defaults to a random seed.

          Possible values:
          - ip-and-user: From IP address, SSH user, and requested address.
                         Recommended if unsure
          - user:        From SSH user and requested address
          - fingerprint: From SSH key fingerprint and requested address
          - address:     From SSH connection socket (address + port) and
                         requested address

      --idle-connection-timeout <IDLE_CONNECTION_TIMEOUT>
          Grace period for dangling/unauthenticated SSH connections before they
          are forcefully disconnected.

          A low value may cause valid proxy/tunnel connections to be erroneously
          removed.

          [default: 2s]

      --authentication-request-timeout <AUTHENTICATION_REQUEST_TIMEOUT>
          Time until a user+password authentication request is canceled. Any
          timed out requests will not authenticate the user

          [default: 5s]

      --http-request-timeout <HTTP_REQUEST_TIMEOUT>
          Time until an outgoing HTTP request is automatically canceled

          [default: 10s]

      --tcp-connection-timeout <TCP_CONNECTION_TIMEOUT>
          How long until TCP connections (including Websockets) are
          automatically garbage-collected.

          By default, these connections are not terminated by Sandhole.

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version