A Tour of Inlets - A Tunnel Built for the Cloud


Inlets is a lightweight tunneling tool. It’s not a SaaS product, it’s a self-hosted tunneling solution. That means you have total control over it. According to the documentation of Inlets PRO, there are various use cases:

  • Exposing services from a private network
  • Self-hosting HTTP endpoints with Let’s Encrypt integration (so you have HTTPS)
  • Connecting local Kubernetes with public IP
  • … etc.

inlets-pro is the main program where the magic happened. Basically it has two modes: TCP and HTTP. Both the server side and the client side run with the same program, but with different subcommands. Whatever you send over the tunnel between the server and the client gets encrypted in transit through the built-in TLS encryption. This is done automatically. A bonus feature is that you do not have to expose your services on the Internet, so can use Inlets like a VPN or SSH tunnel.


As mentioned above, there must be two endpoints in a tunnel: one end is Inlets PRO server, the other end is Inlets PRO client. The node where Inlets PRO server runs is called exit-server. It means that all the responses coming from the other end of the tunnel will take the exit here. The Inlets PRO server exposes its control-plane (websocket) to other Inlets PRO clients, so that the clients can connect and establish the tunnel. You can put Inlets PRO clients on your private server or even on your local computer, then expose private servers with public endpoints through the tunnel. Then you can share the public endpoints with customers to let them access your private services without a hassle.


You can download inlets-pro binary from GitHub release page directly, or you can try the handy inletsctl tool:

$ curl -sLSf https://inletsctl.inlets.dev | sudo sh
$ inletsctl download --pro
$ inlets-pro version
 _       _      _            _
