golang/go

cmd/go: define HTTP authentication extension mechanism

draftcode opened this issue · 86 comments

NOTE: The accepted proposal is #26232 (comment).


Problem

The custom import path mechanism (?go-get=1 and meta tag) works for public URLs, but it doesn't work for auth required URLs. This is because when go-get fetches the URL with ?go-get=1, it uses net/http.DefaultClient, and it doesn't know the credentials it needs to access the URL. A user cannot run go get against private source code hosting service because of this.

Goal

Make go get git.mycompany.com/private-repo work, where https://git.mycompany.com/private-repo requires authentication.

Idea 1 (credential helper)

Introduce a credential helper mechanism like git-credential-helpers. A user specifies a path to a binary via GOGET_CREDENTIAL_HELPER and go get executes that with the import path as an argument. The credential helper binary returns HTTP headers (like "Authorization: Basic blah\n", and go get adds these headers when it tries to fetch go-get=1 URLs.

  • PRO: Straightforward solution to the problem description.
  • CON: Supporting multiple credential helpers becomes complicated. Git's credential helper mechanism supports multiple credential helper, and Git runs each in order. This sometimes makes an unexpected behavior that is hard to debug.

Idea 2 (go-get helper)

Introduce a custom source code fetching mechanism. When go get needs to fetch the source code for the import path git.mycompany.com/private-repo, it looks for the binary go-get-git.mycompany.com based on the host name of the import path. When such binary exists in $PATH, it executes that binary with the import path and a destination in $GOPATH (for example, go-get-git.mycompany.com git.mycompany.com/private-repo $GOPATH/src/git.mycompany.com/private-repo). The binary is responsible for fetching the source code to the specified $GOPATH location.

  • PRO: As a side effect, this make go get work with VCSs other than git/hg/svn/bzr.
  • CON: I'm not sure how this works with go get -f or go get -insecure.

Related issues: #16315 #11032

rsc commented

The original vgo prototype just read $HOME/.netrc. I'd rather do something like that than shell out to a whole separate program.

.netrc works for HTTP basic auth with unlimited lifetime credentials. But it doesn't work for other types, like:

  • OAuth2 access tokens. OAuth2 access tokens usually have a limited lifetime, and it needs to be issued dynamically. Because of this, this cannot be in a static file.
  • Cookie based auth. netrc as a format is not a format that represent an HTTP cookie. Alternatively, Netscape's cookie file format (the same format as .gitcookies) can be used. This cannot support dynamically generated credentials as well. We actually have Git repositories that are behind dynamically generated cookie-based auth at Google (BeyondCorp at Google: https://cloud.google.com/beyondcorp/).
rsc commented

Note that even "access denied" pages can have meta tags - go get does not require a 200 response, exactly for this kind of thing. So git.mycompany.com could serve the tags unauthenticated as one workaround.

Are there any more general "HTTP auth credential helpers" in the world besides git-credential-helper? How do other systems deal with this? If there's something standard to hook into (like .netrc) then that's better than designing our own.

If the access denied page contains the meta tag, it can be used as a prober. Let's say git.mycompany.com has an not-yet announced new product, collaborating with another company other-tech-company.com, and they host a Git repository at git.mycompany.com/ng-product/collab-other-tech-company. An attacker can probe if a repository exists by looking at the go-get's meta tag, and they can learn mycompany.com will have a business relationship other-tech-company.com to create a new product (and maybe benefit from this insider info).

As far as I know, there's no generic mechanism that majority of people use for storing and obtaining credentials through API. The closest thing I can think of is ssh-agent. Or if it's OS X, OS X Keychain.

rsc commented

It's only a prober if you check that the thing exists before serving the meta-tag.

$ curl 'https://rsc.io/asdgihopajdfklgadfglhifsdfj?go-get=1'
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="rsc.io/asdgihopajdfklgadfglhifsdfj git https://github.com/rsc/asdgihopajdfklgadfglhifsdfj">
<meta http-equiv="refresh" content="0; url=https://godoc.org/rsc.io/asdgihopajdfklgadfglhifsdfj">
</head>
<body>
Redirecting to docs at <a href="https://godoc.org/rsc.io/asdgihopajdfklgadfglhifsdfj">godoc.org/rsc.io/asdgihopajdfklgadfglhifsdfj</a>...
</body>
</html>
r$ 
rsc commented

Clearly we could use a better idea here but I'd like to have just one thing that's standard instead of inventing our own (that is, I want something like .netrc).

It's only a prober if you check that the thing exists before serving the meta-tag.

I cannot interpret this. The problem is that an attacker can use this to extract the repository existence by using this meta-tag. If the page checks if the repository exists and change the response, it can be used as a prober.

rsc commented

If the page checks if the repository exists and change the response, it can be used as a prober.

Then don't check, and don't change the response.

Are there any more general "HTTP auth credential helpers" in the world besides git-credential-helper?

https://docs.docker.com/engine/reference/commandline/login/#credentials-store

https://github.com/GoogleCloudPlatform/docker-credential-gcr

Then don't check, and don't change the response.

So the page should not do any auth, and should not contain meta tag? I thought you're suggesting to add a meta tag and in the next response you're saying not to add meta tag?

An example leakage: https://play.golang.org/p/rCYzVWQSQni

We want to avoid this. By adding a meta-tag to the response as rsc suggests, the repository existence is leaked.

@draftcode, I assumed what was meant as you should advertise it for all URLs, including /android/device/g000gle/foo-bar-not-exist , regardless of whether it actually exists.

I assumed what was meant as you should advertise it for all URLs, including /android/device/g000gle/foo-bar-not-exist , regardless of whether it actually exists.

Note that the third part in meta tag should be a git repository URL. This means, if a repository exists, the third part should be a valid Git repository path. In the example, I used example.com/android/device/google/marlin-kernel/foo/bar as an example package path. The valid meta tag for this is <meta name="go-import" content="example.com git http://example.com/android/device/google/marlin-kernel">, not <meta name="go-import" content="example.com git http://example.com/android/device/google/marlin-kernel/foo/bar">. Because of this, I can tell whether a repository exists by adding paths that wouldn't exist. When I make a request, if meta tag's returned path is modified, it means the repository exists.

rsc commented

@draftcode I am saying that if you pick a trivial rule like "example.com/x/y/z/w redirects to the repo at git.example.com/x/y", then you can implement it with no Auth check and no repo validity check, so that it responds to any possible x/y/z/w with the same answer. Then there is no leakage. That was the point of my curl example above: there is no actual package at rsc.io/asdgihopajdfklgadfglhifsdfj but the curl still reads back meta tags for it.

If you need example.com/x/y/z/w to redirect to different git servers based on the exact values of x,y,z,w then that's more difficult of course and leakage is hard to avoid.

This is all just a suggested workaround. What I'd really like is to find out about some plausibly general (not git-specific, not docker-specific) semi-standard way to interact with credential helpers. Maybe none exists and we have to come up with something. But that would be my last choice.

If you need example.com/x/y/z/w to redirect to different git servers based on the exact values of x,y,z,w then that's more difficult of course and leakage is hard to avoid.

OK. We've considered that option when we investigated how GitHub deals with this, and found out that GitHub won't allow slashes in a repository name, so they can do what you've said. We cannot do that, so filed this bug.

What I'd really like is to find out about some plausibly general (not git-specific, not docker-specific) semi-standard way to interact with credential helpers. Maybe none exists and we have to come up with something. But that would be my last choice.

So far, I've shown what Git does for a similar problem. @bradfitz showed what Docker and GCP does for a similar problem (ADC now works only for service accounts, so it's a bit different). If there's a some standard way to get a cred, considering the size of these tools' community, there should be some implementation of that, but it seems there's no such thing. In fact, Docker created a credential helper mechanism following what Git did. From these, such standard, if exists, is something that Git and Docker communities at least are not aware of and are not using. @rsc, @bradfitz, and I are also not aware of such generic way that would be called as "semi-standard", it seems.

