-
Notifications
You must be signed in to change notification settings - Fork 45
feat: support cloning over SSH via private key auth #170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
acff47a
8ff5c1a
015c67a
054abc8
30285c0
64bdbdc
e9f183f
0ead6a0
2c79a1b
e5c39d3
18d0dc2
b0587e2
02226ae
36871a3
39088ff
7f30872
5dd031c
f200030
a6256a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,16 +4,26 @@ import ( | |
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net" | ||
"net/url" | ||
"os" | ||
"strings" | ||
|
||
"github.com/coder/coder/v2/codersdk" | ||
"github.com/go-git/go-billy/v5" | ||
"github.com/go-git/go-git/v5" | ||
"github.com/go-git/go-git/v5/plumbing" | ||
"github.com/go-git/go-git/v5/plumbing/cache" | ||
"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" | ||
"github.com/go-git/go-git/v5/plumbing/protocol/packp/sideband" | ||
"github.com/go-git/go-git/v5/plumbing/transport" | ||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http" | ||
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" | ||
"github.com/go-git/go-git/v5/storage/filesystem" | ||
"github.com/skeema/knownhosts" | ||
"golang.org/x/crypto/ssh" | ||
gossh "golang.org/x/crypto/ssh" | ||
) | ||
|
||
type CloneRepoOptions struct { | ||
|
@@ -113,3 +123,131 @@ func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) { | |
} | ||
return true, nil | ||
} | ||
|
||
// ReadPrivateKey attempts to read an SSH private key from path | ||
// and returns an ssh.Signer. | ||
func ReadPrivateKey(path string) (gossh.Signer, error) { | ||
f, err := os.Open(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("open private key file: %w", err) | ||
} | ||
bs, err := io.ReadAll(f) | ||
if err != nil { | ||
return nil, fmt.Errorf("read private key file: %w", err) | ||
} | ||
k, err := gossh.ParsePrivateKey(bs) | ||
if err != nil { | ||
return nil, fmt.Errorf("parse private key file: %w", err) | ||
} | ||
return k, nil | ||
} | ||
|
||
// LogHostKeyCallback is a HostKeyCallback that just logs host keys | ||
// and does nothing else. | ||
func LogHostKeyCallback(log LoggerFunc) gossh.HostKeyCallback { | ||
return func(hostname string, remote net.Addr, key gossh.PublicKey) error { | ||
var sb strings.Builder | ||
_ = knownhosts.WriteKnownHost(&sb, hostname, remote, key) | ||
// skeema/knownhosts uses a fake public key to determine the host key | ||
// algorithms. Ignore this one. | ||
if s := sb.String(); !strings.Contains(s, "fake-public-key ZmFrZSBwdWJsaWMga2V5") { | ||
log(codersdk.LogLevelInfo, "#1: 🔑 Got host key: %s", strings.TrimSpace(s)) | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
// SetupRepoAuth determines the desired AuthMethod based on options.GitURL: | ||
// | ||
// | Git URL format | GIT_USERNAME | GIT_PASSWORD | Auth Method | | ||
// | ------------------------|--------------|--------------|-------------| | ||
// | https?://host.tld/repo | Not Set | Not Set | None | | ||
// | https?://host.tld/repo | Not Set | Set | HTTP Basic | | ||
// | https?://host.tld/repo | Set | Not Set | HTTP Basic | | ||
// | https?://host.tld/repo | Set | Set | HTTP Basic | | ||
// | All other formats | - | - | SSH | | ||
// | ||
// For SSH authentication, the default username is "git" but will honour | ||
// GIT_USERNAME if set. | ||
// | ||
// If SSH_PRIVATE_KEY_PATH is set, an SSH private key will be read from | ||
// that path and the SSH auth method will be configured with that key. | ||
// | ||
// If SSH_KNOWN_HOSTS is not set, the SSH auth method will be configured | ||
// to accept and log all host keys. Otherwise, host key checking will be | ||
// performed as usual. | ||
func SetupRepoAuth(options *Options) transport.AuthMethod { | ||
if options.GitURL == "" { | ||
options.Logger(codersdk.LogLevelInfo, "#1: ❔ No Git URL supplied!") | ||
return nil | ||
} | ||
if strings.HasPrefix(options.GitURL, "http://") || strings.HasPrefix(options.GitURL, "https://") { | ||
// Special case: no auth | ||
if options.GitUsername == "" && options.GitPassword == "" { | ||
options.Logger(codersdk.LogLevelInfo, "#1: 👤 Using no authentication!") | ||
return nil | ||
} | ||
// Basic Auth | ||
// NOTE: we previously inserted the credentials into the repo URL. | ||
// This was removed in https://github.com/coder/envbuilder/pull/141 | ||
options.Logger(codersdk.LogLevelInfo, "#1: 🔒 Using HTTP basic authentication!") | ||
return &githttp.BasicAuth{ | ||
Username: options.GitUsername, | ||
Password: options.GitPassword, | ||
} | ||
} | ||
|
||
// Generally git clones over SSH use the 'git' user, but respect | ||
// GIT_USERNAME if set. | ||
authUser := options.GitUsername | ||
if authUser == "" { | ||
authUser = "git" | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
// Assume SSH auth for all other formats. | ||
options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Using SSH authentication!") | ||
|
||
var signer ssh.Signer | ||
if options.GitSSHPrivateKeyPath != "" { | ||
s, err := ReadPrivateKey(options.GitSSHPrivateKeyPath) | ||
if err != nil { | ||
options.Logger(codersdk.LogLevelError, "#1: ❌ Failed to read private key from %s: %s", options.GitSSHPrivateKeyPath, err.Error()) | ||
} else { | ||
options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Using %s key!", s.PublicKey().Type()) | ||
signer = s | ||
} | ||
} | ||
|
||
// If no SSH key set, fall back to agent auth. | ||
if signer == nil { | ||
options.Logger(codersdk.LogLevelError, "#1: 🔑 No SSH key found, falling back to agent!") | ||
auth, err := gitssh.NewSSHAgentAuth(authUser) | ||
if err != nil { | ||
options.Logger(codersdk.LogLevelError, "#1: ❌ Failed to connect to SSH agent: %s", err.Error()) | ||
return nil // nothing else we can do | ||
} | ||
if os.Getenv("SSH_KNOWN_HOSTS") == "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: If this was moved higher up, we could avoid duplicating it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried doing this, but we need to either return |
||
options.Logger(codersdk.LogLevelWarn, "#1: 🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!") | ||
auth.HostKeyCallback = LogHostKeyCallback(options.Logger) | ||
} | ||
return auth | ||
} | ||
|
||
auth := &gitssh.PublicKeys{ | ||
User: options.GitUsername, | ||
Signer: signer, | ||
} | ||
|
||
// Generally git clones over SSH use the 'git' user, but respect | ||
// GIT_USERNAME if set. | ||
if auth.User == "" { | ||
auth.User = "git" | ||
} | ||
|
||
// Duplicated code due to Go's type system. | ||
if os.Getenv("SSH_KNOWN_HOSTS") == "" { | ||
options.Logger(codersdk.LogLevelWarn, "#1: 🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!") | ||
auth.HostKeyCallback = LogHostKeyCallback(options.Logger) | ||
} | ||
return auth | ||
} |
Uh oh!
There was an error while loading. Please reload this page.