Look for usable on-cluster credentials using k8schain (#243)

* dep ensure and use k8schain

* checkpoint

* fix vendoring, stuff builds

* Use k8schain for pushes too

* Use NewNoClient

* update ggcr dep

* Move k8schain usage to image_util.go
This commit is contained in:
Jason Hall
2018-07-21 10:22:13 -07:00
committed by priyawadhwa
parent eb6faa05a0
commit 0e3fc0bcd7
796 changed files with 400528 additions and 40 deletions
@@ -0,0 +1,18 @@
// Copyright 2018 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// package k8schain exposes an implementation of the authn.Keychain interface
// based on the semantics the Kubelet follows when pulling the images for a
// Pod in Kubernetes.
package k8schain
@@ -0,0 +1,165 @@
// Copyright 2018 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package k8schain
import (
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/kubernetes/pkg/credentialprovider"
credentialprovidersecrets "k8s.io/kubernetes/pkg/credentialprovider/secrets"
// Credential providers
_ "k8s.io/kubernetes/pkg/credentialprovider/aws"
_ "k8s.io/kubernetes/pkg/credentialprovider/azure"
_ "k8s.io/kubernetes/pkg/credentialprovider/gcp"
// TODO(mattmoor): This doesn't seem to build, figure out why `dep ensure`
// is not working and add constraints.
// _ "k8s.io/kubernetes/pkg/credentialprovider/rancher"
)
// Options holds configuration data for guiding credential resolution.
type Options struct {
// Namespace holds the namespace inside of which we are resolving the
// image reference. If empty, "default" is assumed.
Namespace string
// ServiceAccountName holds the serviceaccount as which the container
// will run (scoped to Namespace). If empty, "default" is assumed.
ServiceAccountName string
// ImagePullSecrets holds the names of the Kubernetes secrets (scoped to
// Namespace) containing credential data to use for the image pull.
ImagePullSecrets []string
}
// origKeyRing is a variable so that testing can adjust it.
var origKeyRing = credentialprovider.NewDockerKeyring()
// New returns a new authn.Keychain suitable for resolving image references as
// scoped by the provided Options. It speaks to Kubernetes through the provided
// client interface.
func New(client kubernetes.Interface, opt Options) (authn.Keychain, error) {
if opt.Namespace == "" {
opt.Namespace = "default"
}
if opt.ServiceAccountName == "" {
opt.ServiceAccountName = "default"
}
// Implement a Kubernetes-style authentication keychain.
// This needs to support roughly the following kinds of authentication:
// 1) The implicit authentication from k8s.io/kubernetes/pkg/credentialprovider
// 2) The explicit authentication from imagePullSecrets on Pod
// 3) The semi-implicit authentication where imagePullSecrets are on the
// Pod's service account.
// First, fetch all of the explicitly declared pull secrets
var pullSecrets []v1.Secret
if client != nil {
for _, name := range opt.ImagePullSecrets {
ps, err := client.CoreV1().Secrets(opt.Namespace).Get(name, metav1.GetOptions{})
if err != nil {
return nil, err
}
pullSecrets = append(pullSecrets, *ps)
}
// Second, fetch all of the pull secrets attached to our service account.
sa, err := client.CoreV1().ServiceAccounts(opt.Namespace).Get(opt.ServiceAccountName, metav1.GetOptions{})
if err != nil {
return nil, err
}
for _, localObj := range sa.ImagePullSecrets {
ps, err := client.CoreV1().Secrets(opt.Namespace).Get(localObj.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
pullSecrets = append(pullSecrets, *ps)
}
}
// Third, extend the default keyring with the pull secrets.
kr, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, origKeyRing)
if err != nil {
return nil, err
}
return &keychain{
keyring: kr,
}, nil
}
// NewInCluster returns a new authn.Keychain suitable for resolving image references as
// scoped by the provided Options, constructing a kubernetes.Interface based on in-cluster
// authentication.
func NewInCluster(opt Options) (authn.Keychain, error) {
clusterConfig, err := rest.InClusterConfig()
if err != nil {
return nil, err
}
client, err := kubernetes.NewForConfig(clusterConfig)
if err != nil {
return nil, err
}
return New(client, opt)
}
// NewNoClient returns a new authn.Keychain that supports the portions of the K8s keychain
// that don't read ImagePullSecrets. This limits it to roughly the Node-identity-based
// authentication schemes in Kubernetes pkg/credentialprovider. This version of the
// k8schain drops the requirement that we run as a K8s serviceaccount with access to all
// of the on-cluster secrets. This drop in fidelity also diminishes its value as a stand-in
// for Kubernetes authentication, but this actually targets a different use-case. What
// remains is an interesting sweet spot: this variant can serve as a credential provider
// for all of the major public clouds, but in library form (vs. an executable you exec).
func NewNoClient() (authn.Keychain, error) {
return New(nil, Options{})
}
type lazyProvider credentialprovider.LazyAuthConfiguration
// Authorization implements Authenticator.
func (lp lazyProvider) Authorization() (string, error) {
authConfig := credentialprovider.LazyProvide(credentialprovider.LazyAuthConfiguration(lp))
if authConfig.Auth != "" {
return "Basic " + authConfig.Auth, nil
}
if authConfig.Username != "" {
basic := authn.Basic{
Username: authConfig.Username,
Password: authConfig.Password,
}
return basic.Authorization()
}
return authn.Anonymous.Authorization()
}
type keychain struct {
keyring credentialprovider.DockerKeyring
}
// Resolve implements authn.Keychain
func (kc *keychain) Resolve(reg name.Registry) (authn.Authenticator, error) {
// TODO(mattmoor): Lookup expects an image reference and we only have a registry,
// find something better than this.
creds, found := kc.keyring.Lookup(reg.String() + "/foo/bar")
if !found || len(creds) < 1 {
return authn.Anonymous, nil
}
// TODO(mattmoor): How to support multiple credentials?
return lazyProvider(creds[0]), nil
}
+7 -7
View File
@@ -21,7 +21,7 @@ import (
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"runtime"
"github.com/google/go-containerregistry/pkg/name"
@@ -43,7 +43,7 @@ func configDir() (string, error) {
return dc, nil
}
if h := dockerUserHomeDir(); h != "" {
return path.Join(dockerUserHomeDir(), ".docker"), nil
return filepath.Join(dockerUserHomeDir(), ".docker"), nil
}
return "", errNoHomeDir
}
@@ -100,19 +100,19 @@ var (
func (dk *defaultKeychain) Resolve(reg name.Registry) (Authenticator, error) {
dir, err := configDir()
if err != nil {
log.Printf("Unable to determine config dir, falling back on anonymous: %v", err)
log.Printf("Unable to determine config dir: %v", err)
return Anonymous, nil
}
file := path.Join(dir, "config.json")
file := filepath.Join(dir, "config.json")
content, err := ioutil.ReadFile(file)
if err != nil {
log.Printf("Unable to read %q, falling back on anonymous: %v", file, err)
log.Printf("Unable to read %q: %v", file, err)
return Anonymous, nil
}
var cf cfg
if err := json.Unmarshal(content, &cf); err != nil {
log.Printf("Unable to parse %q, falling back on anonymous: %v", file, err)
log.Printf("Unable to parse %q: %v", file, err)
return Anonymous, nil
}
@@ -147,6 +147,6 @@ func (dk *defaultKeychain) Resolve(reg name.Registry) (Authenticator, error) {
}
}
log.Printf("No matching credentials found for %v, falling back on anonymous", reg)
// Fallback on anonymous.
return Anonymous, nil
}
@@ -0,0 +1,45 @@
// Copyright 2018 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package authn
import (
"github.com/google/go-containerregistry/pkg/name"
)
type multiKeychain struct {
keychains []Keychain
}
// Assert that our multi-keychain implements Keychain.
var _ (Keychain) = (*multiKeychain)(nil)
// NewMultiKeychain composes a list of keychains into one new keychain.
func NewMultiKeychain(kcs ...Keychain) Keychain {
return &multiKeychain{keychains: kcs}
}
// Resolve implements Keychain.
func (mk *multiKeychain) Resolve(reg name.Registry) (Authenticator, error) {
for _, kc := range mk.keychains {
auth, err := kc.Resolve(reg)
if err != nil {
return nil, err
}
if auth != Anonymous {
return auth, nil
}
}
return Anonymous, nil
}
+1 -1
View File
@@ -79,7 +79,7 @@ func NewRepository(name string, strict Strictness) (Repository, error) {
if len(parts) == 2 && (strings.ContainsRune(parts[0], '.') || strings.ContainsRune(parts[0], ':')) {
// The first part of the repository is treated as the registry domain
// iff it contains a '.' or ':' character, otherwise it is all repository
// and the domain defaults to DockerHub.
// and the domain defaults to Docker Hub.
registry = parts[0]
repo = parts[1]
}
+10 -15
View File
@@ -15,6 +15,7 @@
package remote
import (
"log"
"net/http"
"github.com/google/go-containerregistry/pkg/authn"
@@ -24,7 +25,8 @@ import (
// on a remote image
func WithTransport(t http.RoundTripper) ImageOption {
return func(i *imageOpener) error {
return i.setTransport(t)
i.transport = t
return nil
}
}
@@ -32,7 +34,8 @@ func WithTransport(t http.RoundTripper) ImageOption {
// on a remote image
func WithAuth(auth authn.Authenticator) ImageOption {
return func(i *imageOpener) error {
return i.setAuth(auth)
i.auth = auth
return nil
}
}
@@ -44,18 +47,10 @@ func WithAuthFromKeychain(keys authn.Keychain) ImageOption {
if err != nil {
return err
}
return i.setAuth(auth)
if auth == authn.Anonymous {
log.Println("No matching credentials were found, falling back on anonymous")
}
i.auth = auth
return nil
}
}
// Set client on image using provided transport, and the default authenticator
func (i *imageOpener) setTransport(t http.RoundTripper) error {
i.transport = t
return nil
}
// Set client on image using provided authenticator, and the default transport
func (i *imageOpener) setAuth(auth authn.Authenticator) error {
i.auth = auth
return nil
}