rsc commented

Given that there doesn't seem to be an agreed-upon standard, I guess the next step is for someone to propose a design that Go should use. I looked at git credential helpers but stopped looking when I saw bash snippets.

Questions on the requirements:

  • My impression is that @rsc strongly prefers using a file instead of executing a command and using stdin/stdout. Is this a hard requirement?
    • If this is a requirement, this means that Go cannot support OS-standard credential management mechanisms.
    • Also this makes it hard to protect credentials. If this is done in stdin/stdout, the credential manager side can do a necessary re-auth.
  • This is just "defining a protocol between Go's toolchain and a user-defined credential manager", right? We do not want to create a password manager like LastPass for this purpose.
  • With a user-defined credential manager, users should be able to:
    • Add an HTTP header (including cookies)
    • Add an SSL client certificate
    • Anything else?

My impression is that @rsc strongly prefers using a file instead of executing a command and using stdin/stdout. Is this a hard requirement?

He objected to bash snippets. It is possible (I do not know) that executing a binary rather than a shell might sit better with him.

If it were indeed command execution (a la EDITOR), then e.g. those of us who use 1password could use their command line support. I think macOS keychain has similar support.

rsc commented

Command-line execution seems necessary, since you want to lock things down and give cmd/go access to just one password, not your whole password set. Josh, what did you have in mind? Want to propose a starting point design?

It would be great if we could achieve the stated above goal ("Make go get git.mycompany.com/private-repo work, where https://git.mycompany.com/private-repo requires authentication") without forcing the users to perform custom modifications to their ~/.netrc or ~/.gitconfig files (or installing extra binaries) - at least in the (I believe) common cases of GitHub [Enterprise] with SSH authentication (from the ssh agent).

