Skip to content
Commits on Source (11)
......@@ -88,7 +88,7 @@ go-lint:
golangci:
stage: lint
image: golangci/golangci-lint:v2.1.6@sha256:568ee1c1c53493575fa9494e280e579ac9ca865787bafe4df3023ae59ecf299b
image: golangci/golangci-lint:v2.3.0@sha256:67bf4b8b2d64faa4effe19cbda0c651c1c3b8099ea26ec2e09bc20a383d2daa4
script:
- golangci-lint run
rules:
......
......@@ -13,7 +13,7 @@ rules:
level: warning
ignore:
- chart/templates/15-cm-trusted-user-ca.yaml
- chart/templates/15-cm-authorized-keys.yaml
- chart/templates/20-deployment.yaml
- chart/templates/15-secret-host-keys.yaml
- chart/templates/30-service.yaml
......
# Build the manager binary
FROM golang:1.24@sha256:10c131810f80a4802c49cab0961bbe18a16f4bb2fb99ef16deaa23e4246fc817 AS builder
FROM golang:1.24@sha256:ef5b4be1f94b36c90385abd9b6b4f201723ae28e71acacb76d00687333c17282 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG APP_VERSION
......
......@@ -20,10 +20,13 @@ IMAGE_TAG_BASE ?= registry.core.hpc.ethz.ch/hpc/gojump
CHART_REGISTRY ?= registry.core.hpc.ethz.ch/charts
CHART_VERSION ?=
push-chart:
push-chart: lint-chart
helm push "$$(ls -1tr *.tgz | tail -n 1)" "oci://$(CHART_REGISTRY)"
lint-chart:
args=""; [ -z "$(CHART_VERSION)" ] || args="$$args --version $(CHART_VERSION)"; \
helm package $$args chart/
helm push "$$(ls -1tr *.tgz | tail -n 1)" "oci://$(CHART_REGISTRY)"
helm lint "$$(ls -1tr *.tgz | tail -n 1)"
# Image URL to use all building/pushing image targets
IMG ?= $(IMAGE_TAG_BASE):$(VERSION)
......
......@@ -3,8 +3,8 @@ apiVersion: v2
name: gojump
description: SSH jumphost
type: application
version: 0.1.6
appVersion: 0.1.2
version: 0.1.7
appVersion: 0.1.4
maintainers:
- name: Michal Minář
email: michal.minar@id.ethz.ch
......
{{- if index (default (dict) .Values.trustedUserCAKeys) "value" }}
{{- if or (index (default (dict) .Values.trustedUserCAKeys) "value")
.Values.authorizedKeys.keys
}}
---
apiVersion: v1
kind: ConfigMap
......@@ -8,9 +10,15 @@ metadata:
{{ include "common.labels" . | indent 4 }}
annotations:
{{ include "common.annotations" . | indent 4 }}
name: "{{ include "_helpers.fullname" . }}-trusted-user-ca-keys"
name: "{{ include "_helpers.fullname" . }}-authorized-keys"
namespace: "{{ .Release.Namespace }}"
data:
{{- if .Values.trustedUserCAKeys.value }}
trusted-user-ca-keys: |
{{ .Values.trustedUserCAKeys.value | indent 4 }}
{{- end }}
{{- if .Values.authorizedKeys.keys }}
authorized-keys: |
{{ join "\n" .Values.authorizedKeys.keys | indent 4 }}
{{- end }}
{{- end }}
......@@ -6,6 +6,9 @@ metadata:
app.kubernetes.io/component: gojump
{{ include "common.labels" . | indent 4 }}
annotations:
{{- if .Values.annotations }}
{{ .Values.annotations | toYaml | indent 4 }}
{{- end }}
{{ include "common.annotations" . | indent 4 }}
name: "{{ include "_helpers.fullname" . }}"
namespace: "{{ .Release.Namespace }}"
......@@ -18,6 +21,9 @@ spec:
metadata:
annotations:
kubectl.kubernetes.io/default-container: gojump
{{- if .Values.annotations }}
{{ .Values.annotations | toYaml | indent 8 }}
{{- end }}
{{ include "common.annotations" . | indent 8 }}
labels:
app.kubernetes.io/component: gojump
......@@ -64,10 +70,15 @@ spec:
- --listen-addr=:2022
- --log-level={{ .Values.logLevel | default "info" }}
{{- with $caKeys := index (default (dict) .Values) "trustedUserCAKeys" | default dict }}
{{- if index (default (dict) $caKeys.secret) "name" }}
{{- if or (index (default (dict) $caKeys.secret) "name") ($caKeys.value) }}
- --trusted-user-ca-keys=/etc/gojump/trusted-user-ca-keys
{{- end }}
{{- end }}
{{- with $authKeys := index (default (dict) .Values) "authorizedKeys" | default dict }}
{{- if or (index (default (dict) $authKeys.secret) "name") ($authKeys.keys) }}
- --authorized-keys=/etc/gojump/authorized-keys
{{- end }}
{{- end }}
{{- range $_, $arg := .Values.extraArgs | default (list) }}
- {{ $arg }}
{{- end }}
......@@ -139,6 +150,14 @@ spec:
subPath: {{ if $caKeys.value }}trusted-user-ca-keys{{ else }}{{ $caKeys.secret.subPath | default "trusted-user-ca-keys" }}{{ end }}
{{- end }}
{{- end }}
{{- with $authKeys := index (default (dict) .Values) "authorizedKeys" | default dict }}
{{- if or $authKeys.keys (index (default (dict) $authKeys.secret) "name") }}
- name: authorized-keys
mountPath: /etc/gojump/authorized-keys
readOnly: true
subPath: {{ if $authKeys.keys }}authorized-keys{{ else }}{{ $authKeys.secret.subPath | default "authorized-keys" }}{{ end }}
{{- end }}
{{- end }}
{{- if .Values.extraVolumeMounts }}
{{ .Values.extraVolumeMounts | toYaml | indent 12 }}
{{- end }}
......@@ -176,7 +195,21 @@ spec:
{{- else if $caKeys.value }}
- name: trusted-user-ca-keys
configMap:
name: {{ include "_helpers.fullname" $ }}-trusted-user-ca-keys
name: {{ include "_helpers.fullname" $ }}-authorized-keys
{{- end }}
{{- end }}
{{- with $authKeys := index (default (dict) .Values) "authorizedKeys" | default dict }}
{{- if and (index (default (dict) $authKeys.secret) "name") ($authKeys.keys) }}
{{ fail "authorizedKeys.secret.name and .authorizedKeys.keys are mutually exclusive!" }}
{{- end }}
{{- if index (default (dict) $authKeys.secret) "name" }}
- name: authorized-keys
secret:
secretName: {{ $authKeys.secret.name }}
{{- else if $authKeys.keys }}
- name: authorized-keys
configMap:
name: {{ include "_helpers.fullname" $ }}-authorized-keys
{{- end }}
{{- end }}
{{- if .Values.extraVolumes }}
......
......@@ -11,7 +11,7 @@ commonAnnotations:
image:
fqin: registry.ethz.ch/hpc-registry/gojump
# renovate: datasource=docker depName=registry.ethz.ch/hpc-registry/gojump versioning=docker
tag: v0.1.3@sha256:6b0ba60e789db695bc3a03b70cabf1fb016616c0a41a791536e8a8232aee6b10
tag: v0.1.4@sha256:b392747aafecfa6022ea404d664c42f525330dd39afa79f45d44ce046da0a2ea
# digest: sha256:XYZ***
resources:
......@@ -37,11 +37,20 @@ trustedUserCAKeys: {}
### uncomment either value or secret.name
# value: |
# ssh-ed25519 AAAA...
# ## user-pre-created secret
# ## use pre-created secret
# secret:
# name: trusted-user-ca-keys
# ## key inside the secret containing the CAs
# subPath: trusted-user-ca-keys
authorizedKeys: {}
### uncomment either keys or secret.name
# keys:
# - ssh-ed25519 AAAA...
# ## use pre-created secret
# secret:
# name: authorized-keys
# ## key inside the secret containing the CAs
# subPath: authorized-keys
hostKeys:
#### user-pre-created-secret containing at least one host key and
#### optionally a host certificate like this:
......@@ -63,6 +72,9 @@ serviceAccount:
service:
annotations: {}
# gojump deployment and pod annotations
annotations: {}
logLevel: info
extraEnv: []
extraArgs: []
......
......@@ -36,7 +36,7 @@ import (
"gitlab.ethz.ch/hpc-registry/gojump/pkg/sshserver"
)
var Version = "v0.1.2"
var Version = "v0.1.5"
type arrayFlags []string
......@@ -59,6 +59,7 @@ func (i *arrayFlags) Set(values string) error {
func main() {
var listenAddr string
var hostKeyPaths arrayFlags
var authorizedKeysPaths arrayFlags
var trustedUserCAPaths arrayFlags
var maxSessionDuration time.Duration
var showVersion bool
......@@ -76,6 +77,12 @@ func main() {
"Path to trusted user CA keys file.",
"Can be given multiple times.",
}, " "))
flag.Var(&authorizedKeysPaths, "authorized-keys",
strings.Join([]string{
"Path to SSH authorized keys file.",
"Use sparingly because no restrictions (like permitted source-address) will be applied to these keys.",
"Can be given multiple times.",
}, " "))
flag.DurationVar(&maxSessionDuration, "max-session-lifetime", time.Hour*24,
"Maximum lifetime of a single SSH session. A session old this amount of time will be terminated. 0 means no limit")
flag.BoolVar(&showVersion, "version", false, "Show app version and exit.")
......@@ -101,11 +108,16 @@ func main() {
log.Error(err, "failed to load host keys")
os.Exit(1)
}
authorizedKeys, err := authorizedkeys.LoadAuthorizedKeys(log, []string(trustedUserCAPaths)...)
trustedUserCAKeys, err := authorizedkeys.LoadAuthorizedKeys(log, []string(trustedUserCAPaths)...)
if err != nil {
log.Error(err, "failed to load trusted user CAs")
os.Exit(1)
}
authorizedKeys, err := authorizedkeys.LoadAuthorizedKeys(log, []string(authorizedKeysPaths)...)
if err != nil {
log.Error(err, "failed to load SSH authorized keys")
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
......@@ -115,6 +127,7 @@ func main() {
ssLog,
listenAddr,
hostKeys,
trustedUserCAKeys,
authorizedKeys,
maxSessionDuration,
)
......
......@@ -2,17 +2,17 @@ module gitlab.ethz.ch/hpc-registry/gojump
go 1.23.0
toolchain go1.24.4
toolchain go1.24.5
require (
github.com/go-logr/logr v1.4.3
github.com/go-logr/zapr v1.3.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.39.0
golang.org/x/crypto v0.40.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/sys v0.34.0 // indirect
)
......@@ -51,6 +51,7 @@ type SSHServer struct {
listenAddr string
hostKeys []ssh.Signer
trustedUserCAKeys map[string]ssh.PublicKey
authorizedKeys map[string]ssh.PublicKey
m sync.Mutex
clientConnections map[string]clientConnection
maxLifetime time.Duration
......@@ -67,6 +68,7 @@ func NewSSHServer(
listenAddr string,
hostKeys []ssh.Signer,
trustedUserCAKeys []ssh.PublicKey,
authorizedKeys []ssh.PublicKey,
maxLifetime time.Duration,
) (*SSHServer, error) {
if len(hostKeys) == 0 {
......@@ -80,11 +82,17 @@ func NewSSHServer(
k := trustedUserCAKeys[i]
caks[pub2key(k)] = k
}
aks := make(map[string]ssh.PublicKey)
for i := range authorizedKeys {
k := authorizedKeys[i]
aks[pub2key(k)] = k
}
return &SSHServer{
log: log,
listenAddr: listenAddr,
hostKeys: hostKeys,
trustedUserCAKeys: caks,
authorizedKeys: aks,
clientConnections: make(map[string]clientConnection),
maxLifetime: maxLifetime,
started: make(chan struct{}),
......@@ -205,30 +213,59 @@ func (s *SSHServer) terminate() {
}
}
func (s *SSHServer) pubkeyAuthCallback(
func mkCertChecker(
log logr.Logger,
trustedUserCAKeys map[string]ssh.PublicKey,
authorizedKeys map[string]ssh.PublicKey,
conn ssh.ConnMetadata,
key ssh.PublicKey,
) (*ssh.Permissions, error) {
checker := &ssh.CertChecker{
) *ssh.CertChecker {
return &ssh.CertChecker{
SupportedCriticalOptions: []string{
"source-address",
},
IsUserAuthority: func(auth ssh.PublicKey) bool {
s.log.V(1).Info("checking user authority",
log.V(1).Info("checking user authority",
"remote-address", conn.RemoteAddr(),
"user", conn.User(),
"fingerprint", pub2key(auth),
)
_, ok := s.trustedUserCAKeys[pub2key(auth)]
_, ok := trustedUserCAKeys[pub2key(auth)]
return ok
},
UserKeyFallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
log.V(1).Info("checking user pub key",
"remote-address", conn.RemoteAddr(),
"user", conn.User(),
"fingerprint", pub2key(key),
)
_, ok := authorizedKeys[pub2key(key)]
if !ok {
return false
return nil, fmt.Errorf("no matching authorized key found")
}
return ok
exts := make(map[string]string)
exts[extPermitPortForwarding] = ""
perms := &ssh.Permissions{
CriticalOptions: make(map[string]string),
Extensions: exts,
}
return perms, nil
},
}
}
func (s *SSHServer) pubkeyAuthCallback(
conn ssh.ConnMetadata,
key ssh.PublicKey,
) (*ssh.Permissions, error) {
checker := mkCertChecker(s.log, s.trustedUserCAKeys, s.authorizedKeys, conn)
perms, err := checker.Authenticate(conn, key)
cert, ok := key.(*ssh.Certificate)
if err != nil {
s.log.Info("user certificate failed checks",
msg := "user certificate failed checks"
if !ok {
msg = "user pub key failed checks"
}
s.log.Info(msg,
"remote-address", conn.RemoteAddr(),
"user", conn.User(),
"error", err)
......@@ -238,12 +275,13 @@ func (s *SSHServer) pubkeyAuthCallback(
"user", conn.User(),
"perms", perms)
}
cert, ok := key.(*ssh.Certificate)
if !ok {
return nil, fmt.Errorf("failed to convert pub key to certificate")
}
if _, ok = cert.Extensions[extPermitPortForwarding]; !ok {
return nil, fmt.Errorf("missing certificate extension %q", extPermitPortForwarding)
if len(s.authorizedKeys) == 0 {
if !ok {
return nil, fmt.Errorf("failed to convert pub key to certificate")
}
if _, ok = cert.Extensions[extPermitPortForwarding]; !ok {
return nil, fmt.Errorf("missing certificate extension %q", extPermitPortForwarding)
}
}
return perms, err
}
......