Skip to content

Commit 4c33ac7

Browse files
lucamkingmakerbot
authored andcommitted
WebSSH: Backend
1 parent c55b53f commit 4c33ac7

File tree

22 files changed

+1096
-57
lines changed

22 files changed

+1096
-57
lines changed

.github/workflows/build-matrix.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
"build-args": "COMPONENT=bastion-ssh-tracker",
2828
"harbor-project": "crownlabs-core"
2929
},
30+
{
31+
"component": "webssh-bastion",
32+
"context": "./operators",
33+
"dockerfile": "./operators/build/golang-common/Dockerfile",
34+
"build-args": "COMPONENT=webssh-bastion",
35+
"harbor-project": "crownlabs-core"
36+
},
3037
{
3138
"component": "exam-agent",
3239
"context": "./operators",

deploy/crownlabs/values.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ bastion-operator:
118118
sshTrackerPort: 22
119119
sshTrackerSnaplen: 1600
120120
sshTrackerMetricsAddr: ":8082"
121+
122+
webssh:
123+
ingress:
124+
enabled: true
125+
annotations:
126+
nginx.ingress.kubernetes.io/proxy-buffer-size: "8k"
127+
hostname: crownlabs.example.com
128+
path: /webssh
129+
pathType: Prefix
121130

122131
image-list:
123132
replicaCount: 1

operators/cmd/examagent/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"k8s.io/klog/v2/textlogger"
2626

2727
"github.com/netgroup-polito/CrownLabs/operators/pkg/examagent"
28+
"github.com/netgroup-polito/CrownLabs/operators/pkg/utils"
2829
)
2930

3031
func main() {
@@ -36,7 +37,7 @@ func main() {
3637
os.Exit(1)
3738
}
3839

39-
k8sClient, err := examagent.NewK8sClient()
40+
k8sClient, err := utils.NewK8sClient()
4041
if err != nil {
4142
log.Error(err, "unable to prepare k8s client")
4243
os.Exit(1)

operators/cmd/instance-operator/main.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818
import (
1919
"flag"
2020
"os"
21+
"path/filepath"
2122
"strings"
2223
"time"
2324

@@ -72,6 +73,8 @@ func main() {
7273
"which the controller will work. Different labels (key=value) can be specified, by separating them with a &"+
7374
"( e.g. key1=value1&key2=value2")
7475

76+
websshKeyPathFlag := flag.String("webbastion-master-key-path", "", "Contain the path of the secret where the public key is stored. Used for webssh component.")
77+
7578
sharedVolumeStorageClass := flag.String("shared-volume-storage-class", "rook-nfs", "The StorageClass to be used for all SharedVolumes' PVC (if unique can be used to enforce ResourceQuota on Workspaces, about number and size of ShVols)")
7679

7780
maxConcurrentTerminationReconciles := flag.Int("max-concurrent-reconciles-termination", 1, "The maximum number of concurrent Reconciles which can be run for the Instance Termination controller")
@@ -124,13 +127,27 @@ func main() {
124127

125128
// Configure the Instance controller
126129
const instanceCtrlName = "Instance"
130+
131+
// read the webssh public key form the secret
132+
var pubKeyBytes []byte
133+
if *websshKeyPathFlag != "" {
134+
pubKeyBytes, err = os.ReadFile(filepath.Clean(*websshKeyPathFlag))
135+
if err != nil {
136+
log.Error(err, "failed to read webssh public key", "path", *websshKeyPathFlag)
137+
}
138+
log.Info("webssh public key correctly retrieved")
139+
} else {
140+
log.Error(err, "no path provided for webssh public key")
141+
}
142+
127143
if err = (&instctrl.InstanceReconciler{
128-
Client: mgr.GetClient(),
129-
Scheme: mgr.GetScheme(),
130-
EventsRecorder: mgr.GetEventRecorderFor(instanceCtrlName),
131-
NamespaceWhitelist: nsWhitelist,
132-
ServiceUrls: svcUrls,
133-
ContainerEnvOpts: containerEnvOpts,
144+
Client: mgr.GetClient(),
145+
Scheme: mgr.GetScheme(),
146+
EventsRecorder: mgr.GetEventRecorderFor(instanceCtrlName),
147+
NamespaceWhitelist: nsWhitelist,
148+
ServiceUrls: svcUrls,
149+
ContainerEnvOpts: containerEnvOpts,
150+
WebSSHMasterPublicKey: pubKeyBytes,
134151
}).SetupWithManager(mgr, *maxConcurrentReconciles); err != nil {
135152
log.Error(err, "unable to create controller", "controller", instanceCtrlName)
136153
os.Exit(1)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2020-2025 Politecnico di Torino
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package main contains the entrypoint for webSSH, a WebSocket SSH bridge for CrownLabs.
16+
package main
17+
18+
import (
19+
"flag"
20+
"log"
21+
"os"
22+
"strconv"
23+
"time"
24+
25+
"github.com/go-logr/stdr"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
28+
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
29+
30+
crownlabsv1alpha1 "github.com/netgroup-polito/CrownLabs/operators/api/v1alpha1"
31+
crownlabsv1alpha2 "github.com/netgroup-polito/CrownLabs/operators/api/v1alpha2"
32+
"github.com/netgroup-polito/CrownLabs/operators/pkg/utils"
33+
"github.com/netgroup-polito/CrownLabs/operators/pkg/webssh-bastion"
34+
)
35+
36+
var (
37+
scheme = runtime.NewScheme()
38+
)
39+
40+
func init() {
41+
_ = clientgoscheme.AddToScheme(scheme)
42+
_ = crownlabsv1alpha1.AddToScheme(scheme)
43+
_ = crownlabsv1alpha2.AddToScheme(scheme)
44+
}
45+
46+
func main() {
47+
sshUserFlag := flag.String("websshuser", "crownlabs", "The user to use for SSH connections.")
48+
websshprivatekeypathFlag := flag.String("websshprivatekeypath", "", "The path to the private key file for SSH authentication.")
49+
websshtimeoutdurationFlag := flag.String("websshtimeoutduration", "0", "The timeout duration for SSH connections. In minutes.")
50+
websshmaxconncountFlag := flag.String("websshmaxconncount", "1000", "The maximum number of concurrent SSH connections.")
51+
websshvmport := flag.String("websshvmport", "22", "The default SSH port for VMs.")
52+
websshwebsocketportFlag := flag.String("websshwebsocketport", "8085", "The port on which the WebSocket server listens.")
53+
54+
flag.Parse()
55+
56+
timeout64, err := strconv.ParseInt(*websshtimeoutdurationFlag, 10, 32) // parse directly as int32
57+
if err != nil {
58+
timeout64 = 30
59+
}
60+
61+
maxConn64, err := strconv.ParseInt(*websshmaxconncountFlag, 10, 32)
62+
if err != nil {
63+
maxConn64 = 1000
64+
}
65+
66+
stdLogger := log.New(os.Stderr, "", log.LstdFlags)
67+
baseLogger := stdr.New(stdLogger)
68+
69+
webSSHCtx := &webssh.ServerContext{}
70+
webSSHCtx.BaseLogger = baseLogger
71+
webSSHCtx.SSHUser = *sshUserFlag
72+
webSSHCtx.PrivateKeyPath = *websshprivatekeypathFlag
73+
webSSHCtx.TimeoutDuration = time.Duration(timeout64) * time.Minute
74+
webSSHCtx.MaxConnectionCount = int32(maxConn64)
75+
webSSHCtx.WebsocketPort = *websshwebsocketportFlag
76+
webSSHCtx.VMSSHPort = *websshvmport
77+
webSSHCtx.BaseConfig, err = utils.GetRestConfig()
78+
79+
if err != nil {
80+
webSSHCtx.BaseLogger.Error(err, "Failed to get REST config")
81+
return
82+
}
83+
84+
info, err := os.Stat(webSSHCtx.PrivateKeyPath)
85+
if err != nil {
86+
webSSHCtx.BaseLogger.Error(err, "Cannot access private key file")
87+
return
88+
}
89+
90+
if info.IsDir() {
91+
webSSHCtx.BaseLogger.Error(nil, "Private key path points to a directory, not a file")
92+
return
93+
}
94+
95+
webSSHCtx.BaseLogger.Info("Config loaded",
96+
"SSHUser", webSSHCtx.SSHUser,
97+
"PrivateKeyPath", webSSHCtx.PrivateKeyPath,
98+
"TimeoutDuration", webSSHCtx.TimeoutDuration,
99+
"MaxConnectionCount", webSSHCtx.MaxConnectionCount,
100+
"WebsocketPort", webSSHCtx.WebsocketPort,
101+
"VMSSHPort", webSSHCtx.VMSSHPort)
102+
103+
webSSHCtx.StartWebSSH()
104+
}

operators/deploy/bastion-operator/templates/_helpers.tpl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ app.kubernetes.io/version: {{ include "bastion-operator.version" . | quote }}
5252
app.kubernetes.io/managed-by: {{ .Release.Service }}
5353
{{- end }}
5454

55+
{{- define "webssh.labels" -}}
56+
helm.sh/chart: {{ include "bastion-operator.chart" . }}
57+
{{ include "webssh.selectorLabels" . }}
58+
app.kubernetes.io/version: {{ include "bastion-operator.version" . | quote }}
59+
app.kubernetes.io/managed-by: {{ .Release.Service }}
60+
app.kubernetes.io/component: webssh
61+
{{- end }}
62+
5563
{{/*
5664
Selector labels
5765
*/}}
@@ -60,6 +68,11 @@ app.kubernetes.io/name: {{ include "bastion-operator.name" . }}
6068
app.kubernetes.io/instance: {{ .Release.Name }}
6169
{{- end }}
6270

71+
{{- define "webssh.selectorLabels" -}}
72+
app.kubernetes.io/name: webssh
73+
app.kubernetes.io/instance: {{ .Release.Name }}
74+
{{- end }}
75+
6376
{{/*
6477
Metrics selector additional labels
6578
*/}}

operators/deploy/bastion-operator/templates/deployment.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,59 @@ spec:
122122
matchLabels:
123123
{{- include "bastion-operator.selectorLabels" . | nindent 18 }}
124124
topologyKey: kubernetes.io/hostname
125+
126+
---
127+
128+
apiVersion: apps/v1
129+
kind: Deployment
130+
metadata:
131+
name: {{ include "bastion-operator.fullname" . }}-webssh
132+
labels:
133+
{{ include "webssh.labels" . | nindent 4 }}
134+
spec:
135+
replicas: {{ .Values.replicaCount }}
136+
selector:
137+
matchLabels:
138+
{{ include "webssh.selectorLabels" . | nindent 6 }}
139+
template:
140+
metadata:
141+
labels:
142+
{{ include "webssh.selectorLabels" . | nindent 8 }}
143+
spec:
144+
containers:
145+
- name: webssh
146+
image: "{{ .Values.image.repositoryWebBastion }}:{{ include "bastion-operator.version" . }}"
147+
imagePullPolicy: {{ .Values.image.pullPolicy }}
148+
ports:
149+
- name: {{ .Values.webssh.service.targetPort }}
150+
containerPort: {{ .Values.webssh.config.webSocketPort }}
151+
protocol: TCP
152+
livenessProbe:
153+
httpGet:
154+
path: /healthz
155+
port: {{ .Values.webssh.service.targetPort }}
156+
initialDelaySeconds: 3
157+
periodSeconds: 3
158+
readinessProbe:
159+
httpGet:
160+
path: /ready
161+
port: {{ .Values.webssh.service.targetPort }}
162+
initialDelaySeconds: 3
163+
periodSeconds: 3
164+
volumeMounts:
165+
- mountPath: /web-keys
166+
name: web-keys
167+
resources:
168+
{{- toYaml .Values.resources.webssh | nindent 12 }}
169+
args:
170+
- "--websshuser={{ .Values.webssh.config.user }}"
171+
- "--websshprivatekeypath=/web-keys/{{ .Values.webssh.masterKey.name }}"
172+
- "--websshmaxconncount={{ .Values.webssh.config.maxConnCount }}"
173+
- "--websshvmport={{ .Values.webssh.config.vmPort }}"
174+
- "--websshwebsocketport={{ .Values.webssh.config.webSocketPort }}"
175+
volumes:
176+
- name: web-keys
177+
secret:
178+
secretName: {{ .Values.webssh.masterKey.secretName }}
179+
defaultMode: 0444
180+

operators/deploy/bastion-operator/templates/hook-createsecret.yaml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,27 @@ spec:
8585
- |
8686
ssh-keygen -f /tmp/ssh-keys/ssh_host_key_ecdsa -N "" -t ecdsa -C "" && \
8787
ssh-keygen -f /tmp/ssh-keys/ssh_host_key_ed25519 -N "" -t ed25519 -C "" && \
88-
ssh-keygen -f /tmp/ssh-keys/ssh_host_key_rsa -N "" -t rsa -C ""
88+
ssh-keygen -f /tmp/ssh-keys/ssh_host_key_rsa -N "" -t rsa -C "" && \
89+
ssh-keygen -f /tmp/ssh-keys/{{ .Values.webssh.masterKey.name }} -N "" -t {{ .Values.webssh.masterKey.type }} -C {{ .Values.webssh.masterKey.comment | quote }}
90+
securityContext:
91+
{{- toYaml .Values.securityContexts.hookCreateSecret | nindent 12 }}
92+
resources:
93+
{{- toYaml .Values.resources.hookCreateSecret | nindent 12 }}
94+
volumeMounts:
95+
- name: ssh-keys
96+
mountPath: /tmp/ssh-keys
97+
- name: kubectl-webbastion
98+
image: {{ .Values.sshKeysSecret.kubectlImage }}
99+
command:
100+
- kubectl
101+
args:
102+
- create
103+
- secret
104+
- generic
105+
- {{ .Values.webssh.masterKey.secretName }}
106+
- --namespace={{ .Release.Namespace }}
107+
- --from-file=/tmp/ssh-keys/{{ .Values.webssh.masterKey.name }}
108+
- --from-file=/tmp/ssh-keys/{{ .Values.webssh.masterKey.name }}.pub
89109
securityContext:
90110
{{- toYaml .Values.securityContexts.hookCreateSecret | nindent 12 }}
91111
resources:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{{- if .Values.webssh.ingress.enabled }}
2+
apiVersion: networking.k8s.io/v1
3+
kind: Ingress
4+
metadata:
5+
name: {{ include "bastion-operator.fullname" . }}-webssh
6+
labels:
7+
{{- include "webssh.labels" . | nindent 4 }}
8+
{{- with .Values.webssh.ingress.annotations }}
9+
annotations:
10+
{{- toYaml . | nindent 4 }}
11+
{{- end }}
12+
spec:
13+
rules:
14+
- host: {{ .Values.webssh.ingress.hostname }}
15+
http:
16+
paths:
17+
- path: {{ .Values.webssh.ingress.path }}
18+
pathType: {{ .Values.webssh.ingress.pathType }}
19+
backend:
20+
service:
21+
name: {{ include "bastion-operator.fullname" . }}-webssh
22+
port:
23+
name: webssh
24+
{{- if .Values.webssh.ingress.secret }}
25+
tls:
26+
- hosts:
27+
- {{ .Values.webssh.ingress.hostname }}
28+
secretName: {{ .Values.webssh.ingress.secret }}
29+
{{- end }}
30+
{{- end }}

operators/deploy/bastion-operator/templates/service.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,24 @@ spec:
1818
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
1919
selector:
2020
{{- include "bastion-operator.selectorLabels" . | nindent 4 }}
21+
22+
---
23+
24+
{{- if .Values.webssh.service }}
25+
apiVersion: v1
26+
kind: Service
27+
metadata:
28+
name: {{ include "bastion-operator.fullname" . }}-webssh
29+
labels:
30+
{{- include "webssh.labels" . | nindent 4 }}
31+
{{- include "bastion-operator.metricsAdditionalLabels" . | nindent 4 }}
32+
spec:
33+
type: {{ .Values.webssh.service.type }}
34+
ports:
35+
- port: {{ .Values.webssh.service.port }}
36+
targetPort: {{ .Values.webssh.service.targetPort }}
37+
protocol: TCP
38+
name: webssh
39+
selector:
40+
{{- include "webssh.selectorLabels" . | nindent 4 }}
41+
{{- end }}

0 commit comments

Comments
 (0)