From f5b9f7233f005ca9a91d82cae30eeffc89cae8fc Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Fri, 11 Aug 2023 06:25:58 -0700 Subject: [PATCH] add snap support (#189) * embed keygen * add snap * add generate log --- .github/workflows/release.yaml | 10 +++ .goreleaser.yaml | 24 ++++++- Dockerfile | 1 - cmd/sshpiperd/daemon.go | 24 ++++++- cmd/sshpiperd/key.go | 112 +++++++++++++++++++++++++++++++++ cmd/sshpiperd/main.go | 8 ++- entrypoint.sh | 9 +-- 7 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 cmd/sshpiperd/key.go diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index df94bbe0..7259a090 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -72,6 +72,16 @@ jobs: go-version: 'stable' cache: true + - name: Setup Snap + env: + SNAP_SECRET: ${{ secrets.snap }} + run: | + set -e + + sudo snap install snapcraft --classic + echo $SNAP_SECRET | base64 -d > secret + snap run snapcraft login --with secret + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 83ca08a2..8ecee540 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -216,4 +216,26 @@ snapshot: release: draft: true replace_existing_draft: true - disable: true \ No newline at end of file + disable: true + +snapcrafts: + - builds: + - sshpiperd + - plugin_workingdir + name: sshpiperd + name_template: "sshpiperd_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + summary: The missing reverse proxy for ssh scp + description: | + sshpiperd is a reverse proxy for ssh/scp. It allows you to have a single + ssh key pair for all your servers, behind a single IP address. It also + allows you to connect to internal servers without exposing them to the + internet. + publish: true + grade: stable + confinement: strict + license: MIT + apps: + sshpiperd: + command: sshpiperd --server-key $SNAP_COMMON/ssh_host_ed25519_key --server-key-generate-mode notexist $SNAP/workingdir --root $SNAP_COMMON/workingdir + plugs: [network-bind] + daemon: simple \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5178b2e2..bdd3f8b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,6 @@ ADD entrypoint.sh /sshpiperd FROM busybox LABEL maintainer="Boshi Lian" -COPY --from=ep76/openssh-static:latest /usr/bin/ssh-keygen /bin/ssh-keygen RUN mkdir /etc/ssh/ # Add user nobody with id 1 diff --git a/cmd/sshpiperd/daemon.go b/cmd/sshpiperd/daemon.go index 63f120b5..f8585e74 100644 --- a/cmd/sshpiperd/daemon.go +++ b/cmd/sshpiperd/daemon.go @@ -44,11 +44,33 @@ func newDaemon(ctx *cli.Context) (*daemon, error) { config.AddHostKey(private) } else { - privateKeyFiles, err := filepath.Glob(ctx.String("server-key")) + keyfile := ctx.String("server-key") + privateKeyFiles, err := filepath.Glob(keyfile) if err != nil { return nil, err } + generate := false + + switch ctx.String("server-key-generate-mode") { + case "notexist": + generate = len(privateKeyFiles) == 0 + case "always": + generate = true + case "disable": + default: + return nil, fmt.Errorf("unknown server-key-generate-mode %v", ctx.String("server-key-generate-mode")) + } + + if generate { + log.Infof("generating host key %v", keyfile) + if err := generateSshKey(keyfile); err != nil { + return nil, err + } + + privateKeyFiles = []string{keyfile} + } + if len(privateKeyFiles) == 0 { return nil, fmt.Errorf("no server key found") } diff --git a/cmd/sshpiperd/key.go b/cmd/sshpiperd/key.go new file mode 100644 index 00000000..d1f50cc5 --- /dev/null +++ b/cmd/sshpiperd/key.go @@ -0,0 +1,112 @@ +package main + +import ( + "crypto/ed25519" + crand "crypto/rand" + "encoding/pem" + "math/rand" + "os" + + "golang.org/x/crypto/ssh" +) + +func generateSshKey(keyfile string) error { + _, privateKey, err := ed25519.GenerateKey(crand.Reader) + if err != nil { + return err + } + + privateKeyPEM := &pem.Block{ + Type: "OPENSSH PRIVATE KEY", + Bytes: marshalED25519PrivateKey(privateKey), + } + + privateKeyBytes := pem.EncodeToMemory(privateKeyPEM) + + return os.WriteFile(keyfile, privateKeyBytes, 0600) +} + +// copy from https://github.com/mikesmitty/edkey/blob/master/edkey.go + +/* + Writes ed25519 private keys into the new OpenSSH private key format. + +I have no idea why this isn't implemented anywhere yet, you can do seemingly +everything except write it to disk in the OpenSSH private key format. +*/ +func marshalED25519PrivateKey(key ed25519.PrivateKey) []byte { + // Add our key header (followed by a null byte) + magic := append([]byte("openssh-key-v1"), 0) + + var w struct { + CipherName string + KdfName string + KdfOpts string + NumKeys uint32 + PubKey []byte + PrivKeyBlock []byte + } + + // Fill out the private key fields + pk1 := struct { + Check1 uint32 + Check2 uint32 + Keytype string + Pub []byte + Priv []byte + Comment string + Pad []byte `ssh:"rest"` + }{} + + // Set our check ints + ci := rand.Uint32() + pk1.Check1 = ci + pk1.Check2 = ci + + // Set our key type + pk1.Keytype = ssh.KeyAlgoED25519 + + // Add the pubkey to the optionally-encrypted block + pk, ok := key.Public().(ed25519.PublicKey) + if !ok { + //fmt.Fprintln(os.Stderr, "ed25519.PublicKey type assertion failed on an ed25519 public key. This should never ever happen.") + return nil + } + pubKey := []byte(pk) + pk1.Pub = pubKey + + // Add our private key + pk1.Priv = []byte(key) + + // Might be useful to put something in here at some point + pk1.Comment = "" + + // Add some padding to match the encryption block size within PrivKeyBlock (without Pad field) + // 8 doesn't match the documentation, but that's what ssh-keygen uses for unencrypted keys. *shrug* + bs := 8 + blockLen := len(ssh.Marshal(pk1)) + padLen := (bs - (blockLen % bs)) % bs + pk1.Pad = make([]byte, padLen) + + // Padding is a sequence of bytes like: 1, 2, 3... + for i := 0; i < padLen; i++ { + pk1.Pad[i] = byte(i + 1) + } + + // Generate the pubkey prefix "\0\0\0\nssh-ed25519\0\0\0 " + prefix := []byte{0x0, 0x0, 0x0, 0x0b} + prefix = append(prefix, []byte(ssh.KeyAlgoED25519)...) + prefix = append(prefix, []byte{0x0, 0x0, 0x0, 0x20}...) + + // Only going to support unencrypted keys for now + w.CipherName = "none" + w.KdfName = "none" + w.KdfOpts = "" + w.NumKeys = 1 + w.PubKey = append(prefix, pubKey...) + w.PrivKeyBlock = ssh.Marshal(pk1) + + magic = append(magic, ssh.Marshal(w)...) + + return magic +} diff --git a/cmd/sshpiperd/main.go b/cmd/sshpiperd/main.go index 058be7fc..1cbdc7ed 100644 --- a/cmd/sshpiperd/main.go +++ b/cmd/sshpiperd/main.go @@ -99,9 +99,15 @@ func main() { }, &cli.StringFlag{ Name: "server-key-data", - Usage: "server key in base64 format, server-key will be ignored if set", + Usage: "server key in base64 format, server-key, server-key-generate-mode will be ignored if set", EnvVars: []string{"SSHPIPERD_SERVER_KEY_DATA"}, }, + &cli.StringFlag{ + Name: "server-key-generate-mode", + Usage: "server key generate mode, one of: disable, notexist, always. generated key will be written to `server-key` if notexist or always", + Value: "disable", + EnvVars: []string{"SSHPIPERD_SERVER_KEY_GENERATE_MODE"}, + }, &cli.DurationFlag{ Name: "login-grace-time", Value: 30 * time.Second, diff --git a/entrypoint.sh b/entrypoint.sh index a7b636f0..dd08ecf6 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,12 +1,7 @@ #!/bin/sh set -eo pipefail -if [ -z "$SSHPIPERD_SERVER_KEY" ]; then - if [ ! -f /etc/ssh/ssh_host_ed25519_key ];then - ssh-keygen -t ed25519 -N '' -f /etc/ssh/ssh_host_ed25519_key - fi -fi - PLUGIN=${PLUGIN:-workingdir} +export SSHPIPERD_SERVER_KEY_GENERATE_MODE=${SSHPIPERD_SERVER_KEY_GENERATE_MODE:-notexist} -exec /sshpiperd/sshpiperd /sshpiperd/plugins/$PLUGIN \ No newline at end of file +/sshpiperd/sshpiperd /sshpiperd/plugins/$PLUGIN \ No newline at end of file