github.com/uber/kraken@v0.1.4/lib/backend/registrybackend/security/security.go (about) 1 // Copyright (c) 2016-2019 Uber Technologies, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package security 15 16 import ( 17 "fmt" 18 "net/http" 19 "net/url" 20 "sync" 21 22 "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" 23 "github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api" 24 "github.com/uber/kraken/utils/httputil" 25 "github.com/uber/kraken/utils/log" 26 27 "github.com/docker/distribution/registry/client/auth" 28 "github.com/docker/distribution/registry/client/auth/challenge" 29 "github.com/docker/distribution/registry/client/transport" 30 "github.com/docker/docker-credential-helpers/client" 31 "github.com/docker/engine-api/types" 32 ) 33 34 const ( 35 basePingQuery = "http://%s/v2/" 36 registryVersionHeader = "Docker-Distribution-Api-Version" 37 tokenUsername = "<token>" 38 credentialHelperPrefix = "docker-credential-" 39 ) 40 41 var v2Version = auth.APIVersion{ 42 Type: "registry", 43 Version: "2.0", 44 } 45 46 // Config contains tls and basic auth configuration. 47 type Config struct { 48 TLS httputil.TLSConfig `yaml:"tls"` 49 BasicAuth *types.AuthConfig `yaml:"basic"` 50 RemoteCredentialsStore string `yaml:"credsStore"` 51 EnableHTTPFallback bool `yaml:"enableHTTPFallback"` 52 } 53 54 // Authenticator creates send options to authenticate requests to registry 55 // backends. 56 type Authenticator interface { 57 // Authenticate returns a send option to authenticate to the registry, 58 // scoped to the given image repository. 59 Authenticate(repo string) ([]httputil.SendOption, error) 60 } 61 62 type authenticator struct { 63 address string 64 config Config 65 roundTripper http.RoundTripper 66 credentialStore auth.CredentialStore 67 challengeManager challenge.Manager 68 tokenHandlers sync.Map 69 } 70 71 // NewAuthenticator returns a new authenticator for the given docker registry 72 // address, TLS, and credentials configuration. It supports both basic auth and 73 // token based authentication challenges. If TLS is disabled, no authentication 74 // is attempted. 75 func NewAuthenticator(address string, config Config) (Authenticator, error) { 76 rt := http.DefaultTransport.(*http.Transport).Clone() 77 tlsClientConfig, err := config.TLS.BuildClient() 78 if err != nil { 79 return nil, fmt.Errorf("build tls config for %q: %s", address, err) 80 } 81 rt.TLSClientConfig = tlsClientConfig 82 return &authenticator{ 83 address: address, 84 config: config, 85 roundTripper: rt, 86 credentialStore: newCredentialStore(address, config), 87 challengeManager: challenge.NewSimpleManager(), 88 }, nil 89 } 90 91 func (a *authenticator) Authenticate(repo string) ([]httputil.SendOption, error) { 92 config := a.config 93 94 var opts []httputil.SendOption 95 if config.TLS.Client.Disabled { 96 opts = append(opts, httputil.SendNoop()) 97 return opts, nil 98 } 99 100 if !config.EnableHTTPFallback { 101 opts = append(opts, httputil.DisableHTTPFallback()) 102 } 103 if !a.shouldAuth() { 104 opts = append(opts, httputil.SendTLSTransport(a.roundTripper)) 105 return opts, nil 106 } 107 if err := a.updateChallenge(); err != nil { 108 return nil, fmt.Errorf("could not update auth challenge: %s", err) 109 } 110 opts = append(opts, httputil.SendTLSTransport(a.transport(repo))) 111 return opts, nil 112 } 113 114 func (a *authenticator) shouldAuth() bool { 115 return a.config.BasicAuth != nil || a.config.RemoteCredentialsStore != "" 116 } 117 118 func (a *authenticator) transport(repo string) http.RoundTripper { 119 basicHandler := auth.NewBasicHandler(a.credentialStore) 120 bearerHandler, _ := a.tokenHandlers.LoadOrStore(repo, auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{ 121 Transport: a.roundTripper, 122 Credentials: a.credentialStore, 123 Scopes: []auth.Scope{ 124 auth.RepositoryScope{ 125 Repository: repo, 126 Actions: []string{"pull", "push"}, 127 }, 128 }, 129 ClientID: "docker", 130 })) 131 return transport.NewTransport(a.roundTripper, auth.NewAuthorizer(a.challengeManager, basicHandler, bearerHandler.(auth.AuthenticationHandler))) 132 } 133 134 func (a *authenticator) updateChallenge() error { 135 resp, err := httputil.Send( 136 "GET", 137 fmt.Sprintf(basePingQuery, a.address), 138 httputil.SendTLSTransport(a.roundTripper), 139 httputil.SendAcceptedCodes(http.StatusOK, http.StatusUnauthorized), 140 ) 141 if err != nil { 142 return err 143 } 144 versions := auth.APIVersions(resp, registryVersionHeader) 145 for _, version := range versions { 146 if version == v2Version { 147 if err := a.challengeManager.AddResponse(resp); err != nil { 148 return fmt.Errorf("add response: %s", err) 149 } 150 return nil 151 } 152 } 153 return fmt.Errorf("registry is not v2") 154 } 155 156 type credentialStore struct { 157 address string 158 config Config 159 } 160 161 func newCredentialStore(address string, config Config) *credentialStore { 162 return &credentialStore{ 163 address: address, 164 config: config, 165 } 166 } 167 168 func (c credentialStore) Basic(*url.URL) (string, string) { 169 if username, password := c.credentialsFromHelper(); username != "" && username != tokenUsername { 170 return username, password 171 } 172 basic := c.config.BasicAuth 173 if basic == nil { 174 return "", "" 175 } 176 return basic.Username, basic.Password 177 } 178 179 func (c credentialStore) RefreshToken(*url.URL, string) string { 180 if username, token := c.credentialsFromHelper(); username == tokenUsername { 181 return token 182 } 183 basic := c.config.BasicAuth 184 if basic == nil { 185 return "" 186 } 187 return basic.IdentityToken 188 } 189 190 func (c credentialStore) credentialsFromHelper() (string, string) { 191 switch c.config.RemoteCredentialsStore { 192 case "": 193 // No credential helper configured, caller will use static credentials 194 // from configuration. 195 return "", "" 196 case "ecr-login": 197 client := ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}} 198 username, password, err := client.Get(c.address) 199 if err != nil { 200 log.Errorf("get credentials from helper ECR for %q: %s", c.address, err) 201 } 202 return username, password 203 default: 204 helper := credentialHelperPrefix + c.config.RemoteCredentialsStore 205 creds, err := client.Get(client.NewShellProgramFunc(helper), c.address) 206 if err != nil { 207 log.Errorf("get credentials from helper %s for %q: %s", c.config.RemoteCredentialsStore, c.address, err) 208 return "", "" 209 } 210 return creds.Username, creds.Secret 211 } 212 } 213 214 func (c credentialStore) SetRefreshToken(*url.URL, string, string) {}