Skip to content

Instantly share code, notes, and snippets.

@alexellis
Last active October 19, 2022 15:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexellis/f092251017bfb188071a448209e0d526 to your computer and use it in GitHub Desktop.
Save alexellis/f092251017bfb188071a448209e0d526 to your computer and use it in GitHub Desktop.
Install inlets cloud on your Kubernetes cluster 2022

Install inlets cloud on your Kubernetes cluster

An inlets cloud provides a way for you to create and manage multiple tunnel servers with the ease of a SaaS.

Example usage with the inlets-cloud CLI:

# Create a tunnel for Subhash

./inlets-cloud create subhash
Additional 0 domains: []
Created tunnel subhash. OK.

# Print his connection info, so he can connect a tunnel to the new 
# tunnel server Pod

./inlets-cloud connect --domain tun.example.com subhash
# Access your tunnel via: https://subhash.tun.example.com

inlets-pro http client \
  --token=r9OFd5AkerDmBMbs1lt1A0CvJwshO4zzemQnK67QquqWVSQViAyoGrsE \
  --url=wss://subhash-tunnel.tun.example.com \
  --upstream=http://127.0.0.1:8000 \
  --auto-tls=false

Installation

You'll need a Kubernetes cluster and a domain name with a wildcard entry.

Pre-reqs

  • A Kubernetes cluster, ideally 3 nodes with 2 vCPU and 4GB of RAM. Lower may also work
  • A sub-domain you can use, and a way to set up your DNS01 challenge from cert-manager.
  • An inlets Pro subscription - any tier will work here, but pay attention to the limits of your subscription, don't go over these

Kubernetes

The Kubernetes cluster should be created on the public Internet.

I suggest using DigitalOcean or Google Cloud.

Get the Ingress Controller

arkade install ingress-nginx
arkade install cert-manager

Create an inlets namespace:

kubectl create ns inlets

Create a DNS Zone with your cloud provider

You need to create a DNS Zone in your managed provider.

If your wildcard domain is i.e. *.tun.example.com create the zone as tun.example.com as a NS record.

Now create a DNS A record for *.tun.example.com and use the IP of ingress-nginx from kubectl get svc

Create a cert-manager DNS01 ClusterIssuer

Follow this guide and call your ClusterIssuer letsencrypt-prod

Note since we are using a ClusterIssuer, create your secrets in the cert-manager namespace

ClusterIssuer example for DigitalOcean:

Create an access token in the UI and download it as ~/do-access-token

Create a secret with the value:

kubectl create secret generic \
  -n cert-manager digitalocean-dns \
  --from-file access-token=$HOME/do-access-token

Edit the email field and then apply this file:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  namespace: inlets
spec:
  acme:
    email: webmaster@tun.example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - dns01:
        digitalocean:
            tokenSecretRef:
              name: digitalocean-dns
              key: access-token

Create your license secret

Create a Kubernetes secret for the inlets-cloud-operator to use:

kubectl create secret generic -n inlets \
  inlets-license-key --from-file inlets-license-key=$HOME/.inlets/LICENSE.txt

Create a token for the client-api

There's a REST API aka client-api which can be used to integrate with inlets-cloud from your own applications.

Create a token for it:

export token=$(head -c 16 /dev/urandom | shasum | cut -d" " -f1)
echo -n $token > $HOME/.inlets/client-api-token.txt

kubectl create secret generic \
  client-api-token \
  -n inlets \
  --from-file client-api-token=$HOME/.inlets/client-api-token.txt

Deploy inlets-cloud-operator

The operator detects ExitTunnel resources and creates a: secret, deployment to run inlets-pro http server, service and compatible ingress entry.

Download inlets-cloud.yaml and replace the example domain with your own, then apply it

sed s/tun.example.com/inlets.mydomain.dev/g inlets-cloud.yaml | \
  kubectl apply -f-

Download the CLI from, make sure you have the latest version

https://github.com/alexellis/inlets-cloud-cli/releases/

Test it out

# Create a new ExitTunnel
inlets-cloud-cli create NAME

# Create a new ExitTunnel with more than one domain
# mapped to it
#
# Do not add the domain suffix here
# So "gateway" instead of "gateway.tun.example.com"
# 
# The command prints out how to connect and how to 
# access the tunnel.
inlets-cloud-cli create NAME \
 --upstream gateway \
 --upstream prometheus

# Print inlets client connection string
inlets-cloud-cli connect NAME --domain tun.example.com

# List tunnels
inlets-cloud-cli list

