-
Notifications
You must be signed in to change notification settings - Fork 43
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 all 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 |
---|---|---|
|
@@ -136,7 +136,12 @@ DOCKER_CONFIG_BASE64=ewoJImF1dGhzIjogewoJCSJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8 | |
|
||
## Git Authentication | ||
|
||
`GIT_USERNAME` and `GIT_PASSWORD` are environment variables to provide Git authentication for private repositories. | ||
Two methods of authentication are supported: | ||
|
||
### HTTP Authentication | ||
|
||
If the `GIT_URL` supplied starts with `http://` or `https://`, envbuilder will | ||
supply HTTP basic authentication using `GIT_USERNAME` and `GIT_PASSWORD`, if set. | ||
|
||
For access token-based authentication, follow the following schema (if empty, there's no need to provide the field): | ||
|
||
|
@@ -161,6 +166,42 @@ resource "docker_container" "dev" { | |
} | ||
``` | ||
|
||
### SSH Authentication | ||
|
||
If the `GIT_URL` supplied does not start with `http://` or `https://`, | ||
envbuilder will assume SSH authentication. You have the following options: | ||
|
||
1. Public/Private key authentication: set `GIT_SSH_KEY_PATH` to the path of an | ||
SSH private key mounted inside the container. Envbuilder will use this SSH | ||
key to authenticate. Example: | ||
|
||
```bash | ||
docker run -it --rm \ | ||
-v /tmp/envbuilder:/workspaces \ | ||
-e [email protected]:path/to/private/repo.git \ | ||
-e GIT_SSH_KEY_PATH=/.ssh/id_rsa \ | ||
-v /home/user/id_rsa:/.ssh/id_rsa \ | ||
-e INIT_SCRIPT=bash \ | ||
ghcr.io/coder/envbuilder | ||
``` | ||
|
||
1. Agent-based authentication: set `SSH_AUTH_SOCK` and mount in your agent socket, for example: | ||
|
||
```bash | ||
docker run -it --rm \ | ||
-v /tmp/envbuilder:/workspaces \ | ||
-e [email protected]:path/to/private/repo.git \ | ||
-e INIT_SCRIPT=bash \ | ||
-e SSH_AUTH_SOCK=/tmp/ssh-auth-sock \ | ||
-v $SSH_AUTH_SOCK:/tmp/ssh-auth-sock \ | ||
ghcr.io/coder/envbuilder | ||
``` | ||
|
||
> Note: by default, envbuilder will accept and log all host keys. If you need | ||
> strict host key checking, set `SSH_KNOWN_HOSTS` and mount in a `known_hosts` | ||
> file. | ||
|
||
|
||
## Layer Caching | ||
|
||
Cache layers in a container registry to speed up builds. To enable caching, [authenticate with your registry](#container-registry-authentication) and set the `CACHE_REPO` environment variable. | ||
|
@@ -288,6 +329,7 @@ On MacOS or Windows systems, we recommend either using a VM or the provided `.de | |
| `--git-clone-single-branch` | `GIT_CLONE_SINGLE_BRANCH` | | Clone only a single branch of the Git repository. | | ||
| `--git-username` | `GIT_USERNAME` | | The username to use for Git authentication. This is optional. | | ||
| `--git-password` | `GIT_PASSWORD` | | The password to use for Git authentication. This is optional. | | ||
| `--git-ssh-private-key-path` | `GIT_SSH_PRIVATE_KEY_PATH` | | Path to an SSH private key to be used for Git authentication. | | ||
| `--git-http-proxy-url` | `GIT_HTTP_PROXY_URL` | | The URL for the HTTP proxy. This is optional. | | ||
| `--workspace-folder` | `WORKSPACE_FOLDER` | | The path to the workspace folder that will be built. This is optional. | | ||
| `--ssl-cert-base64` | `SSL_CERT_BASE64` | | The content of an SSL cert file. This is useful for self-signed certificates. | | ||
|
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) | ||
} | ||
defer f.Close() | ||
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. | ||
if options.GitUsername == "" { | ||
options.GitUsername = "git" | ||
} | ||
|
||
// 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(options.GitUsername) | ||
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.