diff --git a/providers/os/connection/container/auth/options.go b/providers/os/connection/container/auth/options.go new file mode 100644 index 0000000000..6f2320e95a --- /dev/null +++ b/providers/os/connection/container/auth/options.go @@ -0,0 +1,71 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package auth + +import ( + "crypto/tls" + "net" + "net/http" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/rs/zerolog/log" + "go.mondoo.com/cnquery/v10/logger" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/vault" +) + +func TransportOption(insecure bool) remote.Option { + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + + if insecure { + tr.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + return remote.WithTransport(tr) +} + +func AuthOption(ref string, credentials []*vault.Credential) remote.Option { + for i := range credentials { + cred := credentials[i] + switch cred.Type { + case vault.CredentialType_password: + log.Debug().Msg("add password authentication") + cfg := authn.AuthConfig{ + Username: cred.User, + Password: string(cred.Secret), + } + return remote.WithAuth((authn.FromConfig(cfg))) + case vault.CredentialType_bearer: + log.Debug().Str("token", string(cred.Secret)).Msg("add bearer authentication") + cfg := authn.AuthConfig{ + Username: cred.User, + RegistryToken: string(cred.Secret), + } + return remote.WithAuth((authn.FromConfig(cfg))) + default: + log.Warn().Msg("unknown credentials for container image") + logger.DebugJSON(credentials) + } + } + log.Debug().Msg("no credentials for container image, falling back to default auth") + return remote.WithAuthFromKeychain(ConstructKeychain(ref)) +} + +func DefaultOpts(ref string, insecure bool) []remote.Option { + return []remote.Option{AuthOption(ref, nil), TransportOption(insecure)} +} diff --git a/providers/os/connection/container/image/registry.go b/providers/os/connection/container/image/registry.go index c83548ba08..a2d5fcee9b 100644 --- a/providers/os/connection/container/image/registry.go +++ b/providers/os/connection/container/image/registry.go @@ -4,139 +4,17 @@ package image import ( - "crypto/tls" - "net" - "net/http" - "time" - - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/rs/zerolog/log" - "go.mondoo.com/cnquery/v10/logger" - "go.mondoo.com/cnquery/v10/providers-sdk/v1/vault" - "go.mondoo.com/cnquery/v10/providers/os/connection/container/auth" ) -// Option is a functional option -// see https://www.sohamkamani.com/golang/options-pattern/ -type Option func(*options) error - -type options struct { - insecure bool - auth authn.Authenticator -} - -func WithInsecure(insecure bool) Option { - return func(o *options) error { - o.insecure = insecure - return nil - } -} - -func WithAuthenticator(auth authn.Authenticator) Option { - return func(o *options) error { - o.auth = auth - return nil - } -} - -func AuthOption(credentials []*vault.Credential) []Option { - remoteOpts := []Option{} - for i := range credentials { - cred := credentials[i] - switch cred.Type { - case vault.CredentialType_password: - log.Debug().Msg("add password authentication") - cfg := authn.AuthConfig{ - Username: cred.User, - Password: string(cred.Secret), - } - remoteOpts = append(remoteOpts, WithAuthenticator((authn.FromConfig(cfg)))) - case vault.CredentialType_bearer: - log.Debug().Str("token", string(cred.Secret)).Msg("add bearer authentication") - cfg := authn.AuthConfig{ - Username: cred.User, - RegistryToken: string(cred.Secret), - } - remoteOpts = append(remoteOpts, WithAuthenticator((authn.FromConfig(cfg)))) - default: - log.Warn().Msg("unknown credentials for container image") - logger.DebugJSON(credentials) - } - } - return remoteOpts -} - -func DefaultAuthOpts(ref name.Reference) (authn.Authenticator, error) { - kc := auth.ConstructKeychain(ref.Name()) - return kc.Resolve(ref.Context()) +func GetImageDescriptor(ref name.Reference, opts ...remote.Option) (*remote.Descriptor, error) { + return remote.Get(ref, opts...) } -func GetImageDescriptor(ref name.Reference, opts ...Option) (*remote.Descriptor, error) { - o := &options{ - insecure: false, - } - - for _, option := range opts { - if err := option(o); err != nil { - return nil, err - } - } - - if o.auth == nil { - auth, err := DefaultAuthOpts(ref) - if err != nil { - return nil, err - } - o.auth = auth - } - - return remote.Get(ref, remote.WithAuth(o.auth)) -} - -func LoadImageFromRegistry(ref name.Reference, opts ...Option) (v1.Image, error) { - o := &options{ - insecure: false, - } - - for _, option := range opts { - if err := option(o); err != nil { - return nil, err - } - } - - if o.auth == nil { - auth, err := DefaultAuthOpts(ref) - if err != nil { - return nil, err - } - o.auth = auth - } - - // mimic http.DefaultTransport - tr := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - - if o.insecure { - tr.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } - } - - img, err := remote.Image(ref, remote.WithAuth(o.auth), remote.WithTransport(tr)) +func LoadImageFromRegistry(ref name.Reference, opts ...remote.Option) (v1.Image, error) { + img, err := remote.Image(ref, opts...) if err != nil { return nil, err } diff --git a/providers/os/connection/container/image_connection.go b/providers/os/connection/container/image_connection.go index ce4cab08eb..f7fe0183bc 100644 --- a/providers/os/connection/container/image_connection.go +++ b/providers/os/connection/container/image_connection.go @@ -11,10 +11,12 @@ import ( "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/rs/zerolog/log" "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/v10/providers/os/connection/container/auth" "go.mondoo.com/cnquery/v10/providers/os/connection/container/image" "go.mondoo.com/cnquery/v10/providers/os/connection/tar" "go.mondoo.com/cnquery/v10/providers/os/id/containerid" @@ -61,10 +63,7 @@ func NewRegistryImage(id uint32, conf *inventory.Config, asset *inventory.Asset) } log.Debug().Str("ref", ref.Name()).Msg("found valid container registry reference") - registryOpts := []image.Option{image.WithInsecure(conf.Insecure)} - remoteOpts := image.AuthOption(conf.Credentials) - registryOpts = append(registryOpts, remoteOpts...) - + registryOpts := []remote.Option{auth.TransportOpt(conf.Insecure), auth.AuthOption(ref.Name(), conf.Credentials)} img, err := image.LoadImageFromRegistry(ref, registryOpts...) if err != nil { return nil, err diff --git a/providers/os/resources/discovery/container_registry/registry.go b/providers/os/resources/discovery/container_registry/registry.go index 88d1c102b6..70ab8f3ad5 100644 --- a/providers/os/resources/discovery/container_registry/registry.go +++ b/providers/os/resources/discovery/container_registry/registry.go @@ -4,12 +4,8 @@ package container_registry import ( - "crypto/tls" "fmt" - "net" - "net/http" "net/url" - "time" "github.com/cockroachdb/errors" "github.com/google/go-containerregistry/pkg/name" @@ -30,49 +26,8 @@ func NewContainerRegistryResolver(opts ...remote.Option) *DockerRegistryImages { } type DockerRegistryImages struct { - opts []remote.Option - Insecure bool - DisableKeychainAuth bool -} - -func (a *DockerRegistryImages) DefaultAuth(name string) remote.Option { - kcs := auth.ConstructKeychain(name) - return remote.WithAuthFromKeychain(kcs) -} - -func (a *DockerRegistryImages) InsecureTransportOpt() remote.Option { - // NOTE: config to get remote running with an insecure registry, we need to override the TLSClientConfig - tr := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - } - return remote.WithTransport(tr) -} - -func (a *DockerRegistryImages) DefaultOpts(name string) []remote.Option { - options := []remote.Option{} - // does not work with bearer auth, therefore it need to be disabled when other remote auth options are used - // TODO: we should implement this a bit differently - if !a.DisableKeychainAuth { - options = append(options, a.DefaultAuth(name)) - } - if a.Insecure { - options = append(options, a.InsecureTransportOpt()) - } - - return options + opts []remote.Option + Insecure bool } func (a *DockerRegistryImages) remoteOptions(name string) []remote.Option { @@ -80,7 +35,8 @@ func (a *DockerRegistryImages) remoteOptions(name string) []remote.Option { if len(a.opts) > 0 { return a.opts } - return a.DefaultOpts(name) + log.Debug().Str("name", name).Msg("using default remote options") + return auth.DefaultOpts(name, a.Insecure) } func (a *DockerRegistryImages) Repositories(reg name.Registry) ([]string, error) { @@ -150,7 +106,8 @@ func (a *DockerRegistryImages) ListRepository(repoName string) ([]*inventory.Ass } // fetch tags - tags, err := remote.List(repo, a.remoteOptions(repo.Name())...) + opts := a.remoteOptions(repo.Name()) + tags, err := remote.List(repo, opts...) if err != nil { return nil, handleUnauthorizedError(err, repo.Name()) } @@ -164,7 +121,7 @@ func (a *DockerRegistryImages) ListRepository(repoName string) ([]*inventory.Ass return nil, fmt.Errorf("parsing reference %q: %v", repoWithTag, err) } - a, err := a.toAsset(ref, nil) + a, err := a.toAsset(ref, nil, opts...) if err != nil { return nil, err } @@ -205,7 +162,7 @@ func (a *DockerRegistryImages) GetImage(ref name.Reference, creds []*vault.Crede } func (a *DockerRegistryImages) toAsset(ref name.Reference, creds []*vault.Credential, opts ...remote.Option) (*inventory.Asset, error) { - desc, err := image.GetImageDescriptor(ref, image.AuthOption(creds)...) + desc, err := image.GetImageDescriptor(ref, opts...) if err != nil { return nil, handleUnauthorizedError(err, ref.Name()) } diff --git a/providers/os/resources/discovery/container_registry/resolver.go b/providers/os/resources/discovery/container_registry/resolver.go index ca99148744..b75c6ff455 100644 --- a/providers/os/resources/discovery/container_registry/resolver.go +++ b/providers/os/resources/discovery/container_registry/resolver.go @@ -7,13 +7,12 @@ import ( "context" "errors" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/rs/zerolog/log" - "go.mondoo.com/cnquery/v10/logger" "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v10/providers-sdk/v1/vault" + "go.mondoo.com/cnquery/v10/providers/os/connection/container/auth" ) type Resolver struct { @@ -35,8 +34,6 @@ func (r *Resolver) Resolve(ctx context.Context, root *inventory.Asset, conf *inv resolved := []*inventory.Asset{} imageFetcher := NewContainerRegistryResolver() - // to support self-signed certs - imageFetcher.Insecure = conf.Insecure // check if the reference is an image // NOTE: we use strict validation here otherwise urls like cr://index.docker.io/mondoo/client are converted @@ -50,12 +47,11 @@ func (r *Resolver) Resolve(ctx context.Context, root *inventory.Asset, conf *inv if err == nil { log.Debug().Str("image", conf.Host).Msg("detected container image in container registry") - remoteOpts := AuthOption(conf.Credentials, credsResolver) - // we need to disable default keychain auth if an auth method was found - if len(remoteOpts) > 0 { - imageFetcher.DisableKeychainAuth = true + remoteOpts := []remote.Option{ + auth.AuthOption(ref.Name(), conf.Credentials), + // to support self-signed certs + auth.TransportOption(conf.Insecure), } - a, err := imageFetcher.GetImage(ref, conf.Credentials, remoteOpts...) if err != nil { return nil, err @@ -105,37 +101,3 @@ func (r *Resolver) Resolve(ctx context.Context, root *inventory.Asset, conf *inv return resolved, nil } - -func AuthOption(credentials []*vault.Credential, credsResolver vault.Resolver) []remote.Option { - remoteOpts := []remote.Option{} - for i := range credentials { - cred := credentials[i] - - // NOTE: normally the motor connection is resolving the credentials but here we need the credential earlier - // we probably want to write some mql resources to support the query of registries itself - resolvedCredential, err := credsResolver.GetCredential(cred) - if err != nil { - log.Warn().Err(err).Msg("could not resolve credential") - } - switch resolvedCredential.Type { - case vault.CredentialType_password: - log.Debug().Msg("add password authentication") - cfg := authn.AuthConfig{ - Username: resolvedCredential.User, - Password: string(resolvedCredential.Secret), - } - remoteOpts = append(remoteOpts, remote.WithAuth(authn.FromConfig(cfg))) - case vault.CredentialType_bearer: - log.Debug().Str("token", string(resolvedCredential.Secret)).Msg("add bearer authentication") - cfg := authn.AuthConfig{ - Username: resolvedCredential.User, - RegistryToken: string(resolvedCredential.Secret), - } - remoteOpts = append(remoteOpts, remote.WithAuth(authn.FromConfig(cfg))) - default: - log.Warn().Msg("unknown credentials for container image") - logger.DebugJSON(credentials) - } - } - return remoteOpts -}