diff --git a/.devcontainer.json b/.devcontainer.json index 2edcf7b6..45ce5fac 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -9,7 +9,8 @@ "features": { "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, "ghcr.io/devcontainers/features/go:1" : {}, - "ghcr.io/guiyomh/features/golangci-lint:0": {} + "ghcr.io/guiyomh/features/golangci-lint:0": {}, + "ghcr.io/devcontainers-contrib/features/kubectl-asdf:2": {} }, "customizations": { "vscode": { diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 98cae6f9..049801a4 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -49,6 +49,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - ../plugin/kubernetes/crd.yaml:/kubernetes/crd.yaml:ro - ./k8sworkload.yaml:/kubernetes/workload.yaml:ro + - kubeconfig:/root/.kube # networks: # - kind # - default @@ -56,19 +57,17 @@ services: - bash - -c - | - # kind delete cluster -n sshpipertest (kind get kubeconfig -q -n sshpipertest || kind create cluster -n sshpipertest) docker network connect kind $$(hostname) # self contain docker network connect e2e_default sshpipertest-control-plane kind export kubeconfig -n sshpipertest --internal - #kubectl delete po -l k8s-app=kube-dns -A kind load docker-image -n sshpipertest e2e_piper-imageonly kubectl wait --for=condition=ready pod -A --all --timeout=2m - kubectl delete -f /kubernetes/crd.yaml - kubectl delete -f /kubernetes/workload.yaml + kubectl delete -f /kubernetes/crd.yaml --force + kubectl delete -f /kubernetes/workload.yaml --force + kubectl wait --for=delete pod --all --timeout=2m # ensure no leftover kubectl apply -f /kubernetes/crd.yaml kubectl apply -f /kubernetes/workload.yaml - #kubectl set image deployment/sshpiper-deployment sshpiper=e2e_piper-imageonly kubectl wait deployment --all --for condition=Available=True kubectl port-forward service/sshpiper --pod-running-timeout=2m --address 0.0.0.0 2222:2222 & kubectl logs -f deployment/sshpiper-deployment @@ -94,6 +93,7 @@ services: - sshconfig_publickey:/sshconfig_publickey - sshconfig_password:/sshconfig_password - /var/run/docker.sock:/var/run/docker.sock + - kubeconfig:/root/.kube:ro command: ["./e2eentry.sh"] privileged: true working_dir: /src/e2e @@ -119,5 +119,7 @@ volumes: sshconfig_password: + kubeconfig: + networks: netdistract: \ No newline at end of file diff --git a/e2e/e2eentry.sh b/e2e/e2eentry.sh index 5f1c7aa3..0bf16299 100755 --- a/e2e/e2eentry.sh +++ b/e2e/e2eentry.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x # use entrypoint.sh to generate the ssh_host_ed25519_key PLUGIN="dummy_badname/" bash /sshpiperd/entrypoint.sh 2>/dev/null diff --git a/e2e/k8sworkload.yaml b/e2e/k8sworkload.yaml index c95d7f77..c6e34dea 100644 --- a/e2e/k8sworkload.yaml +++ b/e2e/k8sworkload.yaml @@ -53,7 +53,10 @@ spec: volumeMounts: - name: sshpiper-server-key mountPath: "/serverkey/" - readOnly: true + readOnly: true + - name: sshpiper-files + mountPath: "/files/" + readOnly: true volumes: - name: sshpiper-server-key secret: @@ -61,6 +64,20 @@ spec: items: - key: server_key path: ssh_host_ed25519_key + - name: sshpiper-files + configMap: + name: sshpiper-files +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: sshpiper-files +data: + # htpasswd -nb htpwdfile htpasswordfile + htpasswd: | + htpwdfile:$apr1$qOeWE6f6$39xTQwVbAT88CMJp234wY/ + authorized_keys: | + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINRGTH325rDUp12tplwukHmR8ytbC9TPZ886gCstynP1 --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -163,3 +180,95 @@ spec: private_key_secret: name: host-publickey-key ignore_hostkey: true + +--- +apiVersion: sshpiper.com/v1beta1 +kind: Pipe +metadata: + name: pipe-publickey-authorized-file +spec: + from: + - username: "authorizedfile" + authorized_keys_file: /files/authorized_keys + to: + host: host-publickey:2222 + username: "user" + private_key_secret: + name: host-publickey-key + ignore_hostkey: true + +# htpasswd pipe to a password based sshd +--- +apiVersion: v1 +stringData: + password: pass +kind: Secret +metadata: + name: host-pass-secret +type: Opaque +--- +apiVersion: sshpiper.com/v1beta1 +kind: Pipe +metadata: + name: pipe-htpwd +spec: + from: + - username: "htpwd" + # htpasswd -nb htpwd htpassword + htpasswd_data: "htpwd:$apr1$FazBEvaq$BZWaLi8Q1efsWNTKcuUgN1" + to: + host: host-password:2222 + username: "user" + password_secret: + name: host-pass-secret + ignore_hostkey: true +--- +apiVersion: sshpiper.com/v1beta1 +kind: Pipe +metadata: + name: pipe-htpwd-file +spec: + from: + - username: "htpwdfile" + htpasswd_file: /files/htpasswd + to: + host: host-password:2222 + username: "user" + password_secret: + name: host-pass-secret + ignore_hostkey: true +--- + +# private key with ca +--- +apiVersion: v1 +stringData: + ssh-privatekey: | + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + QyNTUxOQAAACDURkx99uaw1KddraZcLpB5kfMrWwvUz2fPOoArLcpz9QAAAJC+j0+Svo9P + kgAAAAtzc2gtZWQyNTUxOQAAACDURkx99uaw1KddraZcLpB5kfMrWwvUz2fPOoArLcpz9Q + AAAEDcQgdh2z2r/6blq0ziJ1l6s6IAX8C+9QHfAH931cHNO9RGTH325rDUp12tplwukHmR + 8ytbC9TPZ886gCstynP1AAAADWJvbGlhbkB1YnVudHU= + -----END OPENSSH PRIVATE KEY----- + ssh-publickey: | + ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIMIr7E6Il5fvTpixHHDCszKhlRbzXrB0fnWtxYAYdyVlAAAAINRGTH325rDUp12tplwukHmR8ytbC9TPZ886gCstynP1AAAAAAAAAAAAAAABAAAAC2s4c2UyZV90ZXN0AAAADgAAAApjbGllbnRfMTIzAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIJrzCeQ3a+m8PnA/KUmK+JAfC1tM7SG2gkGmW29nXj1nAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDICm0JT8fakTkWTRjDtbjjMx0dUrvGvPb+Qzi2592/39CqBj8cEqZAF/DesCJhKmO+hyN4jDxTcKPhPM0y2/sH +kind: Secret +metadata: + name: host-publickey-key-ca +type: Opaque +--- +apiVersion: sshpiper.com/v1beta1 +kind: Pipe +metadata: + name: pipe-publickey-ca +spec: + from: + - username: "hostcapublickey" + authorized_keys_file: /files/authorized_keys + to: + host: host-capublickey:2222 + username: "client_123" + private_key_secret: + name: host-publickey-key-ca + ignore_hostkey: true \ No newline at end of file diff --git a/e2e/kubernetes_test.go b/e2e/kubernetes_test.go index 1f03b6e1..db8f3f31 100644 --- a/e2e/kubernetes_test.go +++ b/e2e/kubernetes_test.go @@ -16,131 +16,133 @@ func TestKubernetes(t *testing.T) { piperaddr := piperhost + ":" + piperport waitForEndpointReadyWithTimeout(piperaddr, time.Minute*5) - t.Run("password", func(t *testing.T) { - randtext := uuid.New().String() - targetfie := uuid.New().String() + pubkeycases := []struct { + title string + user string + }{ + { + title: "key_pubkey_cacthall", + user: "anyuser", + }, + { + title: "key_custom_field", + user: "custom_field", + }, + { + title: "key_authorizedfile", + user: "authorizedfile", + }, + { + title: "key_public_ca", + user: "hostcapublickey", + }, + } - c, stdin, stdout, err := runCmd( - "ssh", - "-v", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - "-p", - piperport, - "-l", - "pass", - piperhost, - fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie), - ) + for _, testcase := range pubkeycases { + t.Run(testcase.title, func(t *testing.T) { - if err != nil { - t.Errorf("failed to ssh to piper-fixed, %v", err) - } + keyfiledir, err := os.MkdirTemp("", "") + if err != nil { + t.Errorf("failed to create temp key file: %v", err) + } - defer killCmd(c) + keyfile := path.Join(keyfiledir, "key") - enterPassword(stdin, stdout, "pass") + if err := os.WriteFile(keyfile, []byte(testprivatekey), 0400); err != nil { + t.Errorf("failed to write to test key: %v", err) + } - time.Sleep(time.Second) // wait for file flush + if err := os.WriteFile("/sshconfig_publickey/.ssh/authorized_keys", []byte(`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINRGTH325rDUp12tplwukHmR8ytbC9TPZ886gCstynP1`), 0400); err != nil { + t.Errorf("failed to write to authorized_keys: %v", err) + } - checkSharedFileContent(t, targetfie, randtext) - }) + randtext := uuid.New().String() + targetfie := uuid.New().String() - t.Run("key", func(t *testing.T) { + c, _, _, err := runCmd( + "ssh", + "-v", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-p", + piperport, + "-l", + testcase.user, + "-i", + keyfile, + piperhost, + fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie), + ) - keyfiledir, err := os.MkdirTemp("", "") - if err != nil { - t.Errorf("failed to create temp key file: %v", err) - } + if err != nil { + t.Errorf("failed to ssh to piper-fixed, %v", err) + } - keyfile := path.Join(keyfiledir, "key") + defer killCmd(c) - if err := os.WriteFile(keyfile, []byte(testprivatekey), 0400); err != nil { - t.Errorf("failed to write to test key: %v", err) - } + time.Sleep(time.Second) // wait for file flush - if err := os.WriteFile("/sshconfig_publickey/.ssh/authorized_keys", []byte(`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINRGTH325rDUp12tplwukHmR8ytbC9TPZ886gCstynP1`), 0400); err != nil { - t.Errorf("failed to write to authorized_keys: %v", err) - } + checkSharedFileContent(t, targetfie, randtext) + }) + } - randtext := uuid.New().String() - targetfie := uuid.New().String() + passwordcases := []struct { + title string + user string + password string + }{ + { + title: "password", + user: "pass", + password: "pass", + }, + { + title: "password_htpwd", + user: "htpwd", + password: "htpassword", + }, + { + title: "password_htpasswd_file", + user: "htpwdfile", + password: "htpasswordfile", + }, + } - c, _, _, err := runCmd( - "ssh", - "-v", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - "-p", - piperport, - "-l", - "anyuser", - "-i", - keyfile, - piperhost, - fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie), - ) + for _, testcase := range passwordcases { - if err != nil { - t.Errorf("failed to ssh to piper-fixed, %v", err) - } + t.Run(testcase.title, func(t *testing.T) { + randtext := uuid.New().String() + targetfie := uuid.New().String() - defer killCmd(c) + c, stdin, stdout, err := runCmd( + "ssh", + "-v", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-p", + piperport, + "-l", + testcase.user, + piperhost, + fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie), + ) - time.Sleep(time.Second) // wait for file flush + if err != nil { + t.Errorf("failed to ssh to piper-fixed, %v", err) + } - checkSharedFileContent(t, targetfie, randtext) - }) + defer killCmd(c) - t.Run("key_custom_field", func(t *testing.T) { + enterPassword(stdin, stdout, testcase.password) - keyfiledir, err := os.MkdirTemp("", "") - if err != nil { - t.Errorf("failed to create temp key file: %v", err) - } + time.Sleep(time.Second) // wait for file flush - keyfile := path.Join(keyfiledir, "key") + checkSharedFileContent(t, targetfie, randtext) + }) - if err := os.WriteFile(keyfile, []byte(testprivatekey), 0400); err != nil { - t.Errorf("failed to write to test key: %v", err) - } - - if err := os.WriteFile("/sshconfig_publickey/.ssh/authorized_keys", []byte(`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINRGTH325rDUp12tplwukHmR8ytbC9TPZ886gCstynP1`), 0400); err != nil { - t.Errorf("failed to write to authorized_keys: %v", err) - } - - randtext := uuid.New().String() - targetfie := uuid.New().String() - - c, _, _, err := runCmd( - "ssh", - "-v", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - "-p", - piperport, - "-l", - "custom_field", - "-i", - keyfile, - piperhost, - fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie), - ) - - if err != nil { - t.Errorf("failed to ssh to piper-fixed, %v", err) - } - - defer killCmd(c) - - time.Sleep(time.Second) // wait for file flush - - checkSharedFileContent(t, targetfie, randtext) - }) + } } diff --git a/e2e/kubetools/Dockerfile b/e2e/kubetools/Dockerfile index 5248c6ea..5e1f439d 100644 --- a/e2e/kubetools/Dockerfile +++ b/e2e/kubetools/Dockerfile @@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y \ gnupg \ lsb-release -RUN curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.16.0/kind-linux-amd64 && \ +RUN curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64 && \ chmod +x ./kind && \ mv ./kind /bin/kind diff --git a/e2e/main_test.go b/e2e/main_test.go index b9d52da3..912e5342 100644 --- a/e2e/main_test.go +++ b/e2e/main_test.go @@ -39,8 +39,9 @@ func waitForEndpointReady(addr string) { func waitForEndpointReadyWithTimeout(addr string, timeout time.Duration) { now := time.Now() + timeout = max(timeout, waitTimeout) for { - if time.Since(now) > waitTimeout { + if time.Since(now) > timeout { log.Panic("timeout waiting for endpoint " + addr) } diff --git a/go.mod b/go.mod index f2810202..5cafd2ed 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tg123/sshpiper -go 1.20 +go 1.21 replace golang.org/x/crypto => ./crypto @@ -15,6 +15,7 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pires/go-proxyproto v0.6.2 github.com/sirupsen/logrus v1.9.3 + github.com/tg123/go-htpasswd v1.2.1 github.com/tg123/jobobject v0.1.0 github.com/tg123/remotesigner v0.0.1 github.com/urfave/cli/v2 v2.25.7 @@ -29,6 +30,7 @@ require ( ) require ( + github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.0.0 // indirect diff --git a/go.sum b/go.sum index 23cdffbe..dfecbedb 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= +github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= @@ -24,6 +26,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= @@ -50,6 +53,7 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= @@ -70,6 +74,7 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= @@ -83,6 +88,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -121,7 +127,9 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= +github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= @@ -140,6 +148,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -159,6 +168,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tg123/go-htpasswd v1.2.1 h1:i4wfsX1KvvkyoMiHZzjS0VzbAPWfxzI8INcZAKtutoU= +github.com/tg123/go-htpasswd v1.2.1/go.mod h1:erHp1B86KXdwQf1X5ZrLb7erXZnWueEQezb2dql4q58= github.com/tg123/jobobject v0.1.0 h1:deOWVH+SvsnFtT/M+HFhtZ7t9GMYPzYMvvF25IIMRRE= github.com/tg123/jobobject v0.1.0/go.mod h1:TtbMLKdmTPY6eMo4aqDXiQWv0yTfAgDt1mxv6J9pM8o= github.com/tg123/remotesigner v0.0.1 h1:fFCKgpBOv0nfpWOuuWMke9vECKQu6PTrm4iktDwwMMM= diff --git a/plugin/kubernetes/apis/sshpiper/v1beta1/types.go b/plugin/kubernetes/apis/sshpiper/v1beta1/types.go index 20d4e7f3..80ae658d 100644 --- a/plugin/kubernetes/apis/sshpiper/v1beta1/types.go +++ b/plugin/kubernetes/apis/sshpiper/v1beta1/types.go @@ -23,12 +23,16 @@ type FromSpec struct { Username string `json:"username"` UsernameRegexMatch bool `json:"username_regex_match,omitempty"` AuthorizedKeysData string `json:"authorized_keys_data,omitempty"` + HtpasswdData string `json:"htpasswd_data,omitempty"` + AuthorizedKeysFile string `json:"authorized_keys_file,omitempty"` + HtpasswdFile string `json:"htpasswd_file,omitempty"` } type ToSpec struct { Username string `json:"username,omitempty"` Host string `json:"host"` PrivateKeySecret corev1.LocalObjectReference `json:"private_key_secret,omitempty"` + PasswordSecret corev1.LocalObjectReference `json:"password_secret,omitempty"` KnownHostsData string `json:"known_hosts_data,omitempty"` IgnoreHostkey bool `json:"ignore_hostkey,omitempty"` } diff --git a/plugin/kubernetes/crd.yaml b/plugin/kubernetes/crd.yaml index 1b9f62e4..098d9c05 100644 --- a/plugin/kubernetes/crd.yaml +++ b/plugin/kubernetes/crd.yaml @@ -34,8 +34,14 @@ spec: properties: authorized_keys_data: type: string + authorized_keys_file: + type: string username: type: string + htpasswd_data: + type: string + htpasswd_file: + type: string username_regex_match: type: boolean required: @@ -59,6 +65,15 @@ spec: TODO: Add other useful fields. apiVersion, kind, uid?' type: string type: object + password_secret: + description: LocalObjectReference contains enough information + to let you locate the referenced object inside the same namespace. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object username: type: string required: diff --git a/plugin/kubernetes/generated/clientset/versioned/doc.go b/plugin/kubernetes/generated/clientset/versioned/doc.go deleted file mode 100644 index 0e0c2a89..00000000 --- a/plugin/kubernetes/generated/clientset/versioned/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -// This package has the automatically generated clientset. -package versioned diff --git a/plugin/kubernetes/generated/clientset/versioned/typed/sshpiper/v1beta1/fake/fake_pipe.go b/plugin/kubernetes/generated/clientset/versioned/typed/sshpiper/v1beta1/fake/fake_pipe.go index 740af8ec..d9611698 100644 --- a/plugin/kubernetes/generated/clientset/versioned/typed/sshpiper/v1beta1/fake/fake_pipe.go +++ b/plugin/kubernetes/generated/clientset/versioned/typed/sshpiper/v1beta1/fake/fake_pipe.go @@ -8,7 +8,6 @@ import ( v1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" labels "k8s.io/apimachinery/pkg/labels" - schema "k8s.io/apimachinery/pkg/runtime/schema" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" testing "k8s.io/client-go/testing" @@ -20,9 +19,9 @@ type FakePipes struct { ns string } -var pipesResource = schema.GroupVersionResource{Group: "sshpiper", Version: "v1beta1", Resource: "pipes"} +var pipesResource = v1beta1.SchemeGroupVersion.WithResource("pipes") -var pipesKind = schema.GroupVersionKind{Group: "sshpiper", Version: "v1beta1", Kind: "Pipe"} +var pipesKind = v1beta1.SchemeGroupVersion.WithKind("Pipe") // Get takes name of the pipe, and returns the corresponding pipe object, and an error if there is any. func (c *FakePipes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Pipe, err error) { diff --git a/plugin/kubernetes/kubernetes.go b/plugin/kubernetes/kubernetes.go index 3b25f89e..a934a939 100644 --- a/plugin/kubernetes/kubernetes.go +++ b/plugin/kubernetes/kubernetes.go @@ -5,10 +5,13 @@ import ( "context" "encoding/base64" "fmt" + "os" "regexp" "time" gocache "github.com/patrickmn/go-cache" + log "github.com/sirupsen/logrus" + "github.com/tg123/go-htpasswd" "github.com/tg123/sshpiper/libplugin" piperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1" sshpiper "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned" @@ -93,11 +96,15 @@ func (p *plugin) supportedMethods() ([]string, error) { for _, pipe := range pipes { for _, from := range pipe.Spec.From { - if from.AuthorizedKeysData != "" { + if from.AuthorizedKeysData != "" || from.AuthorizedKeysFile != "" { set["publickey"] = true // found authorized_keys, so we support publickey } else { set["password"] = true // no authorized_keys, so we support password } + + if from.HtpasswdData != "" || from.HtpasswdFile != "" { + set["password"] = true // found htpasswd, so we support password + } } } @@ -140,30 +147,92 @@ func (p *plugin) createUpstream(conn libplugin.ConnMetadata, pipe *piperv1beta1. IgnoreHostKey: to.IgnoreHostkey, } - if originPassword != "" { + if to.PrivateKeySecret.Name != "" { + log.Debugf("mapping to %v private key using secret %v", to.Host, to.PrivateKeySecret.Name) + secret, err := p.k8sclient.Secrets(pipe.Namespace).Get(context.Background(), to.PrivateKeySecret.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + anno := pipe.GetAnnotations() + var publicKey []byte + var privateKey []byte + + for _, k := range []string{"ssh-privatekey", "privatekey", anno["privatekey_field_name"]} { + data := secret.Data[k] + if data != nil { + log.Debugf("found private key in secret %v/%v", to.PrivateKeySecret.Name, k) + privateKey = data + break + } + } + + for _, k := range []string{"ssh-publickey", "publickey", anno["publickey_field_name"]} { + data := secret.Data[k] + if data != nil { + log.Debugf("found publickey key in secret %v/%v", to.PrivateKeySecret.Name, k) + publicKey = data + break + } + } + + if privateKey != nil { + u.Auth = libplugin.CreatePrivateKeyAuth(privateKey, publicKey) + p.cache.Set(conn.UniqueID(), pipe, gocache.DefaultExpiration) + return u, nil + } + } else if to.PasswordSecret.Name != "" { + log.Debugf("mapping to %v password using secret %v", to.Host, to.PasswordSecret.Name) + secret, err := p.k8sclient.Secrets(pipe.Namespace).Get(context.Background(), to.PasswordSecret.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + anno := pipe.GetAnnotations() + for _, k := range []string{"password", anno["password_field_name"]} { + data := secret.Data[k] + if data != nil { + log.Debugf("found password in secret %v/%v", to.PasswordSecret.Name, k) + u.Auth = libplugin.CreatePasswordAuth(data) + p.cache.Set(conn.UniqueID(), pipe, gocache.DefaultExpiration) + return u, nil + } + } + } else if originPassword != "" { + log.Debugf("mapping to %v using user input password", to.Host) u.Auth = libplugin.CreatePasswordAuth([]byte(originPassword)) p.cache.Set(conn.UniqueID(), pipe, gocache.DefaultExpiration) return u, nil } - secret, err := p.k8sclient.Secrets(pipe.Namespace).Get(context.Background(), to.PrivateKeySecret.Name, metav1.GetOptions{}) - if err != nil { - return nil, err - } - - anno := pipe.GetAnnotations() - for _, k := range []string{"ssh-privatekey", "privatekey", anno["privatekey_field_name"]} { - data := secret.Data[k] - if data != nil { - u.Auth = libplugin.CreatePrivateKeyAuth(data) - p.cache.Set(conn.UniqueID(), pipe, gocache.DefaultExpiration) - return u, nil - } - } - return nil, fmt.Errorf("no password or private key found") } +func loadStringAndFile(base64orraw string, filepath string) ([][]byte, error) { + + all := make([][]byte, 0, 2) + + if base64orraw != "" { + data, err := base64.StdEncoding.DecodeString(base64orraw) + if err != nil { + data = []byte(base64orraw) + } + + all = append(all, data) + } + + if filepath != "" { + data, err := os.ReadFile(filepath) + if err != nil { + return nil, err + } + + all = append(all, data) + } + + return all, nil +} + func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password string, publicKey []byte) (*libplugin.Upstream, error) { user := conn.User() @@ -185,23 +254,48 @@ func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password str } if publicKey == nil && password != "" { - return p.createUpstream(conn, pipe, password) - } - rest, err := base64.StdEncoding.DecodeString(from.AuthorizedKeysData) - if err != nil { - return nil, err - } - - var authedPubkey ssh.PublicKey - for len(rest) > 0 { - authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest) + pwds, err := loadStringAndFile(from.HtpasswdData, from.HtpasswdFile) if err != nil { return nil, err } - if bytes.Equal(authedPubkey.Marshal(), publicKey) { - return p.createUpstream(conn, pipe, "") + pwdmatched := len(pwds) == 0 + + for _, data := range pwds { + log.Debugf("try to match password using htpasswd") + auth, err := htpasswd.NewFromReader(bytes.NewReader(data), htpasswd.DefaultSystems, nil) + if err != nil { + return nil, err + } + + if auth.Match(user, password) { + pwdmatched = true + } + } + + if pwdmatched { + return p.createUpstream(conn, pipe, password) + } + } + + log.Debugf("try to match public using authorized key") + pubkeydata, err := loadStringAndFile(from.AuthorizedKeysData, from.AuthorizedKeysFile) + if err != nil { + return nil, err + } + + for _, rest := range pubkeydata { + var authedPubkey ssh.PublicKey + for len(rest) > 0 { + authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest) + if err != nil { + return nil, err + } + + if bytes.Equal(authedPubkey.Marshal(), publicKey) { + return p.createUpstream(conn, pipe, "") + } } } } diff --git a/plugin/kubernetes/update-codegen.sh b/plugin/kubernetes/update-codegen.sh index 0a4710dd..86d3ca53 100755 --- a/plugin/kubernetes/update-codegen.sh +++ b/plugin/kubernetes/update-codegen.sh @@ -15,8 +15,8 @@ CODEGEN_PKG=${REPO_ROOT}/vendor/k8s.io/code-generator # k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir # instead of the $GOPATH directly. For normal projects this can be dropped. # generators deepcopy,client,informer,lister -chmod +x "${CODEGEN_PKG}"/generate-groups.sh -"${CODEGEN_PKG}"/generate-groups.sh \ +chmod +x "${CODEGEN_PKG}"/kube_codegen.sh +"${CODEGEN_PKG}"/kube_codegen.sh \ "deepcopy,client,lister" \ github.com/tg123/sshpiper/plugin/kubernetes/generated \ github.com/tg123/sshpiper/plugin/kubernetes/apis \