NexTerm, WaveTerm and SSH Containers

Published: Dec 24, 2024 by Isaac Johnson

Today let’s look into few decent open source externalized access options that we can use to enter into our network.

One of the ones we’ll look at is Nexterm that not only allow us to connect to hosts with a variety of protocols but also install some Opensource apps such as Uptime Kuma.

I’ll demonstrate a simple way to expose an SSH server just using a container in your cluster and a firewall rule.

Lastly, I’ll dig into Waveterm which is a new SSH Client (and more) for Linux, Windows and Mac.

Let’s start with Nexterm.

Nexterm

I was inspired to check out Nexterm from this MariusHosting blog where he uses Portainer.

We can see the Nexterm setup with its Docker compose file (found here as well as here).

Docker Compose

services:
  nexterm:
    ports:
      - "6989:6989"
    restart: always
    volumes:
      - /docker/nexterm:/app/data
    image: germannewsmaker/nexterm:1.0.2-OPEN-PREVIEW
volumes:
  nexterm:

I’ll quickly convert that to a Kubernetes YAML manifest with a PVC, Deployment and Service

Deployment YAML

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nexterm-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi  # Adjust storage size as needed
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nexterm
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nexterm
  template:
    metadata:
      labels:
        app: nexterm
    spec:
      containers:
      - name: nexterm
        image: germannewsmaker/nexterm:1.0.2-OPEN-PREVIEW
        ports:
        - containerPort: 6989
        volumeMounts:
        - mountPath: /app/data
          name: nexterm-storage
      volumes:
      - name: nexterm-storage
        persistentVolumeClaim:
          claimName: nexterm-pvc

---
apiVersion: v1
kind: Service
metadata:
  name: nexterm-service
spec:
  selector:
    app: nexterm
  ports:
  - protocol: TCP
    port: 6989
    targetPort: 6989

Now let’s apply

$ kubectl apply -f ./test.yaml
persistentvolumeclaim/nexterm-pvc created
deployment.apps/nexterm created
service/nexterm-service created

I can now connect

$ kubectl port-forward svc/nexterm-service 6989:6989
Forwarding from 127.0.0.1:6989 -> 6989
Forwarding from [::1]:6989 -> 6989

The first step is to create an account

/content/images/2024/12/nextterm-01.png

I can now see a landing page

/content/images/2024/12/nextterm-02.png

I’ll go to Networking and add Nexterm there

/content/images/2024/12/nextterm-03.png

I get an error about no SSH servers

/content/images/2024/12/nextterm-04.png

I’ll go to servers and add a folder

/content/images/2024/12/nextterm-05.png

From there I can add a server

/content/images/2024/12/nextterm-06.png

I’ll add a Macbook Air

/content/images/2024/12/nextterm-07.png

I gave it a password in the auth section the tried connecting with it. Worked great

/content/images/2024/12/nextterm-08.png

Ingress

Since we have a password, assuming this is a decently secure app, let’s expose it externally

First, I’ll need an A record we can use for an ingress

$ gcloud dns --project=myanthosproject2 record-sets create Nexterm.steeped.space --zone="steepedspace" --type="A" --ttl="300" --rrdatas="75.73.224.240"
NAME                     TYPE  TTL  DATA
Nexterm.steeped.space.  A     300  75.73.224.240 

Let me then create an ingress

$ cat ingress.Nexterm.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: gcpleprod2
    ingress.kubernetes.io/proxy-body-size: "0"
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
    nginx.org/websocket-services: nexterm-service
  labels:
    app.kubernetes.io/instance: nexterm-service
    app.kubernetes.io/name: nexterm-service
  name: nexterm-service
spec:
  rules:
  - host: Nexterm.steeped.space
    http:
      paths:
      - backend:
          service:
            name: nexterm-service
            port:
              number: 6989
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - Nexterm.steeped.space
    secretName: nexterm-servicegcp-tls

Now apply

$ kubectl apply -f ./ingress.Nexterm.yaml
ingress.networking.k8s.io/nexterm-service created

Once I see it satisified the cert

$ kubectl get cert nexterm-servicegcp-tls
NAME                     READY   SECRET                   AGE
nexterm-servicegcp-tls   True    nexterm-servicegcp-tls   84s

/content/images/2024/12/nextterm-09.png

And I can use it to get through to a host

/content/images/2024/12/nextterm-10.png

I can then install an app to that host

/content/images/2024/12/nextterm-11.png

A window pops up and shows it plans to actually install a lot of things

/content/images/2024/12/nextterm-12.png

I’m slightly worried as it installed Docker on that host which already runs as a Kubernetes master node and I’m pretty sure had docker as it’s the primary utility box used in my AWX job runner

/content/images/2024/12/nextterm-13.png

When I clicked open it tries to fire up a browser to the internal IP. As my traffic is coming from Sweden at the moment, that does not work so hot

/content/images/2024/12/nextterm-14.png

However, If I route into a window in my network, I can reach it

/content/images/2024/12/nextterm-15.png

I tested other things on my VNC session and they weren’t as laggy but as you can see, the speed test works, but the interface is definitely sluggish.

Using it

While testing, I found one of my services was down

/content/images/2024/12/nextterm-16.png

Let’s figure this out.

First, I need to determine if it’s a containerized service in Kubernetes or just one running in docker and exposed via Kubernetes.

To do that, I’ll get the ingress and look at the service

$ kubectl get ingress -A | grep enclosed
default         enclosedingress               <none>   enclosed.tpk.pw                             80, 443   45d

$ kubectl get ingress enclosedingress -o yaml | grep -C 3 service:
    http:
      paths:
      - backend:
          service:
            name: enclosed-external-ip
            port:
              number: 80

Usually I need to look into the service to check on selectors, but I did myself a favour by naming it external IP

$ kubectl get endpoints | grep enclosed
enclosed-external-ip                                    192.168.1.100:8713                                              45d

Let’s now use Nexterm to get there.

I’ll create a folder for Docker boxes

/content/images/2024/12/nextterm-17.png

And add a server

/content/images/2024/12/nextterm-18.png

I can make an entry for that 192.168.1.100 (builder-T100) and connect

/content/images/2024/12/nextterm-19.png

I fear I did some unneccessary pruning. I know it was there but it sure isn’t now

/content/images/2024/12/nextterm-20.png

I was able to look up the blog from 10-29 and run the docker command again

builder@builder-T100:~$ docker run -d --name enclosed -e PORT=8713 -v /home/builder/enclosed:/app/.data -p 8713:8713 corentinth/enclosed:latest
Unable to find image 'corentinth/enclosed:latest' locally
latest: Pulling from corentinth/enclosed
da9db072f522: Already exists 
03d2f4babaac: Pull complete 
a1c7bd30f9ab: Pull complete 
c3f44fc696cb: Pull complete 
4a4bb4ad0c79: Pull complete 
08c6502d02c0: Pull complete 
fbcf34564597: Pull complete 
d5c1ac7fe4ce: Pull complete 
Digest: sha256:ffadec06634cd201a3dfb02dbd19097a1cf2677fe8d85ef3cfa50282e799cb29
Status: Downloaded newer image for corentinth/enclosed:latest
7b52245e735bb567720b3bcca79ece326bdd028bcf2c535b0008469e78d5191a

And I can see it’s up and running

/content/images/2024/12/nextterm-21.png

Nexterm with Btop++

I can also use this a nice wy to get at BTOP++ to view real time stats of the host

/content/images/2024/12/nextterm-23.png

SSH in K8s

Let’s start by creating a username and password and base64’ing the values

$ echo -n 'builder' | base64
YnVpbGRlcg==
$ echo -n 'TestTest1234' | base64
VGVzdFRlc3QxMjM0

Next, I’m going to create a secret with those values

$ cat creds.yaml
apiVersion: v1
kind: Secret
metadata:
  name: ssh-secret
type: Opaque
data:
  username: YnVpbGRlcg==
  password: VGVzdFRlc3QxMjM0
$ kubectl apply -f ./creds.yaml
secret/ssh-secret created

And use it in a deployment that has an ubuntu image with SSHD preloaded:

$ cat manifest.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ssh-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ssh-server
  template:
    metadata:
      labels:
        app: ssh-server
    spec:
      containers:
      - name: ssh-server
        image: rastasheep/ubuntu-sshd:18.04
        ports:
        - containerPort: 22
        env:
        - name: SSH_USERNAME
          valueFrom:
            secretKeyRef:
              name: ssh-secret
              key: username
        - name: SSH_PASSWORD
          valueFrom:
            secretKeyRef:
              name: ssh-secret
              key: password
        volumeMounts:
        - name: ssh-keys
          mountPath: /root/.ssh
      volumes:
      - name: ssh-keys
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: ssh-service
spec:
  type: LoadBalancer
  selector:
    app: ssh-server
  ports:
  - protocol: TCP
    port: 22
    targetPort: 22
    port: 5522
    nodePort: 30022


$ kubectl apply -f ./manifest.yaml
deployment.apps/ssh-server created
service/ssh-service created

This uses the default root and root to login, and did not actually create the builder user

builder@builder-T100:~$ ssh -p 5522 root@192.168.1.57
root@192.168.1.57's password: 
root@ssh-server-7678596449-6wsxs:~# passwd builder
passwd: user 'builder' does not exist
root@ssh-server-7678596449-6wsxs:~# ^C
root@ssh-server-7678596449-6wsxs:~# 

I wonder if I could just create a Dockerfile with a user/pass

$ cat Dockerfile
FROM ubuntu:latest

# Install SSH server
RUN apt-get update && \
    apt-get install -y openssh-server && \
    apt-get clean

# Set the root password for the SSH server (CHANGE THIS PASSWORD!)
RUN echo 'root:TestTest1234' | chpasswd

# Permit root login via SSH
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config

# Enable password authentication
RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config

# Create privilege separation directory with proper permissions
RUN mkdir /run/sshd && chown root:root /run/sshd && chmod 0755 /run/sshd

# SSH port (optional, change if needed)
EXPOSE 22

# Start SSH service
CMD ["/usr/sbin/sshd", "-D"]

I can then build

$ docker build -t harbor.freshbrewed.science/freshbrewedprivate/testsshserver:0.3 .
[+] Building 1.1s (9/9) FINISHED                                                                                                                                                  docker:default
 => [internal] load build definition from Dockerfile                                                                                                                                        0.0s
 => => transferring dockerfile: 709B                                                                                                                                                        0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                                                                                                            1.0s
 => [internal] load .dockerignore                                                                                                                                                           0.0s
 => => transferring context: 2B                                                                                                                                                             0.0s
 => [1/5] FROM docker.io/library/ubuntu:latest@sha256:80dd3c3b9c6cecb9f1667e9290b3bc61b78c2678c02cbdae5f0fea92cc6734ab                                                                      0.0s
 => CACHED [2/5] RUN apt-get update &&     apt-get install -y openssh-server &&     apt-get clean                                                                                           0.0s
 => CACHED [3/5] RUN echo 'root:TestTest1234' | chpasswd                                                                                                                                    0.0s
 => CACHED [4/5] RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config                                                                                0.0s
 => CACHED [5/5] RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config                                                                                0.0s
 => exporting to image                                                                                                                                                                      0.0s
 => => exporting layers                                                                                                                                                                     0.0s
 => => writing image sha256:a7c36966cf91acef38ad6e62d14122bbac8fb5223f36dad3ef8e52508e8378cd                                                                                                0.0s
 => => naming to harbor.freshbrewed.science/freshbrewedprivate/testsshserver:0.1                                                                                                            0.0s

What's Next?
  View a summary of image vulnerabilities and recommendations → docker scout quickview

and push

$ docker push harbor.freshbrewed.science/freshbrewedprivate/testsshserver:0.3
The push refers to repository [harbor.freshbrewed.science/freshbrewedprivate/testsshserver]
7b67a675a8b3: Pushed
cbe99aa61f16: Pushed
fbdd987b8a4e: Pushed
e1e114296ab1: Pushed
687d50f2f6a6: Pushed
0.1: digest: sha256:96ed3aa9cf826d7f6fdaf52b2dd520b6cfdf35002c81a7bacadc5dfa87892cf3 size: 1364