Assuming the server returns a meta tag such as <meta name="go-import" content="git.mycompany.com/org/repo git https://git.mycompany.com/org/repo.git">, could the go tool try the following:

  • shell out git ls-remote -q https://git.mycompany.com/org/repo.git first (as now)
  • if it gets a response with an auth prompt: Username for 'https://git.mycompany.com':, try next the command with the corresponding ssh URL: git ls-remote -q ssh://git@git.mycompany.com/org/repo.git - and if it works, remember / cache that protocol for other git operations with the server (at least, within the same go invocation). (The git user must be standard for GitHub as it is indicated on https://help.github.com/articles/testing-your-ssh-connection/ and https://help.github.com/enterprise/2.14/user/articles/testing-your-ssh-connection/). This "retrial" step can be limited to the cases where server replies with Server: Github.com HTTP header ([1], [2]).

I believe this would make possible to use git with the SSH auth without having everyone to add the insteadOf stanzas in ~/.gitconfig on every server / VM or putting passwords in ~/.netrc etc. (To avoid this, we currently have to use a redirect server on an equivalent of go.mycompany.com that sends back <meta name="go-import" content="git.mycompany.com/org/repo git ssh://git@git.mycompany.com/org/repo"> - but it would be so nice to be able to use the "natural" git.mycompany.com/org/repo as imports / package names instead of go.mycompany.com and forcing everyone to learn and use the git.mycompany.com => go.mycompany.com replacement!) Having all Go users in the company to install an additional binary to bridge go and auth secrets would IMO be even more cumbersome (and the security team will likely consider it as yet another thing to worry about...).

I think it is an incredible strength of the Go tool that you can install and use it "right out of the box" - would love to see that feature and stellar user experience preserved and extended! 😄

[1] https://go-review.googlesource.com/c/go/+/33171 (abandoned; issue https://golang.org/issue/17898)
[2] Russ's comment - #24076 (comment)

Assuming the server returns a meta tag

