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.
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
):
Type | Domain | Data |
---|---|---|
CNAME | my.domain.net | server.com |
TXT | _sandhole.my.domain.net | SHA256: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