Skip to content

Commit ae54a3f

Browse files
committed
fix(auth): resolve credentials against the loaded config and report store errors
The cache looked credentials up through dockercfg.GetRegistryCredentials, which reloads the default config file, so a process configured through DOCKER_AUTH_CONFIG could miss its credentials store. The loaded config is now passed down to the lookup. Errors from the store are reported instead of read as missing credentials, and an empty registry host is never sent to the store.
1 parent 3757d0a commit ae54a3f

3 files changed

Lines changed: 107 additions & 29 deletions

File tree

container.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,14 +383,14 @@ func getAuthConfigsFromDockerfile(c *ContainerRequest) (map[string]registry.Auth
383383
}
384384

385385
// Get the auth configs once for all images as it can be a time-consuming operation.
386-
configs, err := getDockerAuthConfigs()
386+
cfg, configs, err := loadDockerAuth()
387387
if err != nil {
388388
return nil, err
389389
}
390390

391391
authConfigs := map[string]registry.AuthConfig{}
392392
for _, image := range images {
393-
registry, authConfig, err := dockerImageAuth(context.Background(), image, configs)
393+
registry, authConfig, err := dockerImageAuth(context.Background(), image, cfg, configs)
394394
if err != nil {
395395
if !errors.Is(err, dockercfg.ErrCredentialsNotFound) {
396396
return nil, fmt.Errorf("docker image auth %q: %w", image, err)

docker_auth.go

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,48 @@ import (
2323
// defaultRegistryFn is variable overwritten in tests to check for behaviour with different default values.
2424
var defaultRegistryFn = defaultRegistry
2525

26-
// getRegistryCredentials is a variable overwritten in tests to mock the dockercfg.GetRegistryCredentials function.
27-
var getRegistryCredentials = dockercfg.GetRegistryCredentials
26+
// getRegistryCredentials is a variable overwritten in tests to mock the credentials lookup.
27+
// It resolves against the config already loaded, which honours DOCKER_AUTH_CONFIG, rather than
28+
// reloading the default config file.
29+
var getRegistryCredentials = func(cfg *dockercfg.Config, hostname string) (string, string, error) {
30+
return cfg.GetRegistryCredentials(hostname)
31+
}
2832

2933
// DockerImageAuth returns the auth config for the given Docker image, extracting first its Docker registry.
3034
// Finally, it will use the credential helpers to extract the information from the docker config file
3135
// for that registry, if it exists.
3236
func DockerImageAuth(ctx context.Context, image string) (string, registry.AuthConfig, error) {
33-
configs, err := getDockerAuthConfigs()
37+
cfg, configs, err := loadDockerAuth()
3438
if err != nil {
3539
reg := core.ExtractRegistry(image, defaultRegistryFn(ctx))
3640
return reg, registry.AuthConfig{}, err
3741
}
3842

39-
return dockerImageAuth(ctx, image, configs)
43+
return dockerImageAuth(ctx, image, cfg, configs)
44+
}
45+
46+
// loadDockerAuth loads the docker config once and the auth configs derived from it,
47+
// so that both the map lookup and the credentials store lookup see the same config.
48+
// A missing config file is not an error: it yields an empty config.
49+
func loadDockerAuth() (*dockercfg.Config, map[string]registry.AuthConfig, error) {
50+
cfg, err := getDockerConfig()
51+
if err != nil {
52+
if !errors.Is(err, os.ErrNotExist) {
53+
return nil, nil, err
54+
}
55+
cfg = &dockercfg.Config{}
56+
}
57+
58+
configs, err := getDockerAuthConfigsFromConfig(cfg)
59+
if err != nil {
60+
return nil, nil, err
61+
}
62+
63+
return cfg, configs, nil
4064
}
4165

4266
// dockerImageAuth returns the auth config for the given Docker image.
43-
func dockerImageAuth(ctx context.Context, image string, configs map[string]registry.AuthConfig) (string, registry.AuthConfig, error) {
67+
func dockerImageAuth(ctx context.Context, image string, cfg *dockercfg.Config, configs map[string]registry.AuthConfig) (string, registry.AuthConfig, error) {
4468
defaultRegistry := defaultRegistryFn(ctx)
4569
reg := core.ExtractRegistry(image, defaultRegistry)
4670

@@ -51,12 +75,16 @@ func dockerImageAuth(ctx context.Context, image string, configs map[string]regis
5175
reg = defaultRegistry // This is https://index.docker.io/v1/
5276
}
5377

54-
if cfg, ok := getRegistryAuth(reg, configs); ok {
55-
return reg, cfg, nil
78+
if ac, ok := getRegistryAuth(reg, configs); ok {
79+
return reg, ac, nil
5680
}
5781

58-
if cfg, ok := credentialsStoreAuth(reg); ok {
59-
return reg, cfg, nil
82+
ac, ok, err := credentialsStoreAuth(cfg, reg)
83+
if err != nil {
84+
return reg, registry.AuthConfig{}, err
85+
}
86+
if ok {
87+
return reg, ac, nil
6088
}
6189

6290
return reg, registry.AuthConfig{}, dockercfg.ErrCredentialsNotFound
@@ -68,28 +96,29 @@ func dockerImageAuth(ctx context.Context, image string, configs map[string]regis
6896
// A credentials store serves every registry, so unlike auths and credHelpers it has
6997
// no entries to enumerate up front: it can only be asked once the registry is known.
7098
// See https://docs.docker.com/reference/cli/docker/login/#credential-stores
71-
func credentialsStoreAuth(reg string) (registry.AuthConfig, bool) {
72-
cfg, err := getDockerConfig()
73-
if err != nil || cfg.CredentialsStore == "" {
74-
return registry.AuthConfig{}, false
99+
func credentialsStoreAuth(cfg *dockercfg.Config, reg string) (registry.AuthConfig, bool, error) {
100+
// A store cannot be asked about an empty host, and a missing helper binary
101+
// already reads as "no credentials" further down.
102+
if cfg.CredentialsStore == "" || reg == "" {
103+
return registry.AuthConfig{}, false, nil
75104
}
76105

77106
key, err := configKey(cfg)
78107
if err != nil {
79-
return registry.AuthConfig{}, false
108+
return registry.AuthConfig{}, false, err
80109
}
81110

82111
var ac registry.AuthConfig
83-
if err := creds.AuthConfig(reg, key, &ac); err != nil {
84-
return registry.AuthConfig{}, false
112+
if err := creds.AuthConfig(cfg, reg, key, &ac); err != nil {
113+
return registry.AuthConfig{}, false, err
85114
}
86115

87116
// The store reports an unknown registry as empty credentials rather than an error.
88117
if ac.Username == "" && ac.Password == "" && ac.IdentityToken == "" {
89-
return registry.AuthConfig{}, false
118+
return registry.AuthConfig{}, false, nil
90119
}
91120

92-
return ac, true
121+
return ac, true, nil
93122
}
94123

95124
func getRegistryAuth(reg string, cfgs map[string]registry.AuthConfig) (registry.AuthConfig, bool) {
@@ -159,8 +188,8 @@ var creds = &credentialsCache{entries: map[string]credentials{}}
159188

160189
// AuthConfig updates the details in authConfig for the given hostname
161190
// as determined by the details in configKey.
162-
func (c *credentialsCache) AuthConfig(hostname, configKey string, authConfig *registry.AuthConfig) error {
163-
u, p, err := creds.get(hostname, configKey)
191+
func (c *credentialsCache) AuthConfig(cfg *dockercfg.Config, hostname, configKey string, authConfig *registry.AuthConfig) error {
192+
u, p, err := creds.get(cfg, hostname, configKey)
164193
if err != nil {
165194
return err
166195
}
@@ -178,7 +207,7 @@ func (c *credentialsCache) AuthConfig(hostname, configKey string, authConfig *re
178207
// get returns the username and password for the given hostname
179208
// as determined by the details in configPath.
180209
// If the username is empty, the password is an identity token.
181-
func (c *credentialsCache) get(hostname, configKey string) (string, string, error) {
210+
func (c *credentialsCache) get(cfg *dockercfg.Config, hostname, configKey string) (string, string, error) {
182211
key := configKey + ":" + hostname
183212
c.mtx.RLock()
184213
entry, ok := c.entries[key]
@@ -189,7 +218,7 @@ func (c *credentialsCache) get(hostname, configKey string) (string, string, erro
189218
}
190219

191220
// No entry found, request and cache.
192-
user, password, err := getRegistryCredentials(hostname)
221+
user, password, err := getRegistryCredentials(cfg, hostname)
193222
if err != nil {
194223
return "", "", fmt.Errorf("getting credentials for %s: %w", hostname, err)
195224
}
@@ -224,6 +253,12 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
224253
return nil, err
225254
}
226255

256+
return getDockerAuthConfigsFromConfig(cfg)
257+
}
258+
259+
// getDockerAuthConfigsFromConfig returns a map with the auth configs from the given docker config
260+
// using the registry as the key
261+
func getDockerAuthConfigsFromConfig(cfg *dockercfg.Config) (map[string]registry.AuthConfig, error) {
227262
key, err := configKey(cfg)
228263
if err != nil {
229264
return nil, err
@@ -250,7 +285,7 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
250285
switch {
251286
case ac.Username == "" && ac.Password == "":
252287
// Look up credentials from the credential store.
253-
if err := creds.AuthConfig(k, key, &ac); err != nil {
288+
if err := creds.AuthConfig(cfg, k, key, &ac); err != nil {
254289
results <- authConfigResult{err: err}
255290
return
256291
}
@@ -270,7 +305,7 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
270305
defer wg.Done()
271306

272307
var ac registry.AuthConfig
273-
if err := creds.AuthConfig(k, key, &ac); err != nil {
308+
if err := creds.AuthConfig(cfg, k, key, &ac); err != nil {
274309
results <- authConfigResult{err: err}
275310
return
276311
}

docker_auth_test.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
_ "embed"
66
"encoding/base64"
7+
"errors"
78
"fmt"
89
"net"
910
"os"
@@ -147,7 +148,7 @@ func TestDockerImageAuth(t *testing.T) {
147148
getRegistryCredentials = old
148149
creds.reset()
149150
})
150-
getRegistryCredentials = func(hostname string) (string, string, error) {
151+
getRegistryCredentials = func(_ *dockercfg.Config, hostname string) (string, string, error) {
151152
if hostname == exampleAuth {
152153
return "gopher", "secret", nil
153154
}
@@ -161,6 +162,48 @@ func TestDockerImageAuth(t *testing.T) {
161162
require.Equal(t, "secret", cfg.Password)
162163
})
163164

165+
t.Run("retrieve auth from the credentials store for a scheme-less registry", func(t *testing.T) {
166+
// Registries in image references carry no scheme, and that is the host the
167+
// store is asked for, as the docker CLI does.
168+
t.Setenv("DOCKER_AUTH_CONFIG", `{"credsStore":"desktop"}`)
169+
creds.reset()
170+
171+
old := getRegistryCredentials
172+
t.Cleanup(func() {
173+
getRegistryCredentials = old
174+
creds.reset()
175+
})
176+
getRegistryCredentials = func(_ *dockercfg.Config, hostname string) (string, string, error) {
177+
if hostname == "example-auth.com" {
178+
return "gopher", "secret", nil
179+
}
180+
return "", "", nil
181+
}
182+
183+
reg, cfg, err := DockerImageAuth(context.Background(), "example-auth.com/my/image:latest")
184+
require.NoError(t, err)
185+
require.Equal(t, "example-auth.com", reg)
186+
require.Equal(t, "gopher", cfg.Username)
187+
require.Equal(t, "secret", cfg.Password)
188+
})
189+
190+
t.Run("credentials store errors are reported", func(t *testing.T) {
191+
t.Setenv("DOCKER_AUTH_CONFIG", `{"credsStore":"desktop"}`)
192+
creds.reset()
193+
194+
old := getRegistryCredentials
195+
t.Cleanup(func() {
196+
getRegistryCredentials = old
197+
creds.reset()
198+
})
199+
getRegistryCredentials = func(*dockercfg.Config, string) (string, string, error) {
200+
return "", "", errors.New("helper exploded")
201+
}
202+
203+
_, _, err := DockerImageAuth(context.Background(), exampleAuth+"/my/image:latest")
204+
require.ErrorContains(t, err, "helper exploded")
205+
})
206+
164207
t.Run("credentials store without an entry for the registry", func(t *testing.T) {
165208
t.Setenv("DOCKER_AUTH_CONFIG", `{"credsStore":"desktop"}`)
166209
creds.reset()
@@ -171,7 +214,7 @@ func TestDockerImageAuth(t *testing.T) {
171214
creds.reset()
172215
})
173216
// A store reports an unknown registry as empty credentials, not an error.
174-
getRegistryCredentials = func(string) (string, string, error) {
217+
getRegistryCredentials = func(*dockercfg.Config, string) (string, string, error) {
175218
return "", "", nil
176219
}
177220

@@ -467,7 +510,7 @@ func Test_getDockerAuthConfigs(t *testing.T) {
467510
getRegistryCredentials = old
468511
creds.reset() // Ensure our mocked results aren't cached.
469512
})
470-
getRegistryCredentials = func(hostname string) (string, string, error) {
513+
getRegistryCredentials = func(_ *dockercfg.Config, hostname string) (string, string, error) {
471514
switch hostname {
472515
case core.IndexDockerIO:
473516
return "", "identity-token", nil

0 commit comments

Comments
 (0)