Skip to content

Commit

Permalink
feat: add env var for ssh private key (#396)
Browse files Browse the repository at this point in the history
(cherry picked from commit 08bdb8d)
  • Loading branch information
DanielleMaywood authored and mafredri committed Oct 30, 2024
1 parent 179f3c8 commit b2400e8
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 6 deletions.
4 changes: 4 additions & 0 deletions cmd/envbuilder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ func envbuilderCmd() serpent.Command {
}
}

if o.GitSSHPrivateKeyPath != "" && o.GitSSHPrivateKeyBase64 != "" {
return errors.New("cannot have both GIT_SSH_PRIVATE_KEY_PATH and GIT_SSH_PRIVATE_KEY_BASE64 set")
}

if o.GetCachedImage {
img, err := envbuilder.RunCacheProbe(inv.Context(), o)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion docs/env-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
| `--git-clone-single-branch` | `ENVBUILDER_GIT_CLONE_SINGLE_BRANCH` | | Clone only a single branch of the Git repository. |
| `--git-username` | `ENVBUILDER_GIT_USERNAME` | | The username to use for Git authentication. This is optional. |
| `--git-password` | `ENVBUILDER_GIT_PASSWORD` | | The password to use for Git authentication. This is optional. |
| `--git-ssh-private-key-path` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH` | | Path to an SSH private key to be used for Git authentication. |
| `--git-ssh-private-key-path` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH` | | Path to an SSH private key to be used for Git authentication. If this is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set. |
| `--git-ssh-private-key-base64` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64` | | Base64 encoded SSH private key to be used for Git authentication. If this is set, then GIT_SSH_PRIVATE_KEY_PATH cannot be set. |
| `--git-http-proxy-url` | `ENVBUILDER_GIT_HTTP_PROXY_URL` | | The URL for the HTTP proxy. This is optional. |
| `--workspace-folder` | `ENVBUILDER_WORKSPACE_FOLDER` | | The path to the workspace folder that will be built. This is optional. |
| `--ssl-cert-base64` | `ENVBUILDER_SSL_CERT_BASE64` | | The content of an SSL cert file. This is useful for self-signed certificates. |
Expand Down
28 changes: 28 additions & 0 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package git

import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -181,6 +182,22 @@ func ReadPrivateKey(path string) (gossh.Signer, error) {
return k, nil
}

// DecodeBase64PrivateKey attempts to decode a base64 encoded private
// key and returns an ssh.Signer
func DecodeBase64PrivateKey(key string) (gossh.Signer, error) {
bs, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return nil, fmt.Errorf("decode base64: %w", err)
}

k, err := gossh.ParsePrivateKey(bs)
if err != nil {
return nil, fmt.Errorf("parse private key: %w", err)
}

return k, nil
}

// LogHostKeyCallback is a HostKeyCallback that just logs host keys
// and does nothing else.
func LogHostKeyCallback(logger func(string, ...any)) gossh.HostKeyCallback {
Expand Down Expand Up @@ -273,6 +290,17 @@ func SetupRepoAuth(logf func(string, ...any), options *options.Options) transpor
}
}

// If no path was provided, fall back to the environment variable
if options.GitSSHPrivateKeyBase64 != "" {
s, err := DecodeBase64PrivateKey(options.GitSSHPrivateKeyBase64)
if err != nil {
logf("❌ Failed to decode base 64 private key: %s", err.Error())
} else {
logf("🔑 Using %s key!", s.PublicKey().Type())
signer = s
}
}

// If no SSH key set, fall back to agent auth.
if signer == nil {
logf("🔑 No SSH key found, falling back to agent!")
Expand Down
21 changes: 21 additions & 0 deletions git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package git_test
import (
"context"
"crypto/ed25519"
"encoding/base64"
"fmt"
"io"
"net/http/httptest"
Expand Down Expand Up @@ -433,6 +434,22 @@ func TestSetupRepoAuth(t *testing.T) {
require.Equal(t, actualSigner, pk.Signer)
})

t.Run("SSH/Base64PrivateKey", func(t *testing.T) {
opts := &options.Options{
GitURL: "ssh://[email protected]:repo/path",
GitSSHPrivateKeyBase64: base64EncodeTestPrivateKey(),
}
auth := git.SetupRepoAuth(t.Logf, opts)

pk, ok := auth.(*gitssh.PublicKeys)
require.True(t, ok)
require.NotNil(t, pk.Signer)

actualSigner, err := gossh.ParsePrivateKey([]byte(testKey))
require.NoError(t, err)
require.Equal(t, actualSigner, pk.Signer)
})

t.Run("SSH/NoAuthMethods", func(t *testing.T) {
opts := &options.Options{
GitURL: "ssh://[email protected]:repo/path",
Expand Down Expand Up @@ -502,3 +519,7 @@ func writeTestPrivateKey(t *testing.T) string {
require.NoError(t, os.WriteFile(kPath, []byte(testKey), 0o600))
return kPath
}

func base64EncodeTestPrivateKey() string {
return base64.StdEncoding.EncodeToString([]byte(testKey))
}
61 changes: 61 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"encoding/pem"
Expand Down Expand Up @@ -32,6 +33,8 @@ import (
"github.com/coder/envbuilder/testutil/gittest"
"github.com/coder/envbuilder/testutil/mwtest"
"github.com/coder/envbuilder/testutil/registrytest"
"github.com/go-git/go-billy/v5/osfs"
gossh "golang.org/x/crypto/ssh"

clitypes "github.com/docker/cli/cli/config/types"
"github.com/docker/docker/api/types"
Expand All @@ -58,6 +61,16 @@ const (
testContainerLabel = "envbox-integration-test"
testImageAlpine = "localhost:5000/envbuilder-test-alpine:latest"
testImageUbuntu = "localhost:5000/envbuilder-test-ubuntu:latest"

// nolint:gosec // Throw-away key for testing. DO NOT REUSE.
testSSHKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBXOGgAge/EbcejqASqZa6s8PFXZle56DiGEt0VYnljuwAAAKgM05mUDNOZ
lAAAAAtzc2gtZWQyNTUxOQAAACBXOGgAge/EbcejqASqZa6s8PFXZle56DiGEt0VYnljuw
AAAEDCawwtjrM4AGYXD1G6uallnbsgMed4cfkFsQ+mLZtOkFc4aACB78Rtx6OoBKplrqzw
8VdmV7noOIYS3RVieWO7AAAAHmNpYW5AY2RyLW1icC1mdmZmdzBuOHEwNXAuaG9tZQECAw
QFBgc=
-----END OPENSSH PRIVATE KEY-----`
)