@dmitris This proposal is for the servers that cannot return meta tags without an auth (see #26232 (comment)). It seems to me that your problem is not related to this.

rsc commented

@bcmills we should probably make this a Go 1.13 early.

  • OAuth2 access tokens usually have a limited lifetime, and it needs to be issued dynamically. Because of this, this cannot be in a static file.

Heroku takes an interesting approach to that: their CLI obtains a fresh OAuth2 token and writes it to the user's .netrc file.

@dmitris, if the user is able to contact the private HTTPS server, and the server knows that the repo is also private, why couldn't it provide an ssh:// URL in the <meta> tag (instead of an https:// URL) itself?

@bcmills yes that works. Actually the modern GHE provides the Go meta tags "natively" so we were able to switch to git.xyzcompany.com imports and shut down the go.xyzcompany.com redirect server. My only gripe now is that go seems to prefer https:// git URLs + "personal access tokens" (as in https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) while the standard at our company is the SSH authentication and git@git.xyzcompany.com URLs, so everyone has to add the insteadOf incantation in ~/.gitconfig in every environment / VM. To make clear - I'm not blaming Go at all, GitHub itself says cloning with HTTPs is recommended on https://help.github.com/articles/which-remote-url-should-i-use/ - so it is a reasonable pick. I still wish go would try both and use whichever one works so that we could run go install or go get without stumbling on the missing insteadOf config...

I have a tangential question: is it possible to pass to the go tool a client TLS certificate + key + CAcert (in environment variables or otherwise)? It could be useful in environments that take Zero Trust Networking seriously and where go tool needs to talk to the other side (the replace targets) that lives in a public cloud using mutual TLS for authN/Z. (This is hypothetical - we haven't yet had that requirement specifically for go, but mTLS is standard/required for communicating with the clouds)

SSH vs. HTTPS for GitHub in particular is tracked in #26134.

I'm not sure about mTLS — please file that separately.

Here is my proposed design for Go 1.13.

Design

A new environment variable, GOAUTH, will contain a list of authenticator commands to invoke: each command is a space-separated argument list, and the commands themselves are separated by semicolons.

The go command will maintain, in memory, a cache of credentials produced by GOAUTH invocations, and use them for both go-import resolution and the HTTPS module proxy protocol.

Before the first HTTPS fetch, the go command will invoke each GOAUTH command in the list with no additional arguments and no input. The GOAUTH command may reply with zero or more credentials to cache, written to stdout in this format:

  • one per line, one or more URL prefixes to which the credential should apply, beginning with the string https:// and ending at a complete path element.
  • an empty line
  • one per line, zero or more request headers to set to apply the credential
  • another empty line.

For each URL to be fetched, the go command will check the credential cache and apply the first matching credential from the command that appears earliest in the list, appending the corresponding headers to its own request headers (if any).

If the server responds with any 4xx code and the response body does not satisfy the request (for example, by including a valid <meta name="go-import"> tag), the go command will invoke the GOAUTH commands again, this time with the URL as an additional command-line argument. The go command will also write the following to the programs' stdin:

  • the protocol and status from the response
  • the response headers as parsed by the net/http package
  • an empty line.

(Note that, at least for HTTP 1.1, the contents written to stdin can be parsed verbatim as an HTTP HEAD response.)

Each GOAUTH command may reply with credentials as before, and those credentials take precedence over any previous credentials from that command for that prefix. (A non-empty list of prefixes followed by an empty list of request headers removes the previous credentials.) If none of the credentials in the replies match the request, the go command fails the fetch.

The go command will then retry the fetch with the updated credential for the URL. If the server responds with an error again, the fetch fails: a URL-specific GOAUTH will only be attempted once per fetch.
The go command will recognize three builtin GOAUTH commands: off, netrc, and git. The netrc implementation will be equivalent to the go command's existing .netrc file support. The git implementation will use git credential fill. The default value of GOAUTH (if not set in the environment) will be netrc, preserving the credential behavior from Go 1.12.

Rationale

This approach provides very wide latitude for the authentication mechanism, while keeping the protocol close to HTTP itself. (The implementation for git credential is straightforward, and .netrc and cookies.txt should be similar.)

The initial calls to the GOAUTH programs reduce overhead from repeated command invocations (and latency from initial credential misses), while the secondary calls allow the programs to respond intelligently to authentication hints from the server (such as WWW-Authenticate types and realms).

Example exchange

Command:

$GOAUTH https://github.com/bcmills/private-repo

Input to GOAUTH:

HTTP/1.1 404 Not Found
Cache-Control: no-cache
Content-Security-Policy: default-src 'none'; base-uri 'self'; connect-src 'self'; form-action 'self'; img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline'
Content-Type: text/html; charset=utf-8
Date: Wed, 06 Feb 2019 22:47:12 GMT
Expect-Ct: max-age=2592000, report-uri="https://api.github.com/_private/browser/errors"
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Server: GitHub.com
Set-Cookie: has_recent_activity=1; path=/; expires=Wed, 06 Feb 2019 23:47:12 -0000
Set-Cookie: _gh_sess=ZWdxekI4RlFHbS82MEl5djZwcWNrRHJqdzhUNDlpTFM4aVFOa2E2TDRRTWFuZkhJenBDdmVYc24rYXBFZXJOTHN3VGdTZnc2RGVpYndmdTJCVFVTeE4zaTRkbHR0ZHYrODJLQTdqR1llaFQrVklQSk9YSWw2b3lROWxleENtL0ItLUJ2MGs1TnhuTnVPMTBpdXRIcUFSVmc9PQ%3D%3D--47220df4045034db9d8ce85c771e0d20341b29c0; path=/; secure; HttpOnly
Status: 404 Not Found
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Vary: X-PJAX
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Github-Request-Id: CE63:55B5:2AF684:48FB29:5C5B63F0
X-Request-Id: 8c810f80-9be4-48d9-b8c4-9a31b56aa312
X-Xss-Protection: 1; mode=block

Output from GOAUTH:

https://github.com

Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

(Note: the credential above is aladdin:opensesame, the standard example username and password for HTTPS auth protocols. Please be careful not to paste real credentials!)

Change https://golang.org/cl/161698 mentions this issue: cmd/go/internal/web2: make netrc parsing more robust

Change https://golang.org/cl/161666 mentions this issue: cmd/auth/authtest: add a manual-test harness for GOAUTH implementations

Change https://golang.org/cl/161667 mentions this issue: cmd/auth/gitauth: add a reference GOAUTH implementation using 'git credential fill'

Change https://golang.org/cl/161668 mentions this issue: cmd/auth/netrcauth: add a reference GOAUTH implementation using .netrc files

Change https://golang.org/cl/161669 mentions this issue: cmd/auth/cookieauth: add a GOAUTH implementation that reads from a cookiefile

rsc commented

I talked with @bcmills about his proposed solution, and it seems worth trying.
Does anyone object to accepting this?

@bcmills @rsc - looks great, just a couple of questions:

  • Many people who have hit auth-related issues thus far use git over ssh. Am I right in concluding that for these people their lot will not improve, i.e. they will still need to use either a .netrc file or a credential helper?
  • Is there any harm in making the default value of GOAUTH for 1.13 netrc;git? For those people (and selfishly I count myself amongst them) who have a git credential helper set, this will save setting another global environment variable. And in any case I'm guessing using a git credential helper is the direction in which we want to be pushing people?

Many people who have hit auth-related issues thus far use git over ssh. Am I right in concluding that for these people their lot will not improve […]?

The main git-over-ssh use-case is #26134, which we should address separately.

In general, if a repo cannot be accessed via HTTPS at all, then its <meta name="go-import" […]> tag should provide an appropriate ssh:// URL.

We can probably provide some sort of automatic fallback for the hard-coded hosting services, but in general if we receive an HTTPS URL we don't know what username to try for ssh, even if we could infer the rest of the path.

We may want to allow the go-import tag to list multiple alternative URLs (for example, on https:// and one ssh://), but if we do that we'll have to be very careful not to break the existing parsers with it.

Is there any harm in making the default value of GOAUTH for [1.13] netrc;git?

git credential fill is aggressively interactive, so that could jam up folks' CI configurations and generally mess with scripting. In contrast, we can look for a .netrc file without any user interaction whatsoever.

I'd like to keep the default non-interactive if we can.

Thanks, all makes sense now.

In general, if a repo cannot be accessed via HTTPS at all, then its <meta name="go-import" […]> tag should provide an appropriate ssh:// URL.

Right so in the main (?) scenario, we're assuming that the initial ?go-get=1 request will be unauthenticated.

Is it documented anywhere that a go-import meta tag can supply an ssh:// URL?

https://tip.golang.org/cmd/go/#hdr-Remote_import_paths

Or are you saying this is how that use case will be solved in future?

git credential fill is aggressively interactive

Presumably the interactive mode is some sort of fallback; I'm guessing from your response it's not possible to tell git not to use that fallback.

Presumably the interactive mode is some sort of fallback; I'm guessing from your response it's not possible to tell git not to use that fallback.

Correct. To be precise, if $GIT_ASKPASS or $SSH_ASKPASS fails to return a credential, git credential fill will look for (and open) /dev/tty and issue a password prompt itself.

That's the right thing to do if the user actually wants to type it a password (that is, if the go command itself is invoked interactively), but totally wrong in a CI or IDE context.

Is it documented anywhere that a go-import meta tag can supply an ssh:// URL?

I don't think we give an example of it, but if that doesn't work today, then I will make it work. 😤

The GOAUTH command may reply with zero or more credentials to cache, written to stdout in this format:

I want to check if I understand this correctly. GOAUTH may reply multiple sets of the credentials. The response format is:

RESPONSE ::= CREDENTIAL_SET*
CREDENTIAL_SET ::= (URL NEWLINE)+ NEWLINE (HEADER NEWLINE)* NEWLINE
URL ::= /* URL that starts with "https://" */
NEWLINE ::= '\n'
HEADER ::= /* HTTP header */

Right so in the main (?) scenario, we're assuming that the initial ?go-get=1 request will be unauthenticated.

FYI, gitlab need authentication for private repos in nested sub-group. So I think the initial ?go-get=1 request need to support authentication too.

@myitcv

Right so in the main (?) scenario, we're assuming that the initial ?go-get=1 request will be unauthenticated.

Not necessarily. The protocol in this proposal would apply to both the initial ?go-get=1 fetch, any subsequent requests that use the mod protocol, and any HTTPS traffic to the configured GOPROXY server.

(When using VCS tools for direct fetches, it's still up to those individual tools to decide how to authenticate their own internal HTTPS traffic, if any.)

Aren't we reinventing the wheel here? I not sure which other VCS's are supported/used by go get, but in the case of git, authentication is built in for both HTTP and SSH transports. For HTTP there's gitcredentials and for SSH there's ssh_config(5). git also supports other protocols but I think it's safe to say that they are outside scope. I'm sure other VCS's have similar approaches.

If we leave authentication to the VCS, the go tool chain only needs to instruct the VCS which url to use and make this configurable by the user.

Example

Make go consult a config file before downloading a dependency. The file should contain a mapping between import paths and the url passed to the VCS. E.g:

example.com/foo/bar git@example.com:foo/bar.git

As a side effect this also allows uses of simple proxies.

@palsivertsen, the go tool does not make every HTTPS request through a VCS tool. This proposal is specifically for the requests that are not made through a VCS.

Note that git credential fill is one of the supported credential sources under this proposal: if you want to use it, you will be able to set GOAUTH=git and won't need to write any additional configuration files.

Change https://golang.org/cl/170581 mentions this issue: vcs-test/vcweb: add a handler that requires HTTPS Basic Auth

@bcmills Which calls does the go tool need to make calls outside a VCS tool?

Maybe this is the wrong issue, but I'm trying to find a solution for when the following is true for a dependency:

  • Requires authentication
  • Host will deny any existence of resource unless request is authenticated
  • Authenticating HTTP requests is not an option (2FA / weak passwords)

I came here from #29094 which is a symptom of the above case. There are a lot of similar issues like this and so far I've found only one workaround which involves renaming the dependency and tricking the go tool into using ssh.

Which calls does the go tool need to make calls outside a VCS tool?

It makes https://[…]?go-get=1 requests to resolve paths to repos (using go-import meta tags), and if the go-import tag indicates mod then it uses HTTPS to fetch the resulting modules (using the protocol described in https://tip.golang.org/cmd/go/#hdr-Module_proxy_protocol).

I'm trying to find a solution for when the following is true for a dependency:

  • Requires authentication
  • Host will deny any existence of resource unless request is authenticated
  • Authenticating HTTP requests is not an option (2FA / weak passwords)

You only get to pick two of those: “requires authentication” and “authenticating […] is not an option” are mutually exclusive.

But note that the protocol described in #26232 (comment) allows the GOAUTH plugin to do whatever it needs to do to authenticate — including interacting with the user to obtain 2FA credentials — as long as it can encode the result as a URL prefix and a set of HTTP request headers.

Change https://golang.org/cl/170879 mentions this issue: cmd/go/internal/web{,2}: consolidate web packages

Change https://golang.org/cl/170880 mentions this issue: vcs-test/vcweb: fix various extraction issues for serving modules with authentication

rsc commented

Looks good. On Feb 13 I asked if anyone objected to this proposal, and as far as I can tell no one has. Accepting.

You only get to pick two of those: “requires authentication” and “authenticating […] is not an option” are mutually exclusive.

Sorry, poor choice of words from my side. What I was trying to say is that HTTP is not an option, but other protocols are, e.g. SSH. Making HTTP calls that requires authentication is (IMO) a poor choice compared to SSH because it requires either user interaction (2FA) or weak credentials (compared to SSH). Another argument to drop HTTP is that SSH credentials are in most cases only authorized for GIT, but HTTP credentials in some cases gives access to a wider API.

Maybe my usecase is not relevant to what's described in this issue.

@palsivertsen, the protocol-selection issue you describe was discussed in #30304 (and declined on the grounds of not being worth the extra complexity; see #30304 (comment)).

Change https://golang.org/cl/172599 mentions this issue: cmd/go/internal/base: add a Logf function and funnel stderr output through it

Change https://golang.org/cl/172617 mentions this issue: cmd/go: set GIT_TERMINAL_PROMPT on individual git commands instead of process-wide

@bcmills I want to make sure I understand how this protocol works with GOPROXY and private repos. Correct me if wrong.

Option 1. Utilizing the functionalities added for #26334. User specifies GOPROXY=https://public_proxy,direct

  • Go command tries to fetch https://public_proxy/private_repo/package/path
  • The public_proxy will not be able to access the private repo, so returns 4XX.
  • Go command runs $GOAUTH with https://public_proxy/private_repo/package/path as an arg, which fails.
  • Go command tries with the next one in GOPROXY (direct in this example).
  • The private repo requires credential, so returns 4XX.
  • Go command runs $GOAUTH with https://private_repo/package/path as an arg, which succeeds.
  • Go command fetches data from the private repo using the credential.

Option 2. Utilizing the new GONOPROXY env var proposed in
https://go.googlesource.com/proposal/+/master/design/25530-notary.md#command-client.
User specifies GONOPROXY=private_repo GOPROXY=https://public_proxy

  • Go command knows the private_repo shouldn't use the proxy, so directly contact the private_repo, and fails with 4XX (403)
  • Go command runs $GOAUTH with https://private_repo/package/path and acquires the credential.
  • Go command fetches data from the private repo using the credential.

@hyangah, since GOAUTH may be interactive, I think we want to avoid running it until we have exhausted all other options. For your Option 1, that suggests something more like:

  • Go command tries to fetch https://public_proxy/private_repo/package/path
  • The public_proxy will not be able to access the private repo, so returns 4XX.
  • Go command tries with the next one in GOPROXY (direct in this example).
  • The private repo requires credential, so returns 4XX.
  • Go command runs $GOAUTH with https://public_proxy/private_repo/package/path as an arg, which fails, so it does not retry the public proxy.
  • Go command runs $GOAUTH with https://private_repo/package/path as an arg, which succeeds.
  • Go command fetches data from the private repo using the credential.

That reduces the number of user interactions required if the proxy declines the request but the direct fetch succees. However, it implies that if one wants to avoid leaking paths from one proxy to another, the chosen GOPROXY command must report all applicable credentials upfront (during the initial “no URL” phase) instead of waiting for requests for specific URLs.

Thanks for the clarification, @bcmills

However, it implies that if one wants to avoid leaking paths from one proxy to another, the chosen GOPROXY command must report all applicable credentials upfront (during the initial “no URL” phase) instead of waiting for requests for specific URLs.

Does that mean, if the GOAUTH program returns the credential for https://private_repo/ during the initial no URL phase, the Go command will not try to use GOPROXY for the package that prefixes with the private_repo so the private package path and repo names will not leak? That's great.

If I have some private repositories in github.com accessible with my github.com credential, but I still want to utilize the public GOPROXY for other public modules and packages, do I need to configure the GOAUTH program to return the explicit list of private repositories in the github.com with my github.com credentials?

Change https://golang.org/cl/173017 mentions this issue: cmd/go: query modules in parallel

Does that mean, if the GOAUTH program returns the credential for https://private_repo/ during the initial no URL phase, the Go command will not try to use GOPROXY for the package that prefixes with the private_repo so the private package path and repo names will not leak?

I'm not sure exactly what you're asking, but I wasn't planning to use the set of credentials to decide which modules to ask the proxy about.

If I have some private repositories in github.com accessible with my github.com credential, but I still want to utilize the public GOPROXY for other public modules and packages, do I need to configure the GOAUTH program to return the explicit list of private repositories in the github.com with my github.com credentials?

If you don't care about leaking your private repo paths to the proxy, then you can run git config --global credential.helper cache or git config --global credential.helper 'store --file ~/.my-credentials', then git credential approve the credentials for your private repos, and finally set GOAUTH=git to use those stored credentials. (To my knowledge git does not have a API for enumerating existing credentials, so the GOAUTH initial pass won't pick them up anyway.)

If you do care about leaking your private repo paths, then you'll still have to use GONOPROXY or similar.

I had to pause development on this before the 1.13 freeze due to a personal emergency, and as a result it didn't make the freeze.

Given that it appears likely that modules will still be auto rather than on by default (#31857), and given the subtle interactions between GOAUTH, package-to-module resolution, and proxy fallback (#26334), I think this needs to wait for 1.14.

Just to set expectations: I don't think this will make the 1.14 cycle. (There is too much left to do, and the fixed .netrc support in 1.13 seems to have addressed many of the popular hosting platforms.)

Would it be possible to break up work so that the community can contribute? I am interested in this issue, but unclear if I could meaningfuly help, short of just landing the feature in isolation.

@arnottcr, I don't know of any way to break it up. The bulk of the work, I think, is making sure that everything works properly in conjunction with the GOPROXY fallback sequence and the package-to-module search sequence.

(Essentially, we currently have a two-dimensional search space, and GOAUTH adds a third dimension to search, so we need to figure out exactly what order to traverse those dimensions to avoid breaking things like the GOPROXY privacy properties.)

My teams biggest blocker to migrate to use go modules internally right now is not being able to define a client certificate for authentication. We have all of repositories behind a proxy that is set to SSLVerifyClient:required.
All of our git clients are set up to provide client certificates or use just plain ssh. Could the go get when setting GOAUTH=git respect the http configurations defined in the .gitconfig for ssl client authentication be passed on to the go-get=1 request?

.gitconfig

[http]
    sslCert = <path to cert>
    sslKey = <path to key>
    sslVerify = true/false
    sslCAInfo = <path to ca>

@nkralles, this issue is not about client certs. (That is #30119.)

Do you have an estimate for when this will be implemented? E.g. 1.15 or 1.16?

Are there any more general "HTTP auth credential helpers" in the world besides git-credential-helper? How do other systems deal with this? If there's something standard to hook into (like .netrc) then that's better than designing our own.

I'm way late to this party, but in case it's of any interest, this is pretty much what Microsoft's NuGet package managers (nuget.exe and dotnet CLI and Visual Studio) do for authenticated package feeds using a shared nuget credential provider

I'm butting up against this issue in a slightly different way.

We proxy all our traffic to our git server through a kerberos authenticating proxy. Most applications use libcurl at some point, which handles this nicely (performs negotation after receiving a 401).

https://github.com/jcmturner/gokrb5 has a pure go implementation of SPNEGO. I'd be up for writing some GOAUTH handler that generates a preemptive negotiate header.

Do you have an estimate for when this will be implemented? E.g. 1.15 or 1.16?

Go 2? ... Would really be nice to see Go and its toolchain have the same first class support for standard auth mechanisms.

Just bumped into this while trying to get at a gitlab instance that was behind a cloudflare auth. So commenting here to add to the use-cases.

I'd like to be able to set, somehow, a custom http header to allow cloudflare access e.g.

cf-access-token: $TOKEN

If I could configure this custom header, in a .netrc style, so that I can just say that matching hosts have to supply the extra header value, it would have solved the access problem early nicely

Client TLS cert auth to private git repos would be nice also: #53197

Seems like there are a few ways to do authentication, one of them using the header to send a token. I am also facing the same issue as @srowles . As such, would it be useful to split this task into multiple smaller ones? One of them to add the reading of a header and its corresponding value from Go's environment variable (i.e. GOAUTH_HEADER, GOAUTH_HEADER_VALUE)?

Looks like GCP are trying to do stuff with artefact registry now and go module proxy.
https://github.com/GoogleCloudPlatform/artifact-registry-go-tools

So looks like there is a need to support something

I am very late to this party it seems, but I might have a bit more to offer than just a +1.

netrc as the only supported authentication mechanism for module proxies doesn't seem to be a passionate issue anywhere, but like @owenhaynes pointed out, module adoption is widespread and having to manage git/ssh credentials for private modules is a growing thorn. I'll go as far as to say I think programatically updating .netrc is probably more a sign of desperate madness than a robust solution.

For better or worse, I didn't find this issue until after I'd taken a stab at fixing it myself. Fortunately, I came to most of the same conclusions this proposal did, with a few exceptions. I agree entirely with avoiding bespoke protocols where good ones exist, but also that existing protocols that leverage plaintext and hardcoded tokens are best left behind.

The few assumptions I think I might have made differently to @bcmills:

  • it is safe to assume prior knowledge of which helpers will be able to provide credentials for what proxies
  • we avoid interactiveness through caching, but if a proxy has a defined credential helper, we don't even try before fetching a cred
  • the myriad of ways that authentication can happen is not a problem space that can be captured in advance, and the protocol for pulling "credentials" needs to be gracefully extensible so future versions of Go can support new mechanisms (e.g. mTLS) without breaking changes to the interaction between go and the helpers

My design is as follows:

The GOAUTH config behaves like GOVCS in that it's an associative config that maps proxy hosts to helpers that can supply them credentials.

The grammar for GOAUTH is GOAUTH=<proxy-glob>!<helper-bin>[;<arg>[; ...]][, ...]. By example:

GOAUTH='*priv.example.com!/usr/local/bin/corp-credhelper;--user=me,proxy.example.com/private/*!my-credtool'

In this, proxy-glob is a host and path that will be matched with module.MatchPrefixPatterns, and an associated helper-bin that is executed with any corresponding arguments. Earlier matches take precedence, and go only invokes one helper per request.

The protocol between go and the helper is borrowed from a system that seems to be working well in the real world: kubernetes and its client.authentication.k8s.io ExecCredential. This is the helper system that kubectl commands use to authenticate to clusters.

GOAUTH plugins must return a HttpCredential object as JSON over stdout. Go will ignore any unknown fields in this object, allowing for future extensibility without breaking changes. As I've defined it, it supports the arbitrary http headers approach, a shorthand for Bearer tokens, and basic auth. I was going to suggest TLS client cert/secret pairs and CA bundle would be a worthy addition as #26232 (comment) mentioned.

The HttpCredential response includes information about how long the provided credentials will be valid for, allowing Go to cache it both in-memory for repeat requests, or on-disk for caching between commands. GOAUTHCACHE points to a file on disk (default to $USER_CONFIG_DIR/.goauthcache) where it's stored. Since I'm not a big fan of writing creds to disk if I don't have to, I am assuming most cred helpers can either choose to handle the caching themselves, or the disk cache can be disabled with GOAUTHCACHE=off.

Test code

I have this implementation "working" in a dev branch: master...AndrewBurian:go:auth-plugins

This is my first time seriously slogging my way through go's test suite, so forgive... everything. It's mostly a PoC so I could see if the design was even a good idea or not, and seems to work as I'd expect.

Git+SSH?

I'm not sure if this helps or hurts the situation with Github and ssh auth #26134.

My understanding after catching up all the discussion here is that go defers everything about the transport of source code outside of module zips to the VSC tool, and so having to configure git separately with auth for modules being served from private VCS feels correct. The focus of GOAUTH is very much for the distribution of private modules, and GOPRIVATE for private VCS.

There may be some weird behaviour if the http authentication allows go to discover the existence of a repo with the initial https go-get=1 request, but then not be able to clone it if git hasn't had credentials configured. I got turned around in the code, but it seems like with the hardcoded VCS handler for Github, go maybe doesn't even bother with the initial get?

I will continue to play around with this, but after finally getting it to all compile my go seamlessly fetches modules with GOPROXY set to only a private Google Cloud Storage bucket and a 3-line script wrapping up gcloud auth print-access-token as a cred helper. In my opinion, it feels like how I would expect private authentication to work coming from git and k8s.

maybe instead of implementing the myriad of authentication strategies people may want, we just have a making of module prefixes to plain http proxies
and let the proxies be responsible for the auth

The GOAUTH env var is not yet recognized right? At least on 1.20.4 it is being ignored

Any news on this?

I've seen that this has been worked on (https://github.com/golang/tools/tree/master/cmd/auth), but as @rsj-ioki says it does not seem that it is fully implemented or available.

The lack of any authentification mechanism against a GOPROXY is a pretty serious security concern for anyone running their own private proxy because anyone with HTTPS access to the proxy can retrieve all stored modules.

mxk commented

Whoever works on this, please make sure that pkgsite commands use the same mechanism. Currently, it is very difficult to get a private documentation server running, and even once it's up, the lack of even netrc support causes all source links to be broken.

Change https://go.dev/cl/605256 mentions this issue: cmd/go: add GOAUTH mechanism for HTTP authentication

Change https://go.dev/cl/605275 mentions this issue: cmd/go: add built in git mode for GOAUTH

Change https://go.dev/cl/605298 mentions this issue: cmd/go: add user provided auth mode for GOAUTH