(_)_ __ | | ___| |_ ___   __| | _____   __
| | '_ \| |/ _ \ __/ __| / _` |/ _ \ \ / /
| | | | | |  __/ |_\__ \| (_| |  __/\ V /
|_|_| |_|_|\___|\__|___(_)__,_|\___| \_/

  PRO edition
Version: 0.8.9 - 7df6fc42cfc14dd56d93c32930262202967d234b

TCP Mode

One of my use case is to establish a secured tunnel between my homelab server and my office Windows desktop, so I can access office environment through Windows RDP from my Mac. All I have to do is opening up Microsoft Remote Desktop app, and connect to my homelab server on port 3389.

Using Inlets PRO to Expose Windows RDP

Since I prefer commands which will block the terminal to be in the background, making inlets-pro tcp server a systemd service is the way to go. Be sure to provide the public IP address to --auto-tls-san argument, so the tunnel server can use it to generate the certificate. The token here is to ensure that no one but only who knows the token can connect to the tunnel server. It will be used on both server side and client side.

export TOKEN=$(head -c 16 /dev/urandom | shasum | cut -d '-' -f 1)
inlets-pro tcp server \
    --auto-tls \
    --auto-tls-san="" \
    --token="$TOKEN" \
    --generate=systemd | sudo tee -a /etc/systemd/system/inlets-pro.service
sudo systemctl enable inlets-pro.service
sudo systemctl start inlets-pro.service

In order to make the tunnel client outside of my homelab be able to connect to the tunnel server on control-plane (port 8123 for example), I have to setup a port-forwarding rule on my router/firewall. So that anyone connect to <public-ip>:8123 will reach <private-ip>:8123.

On the client side, run inlets-pro.exe tcp client along with a valid license key, the aforementioned token, the URL of the tunnel server, and the ports that you want to expose, i.e. port 3389 for Windows RDP service.

C:\Program Files\Inlets PRO>inlets-pro.exe tcp client --license="INLETS-PRO-LICENSE-KEY" --url="wss://" --token="fe6f868d72123701326e31d3179a07208cc5d80d" --ports=3389
2021/08/09 10:51:30 Starting TCP client. Version 0.8.8 - 57580545a321dc7549a26e8008999e12cb7161de
2021/08/09 10:51:31 Licensed to: <redacted> (Gumroad subscription)
2021/08/09 10:51:31 Upstream server: localhost, for ports: 3389
inlets-pro client. Copyright OpenFaaS Ltd 2021
time="2021/08/09 10:51:31" level=info msg="Connecting to proxy" url="wss://"
time="2021/08/09 10:51:31" level=info msg="Connection established.. OK."

Now that the tunnel has been established. The tunnel server listens on port 3389 for any incoming RDP connection. I can connect to <private-ip>:3389 for my office Windows desktop in home without exposing it to the entire world.


Here I will use “local file sharing via HTTP server” as an example.

  • Local computer is the place (my M1 Mac mini) where the files we wanna share, resides in a private network
  • Exit server is a machine with publicly accessible IP address, usually resides on the cloud

On the exit-server, start an Inlets PRO HTTP server. It will listens on port 8123 and 8000 by default as control port and data port respectively. One can change the ports by providing --control-port and --port if there are port conflicts.

$ inlets-pro http server \
    --auto-tls \
    --auto-tls-san="" \
2021/08/17 16:08:45 Starting HTTP client. Version 0.8.9 - 7df6fc42cfc14dd56d93c32930262202967d234b
2021/08/17 16:08:45 Wrote: /tmp/certs/ca.crt
2021/08/17 16:08:45 Wrote: /tmp/certs/ca.key
2021/08/17 16:08:45 Wrote: /tmp/certs/server.crt
2021/08/17 16:08:45 Wrote: /tmp/certs/server.key
2021/08/17 16:08:45 TLS:, expires in: 2491.999991 days
2021/08/17 16:08:45 Data Plane Listening on
2021/08/17 16:08:45 Control Plane Listening with TLS on

On my local computer, setup an Inlets PRO HTTP fileserver (something similar to python3 -m http.server but with more features such as directory browsing toggle, basic authentication, etc.).

$ inlets-pro http fileserver \
    --webroot="$HOME/Projects" \
    --allow-browsing \
Starting inlets PRO fileserver. Version: 0.8.9-18-gf4fc15b - f4fc15b9604efd0b0ca3cc604c19c200ae6a1d7b
2021/08/17 16:11:13 Serving: /Users/starbops/Projects, on, browsing: true, auth: true

Since the Inlets PRO HTTP fileserver binds to local interface, I’ll start the Inlets PRO HTTP client on the same local computer and specify [localhost](http://localhost) as upstream. So the client will establish the tunnel to the exit-server, and forward related HTTP requests back to the fileserver running on my local computer.

$ inlets-pro http client \
    --url="wss://" \
    --token="fe6f868d72123701326e31d3179a07208cc5d80d" \
Starting HTTP client. Version: 0.8.9-18-gf4fc15b - f4fc15b9604efd0b0ca3cc604c19c200ae6a1d7b
2021/08/17 16:10:59 Licensed to: <redacted> (Gumroad subscription)
2021/08/17 16:10:59 Upstream:  => http://localhost:8080
INFO[2021/08/17 16:10:59] Connecting to proxy                           url="wss://"
INFO[2021/08/17 16:10:59] Connection established                        client_id=5482d06cafa4404786d92eb44a916903

By the way, thanks to @alex, I have the opportunity to test on the RC version of the Darwin ARM64 build. It works on my M1 Mac mini perfectly.

$ inlets-pro version
 _       _      _            _
(_)_ __ | | ___| |_ ___   __| | _____   __
| | '_ \| |/ _ \ __/ __| / _` |/ _ \ \ / /
| | | | | |  __/ |_\__ \| (_| |  __/\ V /
|_|_| |_|_|\___|\__|___(_)__,_|\___| \_/

  PRO edition
Version: 0.8.9-18-gf4fc15b - f4fc15b9604efd0b0ca3cc604c19c200ae6a1d7b

And we are all set. Open a browser then navigate to the address of the exit-server with the designated port (data port specified by --port with inlets-pro http server command). A familiar authentication dialog should pop up. Sign in with the default username (which is admin) and the specified password, the directory list will show up.

Basic Auth of Inlets HTTP Fileserver

Inlets HTTP Fileserver


As mentioned above, you can download the inlets-pro binary through inletsctl:

inletsctl download --pro

While inlets-pro is dedicated to tunnel establishing, inletsctl provides a bunch of additional features about cloud service provider integrations. With inletsctl, one can easily provision an inlets-pro-ready cloud instance (usually VM) as exit-server like a breeze. For example:

$ inletsctl create --provider digitalocean \
    --region sgp1 \
    --access-token-file do-access-token
Using provider: digitalocean
Requesting host: epic-feynman8 in sgp1, from digitalocean
2021/08/09 11:41:59 Provisioning host with DigitalOcean
Host: 258759207, status:
[1/500] Host: 258759207, status: new
[2/500] Host: 258759207, status: new
[3/500] Host: 258759207, status: new
[4/500] Host: 258759207, status: new
[5/500] Host: 258759207, status: new
[6/500] Host: 258759207, status: new
[7/500] Host: 258759207, status: new
[8/500] Host: 258759207, status: new
[9/500] Host: 258759207, status: new
[10/500] Host: 258759207, status: new
[11/500] Host: 258759207, status: new
[12/500] Host: 258759207, status: new
[13/500] Host: 258759207, status: new
[14/500] Host: 258759207, status: new
[15/500] Host: 258759207, status: new
[16/500] Host: 258759207, status: new
[17/500] Host: 258759207, status: active
inlets PRO TCP (0.8.6) server summary:
  Auth-token: EJW4btMsNaC5CKIl9cZ6qGP3baMztheIvW8GtU1zifXkkxuBr3EwtmVI7hM1bmsK


# Obtain a license at https://inlets.dev
# Store it at $HOME/.inlets/LICENSE or use --help for more options
export LICENSE="$HOME/.inlets/LICENSE"

# Give a single value or comma-separated
export PORTS="8000"

# Where to route traffic from the inlets server
export UPSTREAM="localhost"

inlets-pro tcp client --url "wss://" \
  --token "EJW4btMsNaC5CKIl9cZ6qGP3baMztheIvW8GtU1zifXkkxuBr3EwtmVI7hM1bmsK" \
  --upstream $UPSTREAM \
  --ports $PORTS

To delete:
  inletsctl delete --provider digitalocean --id "258759207"

The IP address and the token is shown in the output after finishing the deployment of cloud instance. Use the provided information in inlets-pro tcp client command:

$ export UPSTREAM="nuclear.internal.zespre.com"
$ export PORTS="3000"
$ inlets-pro tcp client \
    --url="wss://" \
    --token="EJW4btMsNaC5CKIl9cZ6qGP3baMztheIvW8GtU1zifXkkxuBr3EwtmVI7hM1bmsK" \
    --upstream="$UPSTREAM" \
2021/08/09 12:00:00 Starting TCP client. Version 0.8.8 - 57580545a321dc7549a26e8008999e12cb7161de
2021/08/09 12:00:00 Licensed to: <redacted> (Gumroad subscription)
2021/08/09 12:00:00 Upstream server: nuclear.internal.zespre.com, for ports: 3000
inlets-pro client. Copyright OpenFaaS Ltd 2021
INFO[2021/08/09 12:00:01] Connecting to proxy                           url="wss://"
INFO[2021/08/09 12:00:01] Connection established.. OK.

Just remember to put a valid Inlets PRO license under $HOME/.inlets/LICENSE so you don’t need to specify the license in the command line.

Delete the cloud instance if the tunnel is no longer needed, otherwise it’ll cost your money:

inletsctl delete --provider digitalocean \
    --access-token-file do-access-token \


IMO Inlets-operator is the most brilliant part over all these cloud native tunneling use cases. As an operator of Inlets PRO in Kubernetes, it monitors on Service resources (especially on LoadBalancer type of Service) and manages Tunnel resource and exit servers. This is perfect for a private/local deployment of Kubernetes cluster. Think about exposing your application running on a Raspberry Pi powered K8s in your homelab, it could be a real pain in the ass. However, with Inlets-operator, things will be different! For more details, check out Ivan Velichko’s article about Kubernetes operator pattern and Inlets-operator.


Install CRDs and Helm charts of Inlets-operator. Here I choose DigitalOcean as my cloud service provider, you can choose whatever suits you best.

git clone https://github.com/inlets/inlets-operator.git
cd inlets-operator/
helm repo add inlets https://inlets.github.io/inlets-operator/
helm repo update
kubectl apply -f ./artifacts/crds/
kubectl create ns inlets-operator
helm upgrade inlets-operator --install inlets/inlets-operator \
    --set provider=digitalocean \
    --set region=sgp1 \
    --set inletsProLicense="$(cat $HOME/.inlets/LICENSE)" \
    --set annotatedOnly=true \
    --namespace inlets-operator

If there is any problem, check the logs generated in Inlets-operator Deployment:

kubectl -n inlets-operator logs deploy/inlets-operator -f

Deploying Example Application

Now the Inlets-operator is ready, let’s deploy an example application to see how it works. Spin up a Deployment with Nginx web server:

$ cat <<EOF | kubectl apply -f -
> apiVersion: apps/v1
> kind: Deployment
> metadata:
>   name: nginx-1
>   labels:
>     app: nginx
> spec:
>   replicas: 1
>   selector:
>     matchLabels:
>       app: nginx
>   template:
>     metadata:
>       labels:
>         app: nginx
>     spec:
>       containers:
>       - name: nginx
>         image: nginx:1.14.2
>         ports:
>         - containerPort: 80
deployment.apps/nginx-1 created

Exposing Private Service

With Nginx running in a Pod, we still need to expose the port on which Nginx listens, so that users can access it. Here we use LoadBalancer type of Service which means Inlets-operator will provision a cloud instance with a public IP address as a load-balancer, then establish a secured tunnel between the cloud instance and the auxiliary Pod running alongside with Nginx Pod on the local K8s for us.

$ cat <<EOF | kubectl apply -f -
> apiVersion: v1
> kind: Service
> metadata:
>   name: nginx-1
>   annotations:
>     metallb.universe.tf/address-pool: "dummy"
>     dev.inlets.manage: "true"
> spec:
>   type: LoadBalancer
>   selector:
>     app: nginx
>   ports:
>   - name: http
>     protocol: TCP
>     port: 80
>     targetPort: 80
service/nginx-1 created

It’s worth mentioning that since in my deployment of K8s, there is MetalLB running as a bare-metal load-balancer, which allocates IP addresses from a private network segment (192.168.xx.0/24) to LoadBalancer Services. So I have to make MetalLB and Inlets-operator know which LoadBalancer type of Service they should take care of. The solution by far is to install Inlets-operator with annotatedOnly=true and add two annotations in every LoadBalancer type of Service:

  • metallb.universe.tf/address-pool: "dummy": “dummy” can be replaced with any other terms which isn’t a valid address pool name in your MetalLB setup. This will make MetalLB ignore the request of load-balancer IP address.
  • dev.inlets.manage: "true": Inlets-operator will only take action on Services with this annotated

Accessing from Outside

After the Service created, we can see that Inlets-operator is provisioning the tunnel for us. And the external IP address is still in the pending status because the cloud instance is spawning.

$ kubectl get svc,tunnel
NAME                 TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/nginx-1      LoadBalancer    <pending>     80:30555/TCP   6s

NAME                                      SERVICE   TUNNEL   HOSTSTATUS     HOSTIP   HOSTID
tunnel.inlets.inlets.dev/nginx-1-tunnel   nginx-1            provisioning            261289194

We can use -w option to watch the progress. Soon there is a public IP address shown up.

$ kubectl get svc -w
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
nginx-1      LoadBalancer    <pending>     80:30555/TCP   28s
nginx-1      LoadBalancer   80:30555/TCP   72s
nginx-1      LoadBalancer,   80:30555/TCP   72s
nginx-1      LoadBalancer                  80:30555/TCP   72s

If you check out the Deployment and Pod lists you’ll notice that there is a new Pod created by Inlets-operator automatically. Actually it is the auxiliary Pod which runs Inlets PRO client! All the tedious works such as generating token, figuring out where the tunnel server and upstream are, are handled by Inlets-operator. It just works.

$ kubectl get deploy,po
NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-1                 1/1     1            1           5m35s
deployment.apps/nginx-1-tunnel-client   1/1     1            1           2m26s

NAME                                        READY   STATUS    RESTARTS   AGE
pod/nginx-1-66b6c48dd5-dksx9                1/1     Running   0          5m35s
pod/nginx-1-tunnel-client-d9cd96c74-ks7vg   1/1     Running   3          2m25s

Now you can access the private Nginx with the above listed public IP address from anywhere (with Internet connection)!

Exposing Private Nginx


Inlets PRO is a swiss army knife. There are various use cases listed in the documents. It can replace SSH tunneling with so much ease. Users can expose their private services efficiently like never before. It is also possible bringing remote services to local using Inlets PRO. Unlike SaaS tunneling solutions like Ngrok, you have total control over your infrastructure without traffic throttling. And the data flow through the tunnel is secured out of the box. If you have a local K8s deployment, definitely give it a try!