# Rotate a secret for a tunnel
inlets-cloud-cli rotate NAME
---
# Source: crds/inletscloud.inlets.dev_exittunnels.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: (unknown)
creationTimestamp: null
name: exittunnels.inletscloud.inlets.dev
spec:
additionalPrinterColumns:
- JSONPath: .name
name: Name
type: string
- JSONPath: .spec.auth_token.name
name: AuthTokenName
type: string
group: inletscloud.inlets.dev
names:
kind: ExitTunnel
listKind: ExitTunnelList
plural: exittunnels
singular: exittunnel
scope: Namespaced
subresources: {}
validation:
openAPIV3Schema:
description: ExitTunnel is a specification for a Tunnel resource
type: object
required:
- spec
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: ExitTunnelSpec is the spec for a Tunnel resource
type: object
properties:
auth_token:
type: object
nullable: true
pod_annotations:
description: PodAnnotations are created on the Pod for the inlets exit-server
type: object
additionalProperties:
type: string
server_deployment:
type: object
nullable: true
server_service:
type: object
nullable: true
status:
description: ExitTunnelStatus is the status for a Tunnel resource
type: object
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
# Source: inlets-cloud-operator/templates/rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: inlets-cloud-operator
namespace: inlets
---
# Source: inlets-cloud-operator/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: inlets-cloud-operator-rw
# namespace: inlets
rules:
- apiGroups: ["inletscloud.inlets.dev"]
resources: ["exittunnels"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
# Source: inlets-cloud-operator/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: inlets-cloud-operator-rw
# namespace: inlets
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: inlets-cloud-operator-rw
subjects:
- kind: ServiceAccount
name: inlets-cloud-operator
namespace: inlets
---
# Source: inlets-cloud-operator/templates/router-svc.yaml
apiVersion: v1
kind: Service
metadata:
annotations:
name: inlets-router
namespace: inlets
spec:
ports:
- name: http
port: 8080
protocol: TCP
targetPort: 8080
selector:
app: inlets-router
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
---
# Source: inlets-cloud-operator/templates/client-api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: client-api
namespace: inlets
labels:
app: client-api
spec:
replicas: 1
selector:
matchLabels:
app: client-api
template:
metadata:
labels:
app: client-api
annotations:
prometheus.io.scrape: "false"
spec:
serviceAccountName: inlets-cloud-operator
containers:
- name: operator
image: "ghcr.io/inlets/client-api:0.1.1-dirty"
imagePullPolicy: "Always"
command:
- "./client-api"
env:
- name: secret_path
value: /var/secrets/inlets/client-api-token
resources:
limits:
memory: 128Mi
requests:
memory: 25Mi
volumeMounts:
- mountPath: /var/secrets/inlets/
name: client-api-token
readOnly: true
volumes:
- name: client-api-token
secret:
defaultMode: 420
secretName: client-api-token
---
# Source: inlets-cloud-operator/templates/operator.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloud-operator
namespace: inlets
labels:
app: cloud-operator
spec:
replicas: 1
selector:
matchLabels:
app: cloud-operator
template:
metadata:
labels:
app: cloud-operator
annotations:
prometheus.io.scrape: "false"
spec:
serviceAccountName: inlets-cloud-operator
containers:
- name: operator
image: "ghcr.io/inlets/cloud-operator:0.1.1-dirty"
imagePullPolicy: "Always"
command:
- "/usr/bin/operator"
- "-license-file=/var/secrets/inlets-license/inlets-license-key"
- "-inlets-version=0.9.3"
- "-domain=*.tun.example.com"
resources:
limits:
memory: 128Mi
requests:
memory: 25Mi
volumeMounts:
- mountPath: /var/secrets/inlets-license/
name: inlets-license-key
readOnly: true
volumes:
- name: inlets-license-key
secret:
defaultMode: 420
secretName: inlets-license-key
---
# Source: inlets-cloud-operator/templates/router.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: inlets-router
namespace: inlets
labels:
app: inlets-router
spec:
replicas: 1
selector:
matchLabels:
app: inlets-router
template:
metadata:
labels:
app: inlets-router
annotations:
prometheus.io.scrape: "false"
spec:
serviceAccountName: inlets-cloud-operator
containers:
- name: router
image: "ghcr.io/inlets/router:0.1.1-dirty"
imagePullPolicy: "Always"
command:
- "./router"
env:
- name: namespace
value: "inlets"
resources:
limits:
memory: 128Mi
requests:
memory: 50Mi
---
# Source: inlets-cloud-operator/templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: inlets-ingress
namespace: inlets
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/limit-connections: "300"
nginx.ingress.kubernetes.io/limit-rpm: "1000"
# 10 minutes for the websocket
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# Up the keepalive timeout to max
nginx.ingress.kubernetes.io/keepalive-timeout: "350"
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
cert-manager.io/cluster-issuer: "letsencrypt-prod"
labels:
app: inlets-router
spec:
tls:
- hosts:
- "*.tun.example.com"
secretName: wildcard-inlets-router-cert
rules:
- host: "*.tun.example.com"
http:
paths:
- path: /
backend:
serviceName: inlets-router
servicePort: 8080
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment