Friday 5th April 2024 7:21 PM

Another app I run is Gitea, to manage git repo's. We need SSH working, so that SSH keys can be used to push and pull, but this is a bit tricky in a Kubernetes environment.

When Traefik accepts requests on ports 80 and 443, it knows that the HTTP protocol is being used, so it checks for a Host: header, and uses that to figure out which Service should handle the request.

There is no such host indicator for SSH traffic, so we need to dedicate a port on the server itself (i.e. exposed to the outside world), and configure Traefik to simply route all traffic on that port to the Gitea container.

Exposing an external server port

To configure Traefik to listen on a new port, we need to change its "static configuration".

When we installed K3s, it auto-installed Traefik using Helm, using HelmChartConfig resources. If we want to change the config, all we have to do is drop a file in /var/lib/rancher/k3s/server/manifests/, where Helm will notice it, apply the changes and restart Traefik[1]It takes time for all this to happen, so watch the pods to see when it actually happens..

Create a file[2]Any name that starts with traefik- will work. called /var/lib/rancher/k3s/server/manifests/traefik-config.yaml that looks like this:

apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system

spec:
  valuesContent: |-
    ports:
      gitea-ssh:
        port: 3022
        expose: true
        exposedPort: 3022

This creates a new "entry point" called gitea-ssh that Trafeik will listen on, along-side its default web (port 80) and websecure (port 443) entry points:

[core@vm-k3s ~]$ kubectl describe service traefik -n kube-system
Name:                     traefik
Namespace:                kube-system
Labels:                   app.kubernetes.io/instance=traefik-kube-system
                          app.kubernetes.io/managed-by=Helm
                          app.kubernetes.io/name=traefik
                          helm.sh/chart=traefik-25.0.2_up25.0.0
Annotations:              meta.helm.sh/release-name: traefik
                          meta.helm.sh/release-namespace: kube-system
Selector:                 app.kubernetes.io/instance=traefik-kube-system,app.kubernetes.io/name=traefik
Type:                     LoadBalancer
IP Family Policy:         PreferDualStack
IP Families:              IPv4
IP:                       10.43.201.207
IPs:                      10.43.201.207
LoadBalancer Ingress:     10.2.2.33
Port:                     gitea-ssh  3022/TCP
TargetPort:               gitea-ssh/TCP
NodePort:                 gitea-ssh  32616/TCP
Endpoints:                10.42.0.14:3022
Port:                     web  80/TCP
TargetPort:               web/TCP
NodePort:                 web  31539/TCP
Endpoints:                10.42.0.14:8000
Port:                     websecure  443/TCP
TargetPort:               websecure/TCP
NodePort:                 websecure  32003/TCP
Endpoints:                10.42.0.14:8443
Session Affinity:         None
External Traffic Policy:  Cluster
Events:
  Type     Reason                   Age                From                Message
  ----     ------                   ----               ----                -------
  Normal   EnsuringLoadBalancer     72s (x2 over 44m)  service-controller  Ensuring load balancer
  Normal   AppliedDaemonSet         72s (x2 over 44m)                      Applied LoadBalancer DaemonSet kube-system/svclb-traefik-52d3e851
  Warning  UnAvailableLoadBalancer  72s                                    There are no available nodes for LoadBalancer
  Normal   UpdatedLoadBalancer      71s (x2 over 44m)                      Updated LoadBalancer with new IPs: [] -> [10.2.2.33]

Configuring Gitea

The relevant part of my Deployment for Gitea looks like this:

containers:
  - name: gitea
    image: gitea/gitea:1.21.8
    imagePullPolicy: Never
    ports:
      - name: web
        containerPort: 3000
      - name: ssh
        containerPort: 22

which tells Kubernetes that the container is listening on 2 ports:

  • 3000 (web): this is the main web interface
  • 22 (ssh): this is for SSH activity

The Service looks like this:

ports:
  - name: web
    protocol: TCP
    port: 80
    targetPort: web
  - name: ssh
    protocol: TCP
    port: 22
    targetPort: ssh

which simply forwards traffic on ports 80 and 22 to the corresponding ports in the Deployment.

The important bits are the IngressRoute's. This one is for the web interface, and is the same as our demo app:

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: gitea-ingress-route
spec:
  entryPoints:
    - web
  routes:
  - match: Host(`gitea.k3s`)
    kind: Rule
    services:
    - name: gitea-service
      port: web

However, for this app, we have a second ingress:

apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
  name: gitea-ingress-route-ssh
spec:
  entryPoints:
    - gitea-ssh
  routes:
  - match: HostSNI(`*`)
    services:
    - name: gitea-service
      port: ssh

It has a different type[3]Since IngressRoute assumes the use of the HTTP protocol., and uses the new gitea-ssh entry point we created above. Routing rules need to have at least one condition, so we include a dummy "match everything" rule, and all traffic received on this entry point will be forwarded to gitea-service's ssh port.



References

References
1 It takes time for all this to happen, so watch the pods to see when it actually happens.
2 Any name that starts with traefik- will work.
3 Since IngressRoute assumes the use of the HTTP protocol.