Skip to content

Commit fcb2418

Browse files
authored
Merge pull request kubernetes#128152 from stlaz/ensure-secret-images
Multi-tenancy in accessing node images via Pod API
2 parents 5a6ace2 + 146515a commit fcb2418

File tree

57 files changed

+4466
-511
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+4466
-511
lines changed

pkg/credentialprovider/keyring.go

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@ limitations under the License.
1717
package credentialprovider
1818

1919
import (
20+
"crypto/sha256"
21+
"encoding/hex"
22+
"encoding/json"
2023
"net"
2124
"net/url"
2225
"path/filepath"
2326
"sort"
2427
"strings"
2528

2629
"k8s.io/apimachinery/pkg/util/sets"
30+
utilfeature "k8s.io/apiserver/pkg/util/feature"
2731
"k8s.io/klog/v2"
32+
"k8s.io/kubernetes/pkg/features"
2833
)
2934

3035
// DockerKeyring tracks a set of docker registry credentials, maintaining a
@@ -35,13 +40,13 @@ import (
3540
// most specific match for a given image
3641
// - iterating a map does not yield predictable results
3742
type DockerKeyring interface {
38-
Lookup(image string) ([]AuthConfig, bool)
43+
Lookup(image string) ([]TrackedAuthConfig, bool)
3944
}
4045

4146
// BasicDockerKeyring is a trivial map-backed implementation of DockerKeyring
4247
type BasicDockerKeyring struct {
4348
index []string
44-
creds map[string][]AuthConfig
49+
creds map[string][]TrackedAuthConfig
4550
}
4651

4752
// providersDockerKeyring is an implementation of DockerKeyring that
@@ -50,6 +55,47 @@ type providersDockerKeyring struct {
5055
Providers []DockerConfigProvider
5156
}
5257

58+
// TrackedAuthConfig wraps the AuthConfig and adds information about the source
59+
// of the credentials.
60+
type TrackedAuthConfig struct {
61+
AuthConfig
62+
AuthConfigHash string
63+
64+
Source *CredentialSource
65+
}
66+
67+
// NewTrackedAuthConfig initializes the TrackedAuthConfig structure by adding
68+
// the source information to the supplied AuthConfig. It also counts a hash of the
69+
// AuthConfig and keeps it in the returned structure.
70+
//
71+
// The supplied CredentialSource is only used when the "KubeletEnsureSecretPulledImages"
72+
// is enabled, the same applies for counting the hash.
73+
func NewTrackedAuthConfig(c *AuthConfig, src *CredentialSource) *TrackedAuthConfig {
74+
if c == nil {
75+
panic("cannot construct TrackedAuthConfig with a nil AuthConfig")
76+
}
77+
78+
authConfig := &TrackedAuthConfig{
79+
AuthConfig: *c,
80+
}
81+
82+
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletEnsureSecretPulledImages) {
83+
authConfig.Source = src
84+
authConfig.AuthConfigHash = hashAuthConfig(c)
85+
}
86+
return authConfig
87+
}
88+
89+
type CredentialSource struct {
90+
Secret SecretCoordinates
91+
}
92+
93+
type SecretCoordinates struct {
94+
UID string
95+
Namespace string
96+
Name string
97+
}
98+
5399
// AuthConfig contains authorization information for connecting to a Registry
54100
// This type mirrors "github.com/docker/docker/api/types.AuthConfig"
55101
type AuthConfig struct {
@@ -72,11 +118,13 @@ type AuthConfig struct {
72118
RegistryToken string `json:"registrytoken,omitempty"`
73119
}
74120

75-
// Add add some docker config in basic docker keyring
76-
func (dk *BasicDockerKeyring) Add(cfg DockerConfig) {
121+
// Add inserts the docker config `cfg` into the basic docker keyring. It attaches
122+
// the `src` information that describes where the docker config `cfg` comes from.
123+
// `src` is nil if the docker config is globally available on the node.
124+
func (dk *BasicDockerKeyring) Add(src *CredentialSource, cfg DockerConfig) {
77125
if dk.index == nil {
78126
dk.index = make([]string, 0)
79-
dk.creds = make(map[string][]AuthConfig)
127+
dk.creds = make(map[string][]TrackedAuthConfig)
80128
}
81129
for loc, ident := range cfg {
82130
creds := AuthConfig{
@@ -111,7 +159,9 @@ func (dk *BasicDockerKeyring) Add(cfg DockerConfig) {
111159
} else {
112160
key = parsed.Host
113161
}
114-
dk.creds[key] = append(dk.creds[key], creds)
162+
trackedCreds := NewTrackedAuthConfig(&creds, src)
163+
164+
dk.creds[key] = append(dk.creds[key], *trackedCreds)
115165
dk.index = append(dk.index, key)
116166
}
117167

@@ -235,9 +285,9 @@ func URLsMatch(globURL *url.URL, targetURL *url.URL) (bool, error) {
235285
// Lookup implements the DockerKeyring method for fetching credentials based on image name.
236286
// Multiple credentials may be returned if there are multiple potentially valid credentials
237287
// available. This allows for rotation.
238-
func (dk *BasicDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
288+
func (dk *BasicDockerKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
239289
// range over the index as iterating over a map does not provide a predictable ordering
240-
ret := []AuthConfig{}
290+
ret := []TrackedAuthConfig{}
241291
for _, k := range dk.index {
242292
// both k and image are schemeless URLs because even though schemes are allowed
243293
// in the credential configurations, we remove them in Add.
@@ -257,30 +307,32 @@ func (dk *BasicDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
257307
}
258308
}
259309

260-
return []AuthConfig{}, false
310+
return []TrackedAuthConfig{}, false
261311
}
262312

263313
// Lookup implements the DockerKeyring method for fetching credentials
264314
// based on image name.
265-
func (dk *providersDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
315+
func (dk *providersDockerKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
266316
keyring := &BasicDockerKeyring{}
267317

268318
for _, p := range dk.Providers {
269-
keyring.Add(p.Provide(image))
319+
// TODO: the source should probably change once we depend on service accounts (KEP-4412).
320+
// Perhaps `Provide()` should return the source modified to accommodate this?
321+
keyring.Add(nil, p.Provide(image))
270322
}
271323

272324
return keyring.Lookup(image)
273325
}
274326

275327
// FakeKeyring a fake config credentials
276328
type FakeKeyring struct {
277-
auth []AuthConfig
329+
auth []TrackedAuthConfig
278330
ok bool
279331
}
280332

281333
// Lookup implements the DockerKeyring method for fetching credentials based on image name
282334
// return fake auth and ok
283-
func (f *FakeKeyring) Lookup(image string) ([]AuthConfig, bool) {
335+
func (f *FakeKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
284336
return f.auth, f.ok
285337
}
286338

@@ -289,8 +341,8 @@ type UnionDockerKeyring []DockerKeyring
289341

290342
// Lookup implements the DockerKeyring method for fetching credentials based on image name.
291343
// return each credentials
292-
func (k UnionDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
293-
authConfigs := []AuthConfig{}
344+
func (k UnionDockerKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
345+
authConfigs := []TrackedAuthConfig{}
294346
for _, subKeyring := range k {
295347
if subKeyring == nil {
296348
continue
@@ -302,3 +354,14 @@ func (k UnionDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
302354

303355
return authConfigs, (len(authConfigs) > 0)
304356
}
357+
358+
func hashAuthConfig(creds *AuthConfig) string {
359+
credBytes, err := json.Marshal(creds)
360+
if err != nil {
361+
return ""
362+
}
363+
364+
hash := sha256.New()
365+
hash.Write([]byte(credBytes))
366+
return hex.EncodeToString(hash.Sum(nil))
367+
}

pkg/credentialprovider/keyring_test.go

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import (
2121
"fmt"
2222
"reflect"
2323
"testing"
24+
25+
utilfeature "k8s.io/apiserver/pkg/util/feature"
26+
featuregatetesting "k8s.io/component-base/featuregate/testing"
27+
"k8s.io/kubernetes/pkg/features"
2428
)
2529

2630
func TestURLsMatch(t *testing.T) {
@@ -222,7 +226,7 @@ func TestDockerKeyringForGlob(t *testing.T) {
222226
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
223227
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
224228
} else {
225-
keyring.Add(cfg)
229+
keyring.Add(nil, cfg)
226230
}
227231

228232
creds, ok := keyring.Lookup(test.targetURL + "/foo/bar")
@@ -290,7 +294,7 @@ func TestKeyringMiss(t *testing.T) {
290294
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
291295
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
292296
} else {
293-
keyring.Add(cfg)
297+
keyring.Add(nil, cfg)
294298
}
295299

296300
_, ok := keyring.Lookup(test.lookupURL + "/foo/bar")
@@ -318,7 +322,7 @@ func TestKeyringMissWithDockerHubCredentials(t *testing.T) {
318322
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
319323
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
320324
} else {
321-
keyring.Add(cfg)
325+
keyring.Add(nil, cfg)
322326
}
323327

324328
val, ok := keyring.Lookup("world.mesos.org/foo/bar")
@@ -344,7 +348,7 @@ func TestKeyringHitWithUnqualifiedDockerHub(t *testing.T) {
344348
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
345349
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
346350
} else {
347-
keyring.Add(cfg)
351+
keyring.Add(nil, cfg)
348352
}
349353

350354
creds, ok := keyring.Lookup("google/docker-registry")
@@ -353,7 +357,7 @@ func TestKeyringHitWithUnqualifiedDockerHub(t *testing.T) {
353357
return
354358
}
355359
if len(creds) > 1 {
356-
t.Errorf("Got more hits than expected: %s", creds)
360+
t.Errorf("Got more hits than expected: %v", creds)
357361
}
358362
val := creds[0]
359363

@@ -385,7 +389,7 @@ func TestKeyringHitWithUnqualifiedLibraryDockerHub(t *testing.T) {
385389
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
386390
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
387391
} else {
388-
keyring.Add(cfg)
392+
keyring.Add(nil, cfg)
389393
}
390394

391395
creds, ok := keyring.Lookup("jenkins")
@@ -394,7 +398,7 @@ func TestKeyringHitWithUnqualifiedLibraryDockerHub(t *testing.T) {
394398
return
395399
}
396400
if len(creds) > 1 {
397-
t.Errorf("Got more hits than expected: %s", creds)
401+
t.Errorf("Got more hits than expected: %v", creds)
398402
}
399403
val := creds[0]
400404

@@ -426,7 +430,7 @@ func TestKeyringHitWithQualifiedDockerHub(t *testing.T) {
426430
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
427431
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
428432
} else {
429-
keyring.Add(cfg)
433+
keyring.Add(nil, cfg)
430434
}
431435

432436
creds, ok := keyring.Lookup(url + "/google/docker-registry")
@@ -435,7 +439,7 @@ func TestKeyringHitWithQualifiedDockerHub(t *testing.T) {
435439
return
436440
}
437441
if len(creds) > 2 {
438-
t.Errorf("Got more hits than expected: %s", creds)
442+
t.Errorf("Got more hits than expected: %v", creds)
439443
}
440444
val := creds[0]
441445

@@ -498,20 +502,24 @@ func TestProvidersDockerKeyring(t *testing.T) {
498502
}
499503

500504
func TestDockerKeyringLookup(t *testing.T) {
505+
// turn on the ensure secret pulled images feature to get the hashes with the creds
506+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletEnsureSecretPulledImages, true)
501507
ada := AuthConfig{
502508
Username: "ada",
503509
Password: "smash", // Fake value for testing.
504510
505511
}
512+
adaHash := "353258b53f5e9a57b059eab3f05312fc35bbeb874f08ce101e7bf0bf46977423"
506513

507514
grace := AuthConfig{
508515
Username: "grace",
509516
Password: "squash", // Fake value for testing.
510517
511518
}
519+
graceHash := "f949b3837a1eb733a951b6aeda0b3327c09ec50c917de9ca35818e8fbf567e29"
512520

513521
dk := &BasicDockerKeyring{}
514-
dk.Add(DockerConfig{
522+
dk.Add(nil, DockerConfig{
515523
"bar.example.com/pong": DockerConfigEntry{
516524
Username: grace.Username,
517525
Password: grace.Password,
@@ -526,27 +534,27 @@ func TestDockerKeyringLookup(t *testing.T) {
526534

527535
tests := []struct {
528536
image string
529-
match []AuthConfig
537+
match []TrackedAuthConfig
530538
ok bool
531539
}{
532540
// direct match
533-
{"bar.example.com", []AuthConfig{ada}, true},
541+
{"bar.example.com", []TrackedAuthConfig{{AuthConfig: ada, AuthConfigHash: adaHash}}, true},
534542

535543
// direct match deeper than other possible matches
536-
{"bar.example.com/pong", []AuthConfig{grace, ada}, true},
544+
{"bar.example.com/pong", []TrackedAuthConfig{{AuthConfig: grace, AuthConfigHash: graceHash}, {AuthConfig: ada, AuthConfigHash: adaHash}}, true},
537545

538546
// no direct match, deeper path ignored
539-
{"bar.example.com/ping", []AuthConfig{ada}, true},
547+
{"bar.example.com/ping", []TrackedAuthConfig{{AuthConfig: ada, AuthConfigHash: adaHash}}, true},
540548

541549
// match first part of path token
542-
{"bar.example.com/pongz", []AuthConfig{grace, ada}, true},
550+
{"bar.example.com/pongz", []TrackedAuthConfig{{AuthConfig: grace, AuthConfigHash: graceHash}, {AuthConfig: ada, AuthConfigHash: adaHash}}, true},
543551

544552
// match regardless of sub-path
545-
{"bar.example.com/pong/pang", []AuthConfig{grace, ada}, true},
553+
{"bar.example.com/pong/pang", []TrackedAuthConfig{{AuthConfig: grace, AuthConfigHash: graceHash}, {AuthConfig: ada, AuthConfigHash: adaHash}}, true},
546554

547555
// no host match
548-
{"example.com", []AuthConfig{}, false},
549-
{"foo.example.com", []AuthConfig{}, false},
556+
{"example.com", []TrackedAuthConfig{}, false},
557+
{"foo.example.com", []TrackedAuthConfig{}, false},
550558
}
551559

552560
for i, tt := range tests {
@@ -565,14 +573,17 @@ func TestDockerKeyringLookup(t *testing.T) {
565573
// by images that only match the hostname.
566574
// NOTE: the above covers the case of a more specific match trumping just hostname.
567575
func TestIssue3797(t *testing.T) {
576+
// turn on the ensure secret pulled images feature to get the hashes with the creds
577+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletEnsureSecretPulledImages, true)
568578
rex := AuthConfig{
569579
Username: "rex",
570580
Password: "tiny arms", // Fake value for testing.
571581
572582
}
583+
rexHash := "899748fec74c8dd761845fca727f4249b05be275ff24026676fcd4351f656363"
573584

574585
dk := &BasicDockerKeyring{}
575-
dk.Add(DockerConfig{
586+
dk.Add(nil, DockerConfig{
576587
"https://quay.io/v1/": DockerConfigEntry{
577588
Username: rex.Username,
578589
Password: rex.Password,
@@ -582,15 +593,15 @@ func TestIssue3797(t *testing.T) {
582593

583594
tests := []struct {
584595
image string
585-
match []AuthConfig
596+
match []TrackedAuthConfig
586597
ok bool
587598
}{
588599
// direct match
589-
{"quay.io", []AuthConfig{rex}, true},
600+
{"quay.io", []TrackedAuthConfig{{AuthConfig: rex, AuthConfigHash: rexHash}}, true},
590601

591602
// partial matches
592-
{"quay.io/foo", []AuthConfig{rex}, true},
593-
{"quay.io/foo/bar", []AuthConfig{rex}, true},
603+
{"quay.io/foo", []TrackedAuthConfig{{AuthConfig: rex, AuthConfigHash: rexHash}}, true},
604+
{"quay.io/foo/bar", []TrackedAuthConfig{{AuthConfig: rex, AuthConfigHash: rexHash}}, true},
594605
}
595606

596607
for i, tt := range tests {

pkg/credentialprovider/plugin/plugins.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,12 @@ func NewExternalCredentialProviderDockerKeyring(podNamespace, podName, podUID, s
8787
return keyring
8888
}
8989

90-
func (k *externalCredentialProviderKeyring) Lookup(image string) ([]credentialprovider.AuthConfig, bool) {
90+
func (k *externalCredentialProviderKeyring) Lookup(image string) ([]credentialprovider.TrackedAuthConfig, bool) {
9191
keyring := &credentialprovider.BasicDockerKeyring{}
9292

9393
for _, p := range k.providers {
94-
keyring.Add(p.Provide(image))
94+
// TODO: modify the credentialprovider.CredentialSource to contain the SA/pod information
95+
keyring.Add(nil, p.Provide(image))
9596
}
9697

9798
return keyring.Lookup(image)

0 commit comments

Comments
 (0)