Skip to content

Commit

Permalink
authn.kubernetes.Resolve now behaves exactly like Kubernetes (#1349)
Browse files Browse the repository at this point in the history
* authn.kubernetes.Resolve now behaves exactly like Kubernetes

Prior we weren't matching host names and partial paths properly

This was breaking Gitlab and Azure tag to digest resolution in Knative

* PR feedback

* bump ggcr root package so this module pulls in the AuthConfig json helpers

* go mod tidy -compat=1.17
  • Loading branch information
dprotaso committed Apr 14, 2022
1 parent f1b7291 commit 892d7a8
Show file tree
Hide file tree
Showing 4 changed files with 379 additions and 228 deletions.
2 changes: 1 addition & 1 deletion pkg/authn/k8schain/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ replace (
require (
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220228164355-396b2034c795
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21
github.com/google/go-containerregistry v0.8.1-0.20220110151055-a61fd0a8e2bb
github.com/google/go-containerregistry v0.8.1-0.20220414133640-f1b729141d33
github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20220301182634-bfe2ffc6b6bd
k8s.io/api v0.23.4
k8s.io/client-go v0.23.4
Expand Down
4 changes: 2 additions & 2 deletions pkg/authn/kubernetes/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ go 1.17
replace github.com/google/go-containerregistry => ../../../

require (
github.com/google/go-containerregistry v0.8.0
github.com/google/go-cmp v0.5.7
github.com/google/go-containerregistry v0.8.1-0.20220414133640-f1b729141d33
k8s.io/api v0.23.4
k8s.io/apimachinery v0.23.4
k8s.io/client-go v0.23.4
Expand All @@ -20,7 +21,6 @@ require (
github.com/go-logr/logr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down
159 changes: 123 additions & 36 deletions pkg/authn/kubernetes/keychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ package kubernetes

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/url"
"path/filepath"
"sort"
"strings"

"github.com/google/go-containerregistry/pkg/authn"
Expand Down Expand Up @@ -101,29 +103,34 @@ func NewInCluster(ctx context.Context, opt Options) (authn.Keychain, error) {
return New(ctx, client, opt)
}

type dockerConfigJSON struct {
Auths map[string]authn.AuthConfig
}

// NewFromPullSecrets returns a new authn.Keychain suitable for resolving image references as
// scoped by the pull secrets.
func NewFromPullSecrets(ctx context.Context, secrets []corev1.Secret) (authn.Keychain, error) {
m := map[string]authn.AuthConfig{}
keyring := &keyring{
index: make([]string, 0),
creds: make(map[string][]authn.AuthConfig),
}

var cfg dockerConfigJSON

// From: https://github.com/kubernetes/kubernetes/blob/0dcafb1f37ee522be3c045753623138e5b907001/pkg/credentialprovider/keyring.go
for _, secret := range secrets {
auths := map[string]authn.AuthConfig{}
if b, exists := secret.Data[corev1.DockerConfigJsonKey]; secret.Type == corev1.SecretTypeDockerConfigJson && exists && len(b) > 0 {
var cfg struct {
Auths map[string]authn.AuthConfig
}
if err := json.Unmarshal(b, &cfg); err != nil {
return nil, err
}
auths = cfg.Auths
}
if b, exists := secret.Data[corev1.DockerConfigKey]; secret.Type == corev1.SecretTypeDockercfg && exists && len(b) > 0 {
if err := json.Unmarshal(b, &auths); err != nil {
if err := json.Unmarshal(b, &cfg.Auths); err != nil {
return nil, err
}
}

for registry, v := range auths {
// From: https://github.com/kubernetes/kubernetes/blob/0dcafb1f37ee522be3c045753623138e5b907001/pkg/credentialprovider/keyring.go
for registry, v := range cfg.Auths {
value := registry
if !strings.HasPrefix(value, "https://") && !strings.HasPrefix(value, "http://") {
value = "https://" + value
Expand All @@ -150,45 +157,125 @@ func NewFromPullSecrets(ctx context.Context, secrets []corev1.Secret) (authn.Key
key = parsed.Host
}

// Don't overwrite previously specified Auths for a given key.
if _, found := m[key]; !found {
m[key] = v
if _, ok := keyring.creds[key]; !ok {
keyring.index = append(keyring.index, key)
}

keyring.creds[key] = append(keyring.creds[key], v)

}

// We reverse sort in to give more specific (aka longer) keys priority
// when matching for creds
sort.Sort(sort.Reverse(sort.StringSlice(keyring.index)))
}
return authsKeychain(m), nil
return keyring, nil
}

type keyring struct {
index []string
creds map[string][]authn.AuthConfig
}

type authsKeychain map[string]authn.AuthConfig

func (kc authsKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
// Check for an auth that matches the repository, then if that's not
// found, one that matches the registry.
var cfg authn.AuthConfig
for _, key := range []string{target.String(), target.RegistryStr()} {
var ok bool
cfg, ok = kc[key]
if ok {
break
func (keyring *keyring) Resolve(target authn.Resource) (authn.Authenticator, error) {
image := target.String()
auths := []authn.AuthConfig{}

for _, k := range keyring.index {
// both k and image are schemeless URLs because even though schemes are allowed
// in the credential configurations, we remove them when constructing the keyring
if matched, _ := urlsMatchStr(k, image); matched {
auths = append(auths, keyring.creds[k]...)
}
}
empty := authn.AuthConfig{}
if cfg == empty {

if len(auths) == 0 {
return authn.Anonymous, nil
}
if cfg.Auth != "" {
dec, err := base64.StdEncoding.DecodeString(cfg.Auth)

return toAuthenticator(auths)
}

// urlsMatchStr is wrapper for URLsMatch, operating on strings instead of URLs.
func urlsMatchStr(glob string, target string) (bool, error) {
globURL, err := parseSchemelessURL(glob)
if err != nil {
return false, err
}
targetURL, err := parseSchemelessURL(target)
if err != nil {
return false, err
}
return urlsMatch(globURL, targetURL)
}

// parseSchemelessURL parses a schemeless url and returns a url.URL
// url.Parse require a scheme, but ours don't have schemes. Adding a
// scheme to make url.Parse happy, then clear out the resulting scheme.
func parseSchemelessURL(schemelessURL string) (*url.URL, error) {
parsed, err := url.Parse("https://" + schemelessURL)
if err != nil {
return nil, err
}
// clear out the resulting scheme
parsed.Scheme = ""
return parsed, nil
}

// splitURL splits the host name into parts, as well as the port
func splitURL(url *url.URL) (parts []string, port string) {
host, port, err := net.SplitHostPort(url.Host)
if err != nil {
// could not parse port
host, port = url.Host, ""
}
return strings.Split(host, "."), port
}

// urlsMatch checks whether the given target url matches the glob url, which may have
// glob wild cards in the host name.
//
// Examples:
// globURL=*.docker.io, targetURL=blah.docker.io => match
// globURL=*.docker.io, targetURL=not.right.io => no match
//
// Note that we don't support wildcards in ports and paths yet.
func urlsMatch(globURL *url.URL, targetURL *url.URL) (bool, error) {
globURLParts, globPort := splitURL(globURL)
targetURLParts, targetPort := splitURL(targetURL)
if globPort != targetPort {
// port doesn't match
return false, nil
}
if len(globURLParts) != len(targetURLParts) {
// host name does not have the same number of parts
return false, nil
}
if !strings.HasPrefix(targetURL.Path, globURL.Path) {
// the path of the credential must be a prefix
return false, nil
}
for k, globURLPart := range globURLParts {
targetURLPart := targetURLParts[k]
matched, err := filepath.Match(globURLPart, targetURLPart)
if err != nil {
return nil, err
return false, err
}
parts := strings.SplitN(string(dec), ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("unable to parse auth field, must be formatted as base64(username:password)")
if !matched {
// glob mismatch for some part
return false, nil
}
cfg.Username = parts[0]
cfg.Password = parts[1]
}
// everything matches
return true, nil
}

func toAuthenticator(configs []authn.AuthConfig) (authn.Authenticator, error) {
cfg := configs[0]

if cfg.Auth != "" {
cfg.Auth = ""
}

return authn.FromConfig(authn.AuthConfig(cfg)), nil
return authn.FromConfig(cfg), nil
}

0 comments on commit 892d7a8

Please sign in to comment.