I can now try using that in the deployment

$ cat manifest.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ssh-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ssh-server
  template:
    metadata:
      labels:
        app: ssh-server
    spec:
      containers:
      - name: ssh-server
        image: harbor.freshbrewed.science/freshbrewedprivate/testsshserver:0.3
        ports:
        - containerPort: 22
      imagePullSecrets:
        - name: myharborreg
---
apiVersion: v1
kind: Service
metadata:
  name: ssh-service
spec:
  type: LoadBalancer
  selector:
    app: ssh-server
  ports:
  - protocol: TCP
    port: 22
    targetPort: 22
    port: 5522
    nodePort: 30022

$ kubectl apply -f ./manifest.yaml
deployment.apps/ssh-server configured
service/ssh-service unchanged

This seemed to work

builder@builder-T100:~$ ssh -p 5522 root@192.168.1.33
The authenticity of host '[192.168.1.33]:5522 ([192.168.1.33]:5522)' can't be established.
ED25519 key fingerprint is SHA256:gcLkbLpLj4QGZVt3njKf+3GbrW3I4RqtodWI1v+LrQE.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[192.168.1.33]:5522' (ED25519) to the list of known hosts.
root@192.168.1.33's password: 
Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 5.15.0-106-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@ssh-server-84c8dbf79c-lg6d8:~# 

Now, if I want to reach this outside of my network, I need to expose a hole through the firewall

/content/images/2024/12/testssh-01.png

And I can test by sending all my traffic out to Sweden and coming back

/content/images/2024/12/testssh-02.png

I, of course, removed the hole when done and then the deployment

$ kubectl delete -f ./manifest.yaml
deployment.apps "ssh-server" deleted
service "ssh-service" deleted

Waveterm

Let’s check out on more SSH related tool, but this one is a client and not a server.

I found Waveterm from the TLDR newsletter.

I’ll start by downloading for windows here (they have binaries for Windows, Mac and Linux).

/content/images/2024/12/waveterm-01.png

That launches a quick installer

/content/images/2024/12/waveterm-02.png

which then launches WaveTerm

/content/images/2024/12/waveterm-03.png

I’m shown a few keybindings before being let loose

/content/images/2024/12/waveterm-04.png

The first thing I did was switch my left shell from a Powershell prompt to a bash shell on WSL. I then created a new shell tab and clicked connect to (gear icon)

/content/images/2024/12/waveterm-05.png

I’ll try adding a connection to my dockerh ost

/content/images/2024/12/waveterm-06.png

I typed in the password

/content/images/2024/12/waveterm-07.png

and was prompted again if I want to use WSL extensions. This time i did check “apply to all”

/content/images/2024/12/waveterm-08.png

Here we can see how we can rearrange blocks and add tabs

The thing about WaveTerm is it works WAY better on a large monitor. Moving it to fullscreen on my primary display makes a rather usable experience (I’m not sure how this will translate in a blog image, but use your biggest monitor to view fullscreen)

/content/images/2024/12/waveterm-10.png

In the above, I am able to watch my build, the Ansible jobs and have some common terminals I might need/

Summary

Today we checked out NexTerm and Waveterm. The former is an interesting web-based SSH client that can also install apps (and docker) to linux hosts. I’m not certain I would use the app install again because I’m not keen on it updating docker each go, but the SSH client feature is handy.

WaveTerm is an interesting option. I’ll have to compare it to the more more tried and true TMux to see which windowed option I like best. I do like the integrated browsers as I’m often looking at code as well as content in Github like PRs or Ansible or Azure DevOps.

Lastly, while i did demonstrate firing up a pod to expose SSH, I would more likely just expose SSH from a Pi in my stack or a Linux machine that is more utility. However, it was a good example of how one could use Kubernetes to accomplish a similar goal of NexTerm.

OpenSource NexTerm WaveTerm Kubernetes Docker

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes