Skip to content
Commits on Source (10)
......@@ -69,23 +69,6 @@ hadolint:
- Dockerfile*
- "**/Dockerfile*"
go-lint:
stage: lint
image: registry.gitlab.com/pipeline-components/go-lint:0.17.0@sha256:b090f2038c30e8116a949e69bb2bc215a7afa782dde03bf002850643bd591fd4
# TODO: resolve
allow_failure: true
script:
- golint -set_exit_status ./...
rules:
- *renovateGuard
- changes:
paths:
- .gitlab-ci.yaml
- .gitlab/lint.yaml
- pkg/**/*.go
- cmd/**/*.go
- "*.go"
golangci:
stage: lint
image: golangci/golangci-lint:v2.6.1@sha256:5f3233d598921b58acef310807332ae5e5ea98130c9ea4c97a9190837de75bca
......
......@@ -9,6 +9,28 @@
":automergeDigest",
":automergeLinters",
],
"separateMultipleMajor": true,
"separateMultipleMinor": true,
separateMultipleMajor: true,
separateMultipleMinor: true,
packageRules: [{
matchFileNames: ["go.mod", "go.sum"],
bumpVersions: [{
name: "gojump app version",
filePatterns: ["pkg/version/version.go"],
bumpType: "patch",
matchStrings: ["\nvar Version = \"(?<version>.+)\"\n"]
}],
}, {
matchFileNames: ["chart/**"],
bumpVersions: [{
name: "gojump chart version",
filePatterns: ["chart/Chart.yaml"],
bumpType: "patch",
matchStrings: ["\nversion: (?<version>.+)\n"]
}],
}],
postUpgradeTasks: {
"commands": ["make set-app-version"],
"fileFilters": ["chart/Chart.yaml", "chart/values.yaml", "pkg/version/version.go"],
"executionMode": "update",
},
}
......@@ -18,5 +18,6 @@ ignore:
- chart/templates/15-secret-host-keys.yaml
- chart/templates/30-service.yaml
- chart/templates/05-sa.yaml
- chart/templates/15-cm-trusted-user-ca-keys.yaml
# ex: ft=yaml et ts=2 sw=2 :
# Build the manager binary
FROM golang:1.24@sha256:62c7ec2059c165fee0750d3ee51429d6c167749f70dd13b7dfea2c85dfd941de AS builder
FROM golang:1.24@sha256:c3ea4172c1dd39e1c90bb36a11ef95af6d0ccbb1d7cdedbb5dd14988c324d689 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG APP_VERSION
......@@ -28,7 +28,7 @@ COPY pkg/ pkg/
RUN flags="-a -o gojump"; \
export CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH}; \
if [ -n "${APP_VERSION}" ]; then \
go build $flags "-ldflags=-X $IMPORT_PATH/cmd.Version=$APP_VERSION" cmd/main.go; \
go build $flags "-ldflags=-X $IMPORT_PATH/pkg/version.Version=$APP_VERSION" cmd/main.go; \
else \
go build $flags cmd/main.go; \
fi
......
......@@ -89,7 +89,7 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
.PHONY: build
build: fmt vet
go build -o bin/gojump "-ldflags=-X $(IMPORT_PATH)/cmd.Version=$(VERSION)" cmd/main.go
go build -o bin/gojump "-ldflags=-X $(IMPORT_PATH)/pkg/version.Version=$(VERSION)" cmd/main.go
.PHONY: run
run: fmt vet
......@@ -155,3 +155,16 @@ kubelint:
make --silent render \
| awk -v o=/dev/stderr '/^(apiVersion:|---)/ { o="/dev/stdout" } { print >o }' \
| kube-linter lint --fail-if-no-objects-found -
# needed by renovate's postUpgradeTask
.PHONY: set-app-version
set-app-version:
IMG_TAG="$$(sed -n '/^image:/,/^$$/ s/^\s\+tag:\s\+v\?\([^@[:space:]]\+\)/\1/p' chart/values.yaml)"; \
[ -n "$$IMG_TAG" ] || exit 1; \
APP_VER="$$(sed -n 's/^var Version = "v\?\([^"]\+\)"$$/\1/p' pkg/version/version.go)"; \
[ -n "$$APP_VER" ] || exit 1; \
LATEST="$$(printf '%s\n%s\n' $$IMG_TAG $$APP_VER | sort -V | tail -n 1)"; \
sed -i "s/^\(appVersion:\)\s\+.*/\1 $${IMG_TAG}/" chart/Chart.yaml && \
if [[ "$${LATEST}" == "$${IMG_TAG}" ]]; then \
sed -i 's/^\(var Version = \)"[^"]\+/\1"v'"$${LATEST}"'/' pkg/version/version.go; \
fi
......@@ -3,8 +3,8 @@ apiVersion: v2
name: gojump
description: SSH jumphost
type: application
version: 0.1.7
appVersion: 0.1.6
version: 0.2.0
appVersion: 0.2.0
maintainers:
- name: Michal Minář
email: michal.minar@id.ethz.ch
......
{{- if or (index (default (dict) .Values.trustedUserCAKeys) "value")
.Values.authorizedKeys.keys
}}
{{- if .Values.authorizedKeys.keys }}
---
apiVersion: v1
kind: ConfigMap
......@@ -13,12 +11,8 @@ metadata:
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 }}
{{- range $user, $keys := .Values.authorizedKeys.keys }}
"{{ $user }}": |
{{ join "\n" $keys | indent 4 }}
{{- end }}
{{- end }}
{{- if index (default (dict) .Values.trustedUserCAKeys) "value" }}
---
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/component: gojump
{{ include "common.labels" . | indent 4 }}
annotations:
{{ include "common.annotations" . | indent 4 }}
name: "{{ include "_helpers.fullname" . }}-trusted-user-ca-keys"
namespace: "{{ .Release.Namespace }}"
data:
{{- if .Values.trustedUserCAKeys.value }}
trusted-user-ca-keys: |
{{ .Values.trustedUserCAKeys.value | indent 4 }}
{{- end }}
{{- end }}
......@@ -25,6 +25,10 @@ spec:
{{ .Values.annotations | toYaml | indent 8 }}
{{- end }}
{{ include "common.annotations" . | indent 8 }}
{{- if index (default (dict) .Values.trustedUserCAKeys) "value" }}
checksum.configmap.hpc.ethz.ch/trusted-user-ca-keys: >-
{{ include (print $.Template.BasePath "/15-cm-trusted-user-ca-keys.yaml") . | sha256sum }}
{{- end }}
labels:
app.kubernetes.io/component: gojump
{{ include "common.labels" . | indent 8 }}
......@@ -76,7 +80,7 @@ spec:
{{- 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
- --authorized-keys-dir=/etc/gojump/authorized-keys
{{- end }}
{{- end }}
{{- range $_, $arg := .Values.extraArgs | default (list) }}
......@@ -155,7 +159,6 @@ spec:
- 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 }}
......
......@@ -11,7 +11,7 @@ commonAnnotations:
image:
fqin: registry.ethz.ch/hpc-public/gojump
# renovate: datasource=docker depName=registry.ethz.ch/hpc-public/gojump versioning=docker
tag: v0.1.6@sha256:3d734079a8ea0e2f6fb0708d07f7a31d9c2f43684d132365554cc6a371743688
tag: v0.1.7@sha256:28ffdfabbe674fc9c7c6b79def8e0ace1a40580603f815de1d2832051492324c
# digest: sha256:XYZ***
resources:
......@@ -45,12 +45,15 @@ trustedUserCAKeys: {}
authorizedKeys: {}
### uncomment either keys or secret.name
# keys:
# - ssh-ed25519 AAAA...
# user1:
# - ssh-ed25519 AAAA...
# user2:
# - ssh-ed25519 AAAA...
# ## use pre-created secret
# ## each secret key is a username and its vault is the new-line separated list of
# ## user's authorized keys
# 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:
......
......@@ -34,10 +34,9 @@ import (
"gitlab.ethz.ch/hpc-public/gojump/pkg/health"
"gitlab.ethz.ch/hpc-public/gojump/pkg/hostkeys"
"gitlab.ethz.ch/hpc-public/gojump/pkg/sshserver"
"gitlab.ethz.ch/hpc-public/gojump/pkg/version"
)
var Version = "v0.1.6"
type arrayFlags []string
func (i *arrayFlags) String() string {
......@@ -59,7 +58,7 @@ func (i *arrayFlags) Set(values string) error {
func main() {
var listenAddr string
var hostKeyPaths arrayFlags
var authorizedKeysPaths arrayFlags
var authorizedKeysDir string
var trustedUserCAPaths arrayFlags
var maxSessionDuration time.Duration
var showVersion bool
......@@ -77,11 +76,13 @@ func main() {
"Path to trusted user CA keys file.",
"Can be given multiple times.",
}, " "))
flag.Var(&authorizedKeysPaths, "authorized-keys",
flag.StringVar(&authorizedKeysDir, "authorized-keys-dir", "",
strings.Join([]string{
"Path to SSH authorized keys file.",
"Path to SSH authorized keys directory.",
"Contains SSH authorized keys in files named ${username} or",
" in arbitrarily named files under ${username}/ directory at depth 1.",
"Each file can contain multiple SSH pub keys separated by new-line.",
"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")
......@@ -96,7 +97,7 @@ func main() {
log := zapr.NewLogger(zl)
if showVersion {
fmt.Printf("gojump %s\n", Version)
fmt.Printf("gojump %s\n", version.Version)
os.Exit(0)
}
......@@ -113,10 +114,17 @@ func main() {
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)
if authorizedKeysDir != "" {
info, err := os.Stat(authorizedKeysDir)
if err != nil {
log.Error(err, "failed to read authorized-keys-dir")
os.Exit(1)
}
if !info.IsDir() {
log.Error(fmt.Errorf("%q is not a directory", authorizedKeysDir), "authorized-keys-dir is not a directory")
os.Exit(1)
}
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
......@@ -128,7 +136,7 @@ func main() {
listenAddr,
hostKeys,
trustedUserCAKeys,
authorizedKeys,
authorizedKeysDir,
maxSessionDuration,
)
if err != nil {
......
......@@ -7,6 +7,7 @@ toolchain go1.24.10
require (
github.com/go-logr/logr v1.4.3
github.com/go-logr/zapr v1.3.0
github.com/hashicorp/golang-lru/v2 v2.0.7
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.43.0
)
......
......@@ -4,6 +4,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
......@@ -14,15 +16,11 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
......@@ -22,6 +22,7 @@ import (
"fmt"
"io"
"os"
"path"
"strings"
"github.com/go-logr/logr"
......@@ -95,3 +96,62 @@ func LoadAuthorizedKeys(log logr.Logger, paths ...string) ([]ssh.PublicKey, erro
}
return pubkeys, nil
}
// LoadUserAuthorizedKeys loads SSH pub keys from dir belonging to the user.
// The dir is expected to have this structure:
//
// user1
// user2/file1
// user2/file2
//
// So either all pubkeys belonging to user1 are in a single file "user1" or
// scattered in files in directory "user1/". Each file can contain multiple
// new-line-separated SSH pub keys.
func LoadUserAuthorizedKeys(log logr.Logger, user string, dir string) ([]ssh.PublicKey, error) {
var pubkeys []ssh.PublicKey
var errArr []error
p := path.Join(dir, user)
info, err := os.Stat(p)
if os.IsNotExist(err) {
log.V(1).Info("no authozized_keys found", "user", user, "location", p)
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to read authorized-keys-dir of user %q at %q: %w", user, p, err)
}
var paths []string
if info.IsDir() {
log.V(1).Info("loading keys from directory", "user", user, "dir", p)
entries, err := os.ReadDir(p)
if err != nil {
return nil, fmt.Errorf("failed to read authorized-keys-dir of user %q at %q: %w", user, p, err)
}
// Extract names
paths = make([]string, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
paths = append(paths, path.Join(dir, entry.Name()))
}
} else {
log.V(1).Info("loading keys from file", "user", user, "file path", p)
paths = []string{p}
}
for _, p := range paths {
pubs, err := LoadAuthorizedKeysFromPath(log, p)
if err != nil {
errArr = append(errArr, err)
log.Info("failed to load authorized key", "path", p, "err", err)
continue
}
pubkeys = append(pubkeys, pubs...)
}
if len(pubkeys) == 0 {
return nil, errors.Join(errArr...)
}
return pubkeys, nil
}
......@@ -28,7 +28,10 @@ import (
"time"
"github.com/go-logr/logr"
"github.com/hashicorp/golang-lru/v2/expirable"
"golang.org/x/crypto/ssh"
"gitlab.ethz.ch/hpc-public/gojump/pkg/authorizedkeys"
)
var (
......@@ -42,21 +45,27 @@ const extPermitPortForwarding = "permit-port-forwarding"
const keepAliveRequestType = "keepalive@openssh.com"
const cacheMaxAuthorizedKeys = 64
const cacheAuthorizedKeysTTL = time.Second * 30
type clientConnection struct {
net.Conn
}
type fp2keyMap map[string]ssh.PublicKey
type SSHServer struct {
log logr.Logger
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
started chan struct{}
stopped chan struct{}
log logr.Logger
listenAddr string
hostKeys []ssh.Signer
trustedUserCAKeys map[string]ssh.PublicKey
authorizedKeysDir string
authorizedKeysCache *expirable.LRU[string, fp2keyMap]
m sync.Mutex
clientConnections map[string]clientConnection
maxLifetime time.Duration
started chan struct{}
stopped chan struct{}
}
func pub2key(pub ssh.PublicKey) string {
......@@ -68,7 +77,7 @@ func NewSSHServer(
listenAddr string,
hostKeys []ssh.Signer,
trustedUserCAKeys []ssh.PublicKey,
authorizedKeys []ssh.PublicKey,
authorizedKeysDir string,
maxLifetime time.Duration,
) (*SSHServer, error) {
if len(hostKeys) == 0 {
......@@ -82,22 +91,22 @@ 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{
ss := &SSHServer{
log: log,
listenAddr: listenAddr,
hostKeys: hostKeys,
trustedUserCAKeys: caks,
authorizedKeys: aks,
authorizedKeysDir: authorizedKeysDir,
clientConnections: make(map[string]clientConnection),
maxLifetime: maxLifetime,
started: make(chan struct{}),
stopped: make(chan struct{}),
}, nil
}
if authorizedKeysDir != "" {
ss.authorizedKeysCache = expirable.NewLRU[string, fp2keyMap](
cacheMaxAuthorizedKeys, nil, cacheAuthorizedKeysTTL)
}
return ss, nil
}
func (s *SSHServer) Start(ctx context.Context) error {
......@@ -213,35 +222,60 @@ func (s *SSHServer) terminate() {
}
}
func mkCertChecker(
log logr.Logger,
trustedUserCAKeys map[string]ssh.PublicKey,
authorizedKeys map[string]ssh.PublicKey,
conn ssh.ConnMetadata,
) *ssh.CertChecker {
return &ssh.CertChecker{
func (s *SSHServer) getUserAuthorizedKeys(user string) (fp2keyMap, error) {
keys, ok := s.authorizedKeysCache.Get(user)
if ok {
s.log.V(1).Info("using cached authorized keys", "user", user)
return keys, nil
}
pubKeys, err := authorizedkeys.LoadUserAuthorizedKeys(s.log, user, s.authorizedKeysDir)
if err != nil {
return nil, err
}
aks := make(fp2keyMap)
for i := range pubKeys {
k := pubKeys[i]
aks[pub2key(k)] = k
}
s.authorizedKeysCache.Add(user, aks)
return aks, nil
}
func (s *SSHServer) mkCertChecker(conn ssh.ConnMetadata) *ssh.CertChecker {
cc := &ssh.CertChecker{
SupportedCriticalOptions: []string{
"source-address",
},
IsUserAuthority: func(auth ssh.PublicKey) bool {
log.V(1).Info("checking user authority",
s.log.V(1).Info("checking user authority",
"remote-address", conn.RemoteAddr(),
"user", conn.User(),
"fingerprint", pub2key(auth),
)
_, ok := trustedUserCAKeys[pub2key(auth)]
_, ok := s.trustedUserCAKeys[pub2key(auth)]
return ok
},
UserKeyFallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
log.V(1).Info("checking user pub key",
}
if s.authorizedKeysDir != "" {
cc.UserKeyFallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
s.log.V(1).Info("checking user pub keys",
"remote-address", conn.RemoteAddr(),
"user", conn.User(),
"fingerprint", pub2key(key),
)
authorizedKeys, err := s.getUserAuthorizedKeys(conn.User())
if err != nil {
return nil, err
}
_, ok := authorizedKeys[pub2key(key)]
if !ok {
return nil, fmt.Errorf("no matching authorized key found")
}
exts := make(map[string]string)
exts[extPermitPortForwarding] = ""
perms := &ssh.Permissions{
......@@ -249,15 +283,16 @@ func mkCertChecker(
Extensions: exts,
}
return perms, nil
},
}
}
return cc
}
func (s *SSHServer) pubkeyAuthCallback(
conn ssh.ConnMetadata,
key ssh.PublicKey,
) (*ssh.Permissions, error) {
checker := mkCertChecker(s.log, s.trustedUserCAKeys, s.authorizedKeys, conn)
checker := s.mkCertChecker(conn)
perms, err := checker.Authenticate(conn, key)
cert, ok := key.(*ssh.Certificate)
if err != nil {
......@@ -280,7 +315,7 @@ func (s *SSHServer) pubkeyAuthCallback(
"remote-address", conn.RemoteAddr(),
"user", conn.User())
}
if len(s.authorizedKeys) == 0 {
if len(s.authorizedKeysDir) == 0 {
if !ok {
return nil, fmt.Errorf("failed to convert pub key to certificate")
}
......
/*
Copyright 2024-2025 Michal Minář <michal.minar@id.ethz.ch>.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package version
var Version = "v0.2.0"