func TestLogs(t *testing.T) {
Expand Down Expand Up @@ -378,6 +391,54 @@ func TestSucceedsGitAuth(t *testing.T) {
require.Contains(t, gitConfig, srv.URL)
}

func TestGitSSHAuth(t *testing.T) {
t.Parallel()

base64Key := base64.StdEncoding.EncodeToString([]byte(testSSHKey))

t.Run("Base64/Success", func(t *testing.T) {
signer, err := gossh.ParsePrivateKey([]byte(testSSHKey))
require.NoError(t, err)
require.NotNil(t, signer)

tmpDir := t.TempDir()
srvFS := osfs.New(tmpDir, osfs.WithChrootOS())

_ = gittest.NewRepo(t, srvFS, gittest.Commit(t, "Dockerfile", "FROM "+testImageAlpine, "Initial commit"))
tr := gittest.NewServerSSH(t, srvFS, signer.PublicKey())

_, err = runEnvbuilder(t, runOpts{env: []string{
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
envbuilderEnv("GIT_URL", tr.String()+"."),
envbuilderEnv("GIT_SSH_PRIVATE_KEY_BASE64", base64Key),
}})
// TODO: Ensure it actually clones but this does mean we have
// successfully authenticated.
require.ErrorContains(t, err, "repository not found")
})

t.Run("Base64/Failure", func(t *testing.T) {
_, randomKey, err := ed25519.GenerateKey(nil)
require.NoError(t, err)
signer, err := gossh.NewSignerFromKey(randomKey)
require.NoError(t, err)
require.NotNil(t, signer)

tmpDir := t.TempDir()
srvFS := osfs.New(tmpDir, osfs.WithChrootOS())

_ = gittest.NewRepo(t, srvFS, gittest.Commit(t, "Dockerfile", "FROM "+testImageAlpine, "Initial commit"))
tr := gittest.NewServerSSH(t, srvFS, signer.PublicKey())

_, err = runEnvbuilder(t, runOpts{env: []string{
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
envbuilderEnv("GIT_URL", tr.String()+"."),
envbuilderEnv("GIT_SSH_PRIVATE_KEY_BASE64", base64Key),
}})
require.ErrorContains(t, err, "handshake failed")
})
}

func TestSucceedsGitAuthInURL(t *testing.T) {
t.Parallel()
srv := gittest.CreateGitServer(t, gittest.Options{
Expand Down
19 changes: 15 additions & 4 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ type Options struct {
// GitSSHPrivateKeyPath is the path to an SSH private key to be used for
// Git authentication.
GitSSHPrivateKeyPath string
// GitSSHPrivateKeyBase64 is the content of an SSH private key to be used
// for Git authentication.
GitSSHPrivateKeyBase64 string
// GitHTTPProxyURL is the URL for the HTTP proxy. This is optional.
GitHTTPProxyURL string
// WorkspaceFolder is the path to the workspace folder that will be built.
Expand Down Expand Up @@ -358,10 +361,18 @@ func (o *Options) CLI() serpent.OptionSet {
Description: "The password to use for Git authentication. This is optional.",
},
{
Flag: "git-ssh-private-key-path",
Env: WithEnvPrefix("GIT_SSH_PRIVATE_KEY_PATH"),
Value: serpent.StringOf(&o.GitSSHPrivateKeyPath),
Description: "Path to an SSH private key to be used for Git authentication.",
Flag: "git-ssh-private-key-path",
Env: WithEnvPrefix("GIT_SSH_PRIVATE_KEY_PATH"),
Value: serpent.StringOf(&o.GitSSHPrivateKeyPath),
Description: "Path to an SSH private key to be used for Git authentication." +
" If this is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set.",
},
{
Flag: "git-ssh-private-key-base64",
Env: WithEnvPrefix("GIT_SSH_PRIVATE_KEY_BASE64"),
Value: serpent.StringOf(&o.GitSSHPrivateKeyBase64),
Description: "Base64 encoded SSH private key to be used for Git authentication." +
" If this is set, then GIT_SSH_PRIVATE_KEY_PATH cannot be set.",
},
{
Flag: "git-http-proxy-url",
Expand Down
7 changes: 6 additions & 1 deletion options/testdata/options.golden
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,13 @@ OPTIONS:
--git-password string, $ENVBUILDER_GIT_PASSWORD
The password to use for Git authentication. This is optional.

--git-ssh-private-key-base64 string, $ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64
Base64 encoded SSH private key to be used for Git authentication. If
this is set, then GIT_SSH_PRIVATE_KEY_PATH cannot be set.

--git-ssh-private-key-path string, $ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH
Path to an SSH private key to be used for Git authentication.
Path to an SSH private key to be used for Git authentication. If this
is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set.

--git-url string, $ENVBUILDER_GIT_URL
The URL of a Git repository containing a Devcontainer or Docker image
Expand Down

0 comments on commit b2400e8

Please sign in to comment.